GeezerVison is showing github.com — module: ensure successful import returns the same result · nodejs/node@14e148e
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboard +45Lines changed: 45 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Tests the impact on eager operations required for policies affecting | | 2 | +// general startup, does not test lazy operations | | 3 | +'use strict'; | | 4 | +const fs = require('node:fs'); | | 5 | +const path = require('node:path'); | | 6 | +const common = require('../common.js'); | | 7 | + | | 8 | +const tmpdir = require('../../test/common/tmpdir.js'); | | 9 | +const { pathToFileURL } = require('node:url'); | | 10 | + | | 11 | +const benchmarkDirectory = pathToFileURL(path.resolve(tmpdir.path, 'benchmark-import')); | | 12 | + | | 13 | +const configs = { | | 14 | + n: [1e3], | | 15 | + specifier: [ | | 16 | + 'data:text/javascript,{i}', | | 17 | + './relative-existing.js', | | 18 | + './relative-nonexistent.js', | | 19 | + 'node:prefixed-nonexistent', | | 20 | + 'node:os', | | 21 | + ], | | 22 | +}; | | 23 | + | | 24 | +const options = { | | 25 | + flags: ['--expose-internals'], | | 26 | +}; | | 27 | + | | 28 | +const bench = common.createBenchmark(main, configs, options); | | 29 | + | | 30 | +async function main(conf) { | | 31 | + tmpdir.refresh(); | | 32 | + | | 33 | + fs.mkdirSync(benchmarkDirectory, { recursive: true }); | | 34 | + fs.writeFileSync(new URL('./relative-existing.js', benchmarkDirectory), '\n'); | | 35 | + | | 36 | + bench.start(); | | 37 | + | | 38 | + for (let i = 0; i < conf.n; i++) { | | 39 | + try { | | 40 | + await import(new URL(conf.specifier.replace('{i}', i), benchmarkDirectory)); | | 41 | + } catch { /* empty */ } | | 42 | + } | | 43 | + | | 44 | + bench.end(conf.n); | | 45 | +} |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/loader.js +27-10Lines changed: 27 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -20,9 +20,14 @@ const { | 20 | 20 | } = require('internal/modules/esm/utils'); | 21 | 21 | let defaultResolve, defaultLoad, importMetaInitializer; | 22 | 22 |
| 23 | | -function newModuleMap() { | 24 | | - const ModuleMap = require('internal/modules/esm/module_map'); | 25 | | - return new ModuleMap(); | | 23 | +function newResolveCache() { | | 24 | + const { ResolveCache } = require('internal/modules/esm/module_map'); | | 25 | + return new ResolveCache(); | | 26 | +} | | 27 | + | | 28 | +function newLoadCache() { | | 29 | + const { LoadCache } = require('internal/modules/esm/module_map'); | | 30 | + return new LoadCache(); | 26 | 31 | } | 27 | 32 |
| 28 | 33 | function getTranslators() { | @@ -73,10 +78,15 @@ class ModuleLoader { | 73 | 78 | */ | 74 | 79 | evalIndex = 0; | 75 | 80 |
| | 81 | + /** | | 82 | + * Registry of resolved specifiers | | 83 | + */ | | 84 | + #resolveCache = newResolveCache(); | | 85 | + | 76 | 86 | /** | 77 | 87 | * Registry of loaded modules, akin to `require.cache` | 78 | 88 | */ | 79 | | - moduleMap = newModuleMap(); | | 89 | + loadCache = newLoadCache(); | 80 | 90 |
| 81 | 91 | /** | 82 | 92 | * Methods which translate input code or other information into ES modules | @@ -195,7 +205,7 @@ class ModuleLoader { | 195 | 205 | const ModuleJob = require('internal/modules/esm/module_job'); | 196 | 206 | const job = new ModuleJob( | 197 | 207 | this, url, undefined, evalInstance, false, false); | 198 | | - this.moduleMap.set(url, undefined, job); | | 208 | + this.loadCache.set(url, undefined, job); | 199 | 209 | const { module } = await job.run(); | 200 | 210 |
| 201 | 211 | return { | @@ -224,11 +234,11 @@ class ModuleLoader { | 224 | 234 | getJobFromResolveResult(resolveResult, parentURL, importAssertions) { | 225 | 235 | const { url, format } = resolveResult; | 226 | 236 | const resolvedImportAssertions = resolveResult.importAssertions ?? importAssertions; | 227 | | - let job = this.moduleMap.get(url, resolvedImportAssertions.type); | | 237 | + let job = this.loadCache.get(url, resolvedImportAssertions.type); | 228 | 238 |
| 229 | 239 | // CommonJS will set functions for lazy job evaluation. | 230 | 240 | if (typeof job === 'function') { | 231 | | - this.moduleMap.set(url, undefined, job = job()); | | 241 | + this.loadCache.set(url, undefined, job = job()); | 232 | 242 | } | 233 | 243 |
| 234 | 244 | if (job === undefined) { | @@ -288,7 +298,7 @@ class ModuleLoader { | 288 | 298 | inspectBrk, | 289 | 299 | ); | 290 | 300 |
| 291 | | - this.moduleMap.set(url, importAssertions.type, job); | | 301 | + this.loadCache.set(url, importAssertions.type, job); | 292 | 302 |
| 293 | 303 | return job; | 294 | 304 | } | @@ -326,13 +336,20 @@ class ModuleLoader { | 326 | 336 | * @param {string} [parentURL] The URL path of the module's parent. | 327 | 337 | * @param {ImportAssertions} importAssertions Assertions from the import | 328 | 338 | * statement or expression. | 329 | | - * @returns {Promise<{ format: string, url: URL['href'] }>} | | 339 | + * @returns {{ format: string, url: URL['href'] }} | 330 | 340 | */ | 331 | 341 | resolve(originalSpecifier, parentURL, importAssertions) { | 332 | 342 | if (this.#customizations) { | 333 | 343 | return this.#customizations.resolve(originalSpecifier, parentURL, importAssertions); | 334 | 344 | } | 335 | | - return this.defaultResolve(originalSpecifier, parentURL, importAssertions); | | 345 | + const requestKey = this.#resolveCache.serializeKey(originalSpecifier, importAssertions); | | 346 | + const cachedResult = this.#resolveCache.get(requestKey, parentURL); | | 347 | + if (cachedResult != null) { | | 348 | + return cachedResult; | | 349 | + } | | 350 | + const result = this.defaultResolve(originalSpecifier, parentURL, importAssertions); | | 351 | + this.#resolveCache.set(requestKey, parentURL, result); | | 352 | + return result; | 336 | 353 | } | 337 | 354 |
| 338 | 355 | /** | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/module_job.js +4-2Lines changed: 4 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -22,7 +22,7 @@ const { | 22 | 22 |
| 23 | 23 | const { ModuleWrap } = internalBinding('module_wrap'); | 24 | 24 |
| 25 | | -const { decorateErrorStack } = require('internal/util'); | | 25 | +const { decorateErrorStack, kEmptyObject } = require('internal/util'); | 26 | 26 | const { | 27 | 27 | getSourceMapsEnabled, | 28 | 28 | } = require('internal/source_map/source_map_cache'); | @@ -141,7 +141,9 @@ class ModuleJob { | 141 | 141 | /module '(.*)' does not provide an export named '(.+)'/, | 142 | 142 | e.message); | 143 | 143 | const { url: childFileURL } = await this.loader.resolve( | 144 | | - childSpecifier, parentFileUrl, | | 144 | + childSpecifier, | | 145 | + parentFileUrl, | | 146 | + kEmptyObject, | 145 | 147 | ); | 146 | 148 | let format; | 147 | 149 | try { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/module_map.js +84-5Lines changed: 84 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 |
| 3 | | -const { kImplicitAssertType } = require('internal/modules/esm/assert'); | 4 | 3 | const { | | 4 | + ArrayPrototypeJoin, | | 5 | + ArrayPrototypeMap, | | 6 | + ArrayPrototypeSort, | | 7 | + JSONStringify, | 5 | 8 | ObjectCreate, | | 9 | + ObjectKeys, | 6 | 10 | SafeMap, | 7 | 11 | } = primordials; | | 12 | +const { kImplicitAssertType } = require('internal/modules/esm/assert'); | 8 | 13 | let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { | 9 | 14 | debug = fn; | 10 | 15 | }); | 11 | 16 | const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes; | 12 | 17 | const { validateString } = require('internal/validators'); | 13 | 18 |
| 14 | | -// Tracks the state of the loader-level module cache | 15 | | -class ModuleMap extends SafeMap { | | 19 | +/** | | 20 | + * Cache the results of the `resolve` step of the module resolution and loading process. | | 21 | + * Future resolutions of the same input (specifier, parent URL and import assertions) | | 22 | + * must return the same result if the first attempt was successful, per | | 23 | + * https://tc39.es/ecma262/#sec-HostLoadImportedModule. | | 24 | + * This cache is *not* used when custom loaders are registered. | | 25 | + */ | | 26 | +class ResolveCache extends SafeMap { | | 27 | + constructor(i) { super(i); } // eslint-disable-line no-useless-constructor | | 28 | + | | 29 | + /** | | 30 | + * Generates the internal serialized cache key and returns it along the actual cache object. | | 31 | + * | | 32 | + * It is exposed to allow more efficient read and overwrite a cache entry. | | 33 | + * @param {string} specifier | | 34 | + * @param {Record<string,string>} importAssertions | | 35 | + * @returns {string} | | 36 | + */ | | 37 | + serializeKey(specifier, importAssertions) { | | 38 | + // To serialize the ModuleRequest (specifier + list of import assertions), | | 39 | + // we need to sort the assertions by key, then stringifying, | | 40 | + // so that different import statements with the same assertions are always treated | | 41 | + // as identical. | | 42 | + const keys = ObjectKeys(importAssertions); | | 43 | + | | 44 | + if (keys.length === 0) { | | 45 | + return specifier + '::'; | | 46 | + } | | 47 | + | | 48 | + return specifier + '::' + ArrayPrototypeJoin( | | 49 | + ArrayPrototypeMap( | | 50 | + ArrayPrototypeSort(keys), | | 51 | + (key) => JSONStringify(key) + JSONStringify(importAssertions[key])), | | 52 | + ','); | | 53 | + } | | 54 | + | | 55 | + #getModuleCachedImports(parentURL) { | | 56 | + let internalCache = super.get(parentURL); | | 57 | + if (internalCache == null) { | | 58 | + super.set(parentURL, internalCache = { __proto__: null }); | | 59 | + } | | 60 | + return internalCache; | | 61 | + } | | 62 | + | | 63 | + /** | | 64 | + * @param {string} serializedKey | | 65 | + * @param {string} parentURL | | 66 | + * @returns {import('./loader').ModuleExports | Promise<import('./loader').ModuleExports>} | | 67 | + */ | | 68 | + get(serializedKey, parentURL) { | | 69 | + return this.#getModuleCachedImports(parentURL)[serializedKey]; | | 70 | + } | | 71 | + | | 72 | + /** | | 73 | + * @param {string} serializedKey | | 74 | + * @param {string} parentURL | | 75 | + * @param {{ format: string, url: URL['href'] }} result | | 76 | + */ | | 77 | + set(serializedKey, parentURL, result) { | | 78 | + this.#getModuleCachedImports(parentURL)[serializedKey] = result; | | 79 | + return this; | | 80 | + } | | 81 | + | | 82 | + has(serializedKey, parentURL) { | | 83 | + return serializedKey in this.#getModuleCachedImports(parentURL); | | 84 | + } | | 85 | +} | | 86 | + | | 87 | +/** | | 88 | + * Cache the results of the `load` step of the module resolution and loading process. | | 89 | + */ | | 90 | +class LoadCache extends SafeMap { | 16 | 91 | constructor(i) { super(i); } // eslint-disable-line no-useless-constructor | 17 | 92 | get(url, type = kImplicitAssertType) { | 18 | 93 | validateString(url, 'url'); | @@ -30,7 +105,7 @@ class ModuleMap extends SafeMap { | 30 | 105 | } | 31 | 106 | debug(`Storing ${url} (${ | 32 | 107 | type === kImplicitAssertType ? 'implicit type' : type | 33 | | - }) in ModuleMap`); | | 108 | + }) in ModuleLoadMap`); | 34 | 109 | const cachedJobsForUrl = super.get(url) ?? ObjectCreate(null); | 35 | 110 | cachedJobsForUrl[type] = job; | 36 | 111 | return super.set(url, cachedJobsForUrl); | @@ -41,4 +116,8 @@ class ModuleMap extends SafeMap { | 41 | 116 | return super.get(url)?.[type] !== undefined; | 42 | 117 | } | 43 | 118 | } | 44 | | -module.exports = ModuleMap; | | 119 | + | | 120 | +module.exports = { | | 121 | + LoadCache, | | 122 | + ResolveCache, | | 123 | +}; |
Collapse file Copy file name to clipboard +25Lines changed: 25 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | +const common = require('../common'); | | 3 | +const tmpdir = require('../common/tmpdir'); | | 4 | + | | 5 | +const assert = require('node:assert'); | | 6 | +const fs = require('node:fs/promises'); | | 7 | +const { pathToFileURL } = require('node:url'); | | 8 | + | | 9 | +tmpdir.refresh(); | | 10 | +const tmpDir = pathToFileURL(tmpdir.path); | | 11 | + | | 12 | +const target = new URL(`./${Math.random()}.mjs`, tmpDir); | | 13 | + | | 14 | +(async () => { | | 15 | + | | 16 | + await assert.rejects(import(target), { code: 'ERR_MODULE_NOT_FOUND' }); | | 17 | + | | 18 | + await fs.writeFile(target, 'export default "actual target"\n'); | | 19 | + | | 20 | + const moduleRecord = await import(target); | | 21 | + | | 22 | + await fs.rm(target); | | 23 | + | | 24 | + assert.strictEqual(await import(target), moduleRecord); | | 25 | +})().then(common.mustCall()); |
Collapse file Copy file name to clipboard +42Lines changed: 42 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import { spawnPromisified } from '../common/index.mjs'; | | 2 | +import tmpdir from '../common/tmpdir.js'; | | 3 | + | | 4 | +import assert from 'node:assert'; | | 5 | +import fs from 'node:fs/promises'; | | 6 | +import { execPath } from 'node:process'; | | 7 | +import { pathToFileURL } from 'node:url'; | | 8 | + | | 9 | +tmpdir.refresh(); | | 10 | +const tmpDir = pathToFileURL(tmpdir.path); | | 11 | + | | 12 | +const target = new URL(`./${Math.random()}.mjs`, tmpDir); | | 13 | + | | 14 | +await assert.rejects(import(target), { code: 'ERR_MODULE_NOT_FOUND' }); | | 15 | + | | 16 | +await fs.writeFile(target, 'export default "actual target"\n'); | | 17 | + | | 18 | +const moduleRecord = await import(target); | | 19 | + | | 20 | +await fs.rm(target); | | 21 | + | | 22 | +assert.strictEqual(await import(target), moduleRecord); | | 23 | + | | 24 | +// Add the file back, it should be deleted by the subprocess. | | 25 | +await fs.writeFile(target, 'export default "actual target"\n'); | | 26 | + | | 27 | +assert.deepStrictEqual( | | 28 | + await spawnPromisified(execPath, [ | | 29 | + '--input-type=module', | | 30 | + '--eval', | | 31 | + [`import * as d from${JSON.stringify(target)};`, | | 32 | + 'import{rm}from"node:fs/promises";', | | 33 | + `await rm(new URL(${JSON.stringify(target)}));`, | | 34 | + 'import{strictEqual}from"node:assert";', | | 35 | + `strictEqual(JSON.stringify(await import(${JSON.stringify(target)})),JSON.stringify(d));`].join(''), | | 36 | + ]), | | 37 | + { | | 38 | + code: 0, | | 39 | + signal: null, | | 40 | + stderr: '', | | 41 | + stdout: '', | | 42 | + }); |
|