mirror of
https://github.com/jlengrand/open-wc.git
synced 2026-03-10 08:31:19 +00:00
feat(resolve): update to latest proposal specs (only mapping and scopes)
This commit is contained in:
@@ -1,60 +1,99 @@
|
||||
# Resolve import-maps
|
||||
|
||||
This will allow you to parse and resolve urls by a given [import-map](https://github.com/WICG/import-maps).
|
||||
Library for parsing and resolving [import maps](https://github.com/WICG/import-maps).
|
||||
|
||||
[//]: # 'AUTO INSERT HEADER PREPUBLISH'
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
yarn add @import-maps/resolve
|
||||
npm i --save-dev @import-maps/resolve
|
||||
```
|
||||
|
||||
You may then override a resolve method in your build process.
|
||||
### Base URL
|
||||
|
||||
Parsing and resolving import maps requires a base URL. This is an instance of the `URL` constructor.
|
||||
|
||||
This can be a browser URL:
|
||||
|
||||
```js
|
||||
import { parseFromString, resolve } from '@import-maps/resolve';
|
||||
|
||||
// you probably want to cache the map processing and not redo it for every resolve
|
||||
// a simple example
|
||||
const importMapCache = null;
|
||||
|
||||
function myResolve(specifier) {
|
||||
const rootDir = process.cwd();
|
||||
const basePath = importer ? importer.replace(rootDir, `${rootDir}::`) : `${rootDir}::`;
|
||||
if (!importMapCache) {
|
||||
const mapString = fs.readFileSync(path.join(rootDir, 'import-map.json'), 'utf-8');
|
||||
mapCache = parseFromString(mapString, basePath);
|
||||
}
|
||||
|
||||
const relativeSource = source.replace(rootDir, '');
|
||||
const resolvedPath = resolve(relativeSource, importMapCache, basePath);
|
||||
|
||||
if (resolvedPath) {
|
||||
return resolvedPath;
|
||||
}
|
||||
}
|
||||
const myUrl = new URL('https://www.example.com/');
|
||||
```
|
||||
|
||||
### Additional info
|
||||
Or a file URL when working with a file system. The `pathToFileURL` function is useful for converting a file path to a URL object:
|
||||
|
||||
The 3rd parameter of `resolve` is the "baseUrl/basePath" and it's format is `/path/to/root::/subdir/foo`.
|
||||
You can compare it with an url `http://example.com/subdir/foo`.
|
||||
```js
|
||||
import path from 'path';
|
||||
import { pathToFileURL } from 'url';
|
||||
|
||||
- Everything before the `::` is sort of the `domain` e.g. `http://example.com/`
|
||||
- Everything after is the path/directory to your appliaction
|
||||
const fileUrl1 = new URL('file:///foo/bar');
|
||||
const fileUrl2 = pathToFileURL(path.join(process.cwd(), 'foo', 'bar'));
|
||||
```
|
||||
|
||||
Such a path is needed as import maps support relative pathes as values.
|
||||
### Parsing an import map from a string
|
||||
|
||||
The `parseFromString` parses an import map from a JSON string. It returns the parsed import map object to be used when resolving import specifiers.
|
||||
|
||||
```js
|
||||
import { parseFromString } from '@import-maps/resolve';
|
||||
|
||||
// get the import map from somewhere, for example read it from a string
|
||||
const importMapString = '{ "imports": { "foo": "./bar.js" } }';
|
||||
// create a base URL to resolve imports relatively to
|
||||
const baseURL = new URL('https://www.example.com/');
|
||||
|
||||
const importMap = parseFromString(importMapString, baseURL);
|
||||
```
|
||||
|
||||
### Parsing an import map from an object
|
||||
|
||||
If you already have an object which represents the import map, it still needs to be parsed to validate it and to prepare it for resolving. You can use the `parse` function for this.
|
||||
|
||||
```js
|
||||
import { parse } from '@import-maps/resolve';
|
||||
|
||||
// get the import map from somewhere, for example read it from a string
|
||||
const rawImportMap = { imports: { foo: './bar.js' } };
|
||||
// create a base URL to resolve imports relatively to
|
||||
const baseURL = new URL('https://www.example.com/');
|
||||
|
||||
const importMap = parse(importMapString, baseURL);
|
||||
```
|
||||
|
||||
### Resolving specifiers
|
||||
|
||||
Once you've created a parsed import map, you can start resolving specifiers. The `resolve` function returns a `URL`, you can use this to either use the resolved `href` or just the `pathname`.
|
||||
|
||||
```js
|
||||
import { resolve } from '@import-maps/resolve';
|
||||
|
||||
const importMap = /* parse import map shown above */;
|
||||
// create a base URL to resolve imports relatively to
|
||||
const baseURL = new URL('https://www.example.com/');
|
||||
|
||||
const resolvedUrl = resolve(importMapString, baseURL);
|
||||
|
||||
// the full url including protocol and domain
|
||||
console.log(resolvedUrl.href);
|
||||
// just the path
|
||||
console.log(resolvedUrl.url);
|
||||
```
|
||||
|
||||
If you need to use the resolved path on the file system, you can use the `fileURLToPath` utility:
|
||||
|
||||
```js
|
||||
import { fileURLToPath } from 'url';
|
||||
import { resolve } from '@import-maps/resolve';
|
||||
|
||||
const resolvedUrl = resolve(importMapString, baseURL);
|
||||
|
||||
// the fully resolved file path
|
||||
console.log(fileURLToPath(resolvedUrl));
|
||||
```
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
This implementation is heavily based on the [import-maps reference implementation](https://github.com/WICG/import-maps/tree/master/reference-implementation).
|
||||
Thanks to @domenic and @guybedford for sharing that prototype.
|
||||
|
||||
Some adjustments have been made
|
||||
|
||||
- Allow to process/resolve node pathes besides urls
|
||||
- Use mocha/chai for testing (already available in our setup)
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: ['babel-plugin-transform-dynamic-import'],
|
||||
ignore: ['./src/generators/*/templates/**/*'],
|
||||
presets: [
|
||||
[
|
||||
'@babel/env',
|
||||
{
|
||||
targets: {
|
||||
node: '10',
|
||||
},
|
||||
corejs: 2,
|
||||
useBuiltIns: 'usage',
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
||||
11
packages/import-maps-resolve/index.js
Normal file
11
packages/import-maps-resolve/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @typedef {import('./src/types').ImportMap} ImportMap */
|
||||
/** @typedef {import('./src/types').ScopesMap} ScopesMap */
|
||||
/** @typedef {import('./src/types').SpecifierMap} SpecifierMap */
|
||||
/** @typedef {import('./src/types').ParsedImportMap} ParsedImportMap */
|
||||
/** @typedef {import('./src/types').ParsedScopesMap} ParsedScopesMap */
|
||||
/** @typedef {import('./src/types').ParsedSpecifierMap} ParsedSpecifierMap */
|
||||
|
||||
const { parseFromString, parse } = require('./src/parser.js');
|
||||
const { resolve } = require('./src/resolver.js');
|
||||
|
||||
module.exports = { parseFromString, parse, resolve };
|
||||
@@ -4,7 +4,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"description": "Read and resolve urls via an import map",
|
||||
"description": "Parse and resolve imports via an import map",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -13,28 +13,19 @@
|
||||
},
|
||||
"author": "open-wc",
|
||||
"homepage": "https://github.com/open-wc/open-wc/tree/master/packages/import-maps-resolve",
|
||||
"main": "dist/index.js",
|
||||
"main": "./index.js",
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir dist --copy-files --include-dotfiles",
|
||||
"prepublishOnly": "npm run build && ../../scripts/insert-header.js",
|
||||
"start": "npm run build && node ./dist/index.js",
|
||||
"test": "npm run test:node",
|
||||
"test:node": "mocha --require @babel/register",
|
||||
"test:watch": "onchange 'src/**/*.js' 'test/**/*.js' -- npm run test --silent"
|
||||
"test:node": "mocha test/run-tests.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"*.d.ts",
|
||||
"*.js",
|
||||
"src"
|
||||
],
|
||||
"keywords": [
|
||||
"import-map",
|
||||
"import-maps"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.0",
|
||||
"@babel/register": "^7.9.0",
|
||||
"babel-plugin-transform-dynamic-import": "^2.1.0",
|
||||
"onchange": "^5.2.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { parseFromString } from './parser.js';
|
||||
export { resolve } from './resolver.js';
|
||||
export { mergeImportMaps } from './utils.js';
|
||||
@@ -1,167 +1,104 @@
|
||||
/* eslint-disable no-continue */
|
||||
/* eslint-disable no-restricted-syntax, no-console */
|
||||
import path from 'path';
|
||||
import {
|
||||
tryURLParse,
|
||||
hasFetchScheme,
|
||||
tryURLLikeSpecifierParse,
|
||||
BUILT_IN_MODULE_PROTOCOL,
|
||||
} from './utils.js';
|
||||
/* eslint-disable no-console, no-continue */
|
||||
/** @typedef {import('./types').ImportMap} ImportMap */
|
||||
/** @typedef {import('./types').ScopesMap} ScopesMap */
|
||||
/** @typedef {import('./types').SpecifierMap} SpecifierMap */
|
||||
/** @typedef {import('./types').ParsedImportMap} ParsedImportMap */
|
||||
/** @typedef {import('./types').ParsedScopesMap} ParsedScopesMap */
|
||||
/** @typedef {import('./types').ParsedSpecifierMap} ParsedSpecifierMap */
|
||||
|
||||
const assert = require('assert');
|
||||
const { tryURLParse, tryURLLikeSpecifierParse } = require('./utils.js');
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isJSONObject(value) {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
return typeof value === 'object' && value != null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function compare(a, b) {
|
||||
/**
|
||||
* @param {string} a
|
||||
* @param {string} b
|
||||
*/
|
||||
function codeUnitCompare(a, b) {
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (b > a) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function longerLengthThenCodeUnitOrder(a, b) {
|
||||
return compare(b.length, a.length) || compare(a, b);
|
||||
|
||||
throw new Error('This should never be reached because this is only used on JSON object keys');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} specifierKey
|
||||
* @param {URL} baseURL
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function normalizeSpecifierKey(specifierKey, baseURL) {
|
||||
// Ignore attempts to use the empty string as a specifier key
|
||||
if (specifierKey === '') {
|
||||
return null;
|
||||
console.warn(`Invalid empty string specifier key.`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const url = tryURLLikeSpecifierParse(specifierKey, baseURL);
|
||||
if (url !== null) {
|
||||
const urlString = url.href;
|
||||
if (url.protocol === BUILT_IN_MODULE_PROTOCOL && urlString.includes('/')) {
|
||||
console.warn(
|
||||
`Invalid specifier key "${urlString}". Built-in module specifiers must not contain "/".`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return urlString;
|
||||
if (url) {
|
||||
return url.href;
|
||||
}
|
||||
|
||||
return specifierKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SpecifierMap} obj
|
||||
* @param {URL} baseURL
|
||||
* @returns {ParsedSpecifierMap}
|
||||
*/
|
||||
function sortAndNormalizeSpecifierMap(obj, baseURL) {
|
||||
if (!isJSONObject(obj)) {
|
||||
throw new Error('needs to be an obj');
|
||||
}
|
||||
assert(isJSONObject(obj));
|
||||
|
||||
const normalized = /** @type {ParsedSpecifierMap} */ ({});
|
||||
|
||||
// Normalize all entries into arrays
|
||||
const normalized = {};
|
||||
for (const [specifierKey, value] of Object.entries(obj)) {
|
||||
const normalizedSpecifierKey = normalizeSpecifierKey(specifierKey, baseURL);
|
||||
if (normalizedSpecifierKey === null) {
|
||||
if (!normalizedSpecifierKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
normalized[normalizedSpecifierKey] = [value];
|
||||
} else if (value === null) {
|
||||
normalized[normalizedSpecifierKey] = [];
|
||||
} else if (Array.isArray(value)) {
|
||||
normalized[normalizedSpecifierKey] = obj[specifierKey];
|
||||
if (typeof value !== 'string') {
|
||||
console.warn(
|
||||
`Invalid address ${JSON.stringify(value)} for the specifier key "${specifierKey}". ` +
|
||||
`Addresses must be strings.`,
|
||||
);
|
||||
normalized[normalizedSpecifierKey] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
const addressURL = tryURLLikeSpecifierParse(value, baseURL);
|
||||
if (!addressURL) {
|
||||
console.warn(`Invalid address "${value}" for the specifier key "${specifierKey}".`);
|
||||
normalized[normalizedSpecifierKey] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (specifierKey.endsWith('/') && !addressURL.href.endsWith('/')) {
|
||||
console.warn(
|
||||
`Invalid address "${addressURL.href}" for package specifier key "${specifierKey}". ` +
|
||||
`Package addresses must end with "/".`,
|
||||
);
|
||||
normalized[normalizedSpecifierKey] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized[normalizedSpecifierKey] = addressURL;
|
||||
}
|
||||
|
||||
// Normalize/validate each potential address in the array
|
||||
for (const [specifierKey, potentialAddresses] of Object.entries(normalized)) {
|
||||
if (!Array.isArray(potentialAddresses)) {
|
||||
throw new Error('should be an array');
|
||||
}
|
||||
|
||||
const validNormalizedAddresses = [];
|
||||
for (const potentialAddress of potentialAddresses) {
|
||||
if (typeof potentialAddress !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const addressURL = tryURLLikeSpecifierParse(potentialAddress, baseURL);
|
||||
let addressUrlString = '';
|
||||
if (addressURL !== null) {
|
||||
if (addressURL.protocol === BUILT_IN_MODULE_PROTOCOL && addressURL.href.includes('/')) {
|
||||
console.warn(
|
||||
`Invalid target address "${potentialAddress}". Built-in module URLs must not contain "/".`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
addressUrlString = addressURL.href;
|
||||
} else if (baseURL.includes('::')) {
|
||||
const [rootPath, basePath] = baseURL.split('::');
|
||||
|
||||
const dirPath = potentialAddress.startsWith('/') ? '' : path.dirname(basePath);
|
||||
addressUrlString = path.normalize(path.join(rootPath, dirPath, potentialAddress));
|
||||
}
|
||||
|
||||
if (specifierKey.endsWith('/') && !addressUrlString.endsWith('/')) {
|
||||
console.warn(
|
||||
`Invalid target address "${addressUrlString}" for package specifier "${specifierKey}". ` +
|
||||
`Package address targets must end with "/".`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (addressUrlString !== '') {
|
||||
validNormalizedAddresses.push(addressUrlString);
|
||||
}
|
||||
}
|
||||
normalized[specifierKey] = validNormalizedAddresses;
|
||||
}
|
||||
|
||||
const sortedAndNormalized = {};
|
||||
const sortedKeys = Object.keys(normalized).sort(longerLengthThenCodeUnitOrder);
|
||||
for (const key of sortedKeys) {
|
||||
sortedAndNormalized[key] = normalized[key];
|
||||
}
|
||||
|
||||
return sortedAndNormalized;
|
||||
}
|
||||
|
||||
function sortAndNormalizeScopes(obj, baseURL) {
|
||||
const normalized = {};
|
||||
for (const [scopePrefix, potentialSpecifierMap] of Object.entries(obj)) {
|
||||
if (!isJSONObject(potentialSpecifierMap)) {
|
||||
throw new TypeError(`The value for the "${scopePrefix}" scope prefix must be an object.`);
|
||||
}
|
||||
|
||||
const scopePrefixURL = tryURLParse(scopePrefix, baseURL);
|
||||
let scopeString = '';
|
||||
if (scopePrefixURL !== null) {
|
||||
if (!hasFetchScheme(scopePrefixURL)) {
|
||||
console.warn(`Invalid scope "${scopePrefixURL}". Scope URLs must have a fetch scheme.`);
|
||||
continue;
|
||||
}
|
||||
scopeString = scopePrefixURL.href;
|
||||
} else {
|
||||
const scopePrefixURLWithoutBase = tryURLParse(scopePrefix);
|
||||
if (scopePrefixURLWithoutBase !== null) {
|
||||
if (!hasFetchScheme(scopePrefixURLWithoutBase)) {
|
||||
console.warn(
|
||||
`Invalid scope "${scopePrefixURLWithoutBase}". Scope URLs must have a fetch scheme.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
scopeString = scopePrefixURLWithoutBase.href;
|
||||
} else if (baseURL.includes('::')) {
|
||||
const [rootPath, basePath] = baseURL.split('::');
|
||||
|
||||
const dirPath = scopePrefix.startsWith('/') ? '' : path.dirname(basePath);
|
||||
scopeString = path.normalize(path.join(rootPath, dirPath, scopePrefix));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
normalized[scopeString] = sortAndNormalizeSpecifierMap(potentialSpecifierMap, baseURL);
|
||||
}
|
||||
|
||||
const sortedAndNormalized = {};
|
||||
const sortedKeys = Object.keys(normalized).sort(longerLengthThenCodeUnitOrder);
|
||||
const sortedAndNormalized = /** @type {ParsedSpecifierMap} */ ({});
|
||||
const sortedKeys = Object.keys(normalized).sort((a, b) => codeUnitCompare(b, a));
|
||||
for (const key of sortedKeys) {
|
||||
sortedAndNormalized[key] = normalized[key];
|
||||
}
|
||||
@@ -170,43 +107,74 @@ function sortAndNormalizeScopes(obj, baseURL) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes and normalizes a given import-map string.
|
||||
*
|
||||
* @example
|
||||
* const importMap = `{ import: {
|
||||
* 'foo': './node_modules/foo/foo.js',
|
||||
* 'bar': '/node_modules/bar/bar.js'
|
||||
* }}`;
|
||||
* parseFromString(importMap, '/path/to/root::/src');
|
||||
* // { import: {
|
||||
* // 'foo': ['/path/to/root/src/node_modules/foo/foo.js'],
|
||||
* // 'bar': ['/path/to/root/node_modules/bar/bar.js']
|
||||
* // }}
|
||||
*
|
||||
* @param {string} input The import map as a string
|
||||
* @param {string} baseURL The base url/path to your root + executing sub directory (separated by ::)
|
||||
* @param {ScopesMap} obj
|
||||
* @param {URL} baseURL
|
||||
*/
|
||||
export function parseFromString(input, baseURL) {
|
||||
const parsed = JSON.parse(input);
|
||||
function sortAndNormalizeScopes(obj, baseURL) {
|
||||
const normalized = /** @type {ParsedScopesMap} */ ({});
|
||||
for (const [scopePrefix, potentialSpecifierMap] of Object.entries(obj)) {
|
||||
if (!isJSONObject(potentialSpecifierMap)) {
|
||||
throw new TypeError(`The value for the "${scopePrefix}" scope prefix must be an object.`);
|
||||
}
|
||||
|
||||
if (!isJSONObject(parsed)) {
|
||||
const scopePrefixURL = tryURLParse(scopePrefix, baseURL);
|
||||
if (!scopePrefixURL) {
|
||||
console.warn(`Invalid scope "${scopePrefix}" (parsed against base URL "${baseURL}").`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedScopePrefix = scopePrefixURL.href;
|
||||
normalized[normalizedScopePrefix] = sortAndNormalizeSpecifierMap(
|
||||
potentialSpecifierMap,
|
||||
baseURL,
|
||||
);
|
||||
}
|
||||
|
||||
const sortedAndNormalized = /** @type {ParsedScopesMap} */ ({});
|
||||
const sortedKeys = Object.keys(normalized).sort((a, b) => codeUnitCompare(b, a));
|
||||
for (const key of sortedKeys) {
|
||||
sortedAndNormalized[key] = normalized[key];
|
||||
}
|
||||
|
||||
return sortedAndNormalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ImportMap} input
|
||||
* @param {URL} baseURL
|
||||
* @returns {ParsedImportMap}
|
||||
*/
|
||||
function parse(input, baseURL) {
|
||||
if (!isJSONObject(input)) {
|
||||
throw new TypeError('Import map JSON must be an object.');
|
||||
}
|
||||
|
||||
let sortedAndNormalizedImports = {};
|
||||
if ('imports' in parsed) {
|
||||
if (!isJSONObject(parsed.imports)) {
|
||||
throw new TypeError("Import map's imports value must be an object.");
|
||||
}
|
||||
sortedAndNormalizedImports = sortAndNormalizeSpecifierMap(parsed.imports, baseURL);
|
||||
if (!(baseURL instanceof URL)) {
|
||||
throw new TypeError('Missing base URL or base URL is not a URL');
|
||||
}
|
||||
|
||||
let sortedAndNormalizedScopes = {};
|
||||
if ('scopes' in parsed) {
|
||||
if (!isJSONObject(parsed.scopes)) {
|
||||
let sortedAndNormalizedImports = /** @type {ParsedSpecifierMap} */ ({});
|
||||
if ('imports' in input) {
|
||||
if (!input.imports || !isJSONObject(input.imports)) {
|
||||
throw new TypeError("Import map's imports value must be an object.");
|
||||
}
|
||||
sortedAndNormalizedImports = sortAndNormalizeSpecifierMap(input.imports, baseURL);
|
||||
}
|
||||
|
||||
let sortedAndNormalizedScopes = /** @type {ParsedScopesMap} */ ({});
|
||||
if ('scopes' in input) {
|
||||
if (!input.scopes || !isJSONObject(input.scopes)) {
|
||||
throw new TypeError("Import map's scopes value must be an object.");
|
||||
}
|
||||
sortedAndNormalizedScopes = sortAndNormalizeScopes(parsed.scopes, baseURL);
|
||||
sortedAndNormalizedScopes = sortAndNormalizeScopes(input.scopes, baseURL);
|
||||
}
|
||||
|
||||
const badTopLevelKeys = new Set(Object.keys(input));
|
||||
badTopLevelKeys.delete('imports');
|
||||
badTopLevelKeys.delete('scopes');
|
||||
|
||||
for (const badKey of badTopLevelKeys) {
|
||||
console.warn(`Invalid top-level key "${badKey}". Only "imports" and "scopes" can be present.`);
|
||||
}
|
||||
|
||||
// Always have these two keys, and exactly these two keys, in the result.
|
||||
@@ -215,3 +183,15 @@ export function parseFromString(input, baseURL) {
|
||||
scopes: sortedAndNormalizedScopes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {URL} baseURL
|
||||
* @returns {ParsedImportMap}
|
||||
*/
|
||||
function parseFromString(input, baseURL) {
|
||||
const importMap = /** @type {ImportMap} */ (JSON.parse(input));
|
||||
return parse(importMap, baseURL);
|
||||
}
|
||||
|
||||
module.exports = { parse, parseFromString };
|
||||
|
||||
@@ -1,136 +1,92 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { URL } from 'url';
|
||||
import path from 'path';
|
||||
import {
|
||||
tryURLLikeSpecifierParse,
|
||||
BUILT_IN_MODULE_SCHEME,
|
||||
BUILT_IN_MODULE_PROTOCOL,
|
||||
isUrlString,
|
||||
} from './utils.js';
|
||||
/** @typedef {import('./types').ParsedImportMap} ParsedImportMap */
|
||||
/** @typedef {import('./types').ParsedScopesMap} ParsedScopesMap */
|
||||
/** @typedef {import('./types').ParsedSpecifierMap} ParsedSpecifierMap */
|
||||
|
||||
const supportedBuiltInModules = new Set([`${BUILT_IN_MODULE_SCHEME}:blank`]);
|
||||
const assert = require('assert');
|
||||
const { tryURLLikeSpecifierParse, tryURLParse } = require('./utils.js');
|
||||
|
||||
/**
|
||||
* @param {string} normalizedSpecifier
|
||||
* @param {ParsedSpecifierMap} specifierMap
|
||||
*/
|
||||
function resolveImportsMatch(normalizedSpecifier, specifierMap) {
|
||||
for (const [specifierKey, rawAddresses] of Object.entries(specifierMap)) {
|
||||
const addresses = rawAddresses.map(address =>
|
||||
isUrlString(address) ? new URL(address) : address,
|
||||
);
|
||||
for (const [specifierKey, resolutionResult] of Object.entries(specifierMap)) {
|
||||
// Exact-match case
|
||||
if (specifierKey === normalizedSpecifier) {
|
||||
if (addresses.length === 0) {
|
||||
throw new TypeError(`Specifier "${normalizedSpecifier}" was mapped to no addresses.`);
|
||||
} else if (addresses.length === 1) {
|
||||
const singleAddress = addresses[0];
|
||||
if (
|
||||
singleAddress.protocol === BUILT_IN_MODULE_PROTOCOL &&
|
||||
!supportedBuiltInModules.has(singleAddress.href)
|
||||
) {
|
||||
throw new TypeError(`The "${singleAddress.href}" built-in module is not implemented.`);
|
||||
}
|
||||
|
||||
return singleAddress.href ? singleAddress.href : singleAddress;
|
||||
} else if (
|
||||
addresses.length === 2 &&
|
||||
addresses[0].protocol === BUILT_IN_MODULE_PROTOCOL &&
|
||||
addresses[1].protocol !== BUILT_IN_MODULE_PROTOCOL
|
||||
) {
|
||||
return supportedBuiltInModules.has(addresses[0].href)
|
||||
? addresses[0].href
|
||||
: addresses[1].href;
|
||||
} else {
|
||||
throw new Error(
|
||||
'The reference implementation for multi-address fallbacks that are not ' +
|
||||
'[built-in module, fetch-scheme URL] is not yet implemented.',
|
||||
);
|
||||
if (!resolutionResult) {
|
||||
throw new TypeError(`Blocked by a null entry for "${specifierKey}"`);
|
||||
}
|
||||
|
||||
assert(resolutionResult instanceof URL);
|
||||
|
||||
return resolutionResult;
|
||||
}
|
||||
|
||||
// Package prefix-match case
|
||||
if (specifierKey.endsWith('/') && normalizedSpecifier.startsWith(specifierKey)) {
|
||||
if (addresses.length === 0) {
|
||||
throw new TypeError(
|
||||
`Specifier "${normalizedSpecifier}" was mapped to no addresses ` +
|
||||
`(via prefix specifier key "${specifierKey}").`,
|
||||
);
|
||||
} else if (addresses.length === 1) {
|
||||
const afterPrefix = normalizedSpecifier.substring(specifierKey.length);
|
||||
if (isUrlString(addresses[0])) {
|
||||
return new URL(afterPrefix, addresses[0]).href;
|
||||
}
|
||||
return `${addresses[0]}${afterPrefix}`;
|
||||
} else {
|
||||
throw new Error(
|
||||
'The reference implementation for multi-address fallbacks that are not ' +
|
||||
'[built-in module, fetch-scheme URL] is not yet implemented.',
|
||||
);
|
||||
if (!resolutionResult) {
|
||||
throw new TypeError(`Blocked by a null entry for "${specifierKey}"`);
|
||||
}
|
||||
|
||||
assert(resolutionResult instanceof URL);
|
||||
|
||||
const afterPrefix = normalizedSpecifier.substring(specifierKey.length);
|
||||
|
||||
// Enforced by parsing
|
||||
assert(resolutionResult.href.endsWith('/'));
|
||||
|
||||
const url = tryURLParse(afterPrefix, resolutionResult);
|
||||
if (!url) {
|
||||
throw new TypeError(`Failed to resolve prefix-match relative URL for "${specifierKey}"`);
|
||||
}
|
||||
|
||||
assert(url instanceof URL);
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a given specifier via a parsedImportMap. Knowledge about the path/url of the currently
|
||||
* executing script is required.
|
||||
*
|
||||
* @example
|
||||
* const importMap = { import: {
|
||||
* 'foo': ['/node_modules/foo/foo.js']
|
||||
* }};
|
||||
* resolve('foo', importMap, '/path/to/root::/src/index.html');
|
||||
* // => /path/to/root/node_modules/foo/foo.js
|
||||
*
|
||||
* resolve('foo', importMap, 'http://example.com/my-app/src/index.html');
|
||||
* // => http://example.com/node_modules/foo/foo.js
|
||||
*
|
||||
* @param {string} specifier can be a full URL or a bare_specifier or bare_specifier + path
|
||||
* @param {object} parsedImportMap normalized map string (already processed by parseFromString)
|
||||
* @param {string} scriptURL the scripts url/path that is requesting the resolve (neded to support scopes)
|
||||
* @param {string} specifier
|
||||
* @param {ParsedImportMap} parsedImportMap
|
||||
* @param {URL} scriptURL
|
||||
*/
|
||||
export function resolve(specifier, parsedImportMap, scriptURL) {
|
||||
function resolve(specifier, parsedImportMap, scriptURL) {
|
||||
const asURL = tryURLLikeSpecifierParse(specifier, scriptURL);
|
||||
const normalizedSpecifier = asURL ? asURL.href : specifier;
|
||||
const scriptURLString = scriptURL.href;
|
||||
|
||||
let nodeSpecifier = null;
|
||||
if (scriptURL.includes('::')) {
|
||||
const [rootPath, basePath] = scriptURL.split('::');
|
||||
|
||||
const dirPath = specifier.startsWith('/') ? '' : path.dirname(basePath);
|
||||
nodeSpecifier = path.normalize(path.join(rootPath, dirPath, specifier));
|
||||
}
|
||||
const scriptURLString = scriptURL.split('::').join('');
|
||||
|
||||
for (const [scopePrefix, scopeImports] of Object.entries(parsedImportMap.scopes)) {
|
||||
for (const [scopePrefix, scopeImports] of Object.entries(parsedImportMap.scopes || {})) {
|
||||
if (
|
||||
scopePrefix === scriptURLString ||
|
||||
(scopePrefix.endsWith('/') && scriptURLString.startsWith(scopePrefix))
|
||||
) {
|
||||
const scopeImportsMatch = resolveImportsMatch(
|
||||
nodeSpecifier || normalizedSpecifier,
|
||||
scopeImports,
|
||||
);
|
||||
const scopeImportsMatch = resolveImportsMatch(normalizedSpecifier, scopeImports);
|
||||
|
||||
if (scopeImportsMatch) {
|
||||
return scopeImportsMatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const topLevelImportsMatch = resolveImportsMatch(normalizedSpecifier, parsedImportMap.imports);
|
||||
const topLevelImportsMatch = resolveImportsMatch(
|
||||
normalizedSpecifier,
|
||||
parsedImportMap.imports || {},
|
||||
);
|
||||
|
||||
if (topLevelImportsMatch) {
|
||||
return topLevelImportsMatch;
|
||||
}
|
||||
|
||||
// The specifier was able to be turned into a URL, but wasn't remapped into anything.
|
||||
if (asURL) {
|
||||
if (asURL.protocol === BUILT_IN_MODULE_PROTOCOL && !supportedBuiltInModules.has(asURL.href)) {
|
||||
throw new TypeError(`The "${asURL.href}" built-in module is not implemented.`);
|
||||
}
|
||||
return asURL.href;
|
||||
}
|
||||
|
||||
if (nodeSpecifier && (specifier.startsWith('/') || specifier.startsWith('.'))) {
|
||||
return nodeSpecifier;
|
||||
return asURL;
|
||||
}
|
||||
|
||||
throw new TypeError(`Unmapped bare specifier "${specifier}"`);
|
||||
}
|
||||
|
||||
module.exports = { resolve };
|
||||
|
||||
17
packages/import-maps-resolve/src/types.d.ts
vendored
Normal file
17
packages/import-maps-resolve/src/types.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
export type SpecifierMap = Record<string, string>;
|
||||
|
||||
export type ScopesMap = Record<string, SpecifierMap>;
|
||||
|
||||
export interface ImportMap {
|
||||
imports?: SpecifierMap;
|
||||
scopes?: ScopesMap;
|
||||
}
|
||||
|
||||
export type ParsedSpecifierMap = Record<string, URL | null>;
|
||||
|
||||
export type ParsedScopesMap = Record<string, ParsedSpecifierMap>;
|
||||
|
||||
export interface ParsedImportMap {
|
||||
imports?: ParsedSpecifierMap;
|
||||
scopes?: ParsedScopesMap;
|
||||
}
|
||||
@@ -1,76 +1,28 @@
|
||||
import { URL } from 'url';
|
||||
|
||||
// https://fetch.spec.whatwg.org/#fetch-scheme
|
||||
const FETCH_SCHEMES = new Set([
|
||||
'http',
|
||||
'https',
|
||||
'ftp',
|
||||
'about',
|
||||
'blob',
|
||||
'data',
|
||||
'file',
|
||||
'filesystem',
|
||||
]);
|
||||
|
||||
// Tentative, so better to centralize so we can change in one place as necessary (including tests).
|
||||
export const BUILT_IN_MODULE_SCHEME = 'std';
|
||||
|
||||
// Useful for comparing to .protocol
|
||||
export const BUILT_IN_MODULE_PROTOCOL = `${BUILT_IN_MODULE_SCHEME}:`;
|
||||
|
||||
export function tryURLParse(string, baseURL) {
|
||||
/**
|
||||
* @param {string} string
|
||||
* @param {URL} [baseURL]
|
||||
* @returns {URL | undefined}
|
||||
*/
|
||||
function tryURLParse(string, baseURL) {
|
||||
try {
|
||||
return new URL(string, baseURL);
|
||||
} catch (e) {
|
||||
// TODO remove useless binding when ESLint and Jest support that
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function isUrlString(string) {
|
||||
return !!tryURLParse(string);
|
||||
}
|
||||
|
||||
export function hasFetchScheme(url) {
|
||||
return FETCH_SCHEMES.has(url.protocol.slice(0, -1));
|
||||
}
|
||||
|
||||
export function tryURLLikeSpecifierParse(specifier, baseURL) {
|
||||
if (baseURL.includes('::')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} specifier
|
||||
* @param {URL} baseURL
|
||||
* @returns {URL | undefined}
|
||||
*/
|
||||
function tryURLLikeSpecifierParse(specifier, baseURL) {
|
||||
if (specifier.startsWith('/') || specifier.startsWith('./') || specifier.startsWith('../')) {
|
||||
return new URL(specifier, baseURL);
|
||||
return tryURLParse(specifier, baseURL);
|
||||
}
|
||||
|
||||
const url = tryURLParse(specifier);
|
||||
|
||||
if (url === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasFetchScheme(url) || url.protocol === BUILT_IN_MODULE_PROTOCOL) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return null;
|
||||
return url;
|
||||
}
|
||||
|
||||
export function mergeImportMaps(mapA, mapB) {
|
||||
const mapAImports = mapA && mapA.imports ? mapA.imports : {};
|
||||
const mapBImports = mapB && mapB.imports ? mapB.imports : {};
|
||||
const mapAScopes = mapA && mapA.scopes ? mapA.scopes : {};
|
||||
const mapBScopes = mapB && mapB.scopes ? mapB.scopes : {};
|
||||
|
||||
return {
|
||||
imports: {
|
||||
...mapAImports,
|
||||
...mapBImports,
|
||||
},
|
||||
scopes: {
|
||||
...mapAScopes,
|
||||
...mapBScopes,
|
||||
},
|
||||
};
|
||||
}
|
||||
module.exports = { tryURLParse, tryURLLikeSpecifierParse };
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import chai from 'chai';
|
||||
import { parseFromString } from '../../src/parser.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
function testWarningHandler(expectedWarnings) {
|
||||
const warnings = [];
|
||||
const { warn } = console;
|
||||
console.warn = warning => {
|
||||
warnings.push(warning);
|
||||
};
|
||||
return () => {
|
||||
console.warn = warn;
|
||||
expect(warnings).to.deep.equal(expectedWarnings);
|
||||
};
|
||||
}
|
||||
|
||||
export function expectSpecifierMap(input, baseURL, output, warnings = []) {
|
||||
const checkWarnings1 = testWarningHandler(warnings);
|
||||
|
||||
expect(parseFromString(`{ "imports": ${input} }`, baseURL)).to.deep.equal({
|
||||
imports: output,
|
||||
scopes: {},
|
||||
});
|
||||
|
||||
checkWarnings1();
|
||||
|
||||
const checkWarnings2 = testWarningHandler(warnings);
|
||||
|
||||
expect(
|
||||
parseFromString(`{ "scopes": { "https://scope.example/": ${input} } }`, baseURL),
|
||||
).to.deep.equal({ imports: {}, scopes: { 'https://scope.example/': output } });
|
||||
|
||||
checkWarnings2();
|
||||
}
|
||||
|
||||
export function expectScopes(inputArray, baseURL, outputArray, warnings = []) {
|
||||
const checkWarnings = testWarningHandler(warnings);
|
||||
|
||||
const inputScopesAsStrings = inputArray.map(scopePrefix => `${JSON.stringify(scopePrefix)}: {}`);
|
||||
const inputString = `{ "scopes": { ${inputScopesAsStrings.join(', ')} } }`;
|
||||
|
||||
const outputScopesObject = {};
|
||||
for (const outputScopePrefix of outputArray) {
|
||||
outputScopesObject[outputScopePrefix] = {};
|
||||
}
|
||||
|
||||
expect(parseFromString(inputString, baseURL)).to.deep.equal({
|
||||
imports: {},
|
||||
scopes: outputScopesObject,
|
||||
});
|
||||
|
||||
checkWarnings();
|
||||
}
|
||||
|
||||
export function expectBad(input, baseURL, warnings = []) {
|
||||
const checkWarnings = testWarningHandler(warnings);
|
||||
expect(() => parseFromString(input, baseURL)).to.throw(TypeError);
|
||||
checkWarnings();
|
||||
}
|
||||
17
packages/import-maps-resolve/test/json/data-base-url.json
Normal file
17
packages/import-maps-resolve/test/json/data-base-url.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"foo/": "data:text/javascript,foo/"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"name": "data: base URL (?)",
|
||||
"tests": {
|
||||
"should favor the most-specific key": {
|
||||
"expectedResults": {
|
||||
"foo/bar": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
packages/import-maps-resolve/test/json/empty-import-map.json
Normal file
56
packages/import-maps-resolve/test/json/empty-import-map.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"importMap": {},
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"tests": {
|
||||
"valid relative specifiers": {
|
||||
"expectedResults": {
|
||||
"./foo": "https://example.com/js/foo",
|
||||
"./foo/bar": "https://example.com/js/foo/bar",
|
||||
"./foo/../bar": "https://example.com/js/bar",
|
||||
"./foo/../../bar": "https://example.com/bar",
|
||||
"../foo": "https://example.com/foo",
|
||||
"../foo/bar": "https://example.com/foo/bar",
|
||||
"../../../foo/bar": "https://example.com/foo/bar",
|
||||
"/foo": "https://example.com/foo",
|
||||
"/foo/bar": "https://example.com/foo/bar",
|
||||
"/../../foo/bar": "https://example.com/foo/bar",
|
||||
"/../foo/../bar": "https://example.com/bar"
|
||||
}
|
||||
},
|
||||
"fetch scheme absolute URLs": {
|
||||
"expectedResults": {
|
||||
"about:fetch-scheme": "about:fetch-scheme",
|
||||
"https://fetch-scheme.net": "https://fetch-scheme.net/",
|
||||
"https:fetch-scheme.org": "https://fetch-scheme.org/",
|
||||
"https://fetch%2Dscheme.com/": "https://fetch-scheme.com/",
|
||||
"https://///fetch-scheme.com///": "https://fetch-scheme.com///"
|
||||
}
|
||||
},
|
||||
"non-fetch scheme absolute URLs": {
|
||||
"expectedResults": {
|
||||
"mailto:non-fetch-scheme": "mailto:non-fetch-scheme",
|
||||
"import:non-fetch-scheme": "import:non-fetch-scheme",
|
||||
"javascript:non-fetch-scheme": "javascript:non-fetch-scheme",
|
||||
"wss:non-fetch-scheme": "wss://non-fetch-scheme/"
|
||||
}
|
||||
},
|
||||
"valid relative URLs that are invalid as specifiers should fail": {
|
||||
"expectedResults": {
|
||||
"invalid-specifier": null,
|
||||
"\\invalid-specifier": null,
|
||||
":invalid-specifier": null,
|
||||
"@invalid-specifier": null,
|
||||
"%2E/invalid-specifier": null,
|
||||
"%2E%2E/invalid-specifier": null,
|
||||
".%2Finvalid-specifier": null
|
||||
}
|
||||
},
|
||||
"invalid absolute URLs should fail": {
|
||||
"expectedResults": {
|
||||
"https://invalid-url.com:demo": null,
|
||||
"http://[invalid-url.com]/": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"name": "should favor the most-specific key",
|
||||
"tests": {
|
||||
"Overlapping entries with trailing slashes": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"a": "/1",
|
||||
"a/": "/2/",
|
||||
"a/b": "/3",
|
||||
"a/b/": "/4/"
|
||||
}
|
||||
},
|
||||
"expectedResults": {
|
||||
"a": "https://example.com/1",
|
||||
"a/": "https://example.com/2/",
|
||||
"a/x": "https://example.com/2/x",
|
||||
"a/b": "https://example.com/3",
|
||||
"a/b/": "https://example.com/4/",
|
||||
"a/b/c": "https://example.com/4/c"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"moment": "/node_modules/moment/src/moment.js",
|
||||
"moment/": "/node_modules/moment/src/",
|
||||
"lodash-dot": "./node_modules/lodash-es/lodash.js",
|
||||
"lodash-dot/": "./node_modules/lodash-es/",
|
||||
"lodash-dotdot": "../node_modules/lodash-es/lodash.js",
|
||||
"lodash-dotdot/": "../node_modules/lodash-es/"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"name": "Package-like scenarios",
|
||||
"link": "https://github.com/WICG/import-maps#packages-via-trailing-slashes",
|
||||
"tests": {
|
||||
"package main modules": {
|
||||
"expectedResults": {
|
||||
"moment": "https://example.com/node_modules/moment/src/moment.js",
|
||||
"lodash-dot": "https://example.com/app/node_modules/lodash-es/lodash.js",
|
||||
"lodash-dotdot": "https://example.com/node_modules/lodash-es/lodash.js"
|
||||
}
|
||||
},
|
||||
"package submodules": {
|
||||
"expectedResults": {
|
||||
"moment/foo": "https://example.com/node_modules/moment/src/foo",
|
||||
"lodash-dot/foo": "https://example.com/app/node_modules/lodash-es/foo",
|
||||
"lodash-dotdot/foo": "https://example.com/node_modules/lodash-es/foo"
|
||||
}
|
||||
},
|
||||
"package names that end in a slash should just pass through": {
|
||||
"expectedResults": {
|
||||
"moment/": "https://example.com/node_modules/moment/src/"
|
||||
}
|
||||
},
|
||||
"package modules that are not declared should fail": {
|
||||
"expectedResults": {
|
||||
"underscore/": null,
|
||||
"underscore/foo": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "Absolute URL addresses",
|
||||
"tests": {
|
||||
"should only accept absolute URL addresses with fetch schemes": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"about": "about:good",
|
||||
"blob": "blob:good",
|
||||
"data": "data:good",
|
||||
"file": "file:///good",
|
||||
"filesystem": "filesystem:http://example.com/good/",
|
||||
"http": "http://good/",
|
||||
"https": "https://good/",
|
||||
"ftp": "ftp://good/",
|
||||
"import": "import:bad",
|
||||
"mailto": "mailto:bad",
|
||||
"javascript": "javascript:bad",
|
||||
"wss": "wss:bad"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://base.example/path1/path2/path3",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"about": "about:good",
|
||||
"blob": "blob:good",
|
||||
"data": "data:good",
|
||||
"file": "file:///good",
|
||||
"filesystem": "filesystem:http://example.com/good/",
|
||||
"http": "http://good/",
|
||||
"https": "https://good/",
|
||||
"ftp": "ftp://good/",
|
||||
"import": "import:bad",
|
||||
"javascript": "javascript:bad",
|
||||
"mailto": "mailto:bad",
|
||||
"wss": "wss://bad/"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"should parse absolute URLs, ignoring unparseable ones": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"unparseable2": "https://example.com:demo",
|
||||
"unparseable3": "http://[www.example.com]/",
|
||||
"invalidButParseable1": "https:example.org",
|
||||
"invalidButParseable2": "https://///example.com///",
|
||||
"prettyNormal": "https://example.net",
|
||||
"percentDecoding": "https://ex%41mple.com/"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://base.example/path1/path2/path3",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"unparseable2": null,
|
||||
"unparseable3": null,
|
||||
"invalidButParseable1": "https://example.org/",
|
||||
"invalidButParseable2": "https://example.com///",
|
||||
"prettyNormal": "https://example.net/",
|
||||
"percentDecoding": "https://example.com/"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "Other invalid addresses",
|
||||
"tests": {
|
||||
"should ignore unprefixed strings that are not absolute URLs": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"foo1": "bar",
|
||||
"foo2": "\\bar",
|
||||
"foo3": "~bar",
|
||||
"foo4": "#bar",
|
||||
"foo5": "?bar"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://base.example/path1/path2/path3",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"foo1": null,
|
||||
"foo2": null,
|
||||
"foo3": null,
|
||||
"foo4": null,
|
||||
"foo5": null
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"name": "Relative URL-like addresses",
|
||||
"tests": {
|
||||
"should accept strings prefixed with ./, ../, or /": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"dotSlash": "./foo",
|
||||
"dotDotSlash": "../foo",
|
||||
"slash": "/foo"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://base.example/path1/path2/path3",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"dotSlash": "https://base.example/path1/path2/foo",
|
||||
"dotDotSlash": "https://base.example/path1/foo",
|
||||
"slash": "https://base.example/foo"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"should not accept strings prefixed with ./, ../, or / for data: base URLs": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"dotSlash": "./foo",
|
||||
"dotDotSlash": "../foo",
|
||||
"slash": "/foo"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "data:text/html,test",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"dotSlash": null,
|
||||
"dotDotSlash": null,
|
||||
"slash": null
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"should accept the literal strings ./, ../, or / with no suffix": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"dotSlash": "./",
|
||||
"dotDotSlash": "../",
|
||||
"slash": "/"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://base.example/path1/path2/path3",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"dotSlash": "https://base.example/path1/path2/",
|
||||
"dotDotSlash": "https://base.example/path1/",
|
||||
"slash": "https://base.example/"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"should ignore percent-encoded variants of ./, ../, or /": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"dotSlash1": "%2E/",
|
||||
"dotDotSlash1": "%2E%2E/",
|
||||
"dotSlash2": ".%2F",
|
||||
"dotDotSlash2": "..%2F",
|
||||
"slash2": "%2F",
|
||||
"dotSlash3": "%2E%2F",
|
||||
"dotDotSlash3": "%2E%2E%2F"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://base.example/path1/path2/path3",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"dotSlash1": null,
|
||||
"dotDotSlash1": null,
|
||||
"dotSlash2": null,
|
||||
"dotDotSlash2": null,
|
||||
"slash2": null,
|
||||
"dotSlash3": null,
|
||||
"dotDotSlash3": null
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Invalid JSON",
|
||||
"importMapBaseURL": "https://base.example/",
|
||||
"importMap": "{imports: {}}",
|
||||
"expectedParsedImportMap": null
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "Normalization",
|
||||
"importMapBaseURL": "https://base.example/",
|
||||
"tests": {
|
||||
"should normalize empty import maps to have imports and scopes keys": {
|
||||
"importMap": {},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"should normalize an import map without imports to have imports": {
|
||||
"importMap": {
|
||||
"scopes": {}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"should normalize an import map without scopes to have scopes": {
|
||||
"importMap": {
|
||||
"imports": {}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "Mismatching scopes schema",
|
||||
"importMapBaseURL": "https://base.example/",
|
||||
"tests": {
|
||||
"should throw if a scope's value is not an object": {
|
||||
"expectedParsedImportMap": null,
|
||||
"tests": {
|
||||
"null": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"https://example.com/": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"boolean": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"https://example.com/": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"https://example.com/": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"string": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"https://example.com/": "foo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"array": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"https://example.com/": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "Mismatching the specifier map schema",
|
||||
"importMapBaseURL": "https://base.example/",
|
||||
"tests": {
|
||||
"should ignore entries where the address is not a string": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"null": null,
|
||||
"boolean": true,
|
||||
"number": 1,
|
||||
"object": {},
|
||||
"array": [],
|
||||
"array2": [
|
||||
"https://example.com/"
|
||||
],
|
||||
"string": "https://example.com/"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"null": null,
|
||||
"boolean": null,
|
||||
"number": null,
|
||||
"object": null,
|
||||
"array": null,
|
||||
"array2": null,
|
||||
"string": "https://example.com/"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"should ignore entries where the specifier key is an empty string": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"": "https://example.com/"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"name": "Mismatching the top-level schema",
|
||||
"importMapBaseURL": "https://base.example/",
|
||||
"tests": {
|
||||
"should throw for top-level non-objects": {
|
||||
"expectedParsedImportMap": null,
|
||||
"tests": {
|
||||
"null": {
|
||||
"importMap": null
|
||||
},
|
||||
"boolean": {
|
||||
"importMap": true
|
||||
},
|
||||
"number": {
|
||||
"importMap": 1
|
||||
},
|
||||
"string": {
|
||||
"importMap": "foo"
|
||||
},
|
||||
"array": {
|
||||
"importMap": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"should throw if imports is a non-object": {
|
||||
"expectedParsedImportMap": null,
|
||||
"tests": {
|
||||
"null": {
|
||||
"importMap": {
|
||||
"imports": null
|
||||
}
|
||||
},
|
||||
"boolean": {
|
||||
"importMap": {
|
||||
"imports": true
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"importMap": {
|
||||
"imports": 1
|
||||
}
|
||||
},
|
||||
"string": {
|
||||
"importMap": {
|
||||
"imports": "foo"
|
||||
}
|
||||
},
|
||||
"array": {
|
||||
"importMap": {
|
||||
"imports": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"should throw if scopes is a non-object": {
|
||||
"expectedParsedImportMap": null,
|
||||
"tests": {
|
||||
"null": {
|
||||
"importMap": {
|
||||
"scopes": null
|
||||
}
|
||||
},
|
||||
"boolean": {
|
||||
"importMap": {
|
||||
"scopes": true
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"importMap": {
|
||||
"scopes": 1
|
||||
}
|
||||
},
|
||||
"string": {
|
||||
"importMap": {
|
||||
"scopes": "foo"
|
||||
}
|
||||
},
|
||||
"array": {
|
||||
"importMap": {
|
||||
"scopes": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"should ignore unspecified top-level entries": {
|
||||
"importMap": {
|
||||
"imports": {},
|
||||
"new-feature": {},
|
||||
"scops": {}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
191
packages/import-maps-resolve/test/json/parsing-scope-keys.json
Normal file
191
packages/import-maps-resolve/test/json/parsing-scope-keys.json
Normal file
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"importMapBaseURL": "https://base.example/path1/path2/path3",
|
||||
"tests": {
|
||||
"Relative URL scope keys should work with no prefix": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"foo": {}
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"https://base.example/path1/path2/foo": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Relative URL scope keys should work with ./, ../, and / prefixes": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"./foo": {},
|
||||
"../foo": {},
|
||||
"/foo": {}
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"https://base.example/path1/path2/foo": {},
|
||||
"https://base.example/path1/foo": {},
|
||||
"https://base.example/foo": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Absolute URL scope keys should ignore relative URL scope keys when the base URL is a data: URL": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"./foo": {},
|
||||
"../foo": {},
|
||||
"/foo": {}
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "data:text/html,test",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Relative URL scope keys should work with ./, ../, or / with no suffix": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"./": {},
|
||||
"../": {},
|
||||
"/": {}
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"https://base.example/path1/path2/": {},
|
||||
"https://base.example/path1/": {},
|
||||
"https://base.example/": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Relative URL scope keys should work with /s, ?s, and #s": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"foo/bar?baz#qux": {}
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"https://base.example/path1/path2/foo/bar?baz#qux": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Relative URL scope keys should work with an empty string scope key": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"": {}
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"https://base.example/path1/path2/path3": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Relative URL scope keys should work with / suffixes": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"foo/": {},
|
||||
"./foo/": {},
|
||||
"../foo/": {},
|
||||
"/foo/": {},
|
||||
"/foo//": {}
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"https://base.example/path1/path2/foo/": {},
|
||||
"https://base.example/path1/foo/": {},
|
||||
"https://base.example/foo/": {},
|
||||
"https://base.example/foo//": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Relative URL scope keys should deduplicate based on URL parsing rules": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"foo/\\": {
|
||||
"1": "./a"
|
||||
},
|
||||
"foo//": {
|
||||
"2": "./b"
|
||||
},
|
||||
"foo\\\\": {
|
||||
"3": "./c"
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"https://base.example/path1/path2/foo//": {
|
||||
"3": "https://base.example/path1/path2/c"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Absolute URL scope keys should accept all absolute URL scope keys, with or without fetch schemes": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"about:good": {},
|
||||
"blob:good": {},
|
||||
"data:good": {},
|
||||
"file:///good": {},
|
||||
"filesystem:http://example.com/good/": {},
|
||||
"http://good/": {},
|
||||
"https://good/": {},
|
||||
"ftp://good/": {},
|
||||
"import:bad": {},
|
||||
"mailto:bad": {},
|
||||
"javascript:bad": {},
|
||||
"wss:ba": {}
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"about:good": {},
|
||||
"blob:good": {},
|
||||
"data:good": {},
|
||||
"file:///good": {},
|
||||
"filesystem:http://example.com/good/": {},
|
||||
"http://good/": {},
|
||||
"https://good/": {},
|
||||
"ftp://good/": {},
|
||||
"import:bad": {},
|
||||
"mailto:bad": {},
|
||||
"javascript:bad": {},
|
||||
"wss://ba/": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Absolute URL scope keys should parse absolute URL scope keys, ignoring unparseable ones": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"https://example.com:demo": {},
|
||||
"http://[www.example.com]/": {},
|
||||
"https:example.org": {},
|
||||
"https://///example.com///": {},
|
||||
"https://example.net": {},
|
||||
"https://ex%41mple.com/foo/": {}
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"https://base.example/path1/path2/example.org": {},
|
||||
"https://example.com///": {},
|
||||
"https://example.net/": {},
|
||||
"https://example.com/foo/": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"importMapBaseURL": "https://base.example/path1/path2/path3",
|
||||
"tests": {
|
||||
"Relative URL specifier keys should absolutize strings prefixed with ./, ../, or / into the corresponding URLs": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"./foo": "/dotslash",
|
||||
"../foo": "/dotdotslash",
|
||||
"/foo": "/slash"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"https://base.example/path1/path2/foo": "https://base.example/dotslash",
|
||||
"https://base.example/path1/foo": "https://base.example/dotdotslash",
|
||||
"https://base.example/foo": "https://base.example/slash"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Relative URL specifier keys should not absolutize strings prefixed with ./, ../, or / with a data: URL base": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"./foo": "https://example.com/dotslash",
|
||||
"../foo": "https://example.com/dotdotslash",
|
||||
"/foo": "https://example.com/slash"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "data:text/html,",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"./foo": "https://example.com/dotslash",
|
||||
"../foo": "https://example.com/dotdotslash",
|
||||
"/foo": "https://example.com/slash"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Relative URL specifier keys should absolutize the literal strings ./, ../, or / with no suffix": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"./": "/dotslash/",
|
||||
"../": "/dotdotslash/",
|
||||
"/": "/slash/"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"https://base.example/path1/path2/": "https://base.example/dotslash/",
|
||||
"https://base.example/path1/": "https://base.example/dotdotslash/",
|
||||
"https://base.example/": "https://base.example/slash/"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Relative URL specifier keys should work with /s, ?s, and #s": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"./foo/bar?baz#qux": "/foo"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"https://base.example/path1/path2/foo/bar?baz#qux": "https://base.example/foo"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Relative URL specifier keys should ignore an empty string key": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"": "/foo"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Relative URL specifier keys should treat percent-encoded variants of ./, ../, or / as bare specifiers": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"%2E/": "/dotSlash1/",
|
||||
"%2E%2E/": "/dotDotSlash1/",
|
||||
".%2F": "/dotSlash2",
|
||||
"..%2F": "/dotDotSlash2",
|
||||
"%2F": "/slash2",
|
||||
"%2E%2F": "/dotSlash3",
|
||||
"%2E%2E%2F": "/dotDotSlash3"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"%2E/": "https://base.example/dotSlash1/",
|
||||
"%2E%2E/": "https://base.example/dotDotSlash1/",
|
||||
".%2F": "https://base.example/dotSlash2",
|
||||
"..%2F": "https://base.example/dotDotSlash2",
|
||||
"%2F": "https://base.example/slash2",
|
||||
"%2E%2F": "https://base.example/dotSlash3",
|
||||
"%2E%2E%2F": "https://base.example/dotDotSlash3"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Relative URL specifier keys should deduplicate based on URL parsing rules": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"./foo/\\": "/foo1",
|
||||
"./foo//": "/foo2",
|
||||
"./foo\\\\": "/foo3"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"https://base.example/path1/path2/foo//": "https://base.example/foo3"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Absolute URL specifier keys should accept all absolute URL specifier keys, with or without fetch schemes": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"about:good": "/about",
|
||||
"blob:good": "/blob",
|
||||
"data:good": "/data",
|
||||
"file:///good": "/file",
|
||||
"filesystem:http://example.com/good/": "/filesystem/",
|
||||
"http://good/": "/http/",
|
||||
"https://good/": "/https/",
|
||||
"ftp://good/": "/ftp/",
|
||||
"import:bad": "/import",
|
||||
"mailto:bad": "/mailto",
|
||||
"javascript:bad": "/javascript",
|
||||
"wss:bad": "/wss"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"about:good": "https://base.example/about",
|
||||
"blob:good": "https://base.example/blob",
|
||||
"data:good": "https://base.example/data",
|
||||
"file:///good": "https://base.example/file",
|
||||
"filesystem:http://example.com/good/": "https://base.example/filesystem/",
|
||||
"http://good/": "https://base.example/http/",
|
||||
"https://good/": "https://base.example/https/",
|
||||
"ftp://good/": "https://base.example/ftp/",
|
||||
"import:bad": "https://base.example/import",
|
||||
"mailto:bad": "https://base.example/mailto",
|
||||
"javascript:bad": "https://base.example/javascript",
|
||||
"wss://bad/": "https://base.example/wss"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Absolute URL specifier keys should parse absolute URLs, treating unparseable ones as bare specifiers": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"https://example.com:demo": "/unparseable2",
|
||||
"http://[www.example.com]/": "/unparseable3/",
|
||||
"https:example.org": "/invalidButParseable1/",
|
||||
"https://///example.com///": "/invalidButParseable2/",
|
||||
"https://example.net": "/prettyNormal/",
|
||||
"https://ex%41mple.com/": "/percentDecoding/"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"https://example.com:demo": "https://base.example/unparseable2",
|
||||
"http://[www.example.com]/": "https://base.example/unparseable3/",
|
||||
"https://example.org/": "https://base.example/invalidButParseable1/",
|
||||
"https://example.com///": "https://base.example/invalidButParseable2/",
|
||||
"https://example.net/": "https://base.example/prettyNormal/",
|
||||
"https://example.com/": "https://base.example/percentDecoding/"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Specifier keys should be sort correctly (issue #181) - Test #1": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"https://example.com/aaa": "https://example.com/aaa",
|
||||
"https://example.com/a": "https://example.com/a"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"https://example.com/aaa": "https://example.com/aaa",
|
||||
"https://example.com/a": "https://example.com/a"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
},
|
||||
"Specifier keys should be sort correctly (issue #181) - Test #2": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"https://example.com/a": "https://example.com/a",
|
||||
"https://example.com/aaa": "https://example.com/aaa"
|
||||
}
|
||||
},
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"https://example.com/aaa": "https://example.com/aaa",
|
||||
"https://example.com/a": "https://example.com/a"
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Failing addresses: mismatched trailing slashes",
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"trailer/": "/notrailer"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://base.example/path1/path2/path3",
|
||||
"expectedParsedImportMap": {
|
||||
"imports": {
|
||||
"trailer/": null
|
||||
},
|
||||
"scopes": {}
|
||||
}
|
||||
}
|
||||
82
packages/import-maps-resolve/test/json/resolving-null.json
Normal file
82
packages/import-maps-resolve/test/json/resolving-null.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"name": "Entries with errors shouldn't allow fallback",
|
||||
"tests": {
|
||||
"No fallback to less-specific prefixes": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"null/": "/1/",
|
||||
"null/b/": null,
|
||||
"null/b/c/": "/1/2/",
|
||||
"invalid-url/": "/1/",
|
||||
"invalid-url/b/": "https://:invalid-url:/",
|
||||
"invalid-url/b/c/": "/1/2/",
|
||||
"without-trailing-slashes/": "/1/",
|
||||
"without-trailing-slashes/b/": "/x",
|
||||
"without-trailing-slashes/b/c/": "/1/2/",
|
||||
"prefix-resolution-error/": "/1/",
|
||||
"prefix-resolution-error/b/": "data:text/javascript,/",
|
||||
"prefix-resolution-error/b/c/": "/1/2/"
|
||||
}
|
||||
},
|
||||
"expectedResults": {
|
||||
"null/x": "https://example.com/1/x",
|
||||
"null/b/x": null,
|
||||
"null/b/c/x": "https://example.com/1/2/x",
|
||||
"invalid-url/x": "https://example.com/1/x",
|
||||
"invalid-url/b/x": null,
|
||||
"invalid-url/b/c/x": "https://example.com/1/2/x",
|
||||
"without-trailing-slashes/x": "https://example.com/1/x",
|
||||
"without-trailing-slashes/b/x": null,
|
||||
"without-trailing-slashes/b/c/x": "https://example.com/1/2/x",
|
||||
"prefix-resolution-error/x": "https://example.com/1/x",
|
||||
"prefix-resolution-error/b/x": null,
|
||||
"prefix-resolution-error/b/c/x": "https://example.com/1/2/x"
|
||||
}
|
||||
},
|
||||
"No fallback to less-specific scopes": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"null": "https://example.com/a",
|
||||
"invalid-url": "https://example.com/b",
|
||||
"without-trailing-slashes/": "https://example.com/c/",
|
||||
"prefix-resolution-error/": "https://example.com/d/"
|
||||
},
|
||||
"scopes": {
|
||||
"/js/": {
|
||||
"null": null,
|
||||
"invalid-url": "https://:invalid-url:/",
|
||||
"without-trailing-slashes/": "/x",
|
||||
"prefix-resolution-error/": "data:text/javascript,/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedResults": {
|
||||
"null": null,
|
||||
"invalid-url": null,
|
||||
"without-trailing-slashes/x": null,
|
||||
"prefix-resolution-error/x": null
|
||||
}
|
||||
},
|
||||
"No fallback to absolute URL parsing": {
|
||||
"importMap": {
|
||||
"imports": {},
|
||||
"scopes": {
|
||||
"/js/": {
|
||||
"https://example.com/null": null,
|
||||
"https://example.com/invalid-url": "https://:invalid-url:/",
|
||||
"https://example.com/without-trailing-slashes/": "/x",
|
||||
"https://example.com/prefix-resolution-error/": "data:text/javascript,/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"expectedResults": {
|
||||
"https://example.com/null": null,
|
||||
"https://example.com/invalid-url": null,
|
||||
"https://example.com/without-trailing-slashes/x": null,
|
||||
"https://example.com/prefix-resolution-error/x": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"name": "Exact vs. prefix based matching",
|
||||
"details": "Scopes are matched with base URLs that are exactly the same or subpaths under the scopes with trailing shashes",
|
||||
"link": "https://wicg.github.io/import-maps/#resolve-a-module-specifier Step 8.1",
|
||||
"tests": {
|
||||
"Scope without trailing slash only": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"/js": {
|
||||
"moment": "/only-triggered-by-exact/moment",
|
||||
"moment/": "/only-triggered-by-exact/moment/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"tests": {
|
||||
"Non-trailing-slash base URL (exact match)": {
|
||||
"baseURL": "https://example.com/js",
|
||||
"expectedResults": {
|
||||
"moment": "https://example.com/only-triggered-by-exact/moment",
|
||||
"moment/foo": "https://example.com/only-triggered-by-exact/moment/foo"
|
||||
}
|
||||
},
|
||||
"Trailing-slash base URL (fail)": {
|
||||
"baseURL": "https://example.com/js/",
|
||||
"expectedResults": {
|
||||
"moment": null,
|
||||
"moment/foo": null
|
||||
}
|
||||
},
|
||||
"Subpath base URL (fail)": {
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"expectedResults": {
|
||||
"moment": null,
|
||||
"moment/foo": null
|
||||
}
|
||||
},
|
||||
"Non-subpath base URL (fail)": {
|
||||
"baseURL": "https://example.com/jsiscool",
|
||||
"expectedResults": {
|
||||
"moment": null,
|
||||
"moment/foo": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scope with trailing slash only": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"/js/": {
|
||||
"moment": "/triggered-by-any-subpath/moment",
|
||||
"moment/": "/triggered-by-any-subpath/moment/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"tests": {
|
||||
"Non-trailing-slash base URL (fail)": {
|
||||
"baseURL": "https://example.com/js",
|
||||
"expectedResults": {
|
||||
"moment": null,
|
||||
"moment/foo": null
|
||||
}
|
||||
},
|
||||
"Trailing-slash base URL (exact match)": {
|
||||
"baseURL": "https://example.com/js/",
|
||||
"expectedResults": {
|
||||
"moment": "https://example.com/triggered-by-any-subpath/moment",
|
||||
"moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo"
|
||||
}
|
||||
},
|
||||
"Subpath base URL (prefix match)": {
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"expectedResults": {
|
||||
"moment": "https://example.com/triggered-by-any-subpath/moment",
|
||||
"moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo"
|
||||
}
|
||||
},
|
||||
"Non-subpath base URL (fail)": {
|
||||
"baseURL": "https://example.com/jsiscool",
|
||||
"expectedResults": {
|
||||
"moment": null,
|
||||
"moment/foo": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Scopes with and without trailing slash": {
|
||||
"importMap": {
|
||||
"scopes": {
|
||||
"/js": {
|
||||
"moment": "/only-triggered-by-exact/moment",
|
||||
"moment/": "/only-triggered-by-exact/moment/"
|
||||
},
|
||||
"/js/": {
|
||||
"moment": "/triggered-by-any-subpath/moment",
|
||||
"moment/": "/triggered-by-any-subpath/moment/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"tests": {
|
||||
"Non-trailing-slash base URL (exact match)": {
|
||||
"baseURL": "https://example.com/js",
|
||||
"expectedResults": {
|
||||
"moment": "https://example.com/only-triggered-by-exact/moment",
|
||||
"moment/foo": "https://example.com/only-triggered-by-exact/moment/foo"
|
||||
}
|
||||
},
|
||||
"Trailing-slash base URL (exact match)": {
|
||||
"baseURL": "https://example.com/js/",
|
||||
"expectedResults": {
|
||||
"moment": "https://example.com/triggered-by-any-subpath/moment",
|
||||
"moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo"
|
||||
}
|
||||
},
|
||||
"Subpath base URL (prefix match)": {
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"expectedResults": {
|
||||
"moment": "https://example.com/triggered-by-any-subpath/moment",
|
||||
"moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo"
|
||||
}
|
||||
},
|
||||
"Non-subpath base URL (fail)": {
|
||||
"baseURL": "https://example.com/jsiscool",
|
||||
"expectedResults": {
|
||||
"moment": null,
|
||||
"moment/foo": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
packages/import-maps-resolve/test/json/scopes.json
Normal file
171
packages/import-maps-resolve/test/json/scopes.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"tests": {
|
||||
"Fallback to toplevel and between scopes": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"a": "/a-1.mjs",
|
||||
"b": "/b-1.mjs",
|
||||
"c": "/c-1.mjs",
|
||||
"d": "/d-1.mjs"
|
||||
},
|
||||
"scopes": {
|
||||
"/scope2/": {
|
||||
"a": "/a-2.mjs",
|
||||
"d": "/d-2.mjs"
|
||||
},
|
||||
"/scope2/scope3/": {
|
||||
"b": "/b-3.mjs",
|
||||
"d": "/d-3.mjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"should fall back to `imports` when no scopes match": {
|
||||
"baseURL": "https://example.com/scope1/foo.mjs",
|
||||
"expectedResults": {
|
||||
"a": "https://example.com/a-1.mjs",
|
||||
"b": "https://example.com/b-1.mjs",
|
||||
"c": "https://example.com/c-1.mjs",
|
||||
"d": "https://example.com/d-1.mjs"
|
||||
}
|
||||
},
|
||||
"should use a direct scope override": {
|
||||
"baseURL": "https://example.com/scope2/foo.mjs",
|
||||
"expectedResults": {
|
||||
"a": "https://example.com/a-2.mjs",
|
||||
"b": "https://example.com/b-1.mjs",
|
||||
"c": "https://example.com/c-1.mjs",
|
||||
"d": "https://example.com/d-2.mjs"
|
||||
}
|
||||
},
|
||||
"should use an indirect scope override": {
|
||||
"baseURL": "https://example.com/scope2/scope3/foo.mjs",
|
||||
"expectedResults": {
|
||||
"a": "https://example.com/a-2.mjs",
|
||||
"b": "https://example.com/b-3.mjs",
|
||||
"c": "https://example.com/c-1.mjs",
|
||||
"d": "https://example.com/d-3.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Relative URL scope keys": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"a": "/a-1.mjs",
|
||||
"b": "/b-1.mjs",
|
||||
"c": "/c-1.mjs"
|
||||
},
|
||||
"scopes": {
|
||||
"": {
|
||||
"a": "/a-empty-string.mjs"
|
||||
},
|
||||
"./": {
|
||||
"b": "/b-dot-slash.mjs"
|
||||
},
|
||||
"../": {
|
||||
"c": "/c-dot-dot-slash.mjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"An empty string scope is a scope with import map base URL": {
|
||||
"baseURL": "https://example.com/app/index.html",
|
||||
"expectedResults": {
|
||||
"a": "https://example.com/a-empty-string.mjs",
|
||||
"b": "https://example.com/b-dot-slash.mjs",
|
||||
"c": "https://example.com/c-dot-dot-slash.mjs"
|
||||
}
|
||||
},
|
||||
"'./' scope is a scope with import map base URL's directory": {
|
||||
"baseURL": "https://example.com/app/foo.mjs",
|
||||
"expectedResults": {
|
||||
"a": "https://example.com/a-1.mjs",
|
||||
"b": "https://example.com/b-dot-slash.mjs",
|
||||
"c": "https://example.com/c-dot-dot-slash.mjs"
|
||||
}
|
||||
},
|
||||
"'../' scope is a scope with import map base URL's parent directory": {
|
||||
"baseURL": "https://example.com/foo.mjs",
|
||||
"expectedResults": {
|
||||
"a": "https://example.com/a-1.mjs",
|
||||
"b": "https://example.com/b-1.mjs",
|
||||
"c": "https://example.com/c-dot-dot-slash.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Package-like scenarios": {
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"moment": "/node_modules/moment/src/moment.js",
|
||||
"moment/": "/node_modules/moment/src/",
|
||||
"lodash-dot": "./node_modules/lodash-es/lodash.js",
|
||||
"lodash-dot/": "./node_modules/lodash-es/",
|
||||
"lodash-dotdot": "../node_modules/lodash-es/lodash.js",
|
||||
"lodash-dotdot/": "../node_modules/lodash-es/"
|
||||
},
|
||||
"scopes": {
|
||||
"/": {
|
||||
"moment": "/node_modules_3/moment/src/moment.js",
|
||||
"vue": "/node_modules_3/vue/dist/vue.runtime.esm.js"
|
||||
},
|
||||
"/js/": {
|
||||
"lodash-dot": "./node_modules_2/lodash-es/lodash.js",
|
||||
"lodash-dot/": "./node_modules_2/lodash-es/",
|
||||
"lodash-dotdot": "../node_modules_2/lodash-es/lodash.js",
|
||||
"lodash-dotdot/": "../node_modules_2/lodash-es/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"Base URLs inside the scope should use the scope if the scope has matching keys": {
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"expectedResults": {
|
||||
"lodash-dot": "https://example.com/app/node_modules_2/lodash-es/lodash.js",
|
||||
"lodash-dot/foo": "https://example.com/app/node_modules_2/lodash-es/foo",
|
||||
"lodash-dotdot": "https://example.com/node_modules_2/lodash-es/lodash.js",
|
||||
"lodash-dotdot/foo": "https://example.com/node_modules_2/lodash-es/foo"
|
||||
}
|
||||
},
|
||||
"Base URLs inside the scope fallback to less specific scope": {
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"expectedResults": {
|
||||
"moment": "https://example.com/node_modules_3/moment/src/moment.js",
|
||||
"vue": "https://example.com/node_modules_3/vue/dist/vue.runtime.esm.js"
|
||||
}
|
||||
},
|
||||
"Base URLs inside the scope fallback to toplevel": {
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"expectedResults": {
|
||||
"moment/foo": "https://example.com/node_modules/moment/src/foo"
|
||||
}
|
||||
},
|
||||
"Base URLs outside a scope shouldn't use the scope even if the scope has matching keys": {
|
||||
"baseURL": "https://example.com/app.mjs",
|
||||
"expectedResults": {
|
||||
"lodash-dot": "https://example.com/app/node_modules/lodash-es/lodash.js",
|
||||
"lodash-dotdot": "https://example.com/node_modules/lodash-es/lodash.js",
|
||||
"lodash-dot/foo": "https://example.com/app/node_modules/lodash-es/foo",
|
||||
"lodash-dotdot/foo": "https://example.com/node_modules/lodash-es/foo"
|
||||
}
|
||||
},
|
||||
"Fallback to toplevel or not, depending on trailing slash match": {
|
||||
"baseURL": "https://example.com/app.mjs",
|
||||
"expectedResults": {
|
||||
"moment": "https://example.com/node_modules_3/moment/src/moment.js",
|
||||
"moment/foo": "https://example.com/node_modules/moment/src/foo"
|
||||
}
|
||||
},
|
||||
"should still fail for package-like specifiers that are not declared": {
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"expectedResults": {
|
||||
"underscore/": null,
|
||||
"underscore/foo": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"package/withslash": "/node_modules/package-with-slash/index.mjs",
|
||||
"not-a-package": "/lib/not-a-package.mjs",
|
||||
"only-slash/": "/lib/only-slash/",
|
||||
".": "/lib/dot.mjs",
|
||||
"..": "/lib/dotdot.mjs",
|
||||
"..\\": "/lib/dotdotbackslash.mjs",
|
||||
"%2E": "/lib/percent2e.mjs",
|
||||
"%2F": "/lib/percent2f.mjs"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"name": "Tricky specifiers",
|
||||
"tests": {
|
||||
"explicitly-mapped specifiers that happen to have a slash": {
|
||||
"expectedResults": {
|
||||
"package/withslash": "https://example.com/node_modules/package-with-slash/index.mjs"
|
||||
}
|
||||
},
|
||||
"specifier with punctuation": {
|
||||
"expectedResults": {
|
||||
".": "https://example.com/lib/dot.mjs",
|
||||
"..": "https://example.com/lib/dotdot.mjs",
|
||||
"..\\": "https://example.com/lib/dotdotbackslash.mjs",
|
||||
"%2E": "https://example.com/lib/percent2e.mjs",
|
||||
"%2F": "https://example.com/lib/percent2f.mjs"
|
||||
}
|
||||
},
|
||||
"submodule of something not declared with a trailing slash should fail": {
|
||||
"expectedResults": {
|
||||
"not-a-package/foo": null
|
||||
}
|
||||
},
|
||||
"module for which only a trailing-slash version is present should fail": {
|
||||
"expectedResults": {
|
||||
"only-slash": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
packages/import-maps-resolve/test/json/url-specifiers.json
Normal file
52
packages/import-maps-resolve/test/json/url-specifiers.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"importMap": {
|
||||
"imports": {
|
||||
"/lib/foo.mjs": "./more/bar.mjs",
|
||||
"./dotrelative/foo.mjs": "/lib/dot.mjs",
|
||||
"../dotdotrelative/foo.mjs": "/lib/dotdot.mjs",
|
||||
"/": "/lib/slash-only/",
|
||||
"./": "/lib/dotslash-only/",
|
||||
"/test/": "/lib/url-trailing-slash/",
|
||||
"./test/": "/lib/url-trailing-slash-dot/",
|
||||
"/test": "/lib/test1.mjs",
|
||||
"../test": "/lib/test2.mjs"
|
||||
}
|
||||
},
|
||||
"importMapBaseURL": "https://example.com/app/index.html",
|
||||
"baseURL": "https://example.com/js/app.mjs",
|
||||
"name": "URL-like specifiers",
|
||||
"tests": {
|
||||
"Ordinal URL-like specifiers": {
|
||||
"expectedResults": {
|
||||
"https://example.com/lib/foo.mjs": "https://example.com/app/more/bar.mjs",
|
||||
"https://///example.com/lib/foo.mjs": "https://example.com/app/more/bar.mjs",
|
||||
"/lib/foo.mjs": "https://example.com/app/more/bar.mjs",
|
||||
"https://example.com/app/dotrelative/foo.mjs": "https://example.com/lib/dot.mjs",
|
||||
"../app/dotrelative/foo.mjs": "https://example.com/lib/dot.mjs",
|
||||
"https://example.com/dotdotrelative/foo.mjs": "https://example.com/lib/dotdot.mjs",
|
||||
"../dotdotrelative/foo.mjs": "https://example.com/lib/dotdot.mjs"
|
||||
}
|
||||
},
|
||||
"Import map entries just composed from / and .": {
|
||||
"expectedResults": {
|
||||
"https://example.com/": "https://example.com/lib/slash-only/",
|
||||
"/": "https://example.com/lib/slash-only/",
|
||||
"../": "https://example.com/lib/slash-only/",
|
||||
"https://example.com/app/": "https://example.com/lib/dotslash-only/",
|
||||
"/app/": "https://example.com/lib/dotslash-only/",
|
||||
"../app/": "https://example.com/lib/dotslash-only/"
|
||||
}
|
||||
},
|
||||
"prefix-matched by keys with trailing slashes": {
|
||||
"expectedResults": {
|
||||
"/test/foo.mjs": "https://example.com/lib/url-trailing-slash/foo.mjs",
|
||||
"https://example.com/app/test/foo.mjs": "https://example.com/lib/url-trailing-slash-dot/foo.mjs"
|
||||
}
|
||||
},
|
||||
"should use the last entry's address when URL-like specifiers parse to the same absolute URL": {
|
||||
"expectedResults": {
|
||||
"/test": "https://example.com/lib/test2.mjs"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import chai from 'chai';
|
||||
import { mergeImportMaps } from '../src/index.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
describe('mergeImportMaps', () => {
|
||||
it('always has at least imports and scopes', () => {
|
||||
expect(mergeImportMaps({}, null)).to.deep.equal({
|
||||
imports: {},
|
||||
scopes: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('merges imports', () => {
|
||||
const mapA = { imports: { foo: '/to/foo.js' } };
|
||||
const mapB = { imports: { bar: '/to/bar.js' } };
|
||||
|
||||
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
|
||||
imports: {
|
||||
foo: '/to/foo.js',
|
||||
bar: '/to/bar.js',
|
||||
},
|
||||
scopes: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('merges scopes', () => {
|
||||
const mapA = { scopes: { '/path/to/foo/': { foo: '/to/foo.js' } } };
|
||||
const mapB = { scopes: { '/path/to/bar/': { foo: '/to/bar.js' } } };
|
||||
|
||||
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
|
||||
imports: {},
|
||||
scopes: {
|
||||
'/path/to/foo/': { foo: '/to/foo.js' },
|
||||
'/path/to/bar/': { foo: '/to/bar.js' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('removes unknown keys', () => {
|
||||
const mapA = { imports: { foo: '/to/foo.js' } };
|
||||
const mapB = { imp: { bar: '/to/bar.js' } };
|
||||
|
||||
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
|
||||
imports: {
|
||||
foo: '/to/foo.js',
|
||||
},
|
||||
scopes: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('overrides keys of imports and scopes of first map with second', () => {
|
||||
const mapA = { imports: { foo: '/to/foo.js' } };
|
||||
const mapB = { imports: { foo: '/to/fooOverride.js' } };
|
||||
|
||||
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
|
||||
imports: {
|
||||
foo: '/to/fooOverride.js',
|
||||
},
|
||||
scopes: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not introspect the scopes so with a conflict the last one wins', () => {
|
||||
const mapA = { scopes: { '/path/to/foo/': { foo: '/to/foo.js' } } };
|
||||
const mapB = { scopes: { '/path/to/foo/': { foo: '/to/fooOverride.js' } } };
|
||||
|
||||
expect(mergeImportMaps(mapA, mapB)).to.deep.equal({
|
||||
imports: {},
|
||||
scopes: {
|
||||
'/path/to/foo/': { foo: '/to/fooOverride.js' },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,292 +0,0 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { expectSpecifierMap } from './helpers/parsing.js';
|
||||
import { BUILT_IN_MODULE_SCHEME } from '../src/utils.js';
|
||||
|
||||
describe('Relative URL-like addresses', () => {
|
||||
it('should accept strings prefixed with ./, ../, or /', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"dotSlash": "./foo",
|
||||
"dotDotSlash": "../foo",
|
||||
"slash": "/foo"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
dotSlash: ['https://base.example/path1/path2/foo'],
|
||||
dotDotSlash: ['https://base.example/path1/foo'],
|
||||
slash: ['https://base.example/foo'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept the literal strings ./, ../, or / with no suffix', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"dotSlash": "./",
|
||||
"dotDotSlash": "../",
|
||||
"slash": "/"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
dotSlash: ['https://base.example/path1/path2/'],
|
||||
dotDotSlash: ['https://base.example/path1/'],
|
||||
slash: ['https://base.example/'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore percent-encoded variants of ./, ../, or /', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"dotSlash1": "%2E/",
|
||||
"dotDotSlash1": "%2E%2E/",
|
||||
"dotSlash2": ".%2F",
|
||||
"dotDotSlash2": "..%2F",
|
||||
"slash2": "%2F",
|
||||
"dotSlash3": "%2E%2F",
|
||||
"dotDotSlash3": "%2E%2E%2F"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
dotSlash1: [],
|
||||
dotDotSlash1: [],
|
||||
dotSlash2: [],
|
||||
dotDotSlash2: [],
|
||||
slash2: [],
|
||||
dotSlash3: [],
|
||||
dotDotSlash3: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Built-in module addresses', () => {
|
||||
it('should accept URLs using the built-in module scheme', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"foo": "${BUILT_IN_MODULE_SCHEME}:foo"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
foo: [`${BUILT_IN_MODULE_SCHEME}:foo`],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore percent-encoded variants of the built-in module scheme', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"foo": "${encodeURIComponent(`${BUILT_IN_MODULE_SCHEME}:`)}foo"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
foo: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore and warn on built-in module URLs that contain "/"', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"bad1": "${BUILT_IN_MODULE_SCHEME}:foo/",
|
||||
"bad2": "${BUILT_IN_MODULE_SCHEME}:foo/bar",
|
||||
"good": "${BUILT_IN_MODULE_SCHEME}:foo\\\\baz"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
bad1: [],
|
||||
bad2: [],
|
||||
good: [`${BUILT_IN_MODULE_SCHEME}:foo\\baz`],
|
||||
},
|
||||
[
|
||||
`Invalid target address "${BUILT_IN_MODULE_SCHEME}:foo/". Built-in module URLs must not contain "/".`,
|
||||
`Invalid target address "${BUILT_IN_MODULE_SCHEME}:foo/bar". Built-in module URLs must not contain "/".`,
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Absolute URL addresses', () => {
|
||||
it('should only accept absolute URL addresses with fetch schemes', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"about": "about:good",
|
||||
"blob": "blob:good",
|
||||
"data": "data:good",
|
||||
"file": "file:///good",
|
||||
"filesystem": "filesystem:good",
|
||||
"http": "http://good/",
|
||||
"https": "https://good/",
|
||||
"ftp": "ftp://good/",
|
||||
"import": "import:bad",
|
||||
"mailto": "mailto:bad",
|
||||
"javascript": "javascript:bad",
|
||||
"wss": "wss:bad"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
about: ['about:good'],
|
||||
blob: ['blob:good'],
|
||||
data: ['data:good'],
|
||||
file: ['file:///good'],
|
||||
filesystem: ['filesystem:good'],
|
||||
http: ['http://good/'],
|
||||
https: ['https://good/'],
|
||||
ftp: ['ftp://good/'],
|
||||
import: [],
|
||||
mailto: [],
|
||||
javascript: [],
|
||||
wss: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should only accept absolute URL addresses with fetch schemes inside arrays', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"about": ["about:good"],
|
||||
"blob": ["blob:good"],
|
||||
"data": ["data:good"],
|
||||
"file": ["file:///good"],
|
||||
"filesystem": ["filesystem:good"],
|
||||
"http": ["http://good/"],
|
||||
"https": ["https://good/"],
|
||||
"ftp": ["ftp://good/"],
|
||||
"import": ["import:bad"],
|
||||
"mailto": ["mailto:bad"],
|
||||
"javascript": ["javascript:bad"],
|
||||
"wss": ["wss:bad"]
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
about: ['about:good'],
|
||||
blob: ['blob:good'],
|
||||
data: ['data:good'],
|
||||
file: ['file:///good'],
|
||||
filesystem: ['filesystem:good'],
|
||||
http: ['http://good/'],
|
||||
https: ['https://good/'],
|
||||
ftp: ['ftp://good/'],
|
||||
import: [],
|
||||
mailto: [],
|
||||
javascript: [],
|
||||
wss: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse absolute URLs, ignoring unparseable ones', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"unparseable1": "https://ex ample.org/",
|
||||
"unparseable2": "https://example.com:demo",
|
||||
"unparseable3": "http://[www.example.com]/",
|
||||
"invalidButParseable1": "https:example.org",
|
||||
"invalidButParseable2": "https://///example.com///",
|
||||
"prettyNormal": "https://example.net",
|
||||
"percentDecoding": "https://ex%41mple.com/",
|
||||
"noPercentDecoding": "https://example.com/%41"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
unparseable1: [],
|
||||
unparseable2: [],
|
||||
unparseable3: [],
|
||||
invalidButParseable1: ['https://example.org/'],
|
||||
invalidButParseable2: ['https://example.com///'],
|
||||
prettyNormal: ['https://example.net/'],
|
||||
percentDecoding: ['https://example.com/'],
|
||||
noPercentDecoding: ['https://example.com/%41'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse absolute URLs, ignoring unparseable ones inside arrays', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"unparseable1": ["https://ex ample.org/"],
|
||||
"unparseable2": ["https://example.com:demo"],
|
||||
"unparseable3": ["http://[www.example.com]/"],
|
||||
"invalidButParseable1": ["https:example.org"],
|
||||
"invalidButParseable2": ["https://///example.com///"],
|
||||
"prettyNormal": ["https://example.net"],
|
||||
"percentDecoding": ["https://ex%41mple.com/"],
|
||||
"noPercentDecoding": ["https://example.com/%41"]
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
unparseable1: [],
|
||||
unparseable2: [],
|
||||
unparseable3: [],
|
||||
invalidButParseable1: ['https://example.org/'],
|
||||
invalidButParseable2: ['https://example.com///'],
|
||||
prettyNormal: ['https://example.net/'],
|
||||
percentDecoding: ['https://example.com/'],
|
||||
noPercentDecoding: ['https://example.com/%41'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Failing addresses: mismatched trailing slashes', () => {
|
||||
it('should warn for the simple case', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"trailer/": "/notrailer"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
'trailer/': [],
|
||||
},
|
||||
[
|
||||
`Invalid target address "https://base.example/notrailer" for package specifier "trailer/". Package address targets must end with "/".`,
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn for a mismatch alone in an array', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"trailer/": ["/notrailer"]
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
'trailer/': [],
|
||||
},
|
||||
[
|
||||
`Invalid target address "https://base.example/notrailer" for package specifier "trailer/". Package address targets must end with "/".`,
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn for a mismatch alongside non-mismatches in an array', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"trailer/": ["/atrailer/", "/notrailer"]
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
'trailer/': ['https://base.example/atrailer/'],
|
||||
},
|
||||
[
|
||||
`Invalid target address "https://base.example/notrailer" for package specifier "trailer/". Package address targets must end with "/".`,
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Other invalid addresses', () => {
|
||||
it('should ignore unprefixed strings that are not absolute URLs', () => {
|
||||
for (const bad of ['bar', '\\bar', '~bar', '#bar', '?bar']) {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"foo": "${bad}"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
foo: [],
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { expectSpecifierMap } from './helpers/parsing.js';
|
||||
// import { BUILT_IN_MODULE_SCHEME } from '../src/utils.js';
|
||||
|
||||
describe('Relative node addresses', () => {
|
||||
it('should accept strings prefixed with ./, ../, or /', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"dotSlash": "./foo",
|
||||
"dotDotSlash": "../foo",
|
||||
"slash": "/foo"
|
||||
}`,
|
||||
'/home/foo/project-a::/path1/path2/path3',
|
||||
{
|
||||
dotSlash: ['/home/foo/project-a/path1/path2/foo'],
|
||||
dotDotSlash: ['/home/foo/project-a/path1/foo'],
|
||||
slash: ['/home/foo/project-a/foo'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept the literal strings ./, ../, or / with no suffix', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"dotSlash": "./",
|
||||
"dotDotSlash": "../",
|
||||
"slash": "/"
|
||||
}`,
|
||||
'/home/foo/project-a::/path1/path2/path3',
|
||||
{
|
||||
dotSlash: ['/home/foo/project-a/path1/path2/'],
|
||||
dotDotSlash: ['/home/foo/project-a/path1/'],
|
||||
slash: ['/home/foo/project-a/'],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,135 +0,0 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import chai from 'chai';
|
||||
import { parseFromString } from '../src/parser.js';
|
||||
import { expectBad, expectSpecifierMap } from './helpers/parsing.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
const nonObjectStrings = ['null', 'true', '1', '"foo"', '[]'];
|
||||
|
||||
// test('Invalid JSON', () => {
|
||||
// expect(() => parseFromString('{ imports: {} }', 'https://base.example/')).toThrow(SyntaxError);
|
||||
// });
|
||||
|
||||
describe('Mismatching the top-level schema', () => {
|
||||
it('should throw for top-level non-objects', () => {
|
||||
for (const nonObject of nonObjectStrings) {
|
||||
expectBad(nonObject, 'https://base.example/');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw if imports is a non-object', () => {
|
||||
for (const nonObject of nonObjectStrings) {
|
||||
expectBad(`{ "imports": ${nonObject} }`, 'https://base.example/');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw if scopes is a non-object', () => {
|
||||
for (const nonObject of nonObjectStrings) {
|
||||
expectBad(`{ "scopes": ${nonObject} }`, 'https://base.example/');
|
||||
}
|
||||
});
|
||||
|
||||
it('should ignore unspecified top-level entries', () => {
|
||||
expect(
|
||||
parseFromString(
|
||||
`{
|
||||
"imports": {},
|
||||
"new-feature": {}
|
||||
}`,
|
||||
'https://base.example/',
|
||||
),
|
||||
).to.deep.equal({ imports: {}, scopes: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mismatching the specifier map schema', () => {
|
||||
const invalidAddressStrings = ['true', '1', '{}'];
|
||||
const invalidInsideArrayStrings = ['null', 'true', '1', '{}', '[]'];
|
||||
|
||||
it('should ignore entries where the address is not a string, array, or null', () => {
|
||||
for (const invalid of invalidAddressStrings) {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"foo": ${invalid},
|
||||
"bar": ["https://example.com/"]
|
||||
}`,
|
||||
'https://base.example/',
|
||||
{
|
||||
bar: ['https://example.com/'],
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should ignore entries where the specifier key is an empty string', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"": ["https://example.com/"]
|
||||
}`,
|
||||
'https://base.example/',
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore members of an address array that are not strings', () => {
|
||||
for (const invalid of invalidInsideArrayStrings) {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"foo": ["https://example.com/", ${invalid}],
|
||||
"bar": ["https://example.com/"]
|
||||
}`,
|
||||
'https://base.example/',
|
||||
{
|
||||
foo: ['https://example.com/'],
|
||||
bar: ['https://example.com/'],
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw if a scope's value is not an object", () => {
|
||||
for (const invalid of nonObjectStrings) {
|
||||
expectBad(`{ "scopes": { "https://scope.example/": ${invalid} } }`, 'https://base.example/');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Normalization', () => {
|
||||
it('should normalize empty import maps to have imports and scopes keys', () => {
|
||||
expect(parseFromString(`{}`, 'https://base.example/')).to.deep.equal({
|
||||
imports: {},
|
||||
scopes: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should normalize an import map without imports to have imports', () => {
|
||||
expect(parseFromString(`{ "scopes": {} }`, 'https://base.example/')).to.deep.equal({
|
||||
imports: {},
|
||||
scopes: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should normalize an import map without scopes to have scopes', () => {
|
||||
expect(parseFromString(`{ "imports": {} }`, 'https://base.example/')).to.deep.equal({
|
||||
imports: {},
|
||||
scopes: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should normalize addresses to arrays', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"foo": "https://example.com/1",
|
||||
"bar": ["https://example.com/2"],
|
||||
"baz": null
|
||||
}`,
|
||||
'https://base.example/',
|
||||
{
|
||||
foo: ['https://example.com/1'],
|
||||
bar: ['https://example.com/2'],
|
||||
baz: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
import { expectScopes } from './helpers/parsing.js';
|
||||
|
||||
describe('Relative URL scope keys', () => {
|
||||
it('should work with no prefix', () => {
|
||||
expectScopes(['foo'], 'https://base.example/path1/path2/path3', [
|
||||
'https://base.example/path1/path2/foo',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work with ./, ../, and / prefixes', () => {
|
||||
expectScopes(['./foo', '../foo', '/foo'], 'https://base.example/path1/path2/path3', [
|
||||
'https://base.example/path1/path2/foo',
|
||||
'https://base.example/path1/foo',
|
||||
'https://base.example/foo',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work with /s, ?s, and #s', () => {
|
||||
expectScopes(['foo/bar?baz#qux'], 'https://base.example/path1/path2/path3', [
|
||||
'https://base.example/path1/path2/foo/bar?baz#qux',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work with an empty string scope key', () => {
|
||||
expectScopes([''], 'https://base.example/path1/path2/path3', [
|
||||
'https://base.example/path1/path2/path3',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work with / suffixes', () => {
|
||||
expectScopes(
|
||||
['foo/', './foo/', '../foo/', '/foo/', '/foo//'],
|
||||
'https://base.example/path1/path2/path3',
|
||||
[
|
||||
'https://base.example/path1/path2/foo/',
|
||||
'https://base.example/path1/path2/foo/',
|
||||
'https://base.example/path1/foo/',
|
||||
'https://base.example/foo/',
|
||||
'https://base.example/foo//',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('should deduplicate based on URL parsing rules', () => {
|
||||
expectScopes(['foo/\\', 'foo//', 'foo\\\\'], 'https://base.example/path1/path2/path3', [
|
||||
'https://base.example/path1/path2/foo//',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Absolute URL scope keys', () => {
|
||||
it('should only accept absolute URL scope keys with fetch schemes', () => {
|
||||
expectScopes(
|
||||
[
|
||||
'about:good',
|
||||
'blob:good',
|
||||
'data:good',
|
||||
'file:///good',
|
||||
'filesystem:good',
|
||||
'http://good/',
|
||||
'https://good/',
|
||||
'ftp://good/',
|
||||
'import:bad',
|
||||
'mailto:bad',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'javascript:bad',
|
||||
'wss:ba',
|
||||
],
|
||||
'https://base.example/path1/path2/path3',
|
||||
[
|
||||
'about:good',
|
||||
'blob:good',
|
||||
'data:good',
|
||||
'file:///good',
|
||||
'filesystem:good',
|
||||
'http://good/',
|
||||
'https://good/',
|
||||
'ftp://good/',
|
||||
],
|
||||
[
|
||||
'Invalid scope "import:bad". Scope URLs must have a fetch scheme.',
|
||||
'Invalid scope "mailto:bad". Scope URLs must have a fetch scheme.',
|
||||
'Invalid scope "javascript:bad". Scope URLs must have a fetch scheme.',
|
||||
'Invalid scope "wss://ba/". Scope URLs must have a fetch scheme.',
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse absolute URL scope keys, ignoring unparseable ones', () => {
|
||||
expectScopes(
|
||||
[
|
||||
'https://ex ample.org/',
|
||||
'https://example.com:demo',
|
||||
'http://[www.example.com]/',
|
||||
'https:example.org',
|
||||
'https://///example.com///',
|
||||
'https://example.net',
|
||||
'https://ex%41mple.com/foo/',
|
||||
'https://example.com/%41',
|
||||
],
|
||||
'https://base.example/path1/path2/path3',
|
||||
[
|
||||
'https://base.example/path1/path2/example.org', // tricky case! remember we have a base URL
|
||||
'https://example.com///',
|
||||
'https://example.net/',
|
||||
'https://example.com/foo/',
|
||||
'https://example.com/%41',
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,145 +0,0 @@
|
||||
import { expectSpecifierMap } from './helpers/parsing.js';
|
||||
import { BUILT_IN_MODULE_SCHEME } from '../src/utils.js';
|
||||
|
||||
const BLANK = `${BUILT_IN_MODULE_SCHEME}:blank`;
|
||||
|
||||
describe('Relative URL-like specifier keys', () => {
|
||||
it('should absolutize strings prefixed with ./, ../, or / into the corresponding URLs', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"./foo": "/dotslash",
|
||||
"../foo": "/dotdotslash",
|
||||
"/foo": "/slash"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
'https://base.example/path1/path2/foo': ['https://base.example/dotslash'],
|
||||
'https://base.example/path1/foo': ['https://base.example/dotdotslash'],
|
||||
'https://base.example/foo': ['https://base.example/slash'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should absolutize the literal strings ./, ../, or / with no suffix', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"./": "/dotslash/",
|
||||
"../": "/dotdotslash/",
|
||||
"/": "/slash/"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
'https://base.example/path1/path2/': ['https://base.example/dotslash/'],
|
||||
'https://base.example/path1/': ['https://base.example/dotdotslash/'],
|
||||
'https://base.example/': ['https://base.example/slash/'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should treat percent-encoded variants of ./, ../, or / as bare specifiers', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"%2E/": "/dotSlash1/",
|
||||
"%2E%2E/": "/dotDotSlash1/",
|
||||
".%2F": "/dotSlash2",
|
||||
"..%2F": "/dotDotSlash2",
|
||||
"%2F": "/slash2",
|
||||
"%2E%2F": "/dotSlash3",
|
||||
"%2E%2E%2F": "/dotDotSlash3"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
'%2E/': ['https://base.example/dotSlash1/'],
|
||||
'%2E%2E/': ['https://base.example/dotDotSlash1/'],
|
||||
'.%2F': ['https://base.example/dotSlash2'],
|
||||
'..%2F': ['https://base.example/dotDotSlash2'],
|
||||
'%2F': ['https://base.example/slash2'],
|
||||
'%2E%2F': ['https://base.example/dotSlash3'],
|
||||
'%2E%2E%2F': ['https://base.example/dotDotSlash3'],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Absolute URL specifier keys', () => {
|
||||
it('should only accept absolute URL specifier keys with fetch schemes, treating others as bare specifiers', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"about:good": "/about",
|
||||
"blob:good": "/blob",
|
||||
"data:good": "/data",
|
||||
"file:///good": "/file",
|
||||
"filesystem:good": "/filesystem",
|
||||
"http://good/": "/http/",
|
||||
"https://good/": "/https/",
|
||||
"ftp://good/": "/ftp/",
|
||||
"import:bad": "/import",
|
||||
"mailto:bad": "/mailto",
|
||||
"javascript:bad": "/javascript",
|
||||
"wss:bad": "/wss"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
'about:good': ['https://base.example/about'],
|
||||
'blob:good': ['https://base.example/blob'],
|
||||
'data:good': ['https://base.example/data'],
|
||||
'file:///good': ['https://base.example/file'],
|
||||
'filesystem:good': ['https://base.example/filesystem'],
|
||||
'http://good/': ['https://base.example/http/'],
|
||||
'https://good/': ['https://base.example/https/'],
|
||||
'ftp://good/': ['https://base.example/ftp/'],
|
||||
'import:bad': ['https://base.example/import'],
|
||||
'mailto:bad': ['https://base.example/mailto'],
|
||||
// eslint-disable-next-line no-script-url
|
||||
'javascript:bad': ['https://base.example/javascript'],
|
||||
'wss:bad': ['https://base.example/wss'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse absolute URLs, treating unparseable ones as bare specifiers', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"https://ex ample.org/": "/unparseable1/",
|
||||
"https://example.com:demo": "/unparseable2",
|
||||
"http://[www.example.com]/": "/unparseable3/",
|
||||
"https:example.org": "/invalidButParseable1/",
|
||||
"https://///example.com///": "/invalidButParseable2/",
|
||||
"https://example.net": "/prettyNormal/",
|
||||
"https://ex%41mple.com/": "/percentDecoding/",
|
||||
"https://example.com/%41": "/noPercentDecoding"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
'https://ex ample.org/': ['https://base.example/unparseable1/'],
|
||||
'https://example.com:demo': ['https://base.example/unparseable2'],
|
||||
'http://[www.example.com]/': ['https://base.example/unparseable3/'],
|
||||
'https://example.org/': ['https://base.example/invalidButParseable1/'],
|
||||
'https://example.com///': ['https://base.example/invalidButParseable2/'],
|
||||
'https://example.net/': ['https://base.example/prettyNormal/'],
|
||||
'https://example.com/': ['https://base.example/percentDecoding/'],
|
||||
'https://example.com/%41': ['https://base.example/noPercentDecoding'],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should only parse built-in module specifier keys without a /', () => {
|
||||
expectSpecifierMap(
|
||||
`{
|
||||
"${BLANK}": "/blank",
|
||||
"${BLANK}/": "/blank/",
|
||||
"${BLANK}/foo": "/blank/foo",
|
||||
"${BLANK}\\\\foo": "/blank/backslashfoo"
|
||||
}`,
|
||||
'https://base.example/path1/path2/path3',
|
||||
{
|
||||
[BLANK]: ['https://base.example/blank'],
|
||||
[`${BLANK}\\foo`]: ['https://base.example/blank/backslashfoo'],
|
||||
},
|
||||
[
|
||||
`Invalid specifier key "${BLANK}/". Built-in module specifiers must not contain "/".`,
|
||||
`Invalid specifier key "${BLANK}/foo". Built-in module specifiers must not contain "/".`,
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
import chai from 'chai';
|
||||
import { parseFromString } from '../src/parser.js';
|
||||
import { resolve } from '../src/resolver.js';
|
||||
import { BUILT_IN_MODULE_SCHEME } from '../src/utils.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
const mapBaseURL = 'https://example.com/app/index.html';
|
||||
const scriptURL = 'https://example.com/js/app.mjs';
|
||||
|
||||
const BLANK = `${BUILT_IN_MODULE_SCHEME}:blank`;
|
||||
const NONE = `${BUILT_IN_MODULE_SCHEME}:none`;
|
||||
|
||||
function makeResolveUnderTest(mapString) {
|
||||
const map = parseFromString(mapString, mapBaseURL);
|
||||
return specifier => resolve(specifier, map, scriptURL);
|
||||
}
|
||||
|
||||
describe('Unmapped built-in module specifiers', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{}`);
|
||||
|
||||
it(`should resolve "${BLANK}" to "${BLANK}"`, () => {
|
||||
expect(resolveUnderTest(BLANK)).to.equal(BLANK);
|
||||
});
|
||||
|
||||
it(`should error resolving "${NONE}"`, () => {
|
||||
expect(() => resolveUnderTest(NONE)).to.throw(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remapping built-in module specifiers', () => {
|
||||
it('should remap built-in modules', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"${BLANK}": "./blank.mjs",
|
||||
"${NONE}": "./none.mjs"
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(resolveUnderTest(BLANK)).to.equal('https://example.com/app/blank.mjs');
|
||||
expect(resolveUnderTest(NONE)).to.equal('https://example.com/app/none.mjs');
|
||||
});
|
||||
|
||||
it('should remap built-in modules with fallbacks', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"${BLANK}": ["${BLANK}", "./blank.mjs"],
|
||||
"${NONE}": ["${NONE}", "./none.mjs"]
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(resolveUnderTest(BLANK)).to.equal(BLANK);
|
||||
expect(resolveUnderTest(NONE)).to.equal('https://example.com/app/none.mjs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remapping to built-in modules', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"blank": "${BLANK}",
|
||||
"/blank": "${BLANK}",
|
||||
"none": "${NONE}",
|
||||
"/none": "${NONE}"
|
||||
}
|
||||
}`);
|
||||
|
||||
it(`should remap to "${BLANK}"`, () => {
|
||||
expect(resolveUnderTest('blank')).to.equal(BLANK);
|
||||
expect(resolveUnderTest('/blank')).to.equal(BLANK);
|
||||
});
|
||||
|
||||
it(`should remap to "${BLANK}" for URL-like specifiers`, () => {
|
||||
expect(resolveUnderTest('/blank')).to.equal(BLANK);
|
||||
expect(resolveUnderTest('https://example.com/blank')).to.equal(BLANK);
|
||||
expect(resolveUnderTest('https://///example.com/blank')).to.equal(BLANK);
|
||||
});
|
||||
|
||||
it(`should fail when remapping to "${NONE}"`, () => {
|
||||
expect(() => resolveUnderTest('none')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('/none')).to.throw(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fallbacks with built-in module addresses', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"blank": [
|
||||
"${BLANK}",
|
||||
"./blank-fallback.mjs"
|
||||
],
|
||||
"none": [
|
||||
"${NONE}",
|
||||
"./none-fallback.mjs"
|
||||
]
|
||||
}
|
||||
}`);
|
||||
|
||||
it(`should resolve to "${BLANK}"`, () => {
|
||||
expect(resolveUnderTest('blank')).to.equal(BLANK);
|
||||
});
|
||||
|
||||
it(`should fall back past "${NONE}"`, () => {
|
||||
expect(resolveUnderTest('none')).to.equal('https://example.com/app/none-fallback.mjs');
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
import chai from 'chai';
|
||||
import { parseFromString } from '../src/parser.js';
|
||||
import { resolve } from '../src/resolver.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
const mapBaseURL = '/home/foo/project-a::/app/index.js';
|
||||
const scriptURL = '/home/foo/project-a::/js/app.js';
|
||||
|
||||
function makeResolveUnderTest(mapString) {
|
||||
const map = parseFromString(mapString, mapBaseURL);
|
||||
return specifier => resolve(specifier, map, scriptURL);
|
||||
}
|
||||
|
||||
describe('Mapped using the "imports" key only (no scopes)', () => {
|
||||
it('should fail when the mapping is to an empty array', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"moment": null,
|
||||
"lodash": []
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(() => resolveUnderTest('moment')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('lodash')).to.throw(TypeError);
|
||||
});
|
||||
|
||||
describe('Package-like scenarios', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"moment": "/node_modules/moment/src/moment.js",
|
||||
"moment/": "/node_modules/moment/src/",
|
||||
"lodash-dot": "./node_modules/lodash-es/lodash.js",
|
||||
"lodash-dot/": "./node_modules/lodash-es/",
|
||||
"lodash-dotdot": "../node_modules/lodash-es/lodash.js",
|
||||
"lodash-dotdot/": "../node_modules/lodash-es/",
|
||||
"nowhere/": []
|
||||
}
|
||||
}`);
|
||||
|
||||
it('should work for package main modules', () => {
|
||||
expect(resolveUnderTest('moment')).to.equal(
|
||||
'/home/foo/project-a/node_modules/moment/src/moment.js',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dot')).to.equal(
|
||||
'/home/foo/project-a/app/node_modules/lodash-es/lodash.js',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dotdot')).to.equal(
|
||||
'/home/foo/project-a/node_modules/lodash-es/lodash.js',
|
||||
);
|
||||
});
|
||||
|
||||
it('should work for package submodules', () => {
|
||||
expect(resolveUnderTest('moment/foo')).to.equal(
|
||||
'/home/foo/project-a/node_modules/moment/src/foo',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dot/foo')).to.equal(
|
||||
'/home/foo/project-a/app/node_modules/lodash-es/foo',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dotdot/foo')).to.equal(
|
||||
'/home/foo/project-a/node_modules/lodash-es/foo',
|
||||
);
|
||||
});
|
||||
|
||||
it('should work for package names that end in a slash by just passing through', () => {
|
||||
// TODO: is this the right behavior, or should we throw?
|
||||
expect(resolveUnderTest('moment/')).to.equal('/home/foo/project-a/node_modules/moment/src/');
|
||||
});
|
||||
|
||||
it('should still fail for package modules that are not declared', () => {
|
||||
expect(() => resolveUnderTest('underscore/')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('underscore/foo')).to.throw(TypeError);
|
||||
});
|
||||
|
||||
it('should fail for package submodules that map to nowhere', () => {
|
||||
expect(() => resolveUnderTest('nowhere/foo')).to.throw(TypeError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unmapped', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{}`);
|
||||
|
||||
it('should resolve ./ specifiers as URLs', () => {
|
||||
expect(resolveUnderTest('./foo')).to.equal('/home/foo/project-a/js/foo');
|
||||
expect(resolveUnderTest('./foo/bar')).to.equal('/home/foo/project-a/js/foo/bar');
|
||||
expect(resolveUnderTest('./foo/../bar')).to.equal('/home/foo/project-a/js/bar');
|
||||
expect(resolveUnderTest('./foo/../../bar')).to.equal('/home/foo/project-a/bar');
|
||||
});
|
||||
|
||||
it('should resolve ../ specifiers as URLs', () => {
|
||||
expect(resolveUnderTest('../foo')).to.equal('/home/foo/project-a/foo');
|
||||
expect(resolveUnderTest('../foo/bar')).to.equal('/home/foo/project-a/foo/bar');
|
||||
// TODO: do not allow to go up higher then root path
|
||||
// expect(resolveUnderTest('../../../foo/bar')).to.equal('/home/foo/project-a/foo/bar');
|
||||
});
|
||||
|
||||
it('should resolve / specifiers as URLs', () => {
|
||||
expect(resolveUnderTest('/foo')).to.equal('/home/foo/project-a/foo');
|
||||
expect(resolveUnderTest('/foo/bar')).to.equal('/home/foo/project-a/foo/bar');
|
||||
// TODO: do not allow to go up higher then root path
|
||||
// expect(resolveUnderTest('/../../foo/bar')).to.equal('/home/foo/project-a/foo/bar');
|
||||
// expect(resolveUnderTest('/../foo/../bar')).to.equal('/home/foo/project-a/bar');
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import chai from 'chai';
|
||||
import { parseFromString } from '../src/parser.js';
|
||||
import { resolve } from '../src/resolver.js';
|
||||
import { BUILT_IN_MODULE_SCHEME } from '../src/utils.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
const mapBaseURL = 'https://example.com/app/index.html';
|
||||
const scriptURL = 'https://example.com/js/app.mjs';
|
||||
|
||||
const BLANK = `${BUILT_IN_MODULE_SCHEME}:blank`;
|
||||
|
||||
function makeResolveUnderTest(mapString) {
|
||||
const map = parseFromString(mapString, mapBaseURL);
|
||||
return specifier => resolve(specifier, map, scriptURL);
|
||||
}
|
||||
|
||||
describe('Fallbacks that are not [built-in, fetch scheme]', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"bad1": [
|
||||
"${BLANK}",
|
||||
"${BLANK}"
|
||||
],
|
||||
"bad2": [
|
||||
"${BLANK}",
|
||||
"/bad2-1.mjs",
|
||||
"/bad2-2.mjs"
|
||||
],
|
||||
"bad3": [
|
||||
"/bad3-1.mjs",
|
||||
"/bad3-2.mjs"
|
||||
]
|
||||
}
|
||||
}`);
|
||||
|
||||
it('should fail for [built-in, built-in]', () => {
|
||||
expect(() => resolveUnderTest('bad1')).to.throw(/not yet implemented/);
|
||||
});
|
||||
|
||||
it('should fail for [built-in, fetch scheme, fetch scheme]', () => {
|
||||
expect(() => resolveUnderTest('bad2')).to.throw(/not yet implemented/);
|
||||
});
|
||||
|
||||
it('should fail for [fetch scheme, fetch scheme]', () => {
|
||||
expect(() => resolveUnderTest('bad3')).to.throw(/not yet implemented/);
|
||||
});
|
||||
});
|
||||
@@ -1,277 +0,0 @@
|
||||
import chai from 'chai';
|
||||
import { URL } from 'url';
|
||||
import { parseFromString } from '../src/parser.js';
|
||||
import { resolve } from '../src/resolver.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
const mapBaseURL = 'https://example.com/app/index.html';
|
||||
|
||||
function makeResolveUnderTest(mapString) {
|
||||
const map = parseFromString(mapString, mapBaseURL);
|
||||
return (specifier, baseURL) => resolve(specifier, map, baseURL);
|
||||
}
|
||||
|
||||
describe('Mapped using scope instead of "imports"', () => {
|
||||
const jsNonDirURL = 'https://example.com/js';
|
||||
const jsPrefixedURL = 'https://example.com/jsiscool';
|
||||
const inJSDirURL = 'https://example.com/js/app.mjs';
|
||||
const topLevelURL = 'https://example.com/app.mjs';
|
||||
|
||||
it('should fail when the mapping is to an empty array', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"scopes": {
|
||||
"/js/": {
|
||||
"moment": null,
|
||||
"lodash": []
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(() => resolveUnderTest('moment', inJSDirURL)).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('lodash', inJSDirURL)).to.throw(TypeError);
|
||||
});
|
||||
|
||||
describe('Exact vs. prefix based matching', () => {
|
||||
it('should match correctly when both are in the map', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"scopes": {
|
||||
"/js": {
|
||||
"moment": "/only-triggered-by-exact/moment",
|
||||
"moment/": "/only-triggered-by-exact/moment/"
|
||||
},
|
||||
"/js/": {
|
||||
"moment": "/triggered-by-any-subpath/moment",
|
||||
"moment/": "/triggered-by-any-subpath/moment/"
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(resolveUnderTest('moment', jsNonDirURL)).to.equal(
|
||||
'https://example.com/only-triggered-by-exact/moment',
|
||||
);
|
||||
expect(resolveUnderTest('moment/foo', jsNonDirURL)).to.equal(
|
||||
'https://example.com/only-triggered-by-exact/moment/foo',
|
||||
);
|
||||
|
||||
expect(resolveUnderTest('moment', inJSDirURL)).to.equal(
|
||||
'https://example.com/triggered-by-any-subpath/moment',
|
||||
);
|
||||
expect(resolveUnderTest('moment/foo', inJSDirURL)).to.equal(
|
||||
'https://example.com/triggered-by-any-subpath/moment/foo',
|
||||
);
|
||||
|
||||
expect(() => resolveUnderTest('moment', jsPrefixedURL)).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('moment/foo', jsPrefixedURL)).to.throw(TypeError);
|
||||
});
|
||||
|
||||
it('should match correctly when only an exact match is in the map', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"scopes": {
|
||||
"/js": {
|
||||
"moment": "/only-triggered-by-exact/moment",
|
||||
"moment/": "/only-triggered-by-exact/moment/"
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(resolveUnderTest('moment', jsNonDirURL)).to.equal(
|
||||
'https://example.com/only-triggered-by-exact/moment',
|
||||
);
|
||||
expect(resolveUnderTest('moment/foo', jsNonDirURL)).to.equal(
|
||||
'https://example.com/only-triggered-by-exact/moment/foo',
|
||||
);
|
||||
|
||||
expect(() => resolveUnderTest('moment', inJSDirURL)).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('moment/foo', inJSDirURL)).to.throw(TypeError);
|
||||
|
||||
expect(() => resolveUnderTest('moment', jsPrefixedURL)).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('moment/foo', jsPrefixedURL)).to.throw(TypeError);
|
||||
});
|
||||
|
||||
it('should match correctly when only a prefix match is in the map', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"scopes": {
|
||||
"/js/": {
|
||||
"moment": "/triggered-by-any-subpath/moment",
|
||||
"moment/": "/triggered-by-any-subpath/moment/"
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(() => resolveUnderTest('moment', jsNonDirURL)).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('moment/foo', jsNonDirURL)).to.throw(TypeError);
|
||||
|
||||
expect(resolveUnderTest('moment', inJSDirURL)).to.equal(
|
||||
'https://example.com/triggered-by-any-subpath/moment',
|
||||
);
|
||||
expect(resolveUnderTest('moment/foo', inJSDirURL)).to.equal(
|
||||
'https://example.com/triggered-by-any-subpath/moment/foo',
|
||||
);
|
||||
|
||||
expect(() => resolveUnderTest('moment', jsPrefixedURL)).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('moment/foo', jsPrefixedURL)).to.throw(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Package-like scenarios', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"moment": "/node_modules/moment/src/moment.js",
|
||||
"moment/": "/node_modules/moment/src/",
|
||||
"lodash-dot": "./node_modules/lodash-es/lodash.js",
|
||||
"lodash-dot/": "./node_modules/lodash-es/",
|
||||
"lodash-dotdot": "../node_modules/lodash-es/lodash.js",
|
||||
"lodash-dotdot/": "../node_modules/lodash-es/"
|
||||
},
|
||||
"scopes": {
|
||||
"/": {
|
||||
"moment": "/node_modules_3/moment/src/moment.js",
|
||||
"vue": "/node_modules_3/vue/dist/vue.runtime.esm.js"
|
||||
},
|
||||
"/js/": {
|
||||
"lodash-dot": "./node_modules_2/lodash-es/lodash.js",
|
||||
"lodash-dot/": "./node_modules_2/lodash-es/",
|
||||
"lodash-dotdot": "../node_modules_2/lodash-es/lodash.js",
|
||||
"lodash-dotdot/": "../node_modules_2/lodash-es/"
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
it('should resolve scoped', () => {
|
||||
expect(resolveUnderTest('lodash-dot', inJSDirURL)).to.equal(
|
||||
'https://example.com/app/node_modules_2/lodash-es/lodash.js',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dotdot', inJSDirURL)).to.equal(
|
||||
'https://example.com/node_modules_2/lodash-es/lodash.js',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dot/foo', inJSDirURL)).to.equal(
|
||||
'https://example.com/app/node_modules_2/lodash-es/foo',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dotdot/foo', inJSDirURL)).to.equal(
|
||||
'https://example.com/node_modules_2/lodash-es/foo',
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply best scope match', () => {
|
||||
expect(resolveUnderTest('moment', topLevelURL)).to.equal(
|
||||
'https://example.com/node_modules_3/moment/src/moment.js',
|
||||
);
|
||||
expect(resolveUnderTest('moment', inJSDirURL)).to.equal(
|
||||
'https://example.com/node_modules_3/moment/src/moment.js',
|
||||
);
|
||||
expect(resolveUnderTest('vue', inJSDirURL)).to.equal(
|
||||
'https://example.com/node_modules_3/vue/dist/vue.runtime.esm.js',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fallback to "imports"', () => {
|
||||
expect(resolveUnderTest('moment/foo', topLevelURL)).to.equal(
|
||||
'https://example.com/node_modules/moment/src/foo',
|
||||
);
|
||||
expect(resolveUnderTest('moment/foo', inJSDirURL)).to.equal(
|
||||
'https://example.com/node_modules/moment/src/foo',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dot', topLevelURL)).to.equal(
|
||||
'https://example.com/app/node_modules/lodash-es/lodash.js',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dotdot', topLevelURL)).to.equal(
|
||||
'https://example.com/node_modules/lodash-es/lodash.js',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dot/foo', topLevelURL)).to.equal(
|
||||
'https://example.com/app/node_modules/lodash-es/foo',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dotdot/foo', topLevelURL)).to.equal(
|
||||
'https://example.com/node_modules/lodash-es/foo',
|
||||
);
|
||||
});
|
||||
|
||||
it('should still fail for package-like specifiers that are not declared', () => {
|
||||
expect(() => resolveUnderTest('underscore/', inJSDirURL)).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('underscore/foo', inJSDirURL)).to.throw(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('The scope inheritance example from the README', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"a": "/a-1.mjs",
|
||||
"b": "/b-1.mjs",
|
||||
"c": "/c-1.mjs"
|
||||
},
|
||||
"scopes": {
|
||||
"/scope2/": {
|
||||
"a": "/a-2.mjs"
|
||||
},
|
||||
"/scope2/scope3/": {
|
||||
"b": "/b-3.mjs"
|
||||
}
|
||||
}
|
||||
}`);
|
||||
|
||||
const scope1URL = 'https://example.com/scope1/foo.mjs';
|
||||
const scope2URL = 'https://example.com/scope2/foo.mjs';
|
||||
const scope3URL = 'https://example.com/scope2/scope3/foo.mjs';
|
||||
|
||||
it('should fall back to "imports" when none match', () => {
|
||||
expect(resolveUnderTest('a', scope1URL)).to.equal('https://example.com/a-1.mjs');
|
||||
expect(resolveUnderTest('b', scope1URL)).to.equal('https://example.com/b-1.mjs');
|
||||
expect(resolveUnderTest('c', scope1URL)).to.equal('https://example.com/c-1.mjs');
|
||||
});
|
||||
|
||||
it('should use a direct scope override', () => {
|
||||
expect(resolveUnderTest('a', scope2URL)).to.equal('https://example.com/a-2.mjs');
|
||||
expect(resolveUnderTest('b', scope2URL)).to.equal('https://example.com/b-1.mjs');
|
||||
expect(resolveUnderTest('c', scope2URL)).to.equal('https://example.com/c-1.mjs');
|
||||
});
|
||||
|
||||
it('should use an indirect scope override', () => {
|
||||
expect(resolveUnderTest('a', scope3URL)).to.equal('https://example.com/a-2.mjs');
|
||||
expect(resolveUnderTest('b', scope3URL)).to.equal('https://example.com/b-3.mjs');
|
||||
expect(resolveUnderTest('c', scope3URL)).to.equal('https://example.com/c-1.mjs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Relative URL scope keys', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"a": "/a-1.mjs",
|
||||
"b": "/b-1.mjs",
|
||||
"c": "/c-1.mjs"
|
||||
},
|
||||
"scopes": {
|
||||
"": {
|
||||
"a": "/a-empty-string.mjs"
|
||||
},
|
||||
"./": {
|
||||
"b": "/b-dot-slash.mjs"
|
||||
},
|
||||
"../": {
|
||||
"c": "/c-dot-dot-slash.mjs"
|
||||
}
|
||||
}
|
||||
}`);
|
||||
const inSameDirAsMap = new URL('./foo.mjs', mapBaseURL).href;
|
||||
const inDirAboveMap = new URL('../foo.mjs', mapBaseURL).href;
|
||||
|
||||
it('should resolve an empty string scope using the import map URL', () => {
|
||||
expect(resolveUnderTest('a', mapBaseURL)).to.equal('https://example.com/a-empty-string.mjs');
|
||||
expect(resolveUnderTest('a', inSameDirAsMap)).to.equal('https://example.com/a-1.mjs');
|
||||
});
|
||||
|
||||
it("should resolve a ./ scope using the import map URL's directory", () => {
|
||||
expect(resolveUnderTest('b', mapBaseURL)).to.equal('https://example.com/b-dot-slash.mjs');
|
||||
expect(resolveUnderTest('b', inSameDirAsMap)).to.equal('https://example.com/b-dot-slash.mjs');
|
||||
});
|
||||
|
||||
it("should resolve a ../ scope using the import map URL's directory", () => {
|
||||
expect(resolveUnderTest('c', mapBaseURL)).to.equal('https://example.com/c-dot-dot-slash.mjs');
|
||||
expect(resolveUnderTest('c', inSameDirAsMap)).to.equal(
|
||||
'https://example.com/c-dot-dot-slash.mjs',
|
||||
);
|
||||
expect(resolveUnderTest('c', inDirAboveMap)).to.equal(
|
||||
'https://example.com/c-dot-dot-slash.mjs',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,308 +0,0 @@
|
||||
import chai from 'chai';
|
||||
import { parseFromString } from '../src/parser.js';
|
||||
import { resolve } from '../src/resolver.js';
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
const mapBaseURL = 'https://example.com/app/index.html';
|
||||
const scriptURL = 'https://example.com/js/app.mjs';
|
||||
|
||||
function makeResolveUnderTest(mapString) {
|
||||
const map = parseFromString(mapString, mapBaseURL);
|
||||
return specifier => resolve(specifier, map, scriptURL);
|
||||
}
|
||||
|
||||
describe('Unmapped', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{}`);
|
||||
|
||||
it('should resolve ./ specifiers as URLs', () => {
|
||||
expect(resolveUnderTest('./foo')).to.equal('https://example.com/js/foo');
|
||||
expect(resolveUnderTest('./foo/bar')).to.equal('https://example.com/js/foo/bar');
|
||||
expect(resolveUnderTest('./foo/../bar')).to.equal('https://example.com/js/bar');
|
||||
expect(resolveUnderTest('./foo/../../bar')).to.equal('https://example.com/bar');
|
||||
});
|
||||
|
||||
it('should resolve ../ specifiers as URLs', () => {
|
||||
expect(resolveUnderTest('../foo')).to.equal('https://example.com/foo');
|
||||
expect(resolveUnderTest('../foo/bar')).to.equal('https://example.com/foo/bar');
|
||||
expect(resolveUnderTest('../../../foo/bar')).to.equal('https://example.com/foo/bar');
|
||||
});
|
||||
|
||||
it('should resolve / specifiers as URLs', () => {
|
||||
expect(resolveUnderTest('/foo')).to.equal('https://example.com/foo');
|
||||
expect(resolveUnderTest('/foo/bar')).to.equal('https://example.com/foo/bar');
|
||||
expect(resolveUnderTest('/../../foo/bar')).to.equal('https://example.com/foo/bar');
|
||||
expect(resolveUnderTest('/../foo/../bar')).to.equal('https://example.com/bar');
|
||||
});
|
||||
|
||||
it('should parse absolute fetch-scheme URLs', () => {
|
||||
expect(resolveUnderTest('about:good')).to.equal('about:good');
|
||||
expect(resolveUnderTest('https://example.net')).to.equal('https://example.net/');
|
||||
expect(resolveUnderTest('https://ex%41mple.com/')).to.equal('https://example.com/');
|
||||
expect(resolveUnderTest('https:example.org')).to.equal('https://example.org/');
|
||||
expect(resolveUnderTest('https://///example.com///')).to.equal('https://example.com///');
|
||||
});
|
||||
|
||||
it('should fail for absolute non-fetch-scheme URLs', () => {
|
||||
expect(() => resolveUnderTest('mailto:bad')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('import:bad')).to.throw(TypeError);
|
||||
// eslint-disable-next-line no-script-url
|
||||
expect(() => resolveUnderTest('javascript:bad')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('wss:bad')).to.throw(TypeError);
|
||||
});
|
||||
|
||||
it('should fail for strings not parseable as absolute URLs and not starting with ./ ../ or /', () => {
|
||||
expect(() => resolveUnderTest('foo')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('\\foo')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest(':foo')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('@foo')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('%2E/foo')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('%2E%2E/foo')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('.%2Ffoo')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('https://ex ample.org/')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('https://example.com:demo')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('http://[www.example.com]/')).to.throw(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mapped using the "imports" key only (no scopes)', () => {
|
||||
it('should fail when the mapping is to an empty array', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"moment": null,
|
||||
"lodash": []
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(() => resolveUnderTest('moment')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('lodash')).to.throw(TypeError);
|
||||
});
|
||||
|
||||
describe('Package-like scenarios', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"moment": "/node_modules/moment/src/moment.js",
|
||||
"moment/": "/node_modules/moment/src/",
|
||||
"lodash-dot": "./node_modules/lodash-es/lodash.js",
|
||||
"lodash-dot/": "./node_modules/lodash-es/",
|
||||
"lodash-dotdot": "../node_modules/lodash-es/lodash.js",
|
||||
"lodash-dotdot/": "../node_modules/lodash-es/",
|
||||
"nowhere/": []
|
||||
}
|
||||
}`);
|
||||
|
||||
it('should work for package main modules', () => {
|
||||
expect(resolveUnderTest('moment')).to.equal(
|
||||
'https://example.com/node_modules/moment/src/moment.js',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dot')).to.equal(
|
||||
'https://example.com/app/node_modules/lodash-es/lodash.js',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dotdot')).to.equal(
|
||||
'https://example.com/node_modules/lodash-es/lodash.js',
|
||||
);
|
||||
});
|
||||
|
||||
it('should work for package submodules', () => {
|
||||
expect(resolveUnderTest('moment/foo')).to.equal(
|
||||
'https://example.com/node_modules/moment/src/foo',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dot/foo')).to.equal(
|
||||
'https://example.com/app/node_modules/lodash-es/foo',
|
||||
);
|
||||
expect(resolveUnderTest('lodash-dotdot/foo')).to.equal(
|
||||
'https://example.com/node_modules/lodash-es/foo',
|
||||
);
|
||||
});
|
||||
|
||||
it('should work for package names that end in a slash by just passing through', () => {
|
||||
// TODO: is this the right behavior, or should we throw?
|
||||
expect(resolveUnderTest('moment/')).to.equal('https://example.com/node_modules/moment/src/');
|
||||
});
|
||||
|
||||
it('should still fail for package modules that are not declared', () => {
|
||||
expect(() => resolveUnderTest('underscore/')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('underscore/foo')).to.throw(TypeError);
|
||||
});
|
||||
|
||||
it('should fail for package submodules that map to nowhere', () => {
|
||||
expect(() => resolveUnderTest('nowhere/foo')).to.throw(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tricky specifiers', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"package/withslash": "/node_modules/package-with-slash/index.mjs",
|
||||
"not-a-package": "/lib/not-a-package.mjs",
|
||||
".": "/lib/dot.mjs",
|
||||
"..": "/lib/dotdot.mjs",
|
||||
"..\\\\": "/lib/dotdotbackslash.mjs",
|
||||
"%2E": "/lib/percent2e.mjs",
|
||||
"%2F": "/lib/percent2f.mjs"
|
||||
}
|
||||
}`);
|
||||
|
||||
it('should work for explicitly-mapped specifiers that happen to have a slash', () => {
|
||||
expect(resolveUnderTest('package/withslash')).to.equal(
|
||||
'https://example.com/node_modules/package-with-slash/index.mjs',
|
||||
);
|
||||
});
|
||||
|
||||
it('should work when the specifier has punctuation', () => {
|
||||
expect(resolveUnderTest('.')).to.equal('https://example.com/lib/dot.mjs');
|
||||
expect(resolveUnderTest('..')).to.equal('https://example.com/lib/dotdot.mjs');
|
||||
expect(resolveUnderTest('..\\')).to.equal('https://example.com/lib/dotdotbackslash.mjs');
|
||||
expect(resolveUnderTest('%2E')).to.equal('https://example.com/lib/percent2e.mjs');
|
||||
expect(resolveUnderTest('%2F')).to.equal('https://example.com/lib/percent2f.mjs');
|
||||
});
|
||||
|
||||
it('should fail for attempting to get a submodule of something not declared with a trailing slash', () => {
|
||||
expect(() => resolveUnderTest('not-a-package/foo')).to.throw(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL-like specifiers', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"/node_modules/als-polyfill/index.mjs": "std:kv-storage",
|
||||
|
||||
"/lib/foo.mjs": "./more/bar.mjs",
|
||||
"./dotrelative/foo.mjs": "/lib/dot.mjs",
|
||||
"../dotdotrelative/foo.mjs": "/lib/dotdot.mjs",
|
||||
|
||||
"/lib/no.mjs": null,
|
||||
"./dotrelative/no.mjs": [],
|
||||
|
||||
"/": "/lib/slash-only/",
|
||||
"./": "/lib/dotslash-only/",
|
||||
|
||||
"/test/": "/lib/url-trailing-slash/",
|
||||
"./test/": "/lib/url-trailing-slash-dot/",
|
||||
|
||||
"/test": "/lib/test1.mjs",
|
||||
"../test": "/lib/test2.mjs"
|
||||
}
|
||||
}`);
|
||||
|
||||
it('should remap to other URLs', () => {
|
||||
expect(resolveUnderTest('https://example.com/lib/foo.mjs')).to.equal(
|
||||
'https://example.com/app/more/bar.mjs',
|
||||
);
|
||||
expect(resolveUnderTest('https://///example.com/lib/foo.mjs')).to.equal(
|
||||
'https://example.com/app/more/bar.mjs',
|
||||
);
|
||||
expect(resolveUnderTest('/lib/foo.mjs')).to.equal('https://example.com/app/more/bar.mjs');
|
||||
|
||||
expect(resolveUnderTest('https://example.com/app/dotrelative/foo.mjs')).to.equal(
|
||||
'https://example.com/lib/dot.mjs',
|
||||
);
|
||||
expect(resolveUnderTest('../app/dotrelative/foo.mjs')).to.equal(
|
||||
'https://example.com/lib/dot.mjs',
|
||||
);
|
||||
|
||||
expect(resolveUnderTest('https://example.com/dotdotrelative/foo.mjs')).to.equal(
|
||||
'https://example.com/lib/dotdot.mjs',
|
||||
);
|
||||
expect(resolveUnderTest('../dotdotrelative/foo.mjs')).to.equal(
|
||||
'https://example.com/lib/dotdot.mjs',
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail for URLs that remap to empty arrays', () => {
|
||||
expect(() => resolveUnderTest('https://example.com/lib/no.mjs')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('/lib/no.mjs')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('../lib/no.mjs')).to.throw(TypeError);
|
||||
|
||||
expect(() => resolveUnderTest('https://example.com/app/dotrelative/no.mjs')).to.throw(
|
||||
TypeError,
|
||||
);
|
||||
expect(() => resolveUnderTest('/app/dotrelative/no.mjs')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('../app/dotrelative/no.mjs')).to.throw(TypeError);
|
||||
});
|
||||
|
||||
it('should remap URLs that are just composed from / and .', () => {
|
||||
expect(resolveUnderTest('https://example.com/')).to.equal(
|
||||
'https://example.com/lib/slash-only/',
|
||||
);
|
||||
expect(resolveUnderTest('/')).to.equal('https://example.com/lib/slash-only/');
|
||||
expect(resolveUnderTest('../')).to.equal('https://example.com/lib/slash-only/');
|
||||
|
||||
expect(resolveUnderTest('https://example.com/app/')).to.equal(
|
||||
'https://example.com/lib/dotslash-only/',
|
||||
);
|
||||
expect(resolveUnderTest('/app/')).to.equal('https://example.com/lib/dotslash-only/');
|
||||
expect(resolveUnderTest('../app/')).to.equal('https://example.com/lib/dotslash-only/');
|
||||
});
|
||||
|
||||
it('should remap URLs that are prefix-matched by keys with trailing slashes', () => {
|
||||
expect(resolveUnderTest('/test/foo.mjs')).to.equal(
|
||||
'https://example.com/lib/url-trailing-slash/foo.mjs',
|
||||
);
|
||||
expect(resolveUnderTest('https://example.com/app/test/foo.mjs')).to.equal(
|
||||
'https://example.com/lib/url-trailing-slash-dot/foo.mjs',
|
||||
);
|
||||
});
|
||||
|
||||
it("should use the last entry's address when URL-like specifiers parse to the same absolute URL", () => {
|
||||
expect(resolveUnderTest('/test')).to.equal('https://example.com/lib/test2.mjs');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Overlapping entries with trailing slashes', () => {
|
||||
it('should favor the most-specific key (no empty arrays)', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"a": "/1",
|
||||
"a/": "/2/",
|
||||
"a/b": "/3",
|
||||
"a/b/": "/4/"
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(resolveUnderTest('a')).to.equal('https://example.com/1');
|
||||
expect(resolveUnderTest('a/')).to.equal('https://example.com/2/');
|
||||
expect(resolveUnderTest('a/b')).to.equal('https://example.com/3');
|
||||
expect(resolveUnderTest('a/b/')).to.equal('https://example.com/4/');
|
||||
expect(resolveUnderTest('a/b/c')).to.equal('https://example.com/4/c');
|
||||
});
|
||||
|
||||
it('should favor the most-specific key when empty arrays are involved for less-specific keys', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"a": [],
|
||||
"a/": [],
|
||||
"a/b": "/3",
|
||||
"a/b/": "/4/"
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(() => resolveUnderTest('a')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('a/')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('a/x')).to.throw(TypeError);
|
||||
expect(resolveUnderTest('a/b')).to.equal('https://example.com/3');
|
||||
expect(resolveUnderTest('a/b/')).to.equal('https://example.com/4/');
|
||||
expect(resolveUnderTest('a/b/c')).to.equal('https://example.com/4/c');
|
||||
expect(() => resolveUnderTest('a/x/c')).to.throw(TypeError);
|
||||
});
|
||||
|
||||
it('should favor the most-specific key when empty arrays are involved for more-specific keys', () => {
|
||||
const resolveUnderTest = makeResolveUnderTest(`{
|
||||
"imports": {
|
||||
"a": "/1",
|
||||
"a/": "/2/",
|
||||
"a/b": [],
|
||||
"a/b/": []
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(resolveUnderTest('a')).to.equal('https://example.com/1');
|
||||
expect(resolveUnderTest('a/')).to.equal('https://example.com/2/');
|
||||
expect(resolveUnderTest('a/x')).to.equal('https://example.com/2/x');
|
||||
expect(() => resolveUnderTest('a/b')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('a/b/')).to.throw(TypeError);
|
||||
expect(() => resolveUnderTest('a/b/c')).to.throw(TypeError);
|
||||
expect(resolveUnderTest('a/x/c')).to.equal('https://example.com/2/x/c');
|
||||
});
|
||||
});
|
||||
});
|
||||
161
packages/import-maps-resolve/test/run-tests.js
Normal file
161
packages/import-maps-resolve/test/run-tests.js
Normal file
@@ -0,0 +1,161 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const chai = require('chai');
|
||||
const { pathToFileURL } = require('url');
|
||||
const { parseFromString } = require('../index');
|
||||
const { resolve } = require('../src/resolver.js');
|
||||
|
||||
const { expect } = chai;
|
||||
|
||||
function assertNoExtraProperties(object, expectedProperties, description) {
|
||||
for (const actualProperty in object) {
|
||||
assert(
|
||||
expectedProperties.indexOf(actualProperty) !== -1,
|
||||
description + ': unexpected property ' + actualProperty,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertOwnProperty(j, name) {
|
||||
assert(name in j);
|
||||
}
|
||||
|
||||
// Parsed import maps in the reference implementation uses `URL`s instead of
|
||||
// strings as the values of specifier maps, while
|
||||
// expected import maps (taken from JSONs) uses strings.
|
||||
// This function converts `m` (expected import maps or its part)
|
||||
// into URL-based, for comparison.
|
||||
function normalizeImportMap(m) {
|
||||
if (typeof m === 'string') {
|
||||
return new URL(m);
|
||||
}
|
||||
|
||||
if (!m || typeof m !== 'object') {
|
||||
return m;
|
||||
}
|
||||
|
||||
const result = {};
|
||||
for (const key in m) {
|
||||
result[key] = normalizeImportMap(m[key]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function runTests(j) {
|
||||
const { tests } = j;
|
||||
delete j.tests;
|
||||
|
||||
if ('importMap' in j) {
|
||||
assertOwnProperty(j, 'importMap');
|
||||
assertOwnProperty(j, 'importMapBaseURL');
|
||||
try {
|
||||
j.parsedImportMap = parseFromString(JSON.stringify(j.importMap), new URL(j.importMapBaseURL));
|
||||
} catch (e) {
|
||||
j.parsedImportMap = e;
|
||||
}
|
||||
delete j.importMap;
|
||||
delete j.importMapBaseURL;
|
||||
}
|
||||
|
||||
assertNoExtraProperties(
|
||||
j,
|
||||
[
|
||||
'expectedResults',
|
||||
'expectedParsedImportMap',
|
||||
'baseURL',
|
||||
'name',
|
||||
'parsedImportMap',
|
||||
'importMap',
|
||||
'importMapBaseURL',
|
||||
'link',
|
||||
'details',
|
||||
],
|
||||
j.name,
|
||||
);
|
||||
|
||||
if (tests) {
|
||||
// Nested node.
|
||||
for (const testName in tests) {
|
||||
let fullTestName = testName;
|
||||
if (j.name) {
|
||||
fullTestName = j.name + ': ' + testName;
|
||||
}
|
||||
tests[testName].name = fullTestName;
|
||||
const k = Object.assign(Object.assign({}, j), tests[testName]);
|
||||
runTests(k);
|
||||
}
|
||||
} else {
|
||||
// Leaf node.
|
||||
for (const key of ['parsedImportMap', 'name']) {
|
||||
assertOwnProperty(j, key, j.name);
|
||||
}
|
||||
assert(
|
||||
'expectedResults' in j || 'expectedParsedImportMap' in j,
|
||||
'expectedResults or expectedParsedImportMap should exist',
|
||||
);
|
||||
|
||||
// Resolution tests.
|
||||
if ('expectedResults' in j) {
|
||||
it(`test case ${j.name}`, () => {
|
||||
assertOwnProperty(j, 'baseURL');
|
||||
|
||||
expect(j.parsedImportMap).not.be.an.instanceOf(Error);
|
||||
|
||||
for (const specifier in j.expectedResults) {
|
||||
const expected = j.expectedResults[specifier];
|
||||
if (!expected) {
|
||||
expect(() => resolve(specifier, j.parsedImportMap, new URL(j.baseURL))).to.throw();
|
||||
} else {
|
||||
// Should be resolved to `expected`.
|
||||
expect(resolve(specifier, j.parsedImportMap, new URL(j.baseURL)).href).equal(expected);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Parsing tests.
|
||||
if ('expectedParsedImportMap' in j) {
|
||||
it(`test case ${j.name}`, () => {
|
||||
if (!j.expectedParsedImportMap) {
|
||||
expect(j.parsedImportMap).to.be.an.instanceOf(TypeError);
|
||||
} else {
|
||||
expect(j.parsedImportMap).to.eql(normalizeImportMap(j.expectedParsedImportMap));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('import-maps-resolve', () => {
|
||||
const testFiles = [
|
||||
'data-base-url',
|
||||
'empty-import-map',
|
||||
'overlapping-entries',
|
||||
'packages-via-trailing-slashes',
|
||||
'parsing-addresses-absolute',
|
||||
'parsing-addresses-invalid',
|
||||
'parsing-addresses',
|
||||
'parsing-invalid-json',
|
||||
'parsing-schema-normalization',
|
||||
'parsing-schema-scope',
|
||||
'parsing-schema-specifier-map',
|
||||
'parsing-schema-toplevel',
|
||||
'parsing-scope-keys',
|
||||
'parsing-specifier-keys',
|
||||
'parsing-trailing-slashes',
|
||||
'resolving-null',
|
||||
'scopes-exact-vs-prefix',
|
||||
'scopes',
|
||||
'tricky-specifiers',
|
||||
'url-specifiers',
|
||||
];
|
||||
|
||||
for (const testFile of testFiles) {
|
||||
const testCase = JSON.parse(
|
||||
fs.readFileSync(new URL(`json/${testFile}.json`, pathToFileURL(__filename)), 'utf-8'),
|
||||
);
|
||||
runTests(testCase);
|
||||
}
|
||||
});
|
||||
6
packages/import-maps-resolve/tsconfig.json
Normal file
6
packages/import-maps-resolve/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
const { parseFromString, resolve, mergeImportMaps } = require('@import-maps/resolve');
|
||||
const deepmerge = require('deepmerge');
|
||||
const { parseFromString, resolve } = require('@import-maps/resolve');
|
||||
const { findInlineEntryId } = require('@open-wc/building-utils/index-html');
|
||||
const { processEntryHtml } = require('./src/process-entry-html.js');
|
||||
const { createOutput } = require('./src/create-output');
|
||||
@@ -104,7 +105,7 @@ module.exports = (pluginConfig = {}) => {
|
||||
if (importMapCache === null) {
|
||||
inlineImportMaps.forEach(importMapString => {
|
||||
const newImportMap = parseFromString(importMapString, basePath);
|
||||
importMapCache = mergeImportMaps(importMapCache, newImportMap);
|
||||
importMapCache = deepmerge(importMapCache, newImportMap);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"packages/semantic-dom-diff/get-diffable-html.js",
|
||||
"packages/semantic-dom-diff/src/utils.js",
|
||||
"packages/polyfills-loader/*.js",
|
||||
"packages/import-maps-resolve/*.js",
|
||||
"packages/scoped-elements/*.js"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2304,16 +2304,16 @@
|
||||
integrity sha512-BONpjHcGX2zFa9mfnwBCLEmlDsOHzT+j6Qt1yfK3MzFXFtAykfzFjAgaxPetu0YbBlCfXuMlfxI4vlRGCGMvFg==
|
||||
|
||||
"@open-wc/testing-karma-bs@file:./packages/testing-karma-bs":
|
||||
version "1.3.58"
|
||||
version "1.3.60"
|
||||
dependencies:
|
||||
"@open-wc/testing-karma" "^3.3.15"
|
||||
"@open-wc/testing-karma" "^3.3.17"
|
||||
"@types/node" "^11.13.0"
|
||||
karma-browserstack-launcher "^1.0.0"
|
||||
|
||||
"@open-wc/testing-karma@file:./packages/testing-karma":
|
||||
version "3.3.15"
|
||||
version "3.3.17"
|
||||
dependencies:
|
||||
"@open-wc/karma-esm" "^2.13.26"
|
||||
"@open-wc/karma-esm" "^2.13.28"
|
||||
"@types/karma" "^5.0.0"
|
||||
"@types/karma-coverage-istanbul-reporter" "^2.1.0"
|
||||
"@types/karma-mocha" "^1.3.0"
|
||||
|
||||
Reference in New Issue
Block a user