From 1422670cd345decb13fb6a01314e1e016644d93b Mon Sep 17 00:00:00 2001 From: Hiroshige Hayashizaki Date: Tue, 15 Oct 2019 14:18:33 -0700 Subject: [PATCH] [Import Maps] Import tests This CL imports post-#176 upstream reference implementation's tests into wpt/import-maps/imported/, and executes them under virtual/import-maps-without-builtin-modules/. The pre-#176 existing tests are still left under wpt/import-maps/builtin-support.tentative/imported/, which are excluded by this CL from virtual/import-maps-without-builtin-modules. To match with the expectation of post-#176 tests, this CL modifies ImportMap::ToString() to output strings (not single-element arrays) as right hand side. https://github.com/WICG/import-maps/pull/176 Bug: 990561, 1010751 Change-Id: I652b35969d48e0147d07aa869ca3a97004f2f7a1 --- .../imported/parsing-addresses.tentative.html | 11 + .../imported/parsing-schema.tentative.html | 11 + .../parsing-scope-keys.tentative.html | 11 + .../parsing-specifier-keys.tentative.html | 11 + .../resolving-builtins.tentative.html | 2 +- ...solving-not-yet-implemented.tentative.html | 2 +- .../imported/resolving-scopes.tentative.html | 11 + .../imported/resolving.tentative.html | 11 + .../imported/resources/helpers/parsing.js | 44 +++ .../imported/resources/parsing-addresses.js | 351 ++++++++++++++++++ .../imported/resources/parsing-schema.js | 139 +++++++ .../imported/resources/parsing-scope-keys.js | 145 ++++++++ .../resources/parsing-specifier-keys.js | 159 ++++++++ .../imported/resources/resolving-builtins.js | 0 .../resolving-not-yet-implemented.js | 0 .../imported/resources/resolving-scopes.js | 230 ++++++++++++ .../imported/resources/resolving.js | 270 ++++++++++++++ .../imported/resources/parsing-addresses.js | 230 ++---------- .../imported/resources/parsing-schema.js | 52 +-- .../imported/resources/parsing-scope-keys.js | 4 +- .../resources/parsing-specifier-keys.js | 97 ++--- .../imported/resources/resolving-scopes.js | 14 - import-maps/imported/resources/resolving.js | 80 ++-- 23 files changed, 1500 insertions(+), 385 deletions(-) create mode 100644 import-maps/builtin-support.tentative/imported/parsing-addresses.tentative.html create mode 100644 import-maps/builtin-support.tentative/imported/parsing-schema.tentative.html create mode 100644 import-maps/builtin-support.tentative/imported/parsing-scope-keys.tentative.html create mode 100644 import-maps/builtin-support.tentative/imported/parsing-specifier-keys.tentative.html rename import-maps/{ => builtin-support.tentative}/imported/resolving-builtins.tentative.html (86%) rename import-maps/{ => builtin-support.tentative}/imported/resolving-not-yet-implemented.tentative.html (87%) create mode 100644 import-maps/builtin-support.tentative/imported/resolving-scopes.tentative.html create mode 100644 import-maps/builtin-support.tentative/imported/resolving.tentative.html create mode 100644 import-maps/builtin-support.tentative/imported/resources/helpers/parsing.js create mode 100644 import-maps/builtin-support.tentative/imported/resources/parsing-addresses.js create mode 100644 import-maps/builtin-support.tentative/imported/resources/parsing-schema.js create mode 100644 import-maps/builtin-support.tentative/imported/resources/parsing-scope-keys.js create mode 100644 import-maps/builtin-support.tentative/imported/resources/parsing-specifier-keys.js rename import-maps/{ => builtin-support.tentative}/imported/resources/resolving-builtins.js (100%) rename import-maps/{ => builtin-support.tentative}/imported/resources/resolving-not-yet-implemented.js (100%) create mode 100644 import-maps/builtin-support.tentative/imported/resources/resolving-scopes.js create mode 100644 import-maps/builtin-support.tentative/imported/resources/resolving.js 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); }); });