Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/bootstrap-esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as fs from 'node:fs';
import { createRequire, isBuiltin, registerHooks } from 'node:module';
import { createRequire, isBuiltin, registerHooks, type Module } from 'node:module';
import { dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { product, pkg } from './bootstrap-meta.js';
Expand Down Expand Up @@ -85,6 +85,15 @@ function enableASARSupport(): void {
return slash === -1 ? specifier : specifier.slice(0, slash);
};

const resolutionCache = new Map<string, Module.ResolveFnOutput>();
const resolutionCacheKey = (specifier: string, parentPath: string, context: Module.ResolveHookContext): string => {
let key = `${specifier}\0${parentPath}\0${context.conditions.join('\0')}`;
for (const [name, value] of Object.entries(context.importAttributes ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
key += `\0${name}\0${value}`;
}
return key;
};
Comment thread
dmitrivMS marked this conversation as resolved.

const appRoot = dirname(import.meta.dirname);
const resourcesPath = process.env['VSCODE_DEV'] ? undefined : normalizeDriveLetter(appRoot);
// Root require.resolve() inside the archive; the leading './' below avoids a node_modules walk.
Expand Down Expand Up @@ -126,6 +135,13 @@ function enableASARSupport(): void {
try { parentPath = normalizeDriveLetter(fileURLToPath(context.parentURL)); } catch { parentPath = undefined; }
if (parentPath && parentPath.startsWith(resourcesPath)) {
trace?.(`resolve "${specifier}" from "${context.parentURL}"`);
const cacheKey = resolutionCacheKey(specifier, parentPath, context);
const cached = resolutionCache.get(cacheKey);
if (cached) {
trace?.(` cache -> ${cached.url} (ACCEPT)`);
return cached;
}

let defaultResult;
let defaultError: Error | undefined;
// A closer dependency bundled by the importer takes precedence over the application archive.
Expand Down Expand Up @@ -165,6 +181,7 @@ function enableASARSupport(): void {
try { selfRefPath = normalizeDriveLetter(fileURLToPath(selfRef.url)); } catch { selfRefPath = undefined; }
if (selfRefPath && selfRefPath.startsWith(resourcesPath)) {
trace?.(` self-ref -> ${selfRef.url} (in app, ACCEPT)`);
resolutionCache.set(cacheKey, { ...selfRef, shortCircuit: true });
return selfRef;
}
trace?.(` self-ref -> ${selfRef.url} (escaped app, reject)`);
Expand All @@ -175,7 +192,9 @@ function enableASARSupport(): void {
const resolved = asarRequire.resolve(`./${specifier}`);
const url = pathToFileURL(resolved).href;
trace?.(` direct -> ${url} (ACCEPT)`);
return { url, shortCircuit: true };
const result = { url, shortCircuit: true };
resolutionCache.set(cacheKey, result);
return result;
}
trace?.(`defer "${specifier}" (parent outside app resources: ${context.parentURL})`);
}
Expand Down
94 changes: 92 additions & 2 deletions src/vs/code/test/node/bootstrapESM.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,31 @@

import assert from 'assert';
import { execFile } from 'child_process';
import { mkdtemp, rm, writeFile } from 'fs/promises';
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises';
import { createRequire } from 'module';
import { tmpdir } from 'os';
import { promisify } from 'util';
import { fileURLToPath, pathToFileURL } from 'url';
import { dirname, join } from '../../../base/common/path.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js';

const execFileAsync = promisify(execFile);
const nodeRequire = createRequire(import.meta.url);
const { createPackage, uncache } = nodeRequire('asar') as {
createPackage(source: string, destination: string): Promise<void>;
uncache(archive: string): boolean;
};

(process.versions['electron'] ? suite : suite.skip)('bootstrap ESM', () => {
ensureNoDisposablesAreLeakedInTestSuite();

let fixtureDirectory: string;
let fixturePath: string;
let reentrantHookPath: string;
let packagedFixturePath: string;
let packagedBootstrapPath: string;
let packagedTracePath: string;
let packagedArchivePath: string;

suiteSetup(async () => {
fixtureDirectory = await mkdtemp(join(tmpdir(), 'vscode-bootstrap-esm-'));
Expand Down Expand Up @@ -62,10 +72,61 @@ const execFileAsync = promisify(execFile);
requiredESMUsesOriginalFs: requiredESM.usesOriginalFs
}));
`);

const outRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../../');
const packagedAppRoot = join(fixtureDirectory, 'resources', 'app');
const packagedOutRoot = join(packagedAppRoot, 'out');
await mkdir(join(packagedOutRoot, 'vs', 'base', 'common'), { recursive: true });
await Promise.all([
copyFile(join(outRoot, 'bootstrap-esm.js'), join(packagedOutRoot, 'bootstrap-esm.js')),
copyFile(join(outRoot, 'bootstrap-meta.js'), join(packagedOutRoot, 'bootstrap-meta.js')),
copyFile(join(outRoot, 'bootstrap-node.js'), join(packagedOutRoot, 'bootstrap-node.js')),
copyFile(join(outRoot, 'vs', 'base', 'common', 'performance.js'), join(packagedOutRoot, 'vs', 'base', 'common', 'performance.js')),
writeFile(join(packagedAppRoot, 'product.json'), '{}'),
writeFile(join(packagedAppRoot, 'package.json'), '{"type":"module"}'),
]);

const archiveSource = join(fixtureDirectory, 'archive-source');
const packageRoot = join(archiveSource, 'cache-test');
await mkdir(packageRoot, { recursive: true });
await writeFile(join(packageRoot, 'package.json'), '{"name":"cache-test","type":"module","exports":"./index.js"}');
await writeFile(join(packageRoot, 'index.js'), 'export const value = 1;');
packagedArchivePath = join(packagedAppRoot, 'node_modules.asar');
await createPackage(archiveSource, packagedArchivePath);

packagedFixturePath = join(packagedOutRoot, 'cache-fixture.mjs');
packagedBootstrapPath = join(packagedOutRoot, 'bootstrap-esm.js');
packagedTracePath = join(fixtureDirectory, 'asar-trace.log');
await writeFile(join(packagedOutRoot, 'cache-parent-a.mjs'), `
export const first = () => import('cache-test');
export const second = () => import('cache-test');
`);
await writeFile(join(packagedOutRoot, 'cache-parent-b.mjs'), `
export const first = () => import('cache-test');
export const second = () => import('cache-test');
`);
await writeFile(packagedFixturePath, `
const firstParent = await import('./cache-parent-a.mjs');
const secondParent = await import('./cache-parent-b.mjs');
const modules = await Promise.all([
firstParent.first(),
firstParent.second(),
secondParent.first(),
secondParent.second()
]);
process.stdout.write(JSON.stringify(modules.map(module => module.value)));
`);
});

suiteTeardown(async () => {
await rm(fixtureDirectory, { recursive: true, force: true });
uncache(packagedArchivePath);
const previousNoAsar = process.noAsar;
process.noAsar = true;
try {
await rm(fixtureDirectory, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
} finally {
process.noAsar = previousNoAsar;
}
});

for (const condition of [undefined, 'require', 'import']) {
Expand Down Expand Up @@ -98,4 +159,33 @@ const execFileAsync = promisify(execFile);
});
});
}

test('caches identical packaged ESM archive resolutions', async () => {
const env: NodeJS.ProcessEnv = {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
VSCODE_ASAR_TRACE: packagedTracePath
};
delete env['NODE_OPTIONS'];
delete env['VSCODE_DEV'];

const { stdout } = await execFileAsync(process.execPath, [
'--import',
pathToFileURL(packagedBootstrapPath).href,
packagedFixturePath
], { env });
const trace = await readFile(packagedTracePath, 'utf8');

assert.deepStrictEqual({
values: JSON.parse(stdout),
resolveCount: trace.match(/resolve "cache-test"/g)?.length,
archiveLookupCount: trace.match(/archive pkg\.json/g)?.length,
cacheHitCount: trace.match(/cache ->/g)?.length,
}, {
values: [1, 1, 1, 1],
resolveCount: 4,
archiveLookupCount: 2,
cacheHitCount: 2,
});
});
});
Loading