From 5663357e142b4c6b144fe4a221b3d8af1f1477d7 Mon Sep 17 00:00:00 2001 From: Wes Todd Date: Fri, 20 Oct 2023 22:17:00 -0500 Subject: [PATCH] module: add import map support --- doc/api/errors.md | 7 + lib/internal/errors.js | 1 + lib/internal/modules/esm/import_map.js | 107 +++++++++ lib/internal/modules/esm/loader.js | 6 + lib/internal/modules/esm/resolve.js | 76 ++++--- lib/internal/modules/run_main.js | 17 +- src/node_options.cc | 4 + src/node_options.h | 1 + test/es-module/test-importmap.mjs | 203 ++++++++++++++++++ .../importmaps/array-imports.json | 3 + .../es-module-loaders/importmaps/baz.mjs | 3 + .../es-module-loaders/importmaps/dataurl.json | 6 + .../es-module-loaders/importmaps/empty.json | 4 + .../es-module-loaders/importmaps/index.mjs | 2 + .../es-module-loaders/importmaps/invalid.json | 4 + .../importmaps/missing-scopes.json | 4 + .../importmaps/node_modules/bar/index.mjs | 3 + .../bar/node_modules/zed/index.mjs | 3 + .../bar/node_modules/zed/package.json | 3 + .../node_modules/bar/node_modules/zed/zed.mjs | 4 + .../importmaps/node_modules/bar/package.json | 3 + .../importmaps/node_modules/bar/zed.mjs | 4 + .../importmaps/node_modules/foo/index.mjs | 4 + .../importmaps/node_modules/foo/package.json | 3 + .../importmaps/node_modules/importmap.json | 10 + .../es-module-loaders/importmaps/qux.mjs | 3 + .../es-module-loaders/importmaps/simple.json | 10 + .../importmaps/unordered-scopes.json | 16 ++ 28 files changed, 486 insertions(+), 28 deletions(-) create mode 100644 lib/internal/modules/esm/import_map.js create mode 100644 test/es-module/test-importmap.mjs create mode 100644 test/fixtures/es-module-loaders/importmaps/array-imports.json create mode 100644 test/fixtures/es-module-loaders/importmaps/baz.mjs create mode 100644 test/fixtures/es-module-loaders/importmaps/dataurl.json create mode 100644 test/fixtures/es-module-loaders/importmaps/empty.json create mode 100644 test/fixtures/es-module-loaders/importmaps/index.mjs create mode 100644 test/fixtures/es-module-loaders/importmaps/invalid.json create mode 100644 test/fixtures/es-module-loaders/importmaps/missing-scopes.json create mode 100644 test/fixtures/es-module-loaders/importmaps/node_modules/bar/index.mjs create mode 100644 test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/index.mjs create mode 100644 test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/package.json create mode 100644 test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/zed.mjs create mode 100644 test/fixtures/es-module-loaders/importmaps/node_modules/bar/package.json create mode 100644 test/fixtures/es-module-loaders/importmaps/node_modules/bar/zed.mjs create mode 100644 test/fixtures/es-module-loaders/importmaps/node_modules/foo/index.mjs create mode 100644 test/fixtures/es-module-loaders/importmaps/node_modules/foo/package.json create mode 100644 test/fixtures/es-module-loaders/importmaps/node_modules/importmap.json create mode 100644 test/fixtures/es-module-loaders/importmaps/qux.mjs create mode 100644 test/fixtures/es-module-loaders/importmaps/simple.json create mode 100644 test/fixtures/es-module-loaders/importmaps/unordered-scopes.json diff --git a/doc/api/errors.md b/doc/api/errors.md index 50e9f658fcbf3a..f9ed5a82058345 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -1947,6 +1947,13 @@ for more information. An invalid HTTP token was supplied. + + +### `ERR_INVALID_IMPORT_MAP` + +An invalid import map file was supplied. This error can throw for a variety +of conditions which will change the error message for added context. + ### `ERR_INVALID_IP_ADDRESS` diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 363b9d3bb8fe8b..c7fbd4aa7e680c 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1415,6 +1415,7 @@ E('ERR_INVALID_FILE_URL_HOST', E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError); E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError); E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError); +E('ERR_INVALID_IMPORT_MAP', 'Invalid import map: %s', Error); E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError); E('ERR_INVALID_MIME_SYNTAX', (production, str, invalidIndex) => { const msg = invalidIndex !== -1 ? ` at ${invalidIndex}` : ''; diff --git a/lib/internal/modules/esm/import_map.js b/lib/internal/modules/esm/import_map.js new file mode 100644 index 00000000000000..e8829f25db60de --- /dev/null +++ b/lib/internal/modules/esm/import_map.js @@ -0,0 +1,107 @@ +'use strict'; +const { isURL, URL } = require('internal/url'); +const { ObjectEntries, ObjectKeys, SafeMap, ArrayIsArray } = primordials; +const { codes: { ERR_INVALID_IMPORT_MAP } } = require('internal/errors'); + +class ImportMap { + #baseURL; + imports = new SafeMap(); + scopes = new SafeMap(); + + constructor(raw, baseURL) { + this.#baseURL = baseURL; + processImportMap(this, this.#baseURL, raw); + } + + get baseURL() { + return this.#baseURL; + } + + resolve(specifier, parentURL = this.baseURL) { + // Process scopes + for (const { 0: prefix, 1: mapping } of this.scopes) { + let mappedSpecifier = mapping.get(specifier); + if (parentURL.pathname.startsWith(prefix.pathname) && mappedSpecifier) { + if (!isURL(mappedSpecifier)) { + mappedSpecifier = new URL(mappedSpecifier, this.baseURL); + mapping.set(specifier, mappedSpecifier); + } + specifier = mappedSpecifier; + break; + } + } + + let spec = specifier; + if (isURL(specifier)) { + spec = specifier.pathname; + } + let importMapping = this.imports.get(spec); + if (importMapping) { + if (!isURL(importMapping)) { + importMapping = new URL(importMapping, this.baseURL); + this.imports.set(spec, importMapping); + } + return importMapping; + } + + return specifier; + } +} + +function processImportMap(importMap, baseURL, raw) { + // Validation and normalization + if (typeof raw.imports !== 'object' || ArrayIsArray(raw.imports)) { + throw new ERR_INVALID_IMPORT_MAP('top level key "imports" is required and must be a plain object'); + } + if (typeof raw.scopes !== 'object' || ArrayIsArray(raw.scopes)) { + throw new ERR_INVALID_IMPORT_MAP('top level key "scopes" is required and must be a plain object'); + } + + // Normalize imports + for (const { 0: specifier, 1: mapping } of ObjectEntries(raw.imports)) { + if (!specifier || typeof specifier !== 'string') { + throw new ERR_INVALID_IMPORT_MAP('module specifier keys must be non-empty strings'); + } + if (!mapping || typeof mapping !== 'string') { + throw new ERR_INVALID_IMPORT_MAP('module specifier values must be non-empty strings'); + } + if (specifier.endsWith('/') && !mapping.endsWith('/')) { + throw new ERR_INVALID_IMPORT_MAP('module specifier values for keys ending with / must also end with /'); + } + + importMap.imports.set(specifier, mapping); + } + + // Normalize scopes + // Sort the keys according to spec and add to the map in order + // which preserves the sorted map requirement + const sortedScopes = ObjectKeys(raw.scopes).sort().reverse(); + for (let scope of sortedScopes) { + const _scopeMap = raw.scopes[scope]; + if (!scope || typeof scope !== 'string') { + throw new ERR_INVALID_IMPORT_MAP('import map scopes keys must be non-empty strings'); + } + if (!_scopeMap || typeof _scopeMap !== 'object') { + throw new ERR_INVALID_IMPORT_MAP(`scope values must be plain objects (${scope} is ${typeof _scopeMap})`); + } + + // Normalize scope + scope = new URL(scope, baseURL); + + const scopeMap = new SafeMap(); + for (const { 0: specifier, 1: mapping } of ObjectEntries(_scopeMap)) { + if (specifier.endsWith('/') && !mapping.endsWith('/')) { + throw new ERR_INVALID_IMPORT_MAP('module specifier values for keys ending with / must also end with /'); + } + scopeMap.set(specifier, mapping); + } + + importMap.scopes.set(scope, scopeMap); + } + + return importMap; +} + +module.exports = { + ImportMap, +}; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 6044765c3709f5..3cc384066ca944 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -129,6 +129,11 @@ class ModuleLoader { */ #customizations; + /** + * The loaders importMap instance + */ + importMap; + constructor(customizations) { if (getOptionValue('--experimental-network-imports')) { emitExperimentalWarning('Network Imports'); @@ -391,6 +396,7 @@ class ModuleLoader { conditions: this.#defaultConditions, importAttributes, parentURL, + importMap: this.importMap, }; return defaultResolve(originalSpecifier, context); diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 06a34c11254a2f..7e06e8714d2aa8 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -1026,6 +1026,35 @@ function throwIfInvalidParentURL(parentURL) { } } +/** + * Process policy + */ +function processPolicy(specifier, context) { + const { parentURL, conditions } = context; + const redirects = policy.manifest.getDependencyMapper(parentURL); + if (redirects) { + const { resolve, reaction } = redirects; + const destination = resolve(specifier, new SafeSet(conditions)); + let missing = true; + if (destination === true) { + missing = false; + } else if (destination) { + const href = destination.href; + return { __proto__: null, url: href }; + } + if (missing) { + // Prevent network requests from firing if resolution would be banned. + // Network requests can extract data by doing things like putting + // secrets in query params + reaction(new ERR_MANIFEST_DEPENDENCY_MISSING( + parentURL, + specifier, + ArrayPrototypeJoin([...conditions], ', ')), + ); + } + } +} + /** * Resolves the given specifier using the provided context, which includes the parent URL and conditions. * Throws an error if the parent URL is invalid or if the resolution is disallowed by the policy manifest. @@ -1037,31 +1066,8 @@ function throwIfInvalidParentURL(parentURL) { */ function defaultResolve(specifier, context = {}) { let { parentURL, conditions } = context; + const { importMap } = context; throwIfInvalidParentURL(parentURL); - if (parentURL && policy?.manifest) { - const redirects = policy.manifest.getDependencyMapper(parentURL); - if (redirects) { - const { resolve, reaction } = redirects; - const destination = resolve(specifier, new SafeSet(conditions)); - let missing = true; - if (destination === true) { - missing = false; - } else if (destination) { - const href = destination.href; - return { __proto__: null, url: href }; - } - if (missing) { - // Prevent network requests from firing if resolution would be banned. - // Network requests can extract data by doing things like putting - // secrets in query params - reaction(new ERR_MANIFEST_DEPENDENCY_MISSING( - parentURL, - specifier, - ArrayPrototypeJoin([...conditions], ', ')), - ); - } - } - } let parsedParentURL; if (parentURL) { @@ -1079,8 +1085,19 @@ function defaultResolve(specifier, context = {}) { } else { parsed = new URL(specifier); } + } catch { + // Ignore exception + } - // Avoid accessing the `protocol` property due to the lazy getters. + // Import maps are processed before policies and data/http handling + // so policies apply to the result of any mapping + if (importMap) { + // Intentionally mutating here as we don't think it is a problem + parsed = specifier = importMap.resolve(parsed || specifier, parsedParentURL); + } + + // Avoid accessing the `protocol` property due to the lazy getters. + if (parsed) { const protocol = parsed.protocol; if (protocol === 'data:' || (experimentalNetworkImports && @@ -1092,8 +1109,13 @@ function defaultResolve(specifier, context = {}) { ) { return { __proto__: null, url: parsed.href }; } - } catch { - // Ignore exception + } + + if (parentURL && policy?.manifest) { + const policyResolution = processPolicy(specifier, context); + if (policyResolution) { + return policyResolution; + } } // There are multiple deep branches that can either throw or return; instead diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index 1f03c313121db0..60653600106283 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -51,6 +51,7 @@ function resolveMainPath(main) { */ function shouldUseESMLoader(mainPath) { if (getOptionValue('--experimental-default-type') === 'module') { return true; } + if (getOptionValue('--experimental-import-map')) { return true; } /** * @type {string[]} userLoaders A list of custom loaders registered by the user @@ -92,10 +93,24 @@ function shouldUseESMLoader(mainPath) { */ function runMainESM(mainPath) { const { loadESM } = require('internal/process/esm_loader'); - const { pathToFileURL } = require('internal/url'); + const { pathToFileURL, URL } = require('internal/url'); + const _importMapPath = getOptionValue('--experimental-import-map'); const main = pathToFileURL(mainPath).href; handleMainPromise(loadESM((esmLoader) => { + // Load import map and throw validation errors + if (_importMapPath) { + const { ImportMap } = require('internal/modules/esm/import_map'); + const { getCWDURL } = require('internal/util'); + + const importMapPath = esmLoader.resolve(_importMapPath, getCWDURL(), { __proto__: null, type: 'json' }); + return esmLoader.import(importMapPath.url, getCWDURL(), { __proto__: null, type: 'json' }) + .then((importedMapFile) => { + esmLoader.importMap = new ImportMap(importedMapFile.default, new URL(importMapPath.url)); + return esmLoader.import(main, undefined, { __proto__: null }); + }); + } + return esmLoader.import(main, undefined, { __proto__: null }); })); } diff --git a/src/node_options.cc b/src/node_options.cc index 29cb7fc6b29b89..461c66e49ea3d0 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -546,6 +546,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { &EnvironmentOptions::prof_process); // Options after --prof-process are passed through to the prof processor. AddAlias("--prof-process", { "--prof-process", "--" }); + AddOption("--experimental-import-map", + "set the path to an import map.json", + &EnvironmentOptions::import_map_path, + kAllowedInEnvvar); #if HAVE_INSPECTOR AddOption("--cpu-prof", "Start the V8 CPU profiler on start up, and write the CPU profile " diff --git a/src/node_options.h b/src/node_options.h index 30955c779714ce..32082b1a15af4d 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -179,6 +179,7 @@ class EnvironmentOptions : public Options { bool extra_info_on_fatal_exception = true; std::string unhandled_rejections; std::vector userland_loaders; + std::string import_map_path; bool verify_base_objects = #ifdef DEBUG true; diff --git a/test/es-module/test-importmap.mjs b/test/es-module/test-importmap.mjs new file mode 100644 index 00000000000000..9241b43651ccbc --- /dev/null +++ b/test/es-module/test-importmap.mjs @@ -0,0 +1,203 @@ +// Flags: --expose-internals + +import { spawnPromisified } from '../common/index.mjs'; +import fixtures from '../common/fixtures.js'; +import tmpdir from '../common/tmpdir.js'; +import { describe, it } from 'node:test'; +import assert from 'node:assert'; +import path from 'node:path'; +import { execPath } from 'node:process'; +import { pathToFileURL } from 'node:url'; +import { writeFile } from 'node:fs/promises'; +import http from 'node:http'; +import import_map from 'internal/modules/esm/import_map'; +const { ImportMap } = import_map; +import binding from 'internal/test/binding'; +const { primordials: { SafeMap, JSONStringify } } = binding; + +const importMapFixtureRoot = fixtures.path('es-module-loaders', 'importmaps'); +const entryPoint = pathToFileURL(path.resolve(importMapFixtureRoot, 'index.mjs')); +const getImportMapPathURL = (name) => { + return pathToFileURL(path.resolve(importMapFixtureRoot, name + '.json')); +}; +const getImportMap = async (name) => { + const url = getImportMapPathURL(name); + const rawMap = await import(url, { with: { type: 'json' } }); + return new ImportMap(rawMap.default, url); +}; + +describe('Import Maps', { concurrency: true }, () => { + tmpdir.refresh(); + it('processImportMap - simple importmap', async () => { + const importMap = await getImportMap('simple'); + assert.deepStrictEqual(importMap.imports, new SafeMap(Object.entries({ + foo: './node_modules/foo/index.mjs' + }))); + const expectedScopes = new SafeMap(); + const fooScopeKey = new URL(importMap.baseURL, pathToFileURL('node_modules/foo')); + const fooScopeMap = new SafeMap(Object.entries({ + bar: './baz.mjs' + })); + expectedScopes.set(fooScopeKey, fooScopeMap); + assert.deepStrictEqual(importMap.scopes, expectedScopes); + }); + + it('processImportMap - invalid importmap', async () => { + assert.rejects( + getImportMap('invalid'), + /^Error \[ERR_INVALID_IMPORT_MAP\]: Invalid import map: top level key "imports" is required and must be a plain object$/ + ); + assert.rejects( + getImportMap('missing-scopes'), + /^Error \[ERR_INVALID_IMPORT_MAP\]: Invalid import map: top level key "scopes" is required and must be a plain object$/ + ); + assert.rejects( + getImportMap('array-imports'), + /^Error \[ERR_INVALID_IMPORT_MAP\]: Invalid import map: top level key "imports" is required and must be a plain object$/ + ); + }); + + it('resolve - empty importmap', async () => { + const importMap = await getImportMap('empty'); + const spec = importMap.resolve('test'); + assert.strictEqual(spec, 'test'); + }); + + it('resolve - simple importmap', async () => { + const importMap = await getImportMap('simple'); + assert.strictEqual( + importMap.resolve('foo').pathname, + new URL('node_modules/foo/index.mjs', entryPoint).pathname + ); + assert.strictEqual( + importMap.resolve('bar', new URL('node_modules/foo/index.mjs', entryPoint)).pathname, + new URL('baz.mjs', entryPoint).pathname + ); + assert.strictEqual(importMap.resolve('bar'), 'bar'); + }); + + it('resolve - nested scopes', async () => { + const importMap = await getImportMap('unordered-scopes'); + assert.strictEqual( + importMap.resolve('zed', new URL('node_modules/bar', entryPoint)).pathname, + new URL('node_modules/bar/node_modules/zed/index.mjs', entryPoint).pathname + ); + assert.strictEqual( + importMap.resolve('zed', new URL('node_modules/bar/node_modules/zed', entryPoint)).pathname, + new URL('baz.mjs', entryPoint).pathname + ); + }); + + it('resolve - data url', async () => { + const importMap = await getImportMap('dataurl'); + assert.strictEqual( + importMap.resolve('foo').href, + 'data:text/javascript,export default () => \'data\'' + ); + }); + + it('should pass --experimental-import-map', async () => { + const importMapPath = fixtures.path('es-module-loaders/importmaps/simple.json'); + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--experimental-import-map', importMapPath, + entryPoint.pathname, + ], { + cwd: fixtures.path('es-module-loaders/importmaps'), + }); + + assert.strictEqual(code, 0, stderr); + assert.strictEqual(stdout, 'baz\n'); + assert.strictEqual(signal, null); + }); + + it('should throw on startup on invalid import map', async () => { + const importMapPath = fixtures.path('es-module-loaders/importmaps/invalid.json'); + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--experimental-import-map', importMapPath, + entryPoint.pathname, + ], { + cwd: fixtures.path('es-module-loaders/importmaps'), + }); + + assert.strictEqual(code, 1); + assert.strictEqual(signal, null); + assert.strictEqual(stdout, ''); + assert(stderr.includes('Invalid import map: top level key "imports" is required')); + }); + + it('should handle import maps with absolute paths', async () => { + const importMapPath = path.resolve(tmpdir.path, 'absolute.json'); + await writeFile(importMapPath, JSONStringify({ + imports: { + foo: fixtures.path('es-module-loaders/importmaps/node_modules/foo/index.mjs'), + [fixtures.path('es-module-loaders/importmaps/baz.mjs')]: fixtures.path('es-module-loaders/importmaps/qux.mjs'), + }, + scopes: { + [fixtures.path('es-module-loaders/importmaps/node_modules/foo')]: { + bar: fixtures.path('es-module-loaders/importmaps/baz.mjs'), + } + } + })); + + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--experimental-import-map', importMapPath, + entryPoint.pathname, + ], { + cwd: fixtures.path('es-module-loaders/importmaps'), + }); + + assert.strictEqual(code, 0, stderr); + assert.strictEqual(signal, null); + assert.strictEqual(stdout, 'qux\n'); + }); + + it('should handle import maps with data urls', async () => { + const importMapPath = getImportMapPathURL('dataurl').pathname; + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--experimental-import-map', importMapPath, + entryPoint.pathname, + ], { + cwd: importMapFixtureRoot, + }); + + assert.strictEqual(code, 0, stderr); + assert.strictEqual(signal, null); + assert.strictEqual(stdout, 'data\n'); + }); + + it('should handle http imports', async () => { + const server = http.createServer((req, res) => { + res + .writeHead(200, { 'Content-Type': 'application/javascript' }) + .end('export default () => \'http\''); + }); + await (new Promise((resolve, reject) => { + server.listen((err) => { + if (err) return reject(err); + resolve(); + }); + })); + const { port } = server.address(); + + const importMapPath = path.resolve(tmpdir.path, 'http.json'); + await writeFile(importMapPath, JSONStringify({ + imports: { + foo: `http://localhost:${port}` + }, + scopes: {} + })); + + const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ + '--experimental-network-imports', + '--experimental-import-map', importMapPath, + entryPoint.pathname, + ], { + cwd: importMapFixtureRoot, + }); + + server.close(); + assert.strictEqual(code, 0, stderr); + assert.strictEqual(signal, null); + assert.strictEqual(stdout, 'http\n'); + }); +}); diff --git a/test/fixtures/es-module-loaders/importmaps/array-imports.json b/test/fixtures/es-module-loaders/importmaps/array-imports.json new file mode 100644 index 00000000000000..b7c16376f5c26a --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/array-imports.json @@ -0,0 +1,3 @@ +{ + "imports": [] +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/baz.mjs b/test/fixtures/es-module-loaders/importmaps/baz.mjs new file mode 100644 index 00000000000000..eecc080ab601de --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/baz.mjs @@ -0,0 +1,3 @@ +export default () => { + return 'baz'; +}; \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/dataurl.json b/test/fixtures/es-module-loaders/importmaps/dataurl.json new file mode 100644 index 00000000000000..6e2be85035f4e5 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/dataurl.json @@ -0,0 +1,6 @@ +{ + "imports": { + "foo": "data:text/javascript,export default () => 'data'" + }, + "scopes": {} +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/empty.json b/test/fixtures/es-module-loaders/importmaps/empty.json new file mode 100644 index 00000000000000..85f87be068855b --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/empty.json @@ -0,0 +1,4 @@ +{ + "imports": {}, + "scopes": {} +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/index.mjs b/test/fixtures/es-module-loaders/importmaps/index.mjs new file mode 100644 index 00000000000000..591640d8bb7a2d --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/index.mjs @@ -0,0 +1,2 @@ +import foo from 'foo'; +console.log(foo()); \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/invalid.json b/test/fixtures/es-module-loaders/importmaps/invalid.json new file mode 100644 index 00000000000000..5d148e2cb573d1 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/invalid.json @@ -0,0 +1,4 @@ +{ + "missing": "the required keys", + "scopes": [] +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/missing-scopes.json b/test/fixtures/es-module-loaders/importmaps/missing-scopes.json new file mode 100644 index 00000000000000..99df1f906a4ae9 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/missing-scopes.json @@ -0,0 +1,4 @@ +{ + "imports": {}, + "missing": "the scopes key" +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/node_modules/bar/index.mjs b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/index.mjs new file mode 100644 index 00000000000000..b47e8304a8897f --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/index.mjs @@ -0,0 +1,3 @@ +export default () => { + return 'bar'; +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/index.mjs b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/index.mjs new file mode 100644 index 00000000000000..564f4b94f08714 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/index.mjs @@ -0,0 +1,3 @@ +export default () => { + return 'zed'; +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/package.json b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/package.json new file mode 100644 index 00000000000000..94d250b64f9ac8 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/package.json @@ -0,0 +1,3 @@ +{ + "main": "index.mjs" +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/zed.mjs b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/zed.mjs new file mode 100644 index 00000000000000..0f33a3c44118c4 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/node_modules/zed/zed.mjs @@ -0,0 +1,4 @@ +import baz from 'zed'; +export default () => { + return baz(); +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/node_modules/bar/package.json b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/package.json new file mode 100644 index 00000000000000..94d250b64f9ac8 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/package.json @@ -0,0 +1,3 @@ +{ + "main": "index.mjs" +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/node_modules/bar/zed.mjs b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/zed.mjs new file mode 100644 index 00000000000000..0ffb0273859feb --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/node_modules/bar/zed.mjs @@ -0,0 +1,4 @@ +import zed from 'zed'; +export default () => { + return zed(); +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/node_modules/foo/index.mjs b/test/fixtures/es-module-loaders/importmaps/node_modules/foo/index.mjs new file mode 100644 index 00000000000000..683543f39a735c --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/node_modules/foo/index.mjs @@ -0,0 +1,4 @@ +import bar from 'bar'; +export default () => { + return bar(); +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/node_modules/foo/package.json b/test/fixtures/es-module-loaders/importmaps/node_modules/foo/package.json new file mode 100644 index 00000000000000..94d250b64f9ac8 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/node_modules/foo/package.json @@ -0,0 +1,3 @@ +{ + "main": "index.mjs" +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/node_modules/importmap.json b/test/fixtures/es-module-loaders/importmaps/node_modules/importmap.json new file mode 100644 index 00000000000000..90973e70f22af1 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/node_modules/importmap.json @@ -0,0 +1,10 @@ +{ + "imports": { + "foo": "node_modules/foo" + }, + "scopes": { + "node_modules/foo": { + "bar": "baz" + } + } +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/qux.mjs b/test/fixtures/es-module-loaders/importmaps/qux.mjs new file mode 100644 index 00000000000000..b0ee0222e13397 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/qux.mjs @@ -0,0 +1,3 @@ +export default () => { + return 'qux'; +}; \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/simple.json b/test/fixtures/es-module-loaders/importmaps/simple.json new file mode 100644 index 00000000000000..01460066610040 --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/simple.json @@ -0,0 +1,10 @@ +{ + "imports": { + "foo": "./node_modules/foo/index.mjs" + }, + "scopes": { + "node_modules/foo": { + "bar": "./baz.mjs" + } + } +} \ No newline at end of file diff --git a/test/fixtures/es-module-loaders/importmaps/unordered-scopes.json b/test/fixtures/es-module-loaders/importmaps/unordered-scopes.json new file mode 100644 index 00000000000000..043b8ad682714e --- /dev/null +++ b/test/fixtures/es-module-loaders/importmaps/unordered-scopes.json @@ -0,0 +1,16 @@ +{ + "imports": { + "foo": "node_modules/foo/index.mjs" + }, + "scopes": { + "node_modules/foo": { + "bar": "node_modules/bar/zed.mjs" + }, + "node_modules/bar": { + "zed": "node_modules/bar/node_modules/zed/index.mjs" + }, + "node_modules/bar/node_modules/zed": { + "zed": "baz.mjs" + } + } +} \ No newline at end of file