feat(resolve): update to latest proposal specs (only mapping and scopes)

This commit is contained in:
Lars den Bakker
2020-04-26 10:12:11 +02:00
parent 4c1a966602
commit 86ab53844e
46 changed files with 1930 additions and 2094 deletions

View File

@@ -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 {

View File

@@ -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',
},
],
],
};

View 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 };

View File

@@ -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"
}
]
}

View File

@@ -1,3 +0,0 @@
export { parseFromString } from './parser.js';
export { resolve } from './resolver.js';
export { mergeImportMaps } from './utils.js';

View File

@@ -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 };

View File

@@ -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 };

View 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;
}

View File

@@ -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 };

View File

@@ -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();
}

View 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
}
}
}
}

View 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
}
}
}
}

View File

@@ -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"
}
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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": {}
}
}
}
}

View File

@@ -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": {}
}
}
}
}

View File

@@ -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": {}
}
}
}
}

View File

@@ -0,0 +1,6 @@
{
"name": "Invalid JSON",
"importMapBaseURL": "https://base.example/",
"importMap": "{imports: {}}",
"expectedParsedImportMap": null
}

View File

@@ -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": {}
}
}
}
}

View File

@@ -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/": []
}
}
}
}
}
}
}

View File

@@ -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": {}
}
}
}
}

View File

@@ -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": {}
}
}
}
}

View 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/": {}
}
}
}
}
}

View File

@@ -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": {}
}
}
}
}

View File

@@ -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": {}
}
}

View 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
}
}
}
}

View File

@@ -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
}
}
}
}
}
}

View 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
}
}
}
}
}
}

View File

@@ -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
}
}
}
}

View 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"
}
}
}
}

View File

@@ -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' },
},
});
});
});

View File

@@ -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: [],
},
);
}
});
});

View File

@@ -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/'],
},
);
});
});

View File

@@ -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: [],
},
);
});
});

View File

@@ -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',
],
);
});
});

View File

@@ -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 "/".`,
],
);
});
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

@@ -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/);
});
});

View File

@@ -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',
);
});
});
});

View File

@@ -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');
});
});
});

View 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);
}
});

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"strict": true
}
}

View File

@@ -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);
});
}

View File

@@ -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"
]
}

View File

@@ -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"