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