diff --git a/import-maps/common/README.md b/import-maps/common/README.md new file mode 100644 index 000000000000000..7ea5266c9f999ea --- /dev/null +++ b/import-maps/common/README.md @@ -0,0 +1,79 @@ +# Import maps test JSON format + +In this directory, test inputs and expectations are expressed as JSON files. +This is in order to share the same JSON files between WPT tests and Jest-based +tests for the reference JavaScript implementation at [WICG repository](https://github.com/WICG/import-maps/tree/master/reference-implementation). + +## Basics + +A **test object** describes a set of parameters (import maps and base URLs) and specifiers to be tested. +Each JSON file under [resources/](resources/) directory consists of a test object. +A minimum test object would be: + +```json +{ + "name": "Main test name", + "importMapBaseURL": "https://example.com/import-map-base-url/index.html", + "importMap": { + "imports": { + "a": "/mapped-a.mjs" + } + }, + "baseURL": "https://example.com/base-url/app.mjs", + "specifiers": { + "a": "https://example.com/mapped-a.mjs", + "b": null + } +} +``` + +Required fields: + +- `name`: Test name. + - In WPT tests, this is used for the test name of `promise_test()` together with specifier to be resolved, like `"Main test name: a"`. +- `importMap` (object or string): the import map to be attached. +- `importMapBaseURL` (string): the base URL used for [parsing the import map](https://wicg.github.io/import-maps/#parse-an-import-map-string). +- `baseURL` (string): the base URL used in [resolving a specifier](https://wicg.github.io/import-maps/#resolve-a-module-specifier) for each specifiers. +- `specifiers` (object; string to (string or null)): Specifiers to be tested. + - The keys are specifiers to be resolved. + - The values are expected resolved URLs. If `null`, resolution should fail. + +Optional fields: + +- `link` and `details` can be used for e.g. linking to specs or adding more detailed descriptions. + - Currently they are simply ignored by the WPT test helper. + +## Nesting and inheritance + +We can organize tests by nesting test objects. +A test object can contain child test objects (*subtests*) using `tests` field. +The Keys of the `tests` value are the names of subtests, and values are test objects. + +For example: + +```json +{ + "name": "Main test name", + "importMapBaseURL": "https://example.com/import-map-base-url/index.html", + "importMap": { + "imports": { + "a": "/mapped-a.mjs" + } + }, + "tests": { + "Subtest1": { + "baseURL": "https://example.com/base-url1/app.mjs", + "specifiers": { "a": "https://example.com/mapped-a.mjs" } + }, + "Subtest2": { + "baseURL": "https://example.com/base-url2/app.mjs", + "specifiers": { "b": null } + } + } +} +``` + +The top-level test object contains two sub test objects, named as `Subtest1` and `Subtest2`, respectively. + +Child test objects inherit fields from their parent test object. +In the example above, the child test objects specifies `baseURL` fields, while they inherits other fields (e.g. `importMapBaseURL`) from the top-level test object. diff --git a/import-maps/common/resolving.tentative.html b/import-maps/common/resolving.tentative.html new file mode 100644 index 000000000000000..fcf8c732acc5687 --- /dev/null +++ b/import-maps/common/resolving.tentative.html @@ -0,0 +1,23 @@ + + + + +
+ diff --git a/import-maps/common/resources/data-base-url.json b/import-maps/common/resources/data-base-url.json new file mode 100644 index 000000000000000..81fcf087425c70e --- /dev/null +++ b/import-maps/common/resources/data-base-url.json @@ -0,0 +1,17 @@ +{ + "importMap": { + "imports": { + "foo/": "data:text/javascript,foo/" + } + }, + "importMapBaseURL": "https://example.com/app/index.html", + "baseURL": "https://example.com/js/app.mjs", + "name": "data: base URL (?)", + "tests": { + "should favor the most-specific key": { + "expectedResults": { + "foo/bar": null + } + } + } +} diff --git a/import-maps/common/resources/empty-import-map.json b/import-maps/common/resources/empty-import-map.json new file mode 100644 index 000000000000000..ce6c185498fa38c --- /dev/null +++ b/import-maps/common/resources/empty-import-map.json @@ -0,0 +1,56 @@ +{ + "importMap": {}, + "importMapBaseURL": "https://example.com/app/index.html", + "baseURL": "https://example.com/js/app.mjs", + "tests": { + "valid relative specifiers": { + "expectedResults": { + "./foo": "https://example.com/js/foo", + "./foo/bar": "https://example.com/js/foo/bar", + "./foo/../bar": "https://example.com/js/bar", + "./foo/../../bar": "https://example.com/bar", + "../foo": "https://example.com/foo", + "../foo/bar": "https://example.com/foo/bar", + "../../../foo/bar": "https://example.com/foo/bar", + "/foo": "https://example.com/foo", + "/foo/bar": "https://example.com/foo/bar", + "/../../foo/bar": "https://example.com/foo/bar", + "/../foo/../bar": "https://example.com/bar" + } + }, + "fetch scheme absolute URLs": { + "expectedResults": { + "about:fetch-scheme": "about:fetch-scheme", + "https://fetch-scheme.net": "https://fetch-scheme.net/", + "https:fetch-scheme.org": "https://fetch-scheme.org/", + "https://fetch%2Dscheme.com/": "https://fetch-scheme.com/", + "https://///fetch-scheme.com///": "https://fetch-scheme.com///" + } + }, + "non-fetch scheme absolute URLs": { + "expectedResults": { + "mailto:non-fetch-scheme": "mailto:non-fetch-scheme", + "import:non-fetch-scheme": "import:non-fetch-scheme", + "javascript:non-fetch-scheme": "javascript:non-fetch-scheme", + "wss:non-fetch-scheme": "wss://non-fetch-scheme/" + } + }, + "valid relative URLs that are invalid as specifiers should fail": { + "expectedResults": { + "invalid-specifier": null, + "\\invalid-specifier": null, + ":invalid-specifier": null, + "@invalid-specifier": null, + "%2E/invalid-specifier": null, + "%2E%2E/invalid-specifier": null, + ".%2Finvalid-specifier": null + } + }, + "invalid absolute URLs should fail": { + "expectedResults": { + "https://invalid-url.com:demo": null, + "http://[invalid-url.com]/": null + } + } + } +} diff --git a/import-maps/common/resources/overlapping-entries.json b/import-maps/common/resources/overlapping-entries.json new file mode 100644 index 000000000000000..21354025451cf0c --- /dev/null +++ b/import-maps/common/resources/overlapping-entries.json @@ -0,0 +1,25 @@ +{ + "importMapBaseURL": "https://example.com/app/index.html", + "baseURL": "https://example.com/js/app.mjs", + "name": "should favor the most-specific key", + "tests": { + "Overlapping entries with trailing slashes": { + "importMap": { + "imports": { + "a": "/1", + "a/": "/2/", + "a/b": "/3", + "a/b/": "/4/" + } + }, + "expectedResults": { + "a": "https://example.com/1", + "a/": "https://example.com/2/", + "a/x": "https://example.com/2/x", + "a/b": "https://example.com/3", + "a/b/": "https://example.com/4/", + "a/b/c": "https://example.com/4/c" + } + } + } +} diff --git a/import-maps/common/resources/packages-via-trailing-slashes.json b/import-maps/common/resources/packages-via-trailing-slashes.json new file mode 100644 index 000000000000000..6b8f0135f50f50c --- /dev/null +++ b/import-maps/common/resources/packages-via-trailing-slashes.json @@ -0,0 +1,43 @@ +{ + "importMap": { + "imports": { + "moment": "/node_modules/moment/src/moment.js", + "moment/": "/node_modules/moment/src/", + "lodash-dot": "./node_modules/lodash-es/lodash.js", + "lodash-dot/": "./node_modules/lodash-es/", + "lodash-dotdot": "../node_modules/lodash-es/lodash.js", + "lodash-dotdot/": "../node_modules/lodash-es/" + } + }, + "importMapBaseURL": "https://example.com/app/index.html", + "baseURL": "https://example.com/js/app.mjs", + "name": "Package-like scenarios", + "link": "https://github.com/WICG/import-maps#packages-via-trailing-slashes", + "tests": { + "package main modules": { + "expectedResults": { + "moment": "https://example.com/node_modules/moment/src/moment.js", + "lodash-dot": "https://example.com/app/node_modules/lodash-es/lodash.js", + "lodash-dotdot": "https://example.com/node_modules/lodash-es/lodash.js" + } + }, + "package submodules": { + "expectedResults": { + "moment/foo": "https://example.com/node_modules/moment/src/foo", + "lodash-dot/foo": "https://example.com/app/node_modules/lodash-es/foo", + "lodash-dotdot/foo": "https://example.com/node_modules/lodash-es/foo" + } + }, + "package names that end in a slash should just pass through": { + "expectedResults": { + "moment/": "https://example.com/node_modules/moment/src/" + } + }, + "package modules that are not declared should fail": { + "expectedResults": { + "underscore/": null, + "underscore/foo": null + } + } + } +} diff --git a/import-maps/common/resources/scopes-exact-vs-prefix.json b/import-maps/common/resources/scopes-exact-vs-prefix.json new file mode 100644 index 000000000000000..3d9d50349f0d644 --- /dev/null +++ b/import-maps/common/resources/scopes-exact-vs-prefix.json @@ -0,0 +1,134 @@ +{ + "name": "Exact vs. prefix based matching", + "details": "Scopes are matched with base URLs that are exactly the same or subpaths under the scopes with trailing shashes", + "link": "https://wicg.github.io/import-maps/#resolve-a-module-specifier Step 8.1", + "tests": { + "Scope without trailing slash only": { + "importMap": { + "scopes": { + "/js": { + "moment": "/only-triggered-by-exact/moment", + "moment/": "/only-triggered-by-exact/moment/" + } + } + }, + "importMapBaseURL": "https://example.com/app/index.html", + "tests": { + "Non-trailing-slash base URL (exact match)": { + "baseURL": "https://example.com/js", + "expectedResults": { + "moment": "https://example.com/only-triggered-by-exact/moment", + "moment/foo": "https://example.com/only-triggered-by-exact/moment/foo" + } + }, + "Trailing-slash base URL (fail)": { + "baseURL": "https://example.com/js/", + "expectedResults": { + "moment": null, + "moment/foo": null + } + }, + "Subpath base URL (fail)": { + "baseURL": "https://example.com/js/app.mjs", + "expectedResults": { + "moment": null, + "moment/foo": null + } + }, + "Non-subpath base URL (fail)": { + "baseURL": "https://example.com/jsiscool", + "expectedResults": { + "moment": null, + "moment/foo": null + } + } + } + }, + "Scope with trailing slash only": { + "importMap": { + "scopes": { + "/js/": { + "moment": "/triggered-by-any-subpath/moment", + "moment/": "/triggered-by-any-subpath/moment/" + } + } + }, + "importMapBaseURL": "https://example.com/app/index.html", + "tests": { + "Non-trailing-slash base URL (fail)": { + "baseURL": "https://example.com/js", + "expectedResults": { + "moment": null, + "moment/foo": null + } + }, + "Trailing-slash base URL (exact match)": { + "baseURL": "https://example.com/js/", + "expectedResults": { + "moment": "https://example.com/triggered-by-any-subpath/moment", + "moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo" + } + }, + "Subpath base URL (prefix match)": { + "baseURL": "https://example.com/js/app.mjs", + "expectedResults": { + "moment": "https://example.com/triggered-by-any-subpath/moment", + "moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo" + } + }, + "Non-subpath base URL (fail)": { + "baseURL": "https://example.com/jsiscool", + "expectedResults": { + "moment": null, + "moment/foo": null + } + } + } + }, + "Scopes with and without trailing slash": { + "importMap": { + "scopes": { + "/js": { + "moment": "/only-triggered-by-exact/moment", + "moment/": "/only-triggered-by-exact/moment/" + }, + "/js/": { + "moment": "/triggered-by-any-subpath/moment", + "moment/": "/triggered-by-any-subpath/moment/" + } + } + }, + "importMapBaseURL": "https://example.com/app/index.html", + "tests": { + "Non-trailing-slash base URL (exact match)": { + "baseURL": "https://example.com/js", + "expectedResults": { + "moment": "https://example.com/only-triggered-by-exact/moment", + "moment/foo": "https://example.com/only-triggered-by-exact/moment/foo" + } + }, + "Trailing-slash base URL (exact match)": { + "baseURL": "https://example.com/js/", + "expectedResults": { + "moment": "https://example.com/triggered-by-any-subpath/moment", + "moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo" + } + }, + "Subpath base URL (prefix match)": { + "baseURL": "https://example.com/js/app.mjs", + "expectedResults": { + "moment": "https://example.com/triggered-by-any-subpath/moment", + "moment/foo": "https://example.com/triggered-by-any-subpath/moment/foo" + } + }, + "Non-subpath base URL (fail)": { + "baseURL": "https://example.com/jsiscool", + "expectedResults": { + "moment": null, + "moment/foo": null + } + } + } + } + } +} diff --git a/import-maps/common/resources/scopes.json b/import-maps/common/resources/scopes.json new file mode 100644 index 000000000000000..c266e4c6c1d7d9d --- /dev/null +++ b/import-maps/common/resources/scopes.json @@ -0,0 +1,171 @@ +{ + "importMapBaseURL": "https://example.com/app/index.html", + "tests": { + "Fallback to toplevel and between scopes": { + "importMap": { + "imports": { + "a": "/a-1.mjs", + "b": "/b-1.mjs", + "c": "/c-1.mjs", + "d": "/d-1.mjs" + }, + "scopes": { + "/scope2/": { + "a": "/a-2.mjs", + "d": "/d-2.mjs" + }, + "/scope2/scope3/": { + "b": "/b-3.mjs", + "d": "/d-3.mjs" + } + } + }, + "tests": { + "should fall back to `imports` when no scopes match": { + "baseURL": "https://example.com/scope1/foo.mjs", + "expectedResults": { + "a": "https://example.com/a-1.mjs", + "b": "https://example.com/b-1.mjs", + "c": "https://example.com/c-1.mjs", + "d": "https://example.com/d-1.mjs" + } + }, + "should use a direct scope override": { + "baseURL": "https://example.com/scope2/foo.mjs", + "expectedResults": { + "a": "https://example.com/a-2.mjs", + "b": "https://example.com/b-1.mjs", + "c": "https://example.com/c-1.mjs", + "d": "https://example.com/d-2.mjs" + } + }, + "should use an indirect scope override": { + "baseURL": "https://example.com/scope2/scope3/foo.mjs", + "expectedResults": { + "a": "https://example.com/a-2.mjs", + "b": "https://example.com/b-3.mjs", + "c": "https://example.com/c-1.mjs", + "d": "https://example.com/d-3.mjs" + } + } + } + }, + "Relative URL scope keys": { + "importMap": { + "imports": { + "a": "/a-1.mjs", + "b": "/b-1.mjs", + "c": "/c-1.mjs" + }, + "scopes": { + "": { + "a": "/a-empty-string.mjs" + }, + "./": { + "b": "/b-dot-slash.mjs" + }, + "../": { + "c": "/c-dot-dot-slash.mjs" + } + } + }, + "tests": { + "An empty string scope is a scope with import map base URL": { + "baseURL": "https://example.com/app/index.html", + "expectedResults": { + "a": "https://example.com/a-empty-string.mjs", + "b": "https://example.com/b-dot-slash.mjs", + "c": "https://example.com/c-dot-dot-slash.mjs" + } + }, + "'./' scope is a scope with import map base URL's directory": { + "baseURL": "https://example.com/app/foo.mjs", + "expectedResults": { + "a": "https://example.com/a-1.mjs", + "b": "https://example.com/b-dot-slash.mjs", + "c": "https://example.com/c-dot-dot-slash.mjs" + } + }, + "'../' scope is a scope with import map base URL's parent directory": { + "baseURL": "https://example.com/foo.mjs", + "expectedResults": { + "a": "https://example.com/a-1.mjs", + "b": "https://example.com/b-1.mjs", + "c": "https://example.com/c-dot-dot-slash.mjs" + } + } + } + }, + "Package-like scenarios": { + "importMap": { + "imports": { + "moment": "/node_modules/moment/src/moment.js", + "moment/": "/node_modules/moment/src/", + "lodash-dot": "./node_modules/lodash-es/lodash.js", + "lodash-dot/": "./node_modules/lodash-es/", + "lodash-dotdot": "../node_modules/lodash-es/lodash.js", + "lodash-dotdot/": "../node_modules/lodash-es/" + }, + "scopes": { + "/": { + "moment": "/node_modules_3/moment/src/moment.js", + "vue": "/node_modules_3/vue/dist/vue.runtime.esm.js" + }, + "/js/": { + "lodash-dot": "./node_modules_2/lodash-es/lodash.js", + "lodash-dot/": "./node_modules_2/lodash-es/", + "lodash-dotdot": "../node_modules_2/lodash-es/lodash.js", + "lodash-dotdot/": "../node_modules_2/lodash-es/" + } + } + }, + "tests": { + "Base URLs inside the scope should use the scope if the scope has matching keys": { + "baseURL": "https://example.com/js/app.mjs", + "expectedResults": { + "lodash-dot": "https://example.com/app/node_modules_2/lodash-es/lodash.js", + "lodash-dot/foo": "https://example.com/app/node_modules_2/lodash-es/foo", + "lodash-dotdot": "https://example.com/node_modules_2/lodash-es/lodash.js", + "lodash-dotdot/foo": "https://example.com/node_modules_2/lodash-es/foo" + } + }, + "Base URLs inside the scope fallback to less specific scope": { + "baseURL": "https://example.com/js/app.mjs", + "expectedResults": { + "moment": "https://example.com/node_modules_3/moment/src/moment.js", + "vue": "https://example.com/node_modules_3/vue/dist/vue.runtime.esm.js" + } + }, + "Base URLs inside the scope fallback to toplevel": { + "baseURL": "https://example.com/js/app.mjs", + "expectedResults": { + "moment/foo": "https://example.com/node_modules/moment/src/foo" + } + }, + "Base URLs outside a scope shouldn't use the scope even if the scope has matching keys": { + "baseURL": "https://example.com/app.mjs", + "expectedResults": { + "lodash-dot": "https://example.com/app/node_modules/lodash-es/lodash.js", + "lodash-dotdot": "https://example.com/node_modules/lodash-es/lodash.js", + "lodash-dot/foo": "https://example.com/app/node_modules/lodash-es/foo", + "lodash-dotdot/foo": "https://example.com/node_modules/lodash-es/foo" + } + }, + "Fallback to toplevel or not, depending on trailing slash match": { + "baseURL": "https://example.com/app.mjs", + "expectedResults": { + "moment": "https://example.com/node_modules_3/moment/src/moment.js", + "moment/foo": "https://example.com/node_modules/moment/src/foo" + } + }, + "should still fail for package-like specifiers that are not declared": { + "baseURL": "https://example.com/js/app.mjs", + "expectedResults": { + "underscore/": null, + "underscore/foo": null + } + } + } + } + } +} diff --git a/import-maps/common/resources/tricky-specifiers.json b/import-maps/common/resources/tricky-specifiers.json new file mode 100644 index 000000000000000..d9c80c947500058 --- /dev/null +++ b/import-maps/common/resources/tricky-specifiers.json @@ -0,0 +1,43 @@ +{ + "importMap": { + "imports": { + "package/withslash": "/node_modules/package-with-slash/index.mjs", + "not-a-package": "/lib/not-a-package.mjs", + "only-slash/": "/lib/only-slash/", + ".": "/lib/dot.mjs", + "..": "/lib/dotdot.mjs", + "..\\": "/lib/dotdotbackslash.mjs", + "%2E": "/lib/percent2e.mjs", + "%2F": "/lib/percent2f.mjs" + } + }, + "importMapBaseURL": "https://example.com/app/index.html", + "baseURL": "https://example.com/js/app.mjs", + "name": "Tricky specifiers", + "tests": { + "explicitly-mapped specifiers that happen to have a slash": { + "expectedResults": { + "package/withslash": "https://example.com/node_modules/package-with-slash/index.mjs" + } + }, + "specifier with punctuation": { + "expectedResults": { + ".": "https://example.com/lib/dot.mjs", + "..": "https://example.com/lib/dotdot.mjs", + "..\\": "https://example.com/lib/dotdotbackslash.mjs", + "%2E": "https://example.com/lib/percent2e.mjs", + "%2F": "https://example.com/lib/percent2f.mjs" + } + }, + "submodule of something not declared with a trailing slash should fail": { + "expectedResults": { + "not-a-package/foo": null + } + }, + "module for which only a trailing-slash version is present should fail": { + "expectedResults": { + "only-slash": null + } + } + } +} diff --git a/import-maps/common/resources/url-specifiers.json b/import-maps/common/resources/url-specifiers.json new file mode 100644 index 000000000000000..aff55c4d9dda05e --- /dev/null +++ b/import-maps/common/resources/url-specifiers.json @@ -0,0 +1,52 @@ +{ + "importMap": { + "imports": { + "/lib/foo.mjs": "./more/bar.mjs", + "./dotrelative/foo.mjs": "/lib/dot.mjs", + "../dotdotrelative/foo.mjs": "/lib/dotdot.mjs", + "/": "/lib/slash-only/", + "./": "/lib/dotslash-only/", + "/test/": "/lib/url-trailing-slash/", + "./test/": "/lib/url-trailing-slash-dot/", + "/test": "/lib/test1.mjs", + "../test": "/lib/test2.mjs" + } + }, + "importMapBaseURL": "https://example.com/app/index.html", + "baseURL": "https://example.com/js/app.mjs", + "name": "URL-like specifiers", + "tests": { + "Ordinal URL-like specifiers": { + "expectedResults": { + "https://example.com/lib/foo.mjs": "https://example.com/app/more/bar.mjs", + "https://///example.com/lib/foo.mjs": "https://example.com/app/more/bar.mjs", + "/lib/foo.mjs": "https://example.com/app/more/bar.mjs", + "https://example.com/app/dotrelative/foo.mjs": "https://example.com/lib/dot.mjs", + "../app/dotrelative/foo.mjs": "https://example.com/lib/dot.mjs", + "https://example.com/dotdotrelative/foo.mjs": "https://example.com/lib/dotdot.mjs", + "../dotdotrelative/foo.mjs": "https://example.com/lib/dotdot.mjs" + } + }, + "Import map entries just composed from / and .": { + "expectedResults": { + "https://example.com/": "https://example.com/lib/slash-only/", + "/": "https://example.com/lib/slash-only/", + "../": "https://example.com/lib/slash-only/", + "https://example.com/app/": "https://example.com/lib/dotslash-only/", + "/app/": "https://example.com/lib/dotslash-only/", + "../app/": "https://example.com/lib/dotslash-only/" + } + }, + "prefix-matched by keys with trailing slashes": { + "expectedResults": { + "/test/foo.mjs": "https://example.com/lib/url-trailing-slash/foo.mjs", + "https://example.com/app/test/foo.mjs": "https://example.com/lib/url-trailing-slash-dot/foo.mjs" + } + }, + "should use the last entry's address when URL-like specifiers parse to the same absolute URL": { + "expectedResults": { + "/test": "https://example.com/lib/test2.mjs" + } + } + } +} diff --git a/import-maps/common/tools/format_and_validate_json.py b/import-maps/common/tools/format_and_validate_json.py new file mode 100644 index 000000000000000..1a8abb9404e5a17 --- /dev/null +++ b/import-maps/common/tools/format_and_validate_json.py @@ -0,0 +1,22 @@ +import collections +import json +import os +import sys +import traceback + + +def main(): + for filename in sys.argv[1:]: + print filename + try: + spec = json.load( + open(filename, 'r'), object_pairs_hook=collections.OrderedDict) + with open(filename, 'w') as f: + f.write(json.dumps(spec, indent=2, separators=(',', ': '))) + f.write('\n') + except: + traceback.print_exc() + + +if __name__ == '__main__': + main() diff --git a/import-maps/imported/resolving-scopes.tentative.html b/import-maps/imported/resolving-scopes.tentative.html deleted file mode 100644 index 4985249f4e29519..000000000000000 --- a/import-maps/imported/resolving-scopes.tentative.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/import-maps/imported/resolving.tentative.html b/import-maps/imported/resolving.tentative.html deleted file mode 100644 index 339026259b0f0b8..000000000000000 --- a/import-maps/imported/resolving.tentative.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/import-maps/imported/resources/resolving-scopes.js b/import-maps/imported/resources/resolving-scopes.js deleted file mode 100644 index d133b50bd2b8d80..000000000000000 --- a/import-maps/imported/resources/resolving-scopes.js +++ /dev/null @@ -1,222 +0,0 @@ -'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'); - - 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", - "d": "/d-1.mjs" - }, - "scopes": { - "/scope2/": { - "a": "/a-2.mjs", - "d": "/d-2.mjs" - }, - "/scope2/scope3/": { - "b": "/b-3.mjs", - "d": "/d-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'); - expect(resolveUnderTest('d', scope1URL)).toMatchURL('https://example.com/d-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'); - expect(resolveUnderTest('d', scope2URL)).toMatchURL('https://example.com/d-2.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'); - expect(resolveUnderTest('d', scope3URL)).toMatchURL('https://example.com/d-3.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/imported/resources/resolving.js b/import-maps/imported/resources/resolving.js deleted file mode 100644 index ef8a4f87d25e7fc..000000000000000 --- a/import-maps/imported/resources/resolving.js +++ /dev/null @@ -1,232 +0,0 @@ -'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 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 /', () => { - 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)', () => { - 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/" - } - }`); - - 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); - }); - }); - - describe('Tricky specifiers', () => { - const resolveUnderTest = makeResolveUnderTest(`{ - "imports": { - "package/withslash": "/node_modules/package-with-slash/index.mjs", - "not-a-package": "/lib/not-a-package.mjs", - "only-slash/": "/lib/only-slash/", - ".": "/lib/dot.mjs", - "..": "/lib/dotdot.mjs", - "..\\\\": "/lib/dotdotbackslash.mjs", - "%2E": "/lib/percent2e.mjs", - "%2F": "/lib/percent2f.mjs" - } - }`); - - 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); - }); - - 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": { - "/lib/foo.mjs": "./more/bar.mjs", - "./dotrelative/foo.mjs": "/lib/dot.mjs", - "../dotdotrelative/foo.mjs": "/lib/dotdot.mjs", - - "/": "/lib/slash-only/", - "./": "/lib/dotslash-only/", - - "/test/": "/lib/url-trailing-slash/", - "./test/": "/lib/url-trailing-slash-dot/", - - "/test": "/lib/test1.mjs", - "../test": "/lib/test2.mjs" - } - }`); - - 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 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', () => { - 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 there are no mappings for less-specific keys', () => { - const resolveUnderTest = makeResolveUnderTest(`{ - "imports": { - "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 deal with data: URL bases', () => { - const resolveUnderTest = makeResolveUnderTest(`{ - "imports": { - "foo/": "data:text/javascript,foo/" - } - }`); - - expect(() => resolveUnderTest('foo/bar')).toThrow(TypeError); - }); -}); diff --git a/import-maps/resources/common-test-helper.js b/import-maps/resources/common-test-helper.js new file mode 100644 index 000000000000000..0e14fcf78a2acde --- /dev/null +++ b/import-maps/resources/common-test-helper.js @@ -0,0 +1,182 @@ +setup({allow_uncaught_exception : true}); + +// Sort keys and then stringify for comparison. +function stringifyImportMap(importMap) { + function getKeys(m) { + if (typeof m !== 'object') + return []; + + let keys = []; + for (const key in m) { + keys.push(key); + keys = keys.concat(getKeys(m[key])); + } + return keys; + } + return JSON.stringify(importMap, getKeys(importMap).sort()); +} + +// Creates a new Document (via