diff --git a/import-maps/builtin-support.tentative/imported/parsing-addresses.tentative.html b/import-maps/builtin-support.tentative/imported/parsing-addresses.tentative.html
new file mode 100644
index 000000000000000..0cc92ce3e5e4be7
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/parsing-addresses.tentative.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/import-maps/builtin-support.tentative/imported/parsing-schema.tentative.html b/import-maps/builtin-support.tentative/imported/parsing-schema.tentative.html
new file mode 100644
index 000000000000000..9e3bca2935bcf3f
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/parsing-schema.tentative.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/import-maps/builtin-support.tentative/imported/parsing-scope-keys.tentative.html b/import-maps/builtin-support.tentative/imported/parsing-scope-keys.tentative.html
new file mode 100644
index 000000000000000..be23c645d0bf071
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/parsing-scope-keys.tentative.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/import-maps/builtin-support.tentative/imported/parsing-specifier-keys.tentative.html b/import-maps/builtin-support.tentative/imported/parsing-specifier-keys.tentative.html
new file mode 100644
index 000000000000000..7bc2d4799f13697
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/parsing-specifier-keys.tentative.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/import-maps/imported/resolving-builtins.tentative.html b/import-maps/builtin-support.tentative/imported/resolving-builtins.tentative.html
similarity index 86%
rename from import-maps/imported/resolving-builtins.tentative.html
rename to import-maps/builtin-support.tentative/imported/resolving-builtins.tentative.html
index c1395c175c77455..065cfa30964da0e 100644
--- a/import-maps/imported/resolving-builtins.tentative.html
+++ b/import-maps/builtin-support.tentative/imported/resolving-builtins.tentative.html
@@ -2,7 +2,7 @@
-
+
+
diff --git a/import-maps/builtin-support.tentative/imported/resolving.tentative.html b/import-maps/builtin-support.tentative/imported/resolving.tentative.html
new file mode 100644
index 000000000000000..1d24eb2031e4097
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/resolving.tentative.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/import-maps/builtin-support.tentative/imported/resources/helpers/parsing.js b/import-maps/builtin-support.tentative/imported/resources/helpers/parsing.js
new file mode 100644
index 000000000000000..daad6d26d220bb0
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/resources/helpers/parsing.js
@@ -0,0 +1,44 @@
+'use strict';
+const { parseFromString } = require('../../lib/parser.js');
+
+// Local modifications from upstream:
+// Currently warnings and scopes are not checked in expectSpecifierMap().
+exports.expectSpecifierMap = (input, baseURL, output, warnings = []) => {
+ expect(parseFromString(`{ "imports": ${input} }`, baseURL))
+ .toEqual({ imports: output, scopes: {} });
+};
+
+exports.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)).toEqual({ imports: {}, scopes: outputScopesObject });
+
+ checkWarnings();
+};
+
+exports.expectBad = (input, baseURL, warnings = []) => {
+ const checkWarnings = testWarningHandler(warnings);
+ expect(() => parseFromString(input, baseURL)).toThrow(TypeError);
+ checkWarnings();
+};
+
+exports.expectWarnings = (input, baseURL, output, warnings = []) => {
+ const checkWarnings = testWarningHandler(warnings);
+ expect(parseFromString(input, baseURL)).toEqual(output);
+
+ checkWarnings();
+};
+
+function testWarningHandler(expectedWarnings) {
+ // We don't check warnings on WPT tests, because there are no
+ // ways to catch console warnings from JavaScript.
+ return () => {};
+}
diff --git a/import-maps/builtin-support.tentative/imported/resources/parsing-addresses.js b/import-maps/builtin-support.tentative/imported/resources/parsing-addresses.js
new file mode 100644
index 000000000000000..0f5fc73506b1222
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/resources/parsing-addresses.js
@@ -0,0 +1,351 @@
+'use strict';
+const { expectSpecifierMap } = require('./helpers/parsing.js');
+const { BUILT_IN_MODULE_SCHEME } = require('../lib/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: [expect.toMatchURL('https://base.example/path1/path2/foo')],
+ dotDotSlash: [expect.toMatchURL('https://base.example/path1/foo')],
+ slash: [expect.toMatchURL('https://base.example/foo')]
+ }
+ );
+ });
+
+ it('should not accept strings prefixed with ./, ../, or / for data: base URLs', () => {
+ expectSpecifierMap(
+ `{
+ "dotSlash": "./foo",
+ "dotDotSlash": "../foo",
+ "slash": "/foo"
+ }`,
+ 'data:text/html,test',
+ {
+ dotSlash: [],
+ dotDotSlash: [],
+ slash: []
+ },
+ [
+ `Invalid address "./foo" for the specifier key "dotSlash".`,
+ `Invalid address "../foo" for the specifier key "dotDotSlash".`,
+ `Invalid address "/foo" for the specifier key "slash".`
+ ]
+ );
+ });
+
+ it('should accept the literal strings ./, ../, or / with no suffix', () => {
+ expectSpecifierMap(
+ `{
+ "dotSlash": "./",
+ "dotDotSlash": "../",
+ "slash": "/"
+ }`,
+ 'https://base.example/path1/path2/path3',
+ {
+ dotSlash: [expect.toMatchURL('https://base.example/path1/path2/')],
+ dotDotSlash: [expect.toMatchURL('https://base.example/path1/')],
+ slash: [expect.toMatchURL('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: []
+ },
+ [
+ `Invalid address "%2E/" for the specifier key "dotSlash1".`,
+ `Invalid address "%2E%2E/" for the specifier key "dotDotSlash1".`,
+ `Invalid address ".%2F" for the specifier key "dotSlash2".`,
+ `Invalid address "..%2F" for the specifier key "dotDotSlash2".`,
+ `Invalid address "%2F" for the specifier key "slash2".`,
+ `Invalid address "%2E%2F" for the specifier key "dotSlash3".`,
+ `Invalid address "%2E%2E%2F" for the specifier key "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: [expect.toMatchURL(`${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: []
+ },
+ [`Invalid address "${encodeURIComponent(BUILT_IN_MODULE_SCHEME + ':')}foo" for the specifier key "foo".`]
+ );
+ });
+
+ it('should allow built-in module URLs that contain "/" or "\\"', () => {
+ expectSpecifierMap(
+ `{
+ "slashEnd": "${BUILT_IN_MODULE_SCHEME}:foo/",
+ "slashMiddle": "${BUILT_IN_MODULE_SCHEME}:foo/bar",
+ "backslash": "${BUILT_IN_MODULE_SCHEME}:foo\\\\baz"
+ }`,
+ 'https://base.example/path1/path2/path3',
+ {
+ slashEnd: [expect.toMatchURL(`${BUILT_IN_MODULE_SCHEME}:foo/`)],
+ slashMiddle: [expect.toMatchURL(`${BUILT_IN_MODULE_SCHEME}:foo/bar`)],
+ backslash: [expect.toMatchURL(`${BUILT_IN_MODULE_SCHEME}:foo\\baz`)]
+ }
+ );
+ });
+});
+
+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: [expect.toMatchURL('about:good')],
+ blob: [expect.toMatchURL('blob:good')],
+ data: [expect.toMatchURL('data:good')],
+ file: [expect.toMatchURL('file:///good')],
+ filesystem: [expect.toMatchURL('filesystem:good')],
+ http: [expect.toMatchURL('http://good/')],
+ https: [expect.toMatchURL('https://good/')],
+ ftp: [expect.toMatchURL('ftp://good/')],
+ import: [],
+ mailto: [],
+ javascript: [],
+ wss: []
+ },
+ [
+ `Invalid address "import:bad" for the specifier key "import".`,
+ `Invalid address "mailto:bad" for the specifier key "mailto".`,
+ `Invalid address "javascript:bad" for the specifier key "javascript".`,
+ `Invalid address "wss:bad" for the specifier key "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: [expect.toMatchURL('about:good')],
+ blob: [expect.toMatchURL('blob:good')],
+ data: [expect.toMatchURL('data:good')],
+ file: [expect.toMatchURL('file:///good')],
+ filesystem: [expect.toMatchURL('filesystem:good')],
+ http: [expect.toMatchURL('http://good/')],
+ https: [expect.toMatchURL('https://good/')],
+ ftp: [expect.toMatchURL('ftp://good/')],
+ import: [],
+ mailto: [],
+ javascript: [],
+ wss: []
+ },
+ [
+ `Invalid address "import:bad" for the specifier key "import".`,
+ `Invalid address "mailto:bad" for the specifier key "mailto".`,
+ `Invalid address "javascript:bad" for the specifier key "javascript".`,
+ `Invalid address "wss:bad" for the specifier key "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: [expect.toMatchURL('https://example.org/')],
+ invalidButParseable2: [expect.toMatchURL('https://example.com///')],
+ prettyNormal: [expect.toMatchURL('https://example.net/')],
+ percentDecoding: [expect.toMatchURL('https://example.com/')],
+ noPercentDecoding: [expect.toMatchURL('https://example.com/%41')]
+ },
+ [
+ `Invalid address "https://ex ample.org/" for the specifier key "unparseable1".`,
+ `Invalid address "https://example.com:demo" for the specifier key "unparseable2".`,
+ `Invalid address "http://[www.example.com]/" for the specifier key "unparseable3".`
+ ]
+ );
+ });
+
+ 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: [expect.toMatchURL('https://example.org/')],
+ invalidButParseable2: [expect.toMatchURL('https://example.com///')],
+ prettyNormal: [expect.toMatchURL('https://example.net/')],
+ percentDecoding: [expect.toMatchURL('https://example.com/')],
+ noPercentDecoding: [expect.toMatchURL('https://example.com/%41')]
+ },
+ [
+ `Invalid address "https://ex ample.org/" for the specifier key "unparseable1".`,
+ `Invalid address "https://example.com:demo" for the specifier key "unparseable2".`,
+ `Invalid address "http://[www.example.com]/" for the specifier key "unparseable3".`
+ ]
+ );
+ });
+});
+
+describe('Failing addresses: mismatched trailing slashes', () => {
+ it('should warn for the simple case', () => {
+ expectSpecifierMap(
+ `{
+ "trailer/": "/notrailer",
+ "${BUILT_IN_MODULE_SCHEME}:trailer/": "/bim-notrailer"
+ }`,
+ 'https://base.example/path1/path2/path3',
+ {
+ 'trailer/': [],
+ [`${BUILT_IN_MODULE_SCHEME}:trailer/`]: []
+ },
+ [
+ `Invalid address "https://base.example/notrailer" for package specifier key "trailer/". Package addresses must end with "/".`,
+ `Invalid address "https://base.example/bim-notrailer" for package specifier key "${BUILT_IN_MODULE_SCHEME}:trailer/". Package addresses must end with "/".`
+ ]
+ );
+ });
+
+ it('should warn for a mismatch alone in an array', () => {
+ expectSpecifierMap(
+ `{
+ "trailer/": ["/notrailer"],
+ "${BUILT_IN_MODULE_SCHEME}:trailer/": ["/bim-notrailer"]
+ }`,
+ 'https://base.example/path1/path2/path3',
+ {
+ 'trailer/': [],
+ [`${BUILT_IN_MODULE_SCHEME}:trailer/`]: []
+ },
+ [
+ `Invalid address "https://base.example/notrailer" for package specifier key "trailer/". Package addresses must end with "/".`,
+ `Invalid address "https://base.example/bim-notrailer" for package specifier key "${BUILT_IN_MODULE_SCHEME}:trailer/". Package addresses must end with "/".`
+ ]
+ );
+ });
+
+ it('should warn for a mismatch alongside non-mismatches in an array', () => {
+ expectSpecifierMap(
+ `{
+ "trailer/": ["/atrailer/", "/notrailer"],
+ "${BUILT_IN_MODULE_SCHEME}:trailer/": ["/bim-atrailer/", "/bim-notrailer"]
+ }`,
+ 'https://base.example/path1/path2/path3',
+ {
+ 'trailer/': [expect.toMatchURL('https://base.example/atrailer/')],
+ [`${BUILT_IN_MODULE_SCHEME}:trailer/`]: [expect.toMatchURL('https://base.example/bim-atrailer/')]
+ },
+ [
+ `Invalid address "https://base.example/notrailer" for package specifier key "trailer/". Package addresses must end with "/".`,
+ `Invalid address "https://base.example/bim-notrailer" for package specifier key "${BUILT_IN_MODULE_SCHEME}:trailer/". Package addresses 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": ${JSON.stringify(bad)}
+ }`,
+ 'https://base.example/path1/path2/path3',
+ {
+ foo: []
+ },
+ [`Invalid address "${bad}" for the specifier key "foo".`]
+ );
+ }
+ });
+});
diff --git a/import-maps/builtin-support.tentative/imported/resources/parsing-schema.js b/import-maps/builtin-support.tentative/imported/resources/parsing-schema.js
new file mode 100644
index 000000000000000..695034533c7faa2
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/resources/parsing-schema.js
@@ -0,0 +1,139 @@
+'use strict';
+const { parseFromString } = require('../lib/parser.js');
+const { expectBad, expectWarnings, expectSpecifierMap } = require('./helpers/parsing.js');
+
+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', () => {
+ expectWarnings(
+ `{
+ "imports": {},
+ "new-feature": {},
+ "scops": {}
+ }`,
+ 'https://base.example/',
+ { imports: {}, scopes: {} },
+ [
+ `Invalid top-level key "new-feature". Only "imports" and "scopes" can be present.`,
+ `Invalid top-level key "scops". Only "imports" and "scopes" can be present.`
+ ]
+ );
+ });
+});
+
+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: [expect.toMatchURL('https://example.com/')]
+ },
+ [
+ `Invalid address ${invalid} for the specifier key "foo". ` +
+ `Addresses must be strings, arrays, or null.`
+ ]
+ );
+ }
+ });
+
+ it('should ignore entries where the specifier key is an empty string', () => {
+ expectSpecifierMap(
+ `{
+ "": ["https://example.com/"]
+ }`,
+ 'https://base.example/',
+ {},
+ [`Invalid empty string specifier key.`]
+ );
+ });
+
+ 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: [expect.toMatchURL('https://example.com/')],
+ bar: [expect.toMatchURL('https://example.com/')]
+ },
+ [
+ `Invalid address ${invalid} inside the address array for the specifier key "foo". ` +
+ `Address arrays must only contain strings.`
+ ]
+ );
+ }
+ });
+
+ 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/'))
+ .toEqual({ imports: {}, scopes: {} });
+ });
+
+ it('should normalize an import map without imports to have imports', () => {
+ expect(parseFromString(`{ "scopes": {} }`, 'https://base.example/'))
+ .toEqual({ imports: {}, scopes: {} });
+ });
+
+ it('should normalize an import map without scopes to have scopes', () => {
+ expect(parseFromString(`{ "imports": {} }`, 'https://base.example/'))
+ .toEqual({ 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: [expect.toMatchURL('https://example.com/1')],
+ bar: [expect.toMatchURL('https://example.com/2')],
+ baz: []
+ }
+ );
+ });
+});
diff --git a/import-maps/builtin-support.tentative/imported/resources/parsing-scope-keys.js b/import-maps/builtin-support.tentative/imported/resources/parsing-scope-keys.js
new file mode 100644
index 000000000000000..cd1d9b34890971d
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/resources/parsing-scope-keys.js
@@ -0,0 +1,145 @@
+'use strict';
+const { expectScopes } = require('./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',
+ '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'
+ ],
+ [
+ 'Invalid scope "https://ex ample.org/" (parsed against base URL "https://base.example/path1/path2/path3").',
+ 'Invalid scope "https://example.com:demo" (parsed against base URL "https://base.example/path1/path2/path3").',
+ 'Invalid scope "http://[www.example.com]/" (parsed against base URL "https://base.example/path1/path2/path3").'
+ ]
+ );
+ });
+
+ it('should ignore relative URL scope keys when the base URL is a data: URL', () => {
+ expectScopes(
+ [
+ './foo',
+ '../foo',
+ '/foo'
+ ],
+ 'data:text/html,test',
+ [],
+ [
+ 'Invalid scope "./foo" (parsed against base URL "data:text/html,test").',
+ 'Invalid scope "../foo" (parsed against base URL "data:text/html,test").',
+ 'Invalid scope "/foo" (parsed against base URL "data:text/html,test").'
+ ]
+ );
+ });
+});
diff --git a/import-maps/builtin-support.tentative/imported/resources/parsing-specifier-keys.js b/import-maps/builtin-support.tentative/imported/resources/parsing-specifier-keys.js
new file mode 100644
index 000000000000000..9eb423a19eb1fb4
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/resources/parsing-specifier-keys.js
@@ -0,0 +1,159 @@
+'use strict';
+const { expectSpecifierMap } = require('./helpers/parsing.js');
+const { BUILT_IN_MODULE_SCHEME } = require('../lib/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': [expect.toMatchURL('https://base.example/dotslash')],
+ 'https://base.example/path1/foo': [expect.toMatchURL('https://base.example/dotdotslash')],
+ 'https://base.example/foo': [expect.toMatchURL('https://base.example/slash')]
+ }
+ );
+ });
+
+ it('should not absolutize strings prefixed with ./, ../, or / with a data: URL base', () => {
+ expectSpecifierMap(
+ `{
+ "./foo": "https://example.com/dotslash",
+ "../foo": "https://example.com/dotdotslash",
+ "/foo": "https://example.com/slash"
+ }`,
+ 'data:text/html,test',
+ {
+ './foo': [expect.toMatchURL('https://example.com/dotslash')],
+ '../foo': [expect.toMatchURL('https://example.com/dotdotslash')],
+ '/foo': [expect.toMatchURL('https://example.com/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/': [expect.toMatchURL('https://base.example/dotslash/')],
+ 'https://base.example/path1/': [expect.toMatchURL('https://base.example/dotdotslash/')],
+ 'https://base.example/': [expect.toMatchURL('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/': [expect.toMatchURL('https://base.example/dotSlash1/')],
+ '%2E%2E/': [expect.toMatchURL('https://base.example/dotDotSlash1/')],
+ '.%2F': [expect.toMatchURL('https://base.example/dotSlash2')],
+ '..%2F': [expect.toMatchURL('https://base.example/dotDotSlash2')],
+ '%2F': [expect.toMatchURL('https://base.example/slash2')],
+ '%2E%2F': [expect.toMatchURL('https://base.example/dotSlash3')],
+ '%2E%2E%2F': [expect.toMatchURL('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': [expect.toMatchURL('https://base.example/about')],
+ 'blob:good': [expect.toMatchURL('https://base.example/blob')],
+ 'data:good': [expect.toMatchURL('https://base.example/data')],
+ 'file:///good': [expect.toMatchURL('https://base.example/file')],
+ 'filesystem:good': [expect.toMatchURL('https://base.example/filesystem')],
+ 'http://good/': [expect.toMatchURL('https://base.example/http/')],
+ 'https://good/': [expect.toMatchURL('https://base.example/https/')],
+ 'ftp://good/': [expect.toMatchURL('https://base.example/ftp/')],
+ 'import:bad': [expect.toMatchURL('https://base.example/import')],
+ 'mailto:bad': [expect.toMatchURL('https://base.example/mailto')],
+ 'javascript:bad': [expect.toMatchURL('https://base.example/javascript')],
+ 'wss:bad': [expect.toMatchURL('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/': [expect.toMatchURL('https://base.example/unparseable1/')],
+ 'https://example.com:demo': [expect.toMatchURL('https://base.example/unparseable2')],
+ 'http://[www.example.com]/': [expect.toMatchURL('https://base.example/unparseable3/')],
+ 'https://example.org/': [expect.toMatchURL('https://base.example/invalidButParseable1/')],
+ 'https://example.com///': [expect.toMatchURL('https://base.example/invalidButParseable2/')],
+ 'https://example.net/': [expect.toMatchURL('https://base.example/prettyNormal/')],
+ 'https://example.com/': [expect.toMatchURL('https://base.example/percentDecoding/')],
+ 'https://example.com/%41': [expect.toMatchURL('https://base.example/noPercentDecoding')]
+ }
+ );
+ });
+
+ it('should parse built-in module specifier keys, including with a "/"', () => {
+ expectSpecifierMap(
+ `{
+ "${BLANK}": "/blank",
+ "${BLANK}/": "/blank/",
+ "${BLANK}/foo": "/blank/foo",
+ "${BLANK}\\\\foo": "/blank/backslashfoo"
+ }`,
+ 'https://base.example/path1/path2/path3',
+ {
+ [BLANK]: [expect.toMatchURL('https://base.example/blank')],
+ [`${BLANK}/`]: [expect.toMatchURL('https://base.example/blank/')],
+ [`${BLANK}/foo`]: [expect.toMatchURL('https://base.example/blank/foo')],
+ [`${BLANK}\\foo`]: [expect.toMatchURL('https://base.example/blank/backslashfoo')]
+ }
+ );
+ });
+});
diff --git a/import-maps/imported/resources/resolving-builtins.js b/import-maps/builtin-support.tentative/imported/resources/resolving-builtins.js
similarity index 100%
rename from import-maps/imported/resources/resolving-builtins.js
rename to import-maps/builtin-support.tentative/imported/resources/resolving-builtins.js
diff --git a/import-maps/imported/resources/resolving-not-yet-implemented.js b/import-maps/builtin-support.tentative/imported/resources/resolving-not-yet-implemented.js
similarity index 100%
rename from import-maps/imported/resources/resolving-not-yet-implemented.js
rename to import-maps/builtin-support.tentative/imported/resources/resolving-not-yet-implemented.js
diff --git a/import-maps/builtin-support.tentative/imported/resources/resolving-scopes.js b/import-maps/builtin-support.tentative/imported/resources/resolving-scopes.js
new file mode 100644
index 000000000000000..ca19a66840601ef
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/resources/resolving-scopes.js
@@ -0,0 +1,230 @@
+'use strict';
+const { URL } = require('url');
+const { parseFromString } = require('../lib/parser.js');
+const { resolve } = require('../lib/resolver.js');
+
+const mapBaseURL = new URL('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 = new URL('https://example.com/js');
+ const jsPrefixedURL = new URL('https://example.com/jsiscool');
+ const inJSDirURL = new URL('https://example.com/js/app.mjs');
+ const topLevelURL = new URL('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)).toThrow(TypeError);
+ expect(() => resolveUnderTest('lodash', inJSDirURL)).toThrow(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)).toMatchURL('https://example.com/only-triggered-by-exact/moment');
+ expect(resolveUnderTest('moment/foo', jsNonDirURL)).toMatchURL('https://example.com/only-triggered-by-exact/moment/foo');
+
+ expect(resolveUnderTest('moment', inJSDirURL)).toMatchURL('https://example.com/triggered-by-any-subpath/moment');
+ expect(resolveUnderTest('moment/foo', inJSDirURL)).toMatchURL('https://example.com/triggered-by-any-subpath/moment/foo');
+
+ expect(() => resolveUnderTest('moment', jsPrefixedURL)).toThrow(TypeError);
+ expect(() => resolveUnderTest('moment/foo', jsPrefixedURL)).toThrow(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)).toMatchURL('https://example.com/only-triggered-by-exact/moment');
+ expect(resolveUnderTest('moment/foo', jsNonDirURL)).toMatchURL('https://example.com/only-triggered-by-exact/moment/foo');
+
+ expect(() => resolveUnderTest('moment', inJSDirURL)).toThrow(TypeError);
+ expect(() => resolveUnderTest('moment/foo', inJSDirURL)).toThrow(TypeError);
+
+ expect(() => resolveUnderTest('moment', jsPrefixedURL)).toThrow(TypeError);
+ expect(() => resolveUnderTest('moment/foo', jsPrefixedURL)).toThrow(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)).toThrow(TypeError);
+ expect(() => resolveUnderTest('moment/foo', jsNonDirURL)).toThrow(TypeError);
+
+ expect(resolveUnderTest('moment', inJSDirURL)).toMatchURL('https://example.com/triggered-by-any-subpath/moment');
+ expect(resolveUnderTest('moment/foo', inJSDirURL)).toMatchURL('https://example.com/triggered-by-any-subpath/moment/foo');
+
+ expect(() => resolveUnderTest('moment', jsPrefixedURL)).toThrow(TypeError);
+ expect(() => resolveUnderTest('moment/foo', jsPrefixedURL)).toThrow(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)).toMatchURL('https://example.com/app/node_modules_2/lodash-es/lodash.js');
+ expect(resolveUnderTest('lodash-dotdot', inJSDirURL)).toMatchURL('https://example.com/node_modules_2/lodash-es/lodash.js');
+ expect(resolveUnderTest('lodash-dot/foo', inJSDirURL)).toMatchURL('https://example.com/app/node_modules_2/lodash-es/foo');
+ expect(resolveUnderTest('lodash-dotdot/foo', inJSDirURL)).toMatchURL('https://example.com/node_modules_2/lodash-es/foo');
+ });
+
+ it('should apply best scope match', () => {
+ expect(resolveUnderTest('moment', topLevelURL)).toMatchURL('https://example.com/node_modules_3/moment/src/moment.js');
+ expect(resolveUnderTest('moment', inJSDirURL)).toMatchURL('https://example.com/node_modules_3/moment/src/moment.js');
+ expect(resolveUnderTest('vue', inJSDirURL)).toMatchURL('https://example.com/node_modules_3/vue/dist/vue.runtime.esm.js');
+ });
+
+ it('should fallback to "imports"', () => {
+ expect(resolveUnderTest('moment/foo', topLevelURL)).toMatchURL('https://example.com/node_modules/moment/src/foo');
+ expect(resolveUnderTest('moment/foo', inJSDirURL)).toMatchURL('https://example.com/node_modules/moment/src/foo');
+ expect(resolveUnderTest('lodash-dot', topLevelURL)).toMatchURL('https://example.com/app/node_modules/lodash-es/lodash.js');
+ expect(resolveUnderTest('lodash-dotdot', topLevelURL)).toMatchURL('https://example.com/node_modules/lodash-es/lodash.js');
+ expect(resolveUnderTest('lodash-dot/foo', topLevelURL)).toMatchURL('https://example.com/app/node_modules/lodash-es/foo');
+ expect(resolveUnderTest('lodash-dotdot/foo', topLevelURL)).toMatchURL('https://example.com/node_modules/lodash-es/foo');
+ });
+
+ it('should still fail for package-like specifiers that are not declared', () => {
+ expect(() => resolveUnderTest('underscore/', inJSDirURL)).toThrow(TypeError);
+ expect(() => resolveUnderTest('underscore/foo', inJSDirURL)).toThrow(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 = new URL('https://example.com/scope1/foo.mjs');
+ const scope2URL = new URL('https://example.com/scope2/foo.mjs');
+ const scope3URL = new URL('https://example.com/scope2/scope3/foo.mjs');
+
+ it('should fall back to "imports" when none match', () => {
+ expect(resolveUnderTest('a', scope1URL)).toMatchURL('https://example.com/a-1.mjs');
+ expect(resolveUnderTest('b', scope1URL)).toMatchURL('https://example.com/b-1.mjs');
+ expect(resolveUnderTest('c', scope1URL)).toMatchURL('https://example.com/c-1.mjs');
+ });
+
+ it('should use a direct scope override', () => {
+ expect(resolveUnderTest('a', scope2URL)).toMatchURL('https://example.com/a-2.mjs');
+ expect(resolveUnderTest('b', scope2URL)).toMatchURL('https://example.com/b-1.mjs');
+ expect(resolveUnderTest('c', scope2URL)).toMatchURL('https://example.com/c-1.mjs');
+ });
+
+ it('should use an indirect scope override', () => {
+ expect(resolveUnderTest('a', scope3URL)).toMatchURL('https://example.com/a-2.mjs');
+ expect(resolveUnderTest('b', scope3URL)).toMatchURL('https://example.com/b-3.mjs');
+ expect(resolveUnderTest('c', scope3URL)).toMatchURL('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);
+ const inDirAboveMap = new URL('../foo.mjs', mapBaseURL);
+
+ it('should resolve an empty string scope using the import map URL', () => {
+ expect(resolveUnderTest('a', mapBaseURL)).toMatchURL('https://example.com/a-empty-string.mjs');
+ expect(resolveUnderTest('a', inSameDirAsMap)).toMatchURL('https://example.com/a-1.mjs');
+ });
+
+ it('should resolve a ./ scope using the import map URL\'s directory', () => {
+ expect(resolveUnderTest('b', mapBaseURL)).toMatchURL('https://example.com/b-dot-slash.mjs');
+ expect(resolveUnderTest('b', inSameDirAsMap)).toMatchURL('https://example.com/b-dot-slash.mjs');
+ });
+
+ it('should resolve a ../ scope using the import map URL\'s directory', () => {
+ expect(resolveUnderTest('c', mapBaseURL)).toMatchURL('https://example.com/c-dot-dot-slash.mjs');
+ expect(resolveUnderTest('c', inSameDirAsMap)).toMatchURL('https://example.com/c-dot-dot-slash.mjs');
+ expect(resolveUnderTest('c', inDirAboveMap)).toMatchURL('https://example.com/c-dot-dot-slash.mjs');
+ });
+ });
+});
+
diff --git a/import-maps/builtin-support.tentative/imported/resources/resolving.js b/import-maps/builtin-support.tentative/imported/resources/resolving.js
new file mode 100644
index 000000000000000..29ee31ccbc93678
--- /dev/null
+++ b/import-maps/builtin-support.tentative/imported/resources/resolving.js
@@ -0,0 +1,270 @@
+'use strict';
+const { URL } = require('url');
+const { parseFromString } = require('../lib/parser.js');
+const { resolve } = require('../lib/resolver.js');
+
+const mapBaseURL = new URL('https://example.com/app/index.html');
+const scriptURL = new URL('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')).toMatchURL('https://example.com/js/foo');
+ expect(resolveUnderTest('./foo/bar')).toMatchURL('https://example.com/js/foo/bar');
+ expect(resolveUnderTest('./foo/../bar')).toMatchURL('https://example.com/js/bar');
+ expect(resolveUnderTest('./foo/../../bar')).toMatchURL('https://example.com/bar');
+ });
+
+ it('should resolve ../ specifiers as URLs', () => {
+ expect(resolveUnderTest('../foo')).toMatchURL('https://example.com/foo');
+ expect(resolveUnderTest('../foo/bar')).toMatchURL('https://example.com/foo/bar');
+ expect(resolveUnderTest('../../../foo/bar')).toMatchURL('https://example.com/foo/bar');
+ });
+
+ it('should resolve / specifiers as URLs', () => {
+ expect(resolveUnderTest('/foo')).toMatchURL('https://example.com/foo');
+ expect(resolveUnderTest('/foo/bar')).toMatchURL('https://example.com/foo/bar');
+ expect(resolveUnderTest('/../../foo/bar')).toMatchURL('https://example.com/foo/bar');
+ expect(resolveUnderTest('/../foo/../bar')).toMatchURL('https://example.com/bar');
+ });
+
+ it('should parse absolute fetch-scheme URLs', () => {
+ expect(resolveUnderTest('about:good')).toMatchURL('about:good');
+ expect(resolveUnderTest('https://example.net')).toMatchURL('https://example.net/');
+ expect(resolveUnderTest('https://ex%41mple.com/')).toMatchURL('https://example.com/');
+ expect(resolveUnderTest('https:example.org')).toMatchURL('https://example.org/');
+ expect(resolveUnderTest('https://///example.com///')).toMatchURL('https://example.com///');
+ });
+
+ it('should fail for absolute non-fetch-scheme URLs', () => {
+ expect(() => resolveUnderTest('mailto:bad')).toThrow(TypeError);
+ expect(() => resolveUnderTest('import:bad')).toThrow(TypeError);
+ expect(() => resolveUnderTest('javascript:bad')).toThrow(TypeError);
+ expect(() => resolveUnderTest('wss:bad')).toThrow(TypeError);
+ });
+
+ it('should fail for strings not parseable as absolute URLs and not starting with ./ ../ or /', () => {
+ expect(() => resolveUnderTest('foo')).toThrow(TypeError);
+ expect(() => resolveUnderTest('\\foo')).toThrow(TypeError);
+ expect(() => resolveUnderTest(':foo')).toThrow(TypeError);
+ expect(() => resolveUnderTest('@foo')).toThrow(TypeError);
+ expect(() => resolveUnderTest('%2E/foo')).toThrow(TypeError);
+ expect(() => resolveUnderTest('%2E%2E/foo')).toThrow(TypeError);
+ expect(() => resolveUnderTest('.%2Ffoo')).toThrow(TypeError);
+ expect(() => resolveUnderTest('https://ex ample.org/')).toThrow(TypeError);
+ expect(() => resolveUnderTest('https://example.com:demo')).toThrow(TypeError);
+ expect(() => resolveUnderTest('http://[www.example.com]/')).toThrow(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')).toThrow(TypeError);
+ expect(() => resolveUnderTest('lodash')).toThrow(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')).toMatchURL('https://example.com/node_modules/moment/src/moment.js');
+ expect(resolveUnderTest('lodash-dot')).toMatchURL('https://example.com/app/node_modules/lodash-es/lodash.js');
+ expect(resolveUnderTest('lodash-dotdot')).toMatchURL('https://example.com/node_modules/lodash-es/lodash.js');
+ });
+
+ it('should work for package submodules', () => {
+ expect(resolveUnderTest('moment/foo')).toMatchURL('https://example.com/node_modules/moment/src/foo');
+ expect(resolveUnderTest('lodash-dot/foo')).toMatchURL('https://example.com/app/node_modules/lodash-es/foo');
+ expect(resolveUnderTest('lodash-dotdot/foo')).toMatchURL('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/')).toMatchURL('https://example.com/node_modules/moment/src/');
+ });
+
+ it('should still fail for package modules that are not declared', () => {
+ expect(() => resolveUnderTest('underscore/')).toThrow(TypeError);
+ expect(() => resolveUnderTest('underscore/foo')).toThrow(TypeError);
+ });
+
+ it('should fail for package submodules that map to nowhere', () => {
+ expect(() => resolveUnderTest('nowhere/foo')).toThrow(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')).toMatchURL('https://example.com/node_modules/package-with-slash/index.mjs');
+ });
+
+ it('should work when the specifier has punctuation', () => {
+ expect(resolveUnderTest('.')).toMatchURL('https://example.com/lib/dot.mjs');
+ expect(resolveUnderTest('..')).toMatchURL('https://example.com/lib/dotdot.mjs');
+ expect(resolveUnderTest('..\\')).toMatchURL('https://example.com/lib/dotdotbackslash.mjs');
+ expect(resolveUnderTest('%2E')).toMatchURL('https://example.com/lib/percent2e.mjs');
+ expect(resolveUnderTest('%2F')).toMatchURL('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')).toThrow(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')).toMatchURL('https://example.com/app/more/bar.mjs');
+ expect(resolveUnderTest('https://///example.com/lib/foo.mjs')).toMatchURL('https://example.com/app/more/bar.mjs');
+ expect(resolveUnderTest('/lib/foo.mjs')).toMatchURL('https://example.com/app/more/bar.mjs');
+
+ expect(resolveUnderTest('https://example.com/app/dotrelative/foo.mjs')).toMatchURL('https://example.com/lib/dot.mjs');
+ expect(resolveUnderTest('../app/dotrelative/foo.mjs')).toMatchURL('https://example.com/lib/dot.mjs');
+
+ expect(resolveUnderTest('https://example.com/dotdotrelative/foo.mjs')).toMatchURL('https://example.com/lib/dotdot.mjs');
+ expect(resolveUnderTest('../dotdotrelative/foo.mjs')).toMatchURL('https://example.com/lib/dotdot.mjs');
+ });
+
+ it('should fail for URLs that remap to empty arrays', () => {
+ expect(() => resolveUnderTest('https://example.com/lib/no.mjs')).toThrow(TypeError);
+ expect(() => resolveUnderTest('/lib/no.mjs')).toThrow(TypeError);
+ expect(() => resolveUnderTest('../lib/no.mjs')).toThrow(TypeError);
+
+ expect(() => resolveUnderTest('https://example.com/app/dotrelative/no.mjs')).toThrow(TypeError);
+ expect(() => resolveUnderTest('/app/dotrelative/no.mjs')).toThrow(TypeError);
+ expect(() => resolveUnderTest('../app/dotrelative/no.mjs')).toThrow(TypeError);
+ });
+
+ it('should remap URLs that are just composed from / and .', () => {
+ expect(resolveUnderTest('https://example.com/')).toMatchURL('https://example.com/lib/slash-only/');
+ expect(resolveUnderTest('/')).toMatchURL('https://example.com/lib/slash-only/');
+ expect(resolveUnderTest('../')).toMatchURL('https://example.com/lib/slash-only/');
+
+ expect(resolveUnderTest('https://example.com/app/')).toMatchURL('https://example.com/lib/dotslash-only/');
+ expect(resolveUnderTest('/app/')).toMatchURL('https://example.com/lib/dotslash-only/');
+ expect(resolveUnderTest('../app/')).toMatchURL('https://example.com/lib/dotslash-only/');
+ });
+
+ it('should remap URLs that are prefix-matched by keys with trailing slashes', () => {
+ expect(resolveUnderTest('/test/foo.mjs')).toMatchURL('https://example.com/lib/url-trailing-slash/foo.mjs');
+ expect(resolveUnderTest('https://example.com/app/test/foo.mjs')).toMatchURL('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')).toMatchURL('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')).toMatchURL('https://example.com/1');
+ expect(resolveUnderTest('a/')).toMatchURL('https://example.com/2/');
+ expect(resolveUnderTest('a/b')).toMatchURL('https://example.com/3');
+ expect(resolveUnderTest('a/b/')).toMatchURL('https://example.com/4/');
+ expect(resolveUnderTest('a/b/c')).toMatchURL('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')).toThrow(TypeError);
+ expect(() => resolveUnderTest('a/')).toThrow(TypeError);
+ expect(() => resolveUnderTest('a/x')).toThrow(TypeError);
+ expect(resolveUnderTest('a/b')).toMatchURL('https://example.com/3');
+ expect(resolveUnderTest('a/b/')).toMatchURL('https://example.com/4/');
+ expect(resolveUnderTest('a/b/c')).toMatchURL('https://example.com/4/c');
+ expect(() => resolveUnderTest('a/x/c')).toThrow(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')).toMatchURL('https://example.com/1');
+ expect(resolveUnderTest('a/')).toMatchURL('https://example.com/2/');
+ expect(resolveUnderTest('a/x')).toMatchURL('https://example.com/2/x');
+ expect(() => resolveUnderTest('a/b')).toThrow(TypeError);
+ expect(() => resolveUnderTest('a/b/')).toThrow(TypeError);
+ expect(() => resolveUnderTest('a/b/c')).toThrow(TypeError);
+ expect(resolveUnderTest('a/x/c')).toMatchURL('https://example.com/2/x/c');
+ });
+ });
+});
diff --git a/import-maps/imported/resources/parsing-addresses.js b/import-maps/imported/resources/parsing-addresses.js
index 0f5fc73506b1222..92d7714ade9b965 100644
--- a/import-maps/imported/resources/parsing-addresses.js
+++ b/import-maps/imported/resources/parsing-addresses.js
@@ -1,6 +1,5 @@
'use strict';
const { expectSpecifierMap } = require('./helpers/parsing.js');
-const { BUILT_IN_MODULE_SCHEME } = require('../lib/utils.js');
describe('Relative URL-like addresses', () => {
it('should accept strings prefixed with ./, ../, or /', () => {
@@ -12,9 +11,9 @@ describe('Relative URL-like addresses', () => {
}`,
'https://base.example/path1/path2/path3',
{
- dotSlash: [expect.toMatchURL('https://base.example/path1/path2/foo')],
- dotDotSlash: [expect.toMatchURL('https://base.example/path1/foo')],
- slash: [expect.toMatchURL('https://base.example/foo')]
+ dotSlash: expect.toMatchURL('https://base.example/path1/path2/foo'),
+ dotDotSlash: expect.toMatchURL('https://base.example/path1/foo'),
+ slash: expect.toMatchURL('https://base.example/foo')
}
);
});
@@ -28,9 +27,6 @@ describe('Relative URL-like addresses', () => {
}`,
'data:text/html,test',
{
- dotSlash: [],
- dotDotSlash: [],
- slash: []
},
[
`Invalid address "./foo" for the specifier key "dotSlash".`,
@@ -49,9 +45,9 @@ describe('Relative URL-like addresses', () => {
}`,
'https://base.example/path1/path2/path3',
{
- dotSlash: [expect.toMatchURL('https://base.example/path1/path2/')],
- dotDotSlash: [expect.toMatchURL('https://base.example/path1/')],
- slash: [expect.toMatchURL('https://base.example/')]
+ dotSlash: expect.toMatchURL('https://base.example/path1/path2/'),
+ dotDotSlash: expect.toMatchURL('https://base.example/path1/'),
+ slash: expect.toMatchURL('https://base.example/')
}
);
});
@@ -69,13 +65,6 @@ describe('Relative URL-like addresses', () => {
}`,
'https://base.example/path1/path2/path3',
{
- dotSlash1: [],
- dotDotSlash1: [],
- dotSlash2: [],
- dotDotSlash2: [],
- slash2: [],
- dotSlash3: [],
- dotDotSlash3: []
},
[
`Invalid address "%2E/" for the specifier key "dotSlash1".`,
@@ -90,49 +79,6 @@ describe('Relative URL-like addresses', () => {
});
});
-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: [expect.toMatchURL(`${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: []
- },
- [`Invalid address "${encodeURIComponent(BUILT_IN_MODULE_SCHEME + ':')}foo" for the specifier key "foo".`]
- );
- });
-
- it('should allow built-in module URLs that contain "/" or "\\"', () => {
- expectSpecifierMap(
- `{
- "slashEnd": "${BUILT_IN_MODULE_SCHEME}:foo/",
- "slashMiddle": "${BUILT_IN_MODULE_SCHEME}:foo/bar",
- "backslash": "${BUILT_IN_MODULE_SCHEME}:foo\\\\baz"
- }`,
- 'https://base.example/path1/path2/path3',
- {
- slashEnd: [expect.toMatchURL(`${BUILT_IN_MODULE_SCHEME}:foo/`)],
- slashMiddle: [expect.toMatchURL(`${BUILT_IN_MODULE_SCHEME}:foo/bar`)],
- backslash: [expect.toMatchURL(`${BUILT_IN_MODULE_SCHEME}:foo\\baz`)]
- }
- );
- });
-});
-
describe('Absolute URL addresses', () => {
it('should only accept absolute URL addresses with fetch schemes', () => {
expectSpecifierMap(
@@ -141,7 +87,7 @@ describe('Absolute URL addresses', () => {
"blob": "blob:good",
"data": "data:good",
"file": "file:///good",
- "filesystem": "filesystem:good",
+ "filesystem": "filesystem:http://example.com/good/",
"http": "http://good/",
"https": "https://good/",
"ftp": "ftp://good/",
@@ -152,65 +98,20 @@ describe('Absolute URL addresses', () => {
}`,
'https://base.example/path1/path2/path3',
{
- about: [expect.toMatchURL('about:good')],
- blob: [expect.toMatchURL('blob:good')],
- data: [expect.toMatchURL('data:good')],
- file: [expect.toMatchURL('file:///good')],
- filesystem: [expect.toMatchURL('filesystem:good')],
- http: [expect.toMatchURL('http://good/')],
- https: [expect.toMatchURL('https://good/')],
- ftp: [expect.toMatchURL('ftp://good/')],
- import: [],
- mailto: [],
- javascript: [],
- wss: []
+ about: expect.toMatchURL('about:good'),
+ blob: expect.toMatchURL('blob:good'),
+ data: expect.toMatchURL('data:good'),
+ file: expect.toMatchURL('file:///good'),
+ filesystem: expect.toMatchURL('filesystem:http://example.com/good/'),
+ http: expect.toMatchURL('http://good/'),
+ https: expect.toMatchURL('https://good/'),
+ ftp: expect.toMatchURL('ftp://good/'),
+ import: expect.toMatchURL('import:bad'),
+ javascript: expect.toMatchURL('javascript:bad'),
+ mailto: expect.toMatchURL('mailto:bad'),
+ wss: expect.toMatchURL('wss://bad/')
},
- [
- `Invalid address "import:bad" for the specifier key "import".`,
- `Invalid address "mailto:bad" for the specifier key "mailto".`,
- `Invalid address "javascript:bad" for the specifier key "javascript".`,
- `Invalid address "wss:bad" for the specifier key "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: [expect.toMatchURL('about:good')],
- blob: [expect.toMatchURL('blob:good')],
- data: [expect.toMatchURL('data:good')],
- file: [expect.toMatchURL('file:///good')],
- filesystem: [expect.toMatchURL('filesystem:good')],
- http: [expect.toMatchURL('http://good/')],
- https: [expect.toMatchURL('https://good/')],
- ftp: [expect.toMatchURL('ftp://good/')],
- import: [],
- mailto: [],
- javascript: [],
- wss: []
- },
- [
- `Invalid address "import:bad" for the specifier key "import".`,
- `Invalid address "mailto:bad" for the specifier key "mailto".`,
- `Invalid address "javascript:bad" for the specifier key "javascript".`,
- `Invalid address "wss:bad" for the specifier key "wss".`
- ]
+ []
);
});
@@ -228,45 +129,11 @@ describe('Absolute URL addresses', () => {
}`,
'https://base.example/path1/path2/path3',
{
- unparseable1: [],
- unparseable2: [],
- unparseable3: [],
- invalidButParseable1: [expect.toMatchURL('https://example.org/')],
- invalidButParseable2: [expect.toMatchURL('https://example.com///')],
- prettyNormal: [expect.toMatchURL('https://example.net/')],
- percentDecoding: [expect.toMatchURL('https://example.com/')],
- noPercentDecoding: [expect.toMatchURL('https://example.com/%41')]
- },
- [
- `Invalid address "https://ex ample.org/" for the specifier key "unparseable1".`,
- `Invalid address "https://example.com:demo" for the specifier key "unparseable2".`,
- `Invalid address "http://[www.example.com]/" for the specifier key "unparseable3".`
- ]
- );
- });
-
- 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: [expect.toMatchURL('https://example.org/')],
- invalidButParseable2: [expect.toMatchURL('https://example.com///')],
- prettyNormal: [expect.toMatchURL('https://example.net/')],
- percentDecoding: [expect.toMatchURL('https://example.com/')],
- noPercentDecoding: [expect.toMatchURL('https://example.com/%41')]
+ invalidButParseable1: expect.toMatchURL('https://example.org/'),
+ invalidButParseable2: expect.toMatchURL('https://example.com///'),
+ prettyNormal: expect.toMatchURL('https://example.net/'),
+ percentDecoding: expect.toMatchURL('https://example.com/'),
+ noPercentDecoding: expect.toMatchURL('https://example.com/%41')
},
[
`Invalid address "https://ex ample.org/" for the specifier key "unparseable1".`,
@@ -281,54 +148,12 @@ describe('Failing addresses: mismatched trailing slashes', () => {
it('should warn for the simple case', () => {
expectSpecifierMap(
`{
- "trailer/": "/notrailer",
- "${BUILT_IN_MODULE_SCHEME}:trailer/": "/bim-notrailer"
+ "trailer/": "/notrailer"
}`,
'https://base.example/path1/path2/path3',
{
- 'trailer/': [],
- [`${BUILT_IN_MODULE_SCHEME}:trailer/`]: []
},
- [
- `Invalid address "https://base.example/notrailer" for package specifier key "trailer/". Package addresses must end with "/".`,
- `Invalid address "https://base.example/bim-notrailer" for package specifier key "${BUILT_IN_MODULE_SCHEME}:trailer/". Package addresses must end with "/".`
- ]
- );
- });
-
- it('should warn for a mismatch alone in an array', () => {
- expectSpecifierMap(
- `{
- "trailer/": ["/notrailer"],
- "${BUILT_IN_MODULE_SCHEME}:trailer/": ["/bim-notrailer"]
- }`,
- 'https://base.example/path1/path2/path3',
- {
- 'trailer/': [],
- [`${BUILT_IN_MODULE_SCHEME}:trailer/`]: []
- },
- [
- `Invalid address "https://base.example/notrailer" for package specifier key "trailer/". Package addresses must end with "/".`,
- `Invalid address "https://base.example/bim-notrailer" for package specifier key "${BUILT_IN_MODULE_SCHEME}:trailer/". Package addresses must end with "/".`
- ]
- );
- });
-
- it('should warn for a mismatch alongside non-mismatches in an array', () => {
- expectSpecifierMap(
- `{
- "trailer/": ["/atrailer/", "/notrailer"],
- "${BUILT_IN_MODULE_SCHEME}:trailer/": ["/bim-atrailer/", "/bim-notrailer"]
- }`,
- 'https://base.example/path1/path2/path3',
- {
- 'trailer/': [expect.toMatchURL('https://base.example/atrailer/')],
- [`${BUILT_IN_MODULE_SCHEME}:trailer/`]: [expect.toMatchURL('https://base.example/bim-atrailer/')]
- },
- [
- `Invalid address "https://base.example/notrailer" for package specifier key "trailer/". Package addresses must end with "/".`,
- `Invalid address "https://base.example/bim-notrailer" for package specifier key "${BUILT_IN_MODULE_SCHEME}:trailer/". Package addresses must end with "/".`
- ]
+ [`Invalid address "https://base.example/notrailer" for package specifier key "trailer/". Package addresses must end with "/".`]
);
});
});
@@ -342,7 +167,6 @@ describe('Other invalid addresses', () => {
}`,
'https://base.example/path1/path2/path3',
{
- foo: []
},
[`Invalid address "${bad}" for the specifier key "foo".`]
);
diff --git a/import-maps/imported/resources/parsing-schema.js b/import-maps/imported/resources/parsing-schema.js
index 695034533c7faa2..f60422ae62bce21 100644
--- a/import-maps/imported/resources/parsing-schema.js
+++ b/import-maps/imported/resources/parsing-schema.js
@@ -45,24 +45,20 @@ describe('Mismatching the top-level schema', () => {
});
describe('Mismatching the specifier map schema', () => {
- const invalidAddressStrings = ['true', '1', '{}'];
- const invalidInsideArrayStrings = ['null', 'true', '1', '{}', '[]'];
+ const invalidAddressStrings = ['null', 'true', '1', '{}', '[]', '["https://example.com/"]'];
- it('should ignore entries where the address is not a string, array, or null', () => {
+ it('should ignore entries where the address is not a string', () => {
for (const invalid of invalidAddressStrings) {
expectSpecifierMap(
`{
"foo": ${invalid},
- "bar": ["https://example.com/"]
+ "bar": "https://example.com/"
}`,
'https://base.example/',
{
- bar: [expect.toMatchURL('https://example.com/')]
+ bar: expect.toMatchURL('https://example.com/')
},
- [
- `Invalid address ${invalid} for the specifier key "foo". ` +
- `Addresses must be strings, arrays, or null.`
- ]
+ [`Invalid address ${invalid} for the specifier key "foo". Addresses must be strings.`]
);
}
});
@@ -70,7 +66,7 @@ describe('Mismatching the specifier map schema', () => {
it('should ignore entries where the specifier key is an empty string', () => {
expectSpecifierMap(
`{
- "": ["https://example.com/"]
+ "": "https://example.com/"
}`,
'https://base.example/',
{},
@@ -78,26 +74,6 @@ describe('Mismatching the specifier map schema', () => {
);
});
- 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: [expect.toMatchURL('https://example.com/')],
- bar: [expect.toMatchURL('https://example.com/')]
- },
- [
- `Invalid address ${invalid} inside the address array for the specifier key "foo". ` +
- `Address arrays must only contain strings.`
- ]
- );
- }
- });
-
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/');
@@ -120,20 +96,4 @@ describe('Normalization', () => {
expect(parseFromString(`{ "imports": {} }`, 'https://base.example/'))
.toEqual({ 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: [expect.toMatchURL('https://example.com/1')],
- bar: [expect.toMatchURL('https://example.com/2')],
- baz: []
- }
- );
- });
});
diff --git a/import-maps/imported/resources/parsing-scope-keys.js b/import-maps/imported/resources/parsing-scope-keys.js
index cd1d9b34890971d..6b50c2c5405a321 100644
--- a/import-maps/imported/resources/parsing-scope-keys.js
+++ b/import-maps/imported/resources/parsing-scope-keys.js
@@ -69,7 +69,7 @@ describe('Absolute URL scope keys', () => {
'blob:good',
'data:good',
'file:///good',
- 'filesystem:good',
+ 'filesystem:http://example.com/good/',
'http://good/',
'https://good/',
'ftp://good/',
@@ -84,7 +84,7 @@ describe('Absolute URL scope keys', () => {
'blob:good',
'data:good',
'file:///good',
- 'filesystem:good',
+ 'filesystem:http://example.com/good/',
'http://good/',
'https://good/',
'ftp://good/'
diff --git a/import-maps/imported/resources/parsing-specifier-keys.js b/import-maps/imported/resources/parsing-specifier-keys.js
index 9eb423a19eb1fb4..77c092b741bd7de 100644
--- a/import-maps/imported/resources/parsing-specifier-keys.js
+++ b/import-maps/imported/resources/parsing-specifier-keys.js
@@ -1,8 +1,5 @@
'use strict';
const { expectSpecifierMap } = require('./helpers/parsing.js');
-const { BUILT_IN_MODULE_SCHEME } = require('../lib/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', () => {
@@ -14,9 +11,9 @@ describe('Relative URL-like specifier keys', () => {
}`,
'https://base.example/path1/path2/path3',
{
- 'https://base.example/path1/path2/foo': [expect.toMatchURL('https://base.example/dotslash')],
- 'https://base.example/path1/foo': [expect.toMatchURL('https://base.example/dotdotslash')],
- 'https://base.example/foo': [expect.toMatchURL('https://base.example/slash')]
+ 'https://base.example/path1/path2/foo': expect.toMatchURL('https://base.example/dotslash'),
+ 'https://base.example/path1/foo': expect.toMatchURL('https://base.example/dotdotslash'),
+ 'https://base.example/foo': expect.toMatchURL('https://base.example/slash')
}
);
});
@@ -30,9 +27,9 @@ describe('Relative URL-like specifier keys', () => {
}`,
'data:text/html,test',
{
- './foo': [expect.toMatchURL('https://example.com/dotslash')],
- '../foo': [expect.toMatchURL('https://example.com/dotdotslash')],
- '/foo': [expect.toMatchURL('https://example.com/slash')]
+ './foo': expect.toMatchURL('https://example.com/dotslash'),
+ '../foo': expect.toMatchURL('https://example.com/dotdotslash'),
+ '/foo': expect.toMatchURL('https://example.com/slash')
}
);
});
@@ -46,9 +43,9 @@ describe('Relative URL-like specifier keys', () => {
}`,
'https://base.example/path1/path2/path3',
{
- 'https://base.example/path1/path2/': [expect.toMatchURL('https://base.example/dotslash/')],
- 'https://base.example/path1/': [expect.toMatchURL('https://base.example/dotdotslash/')],
- 'https://base.example/': [expect.toMatchURL('https://base.example/slash/')]
+ 'https://base.example/path1/path2/': expect.toMatchURL('https://base.example/dotslash/'),
+ 'https://base.example/path1/': expect.toMatchURL('https://base.example/dotdotslash/'),
+ 'https://base.example/': expect.toMatchURL('https://base.example/slash/')
}
);
});
@@ -66,27 +63,27 @@ describe('Relative URL-like specifier keys', () => {
}`,
'https://base.example/path1/path2/path3',
{
- '%2E/': [expect.toMatchURL('https://base.example/dotSlash1/')],
- '%2E%2E/': [expect.toMatchURL('https://base.example/dotDotSlash1/')],
- '.%2F': [expect.toMatchURL('https://base.example/dotSlash2')],
- '..%2F': [expect.toMatchURL('https://base.example/dotDotSlash2')],
- '%2F': [expect.toMatchURL('https://base.example/slash2')],
- '%2E%2F': [expect.toMatchURL('https://base.example/dotSlash3')],
- '%2E%2E%2F': [expect.toMatchURL('https://base.example/dotDotSlash3')]
+ '%2E/': expect.toMatchURL('https://base.example/dotSlash1/'),
+ '%2E%2E/': expect.toMatchURL('https://base.example/dotDotSlash1/'),
+ '.%2F': expect.toMatchURL('https://base.example/dotSlash2'),
+ '..%2F': expect.toMatchURL('https://base.example/dotDotSlash2'),
+ '%2F': expect.toMatchURL('https://base.example/slash2'),
+ '%2E%2F': expect.toMatchURL('https://base.example/dotSlash3'),
+ '%2E%2E%2F': expect.toMatchURL('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', () => {
+ it('Accept all absolute URL specifier keys even with fetch schemes as URLs', () => {
expectSpecifierMap(
`{
"about:good": "/about",
"blob:good": "/blob",
"data:good": "/data",
"file:///good": "/file",
- "filesystem:good": "/filesystem",
+ "filesystem:http://example.com/good/": "/filesystem/",
"http://good/": "/http/",
"https://good/": "/https/",
"ftp://good/": "/ftp/",
@@ -97,18 +94,18 @@ describe('Absolute URL specifier keys', () => {
}`,
'https://base.example/path1/path2/path3',
{
- 'about:good': [expect.toMatchURL('https://base.example/about')],
- 'blob:good': [expect.toMatchURL('https://base.example/blob')],
- 'data:good': [expect.toMatchURL('https://base.example/data')],
- 'file:///good': [expect.toMatchURL('https://base.example/file')],
- 'filesystem:good': [expect.toMatchURL('https://base.example/filesystem')],
- 'http://good/': [expect.toMatchURL('https://base.example/http/')],
- 'https://good/': [expect.toMatchURL('https://base.example/https/')],
- 'ftp://good/': [expect.toMatchURL('https://base.example/ftp/')],
- 'import:bad': [expect.toMatchURL('https://base.example/import')],
- 'mailto:bad': [expect.toMatchURL('https://base.example/mailto')],
- 'javascript:bad': [expect.toMatchURL('https://base.example/javascript')],
- 'wss:bad': [expect.toMatchURL('https://base.example/wss')]
+ 'about:good': expect.toMatchURL('https://base.example/about'),
+ 'blob:good': expect.toMatchURL('https://base.example/blob'),
+ 'data:good': expect.toMatchURL('https://base.example/data'),
+ 'file:///good': expect.toMatchURL('https://base.example/file'),
+ 'filesystem:http://example.com/good/': expect.toMatchURL('https://base.example/filesystem/'),
+ 'http://good/': expect.toMatchURL('https://base.example/http/'),
+ 'https://good/': expect.toMatchURL('https://base.example/https/'),
+ 'ftp://good/': expect.toMatchURL('https://base.example/ftp/'),
+ 'import:bad': expect.toMatchURL('https://base.example/import'),
+ 'mailto:bad': expect.toMatchURL('https://base.example/mailto'),
+ 'javascript:bad': expect.toMatchURL('https://base.example/javascript'),
+ 'wss://bad/': expect.toMatchURL('https://base.example/wss')
}
);
});
@@ -127,32 +124,14 @@ describe('Absolute URL specifier keys', () => {
}`,
'https://base.example/path1/path2/path3',
{
- 'https://ex ample.org/': [expect.toMatchURL('https://base.example/unparseable1/')],
- 'https://example.com:demo': [expect.toMatchURL('https://base.example/unparseable2')],
- 'http://[www.example.com]/': [expect.toMatchURL('https://base.example/unparseable3/')],
- 'https://example.org/': [expect.toMatchURL('https://base.example/invalidButParseable1/')],
- 'https://example.com///': [expect.toMatchURL('https://base.example/invalidButParseable2/')],
- 'https://example.net/': [expect.toMatchURL('https://base.example/prettyNormal/')],
- 'https://example.com/': [expect.toMatchURL('https://base.example/percentDecoding/')],
- 'https://example.com/%41': [expect.toMatchURL('https://base.example/noPercentDecoding')]
- }
- );
- });
-
- it('should parse built-in module specifier keys, including with a "/"', () => {
- expectSpecifierMap(
- `{
- "${BLANK}": "/blank",
- "${BLANK}/": "/blank/",
- "${BLANK}/foo": "/blank/foo",
- "${BLANK}\\\\foo": "/blank/backslashfoo"
- }`,
- 'https://base.example/path1/path2/path3',
- {
- [BLANK]: [expect.toMatchURL('https://base.example/blank')],
- [`${BLANK}/`]: [expect.toMatchURL('https://base.example/blank/')],
- [`${BLANK}/foo`]: [expect.toMatchURL('https://base.example/blank/foo')],
- [`${BLANK}\\foo`]: [expect.toMatchURL('https://base.example/blank/backslashfoo')]
+ 'https://ex ample.org/': expect.toMatchURL('https://base.example/unparseable1/'),
+ 'https://example.com:demo': expect.toMatchURL('https://base.example/unparseable2'),
+ 'http://[www.example.com]/': expect.toMatchURL('https://base.example/unparseable3/'),
+ 'https://example.org/': expect.toMatchURL('https://base.example/invalidButParseable1/'),
+ 'https://example.com///': expect.toMatchURL('https://base.example/invalidButParseable2/'),
+ 'https://example.net/': expect.toMatchURL('https://base.example/prettyNormal/'),
+ 'https://example.com/': expect.toMatchURL('https://base.example/percentDecoding/'),
+ 'https://example.com/%41': expect.toMatchURL('https://base.example/noPercentDecoding')
}
);
});
diff --git a/import-maps/imported/resources/resolving-scopes.js b/import-maps/imported/resources/resolving-scopes.js
index ca19a66840601ef..e5f6acc7d1167b3 100644
--- a/import-maps/imported/resources/resolving-scopes.js
+++ b/import-maps/imported/resources/resolving-scopes.js
@@ -16,20 +16,6 @@ describe('Mapped using scope instead of "imports"', () => {
const inJSDirURL = new URL('https://example.com/js/app.mjs');
const topLevelURL = new URL('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)).toThrow(TypeError);
- expect(() => resolveUnderTest('lodash', inJSDirURL)).toThrow(TypeError);
- });
-
describe('Exact vs. prefix based matching', () => {
it('should match correctly when both are in the map', () => {
const resolveUnderTest = makeResolveUnderTest(`{
diff --git a/import-maps/imported/resources/resolving.js b/import-maps/imported/resources/resolving.js
index 29ee31ccbc93678..ef8a4f87d25e7fc 100644
--- a/import-maps/imported/resources/resolving.js
+++ b/import-maps/imported/resources/resolving.js
@@ -42,11 +42,11 @@ describe('Unmapped', () => {
expect(resolveUnderTest('https://///example.com///')).toMatchURL('https://example.com///');
});
- it('should fail for absolute non-fetch-scheme URLs', () => {
- expect(() => resolveUnderTest('mailto:bad')).toThrow(TypeError);
- expect(() => resolveUnderTest('import:bad')).toThrow(TypeError);
- expect(() => resolveUnderTest('javascript:bad')).toThrow(TypeError);
- expect(() => resolveUnderTest('wss:bad')).toThrow(TypeError);
+ it('should parse absolute non-fetch-scheme URLs', () => {
+ expect(resolveUnderTest('mailto:bad')).toMatchURL('mailto:bad');
+ expect(resolveUnderTest('import:bad')).toMatchURL('import:bad');
+ expect(resolveUnderTest('javascript:bad')).toMatchURL('javascript:bad');
+ expect(resolveUnderTest('wss:bad')).toMatchURL('wss://bad/');
});
it('should fail for strings not parseable as absolute URLs and not starting with ./ ../ or /', () => {
@@ -64,18 +64,6 @@ describe('Unmapped', () => {
});
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')).toThrow(TypeError);
- expect(() => resolveUnderTest('lodash')).toThrow(TypeError);
- });
-
describe('Package-like scenarios', () => {
const resolveUnderTest = makeResolveUnderTest(`{
"imports": {
@@ -84,8 +72,7 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
"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/": []
+ "lodash-dotdot/": "../node_modules/lodash-es/"
}
}`);
@@ -110,10 +97,6 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
expect(() => resolveUnderTest('underscore/')).toThrow(TypeError);
expect(() => resolveUnderTest('underscore/foo')).toThrow(TypeError);
});
-
- it('should fail for package submodules that map to nowhere', () => {
- expect(() => resolveUnderTest('nowhere/foo')).toThrow(TypeError);
- });
});
describe('Tricky specifiers', () => {
@@ -121,6 +104,7 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
"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",
@@ -144,20 +128,19 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
it('should fail for attempting to get a submodule of something not declared with a trailing slash', () => {
expect(() => resolveUnderTest('not-a-package/foo')).toThrow(TypeError);
});
+
+ it('should fail for attempting to get a module if only a trailing-slash version is present', () => {
+ expect(() => resolveUnderTest('only-slash')).toThrow(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/",
@@ -181,16 +164,6 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
expect(resolveUnderTest('../dotdotrelative/foo.mjs')).toMatchURL('https://example.com/lib/dotdot.mjs');
});
- it('should fail for URLs that remap to empty arrays', () => {
- expect(() => resolveUnderTest('https://example.com/lib/no.mjs')).toThrow(TypeError);
- expect(() => resolveUnderTest('/lib/no.mjs')).toThrow(TypeError);
- expect(() => resolveUnderTest('../lib/no.mjs')).toThrow(TypeError);
-
- expect(() => resolveUnderTest('https://example.com/app/dotrelative/no.mjs')).toThrow(TypeError);
- expect(() => resolveUnderTest('/app/dotrelative/no.mjs')).toThrow(TypeError);
- expect(() => resolveUnderTest('../app/dotrelative/no.mjs')).toThrow(TypeError);
- });
-
it('should remap URLs that are just composed from / and .', () => {
expect(resolveUnderTest('https://example.com/')).toMatchURL('https://example.com/lib/slash-only/');
expect(resolveUnderTest('/')).toMatchURL('https://example.com/lib/slash-only/');
@@ -212,7 +185,7 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
});
describe('Overlapping entries with trailing slashes', () => {
- it('should favor the most-specific key (no empty arrays)', () => {
+ it('should favor the most-specific key', () => {
const resolveUnderTest = makeResolveUnderTest(`{
"imports": {
"a": "/1",
@@ -229,11 +202,9 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
expect(resolveUnderTest('a/b/c')).toMatchURL('https://example.com/4/c');
});
- it('should favor the most-specific key when empty arrays are involved for less-specific keys', () => {
+ it('should favor the most-specific key when there are no mappings for less-specific keys', () => {
const resolveUnderTest = makeResolveUnderTest(`{
"imports": {
- "a": [],
- "a/": [],
"a/b": "/3",
"a/b/": "/4/"
}
@@ -247,24 +218,15 @@ describe('Mapped using the "imports" key only (no scopes)', () => {
expect(resolveUnderTest('a/b/c')).toMatchURL('https://example.com/4/c');
expect(() => resolveUnderTest('a/x/c')).toThrow(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/": []
- }
- }`);
+ it('should deal with data: URL bases', () => {
+ const resolveUnderTest = makeResolveUnderTest(`{
+ "imports": {
+ "foo/": "data:text/javascript,foo/"
+ }
+ }`);
- expect(resolveUnderTest('a')).toMatchURL('https://example.com/1');
- expect(resolveUnderTest('a/')).toMatchURL('https://example.com/2/');
- expect(resolveUnderTest('a/x')).toMatchURL('https://example.com/2/x');
- expect(() => resolveUnderTest('a/b')).toThrow(TypeError);
- expect(() => resolveUnderTest('a/b/')).toThrow(TypeError);
- expect(() => resolveUnderTest('a/b/c')).toThrow(TypeError);
- expect(resolveUnderTest('a/x/c')).toMatchURL('https://example.com/2/x/c');
- });
+ expect(() => resolveUnderTest('foo/bar')).toThrow(TypeError);
});
});