Skip to content

Commit

Permalink
esm: share package.json string cache between ESM and CJS loaders
Browse files Browse the repository at this point in the history
Refs: #30674
  • Loading branch information
shackijj committed May 18, 2020
1 parent 1d4a52c commit 3d0ac05
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 108 deletions.
30 changes: 10 additions & 20 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,8 @@ const fs = require('fs');
const internalFS = require('internal/fs/utils');
const path = require('path');
const { emitWarningSync } = require('internal/process/warning');
const {
internalModuleReadJSON,
internalModuleStat
} = internalBinding('fs');
const { internalModuleStat } = internalBinding('fs');
const packageJsonReader = require('internal/modules/package_json_reader');
const { safeGetenv } = internalBinding('credentials');
const {
makeRequireFunction,
Expand Down Expand Up @@ -94,10 +92,8 @@ const {
const { validateString } = require('internal/validators');
const pendingDeprecation = getOptionValue('--pending-deprecation');

const packageJsonCache = new SafeMap();

module.exports = {
wrapSafe, Module, toRealPath, readPackageScope, readPackage, packageJsonCache,
wrapSafe, Module, toRealPath, readPackageScope,
get hasLoadedAnyUserCJSModule() { return hasLoadedAnyUserCJSModule; }
};

Expand Down Expand Up @@ -241,22 +237,17 @@ Module._debug = deprecate(debug, 'Module._debug is deprecated.', 'DEP0077');
// -> a
// -> a.<ext>
// -> a/index.<ext>
/**
*
* @param {string} jsonPath
* @returns {string|undefined}
*/
function readJsonDefaultStrategy(jsonPath) {
return internalModuleReadJSON(path.toNamespacedPath(jsonPath));
}

function readPackage(requestPath, readJsonStrategy = readJsonDefaultStrategy) {
const packageJsonCache = new SafeMap();

function readPackage(requestPath) {
const jsonPath = path.resolve(requestPath, 'package.json');

const existing = packageJsonCache.get(jsonPath);
if (existing !== undefined) return existing;

const json = readJsonStrategy(jsonPath);
const result = packageJsonReader.read(path.toNamespacedPath(jsonPath));
const json = result.containsKeys === false ? '{}' : result.string;
if (json === undefined) {
packageJsonCache.set(jsonPath, false);
return false;
Expand Down Expand Up @@ -284,8 +275,7 @@ function readPackage(requestPath, readJsonStrategy = readJsonDefaultStrategy) {
}
}

function readPackageScope(
checkPath, readJsonStrategy = readJsonDefaultStrategy) {
function readPackageScope(checkPath) {
const rootSeparatorIndex = checkPath.indexOf(path.sep);
let separatorIndex;
while (
Expand All @@ -294,7 +284,7 @@ function readPackageScope(
checkPath = checkPath.slice(0, separatorIndex);
if (checkPath.endsWith(path.sep + 'node_modules'))
return false;
const pjson = readPackage(checkPath, readJsonStrategy);
const pjson = readPackage(checkPath);
if (pjson) return {
path: checkPath,
data: pjson
Expand Down
148 changes: 84 additions & 64 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
ArrayIsArray,
ArrayPrototypeJoin,
ArrayPrototypeShift,
JSONParse,
JSONStringify,
ObjectFreeze,
ObjectGetOwnPropertyNames,
Expand All @@ -23,23 +24,14 @@ const {
const assert = require('internal/assert');
const internalFS = require('internal/fs/utils');
const { NativeModule } = require('internal/bootstrap/loaders');
const {
Module: CJSModule,
readPackageScope,
readPackage,
packageJsonCache
} = require('internal/modules/cjs/loader');
const {
realpathSync,
statSync,
Stats,
openSync,
fstatSync,
readFileSync,
closeSync
} = require('fs');
const { getOptionValue } = require('internal/options');
const { sep, relative, join } = require('path');
const { sep, relative } = require('path');
const { Module: CJSModule } = require('internal/modules/cjs/loader');
const preserveSymlinks = getOptionValue('--preserve-symlinks');
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
const typeFlag = getOptionValue('--input-type');
Expand All @@ -55,6 +47,7 @@ const {
ERR_UNSUPPORTED_ESM_URL_SCHEME,
} = require('internal/errors').codes;

const packageJsonReader = require('internal/modules/package_json_reader');
const DEFAULT_CONDITIONS = ObjectFreeze(['node', 'import']);
const DEFAULT_CONDITIONS_SET = new SafeSet(DEFAULT_CONDITIONS);

Expand All @@ -70,6 +63,7 @@ function getConditionsSet(conditions) {
}

const realpathCache = new SafeMap();
const packageJSONCache = new SafeMap(); /* string -> PackageConfig */

function tryStatSync(path) {
try {
Expand All @@ -79,6 +73,77 @@ function tryStatSync(path) {
}
}

function getPackageConfig(path) {
const existing = packageJSONCache.get(path);
if (existing !== undefined) {
return existing;
}
const source = packageJsonReader.read(path).string;
if (source === undefined) {
const packageConfig = {
exists: false,
main: undefined,
name: undefined,
type: 'none',
exports: undefined
};
packageJSONCache.set(path, packageConfig);
return packageConfig;
}

let packageJSON;
try {
packageJSON = JSONParse(source);
} catch (error) {
const errorPath = StringPrototypeSlice(path, 0, path.length - 13);
throw new ERR_INVALID_PACKAGE_CONFIG(errorPath, error.message, true);
}

let { main, name, type } = packageJSON;
const { exports } = packageJSON;
if (typeof main !== 'string') main = undefined;
if (typeof name !== 'string') name = undefined;
// Ignore unknown types for forwards compatibility
if (type !== 'module' && type !== 'commonjs') type = 'none';

const packageConfig = {
exists: true,
main,
name,
type,
exports
};
packageJSONCache.set(path, packageConfig);
return packageConfig;
}

function getPackageScopeConfig(resolved, base) {
let packageJSONUrl = new URL('./package.json', resolved);
while (true) {
const packageJSONPath = packageJSONUrl.pathname;
if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json'))
break;
const packageConfig = getPackageConfig(fileURLToPath(packageJSONUrl), base);
if (packageConfig.exists) return packageConfig;

const lastPackageJSONUrl = packageJSONUrl;
packageJSONUrl = new URL('../package.json', packageJSONUrl);

// Terminates at root where ../package.json equals ../../package.json
// (can't just check "/package.json" for Windows support).
if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) break;
}
const packageConfig = {
exists: false,
main: undefined,
name: undefined,
type: 'none',
exports: undefined
};
packageJSONCache.set(fileURLToPath(packageJSONUrl), packageConfig);
return packageConfig;
}

/*
* Legacy CommonJS main resolution:
* 1. let M = pkg_url + (json main field)
Expand Down Expand Up @@ -331,7 +396,7 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) {


function packageMainResolve(packageJSONUrl, packageConfig, base, conditions) {
if (packageConfig) {
if (packageConfig.exists) {
const exports = packageConfig.exports;
if (exports !== undefined) {
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
Expand Down Expand Up @@ -408,51 +473,9 @@ function packageExportsResolve(
throwExportsNotFound(packageSubpath, packageJSONUrl, base);
}

function readIfFile(path) {
let fd;
try {
fd = openSync(path, 'r');
} catch {
return undefined;
}
try {
if (!fstatSync(fd).isFile()) return undefined;
return readFileSync(fd, 'utf8');
} finally {
closeSync(fd);
}
}

function catchInvalidPackage(error, path) {
if (error instanceof SyntaxError) {
const packageJsonPath = join(path, 'package.json');
const message = StringPrototypeSlice(
error.message, ('Error parsing ' + packageJsonPath + ': ').length);
throw new ERR_INVALID_PACKAGE_CONFIG(path, message, true);
}
throw error;
}

function readPackageESM(path) {
try {
return readPackage(path, readIfFile);
} catch (error) {
catchInvalidPackage(error, path);
}
}

function readPackageScopeESM(url) {
const path = fileURLToPath(url);
try {
return readPackageScope(path, readIfFile);
} catch (error) {
catchInvalidPackage(error, path);
}
}

function getPackageType(url) {
const packageConfig = readPackageScopeESM(url);
return packageConfig ? packageConfig.data.type : 'none';
const packageConfig = getPackageScopeConfig(url, url);
return packageConfig.type;
}

/**
Expand Down Expand Up @@ -496,13 +519,12 @@ function packageResolve(specifier, base, conditions) {
'' : '.' + StringPrototypeSlice(specifier, separatorIndex);

// ResolveSelf
const packageScope = readPackageScopeESM(base);
if (packageScope !== false) {
const packageConfig = packageScope.data;
const packageConfig = getPackageScopeConfig(base, base);
if (packageConfig.exists) {
// TODO(jkrems): Find a way to forward the pair/iterator already generated
// while executing GetPackageScopeConfig
let packageJSONUrl;
for (const [ filename, packageConfigCandidate ] of packageJsonCache) {
for (const [ filename, packageConfigCandidate ] of packageJSONCache) {
if (packageConfig === packageConfigCandidate) {
packageJSONUrl = pathToFileURL(filename);
break;
Expand Down Expand Up @@ -540,15 +562,13 @@ function packageResolve(specifier, base, conditions) {
}

// Package match.
const packagePath = StringPrototypeSlice(
packageJSONPath, 0, packageJSONPath.length - 13);
const packageConfig = readPackageESM(packagePath);
const packageConfig = getPackageConfig(packageJSONPath, base);
if (packageSubpath === './') {
return new URL('./', packageJSONUrl);
} else if (packageSubpath === '') {
return packageMainResolve(packageJSONUrl, packageConfig, base,
conditions);
} else if (packageConfig && packageConfig.exports !== undefined) {
} else if (packageConfig.exports !== undefined) {
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
} else {
Expand Down
22 changes: 22 additions & 0 deletions lib/internal/modules/package_json_reader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const { SafeMap } = primordials;
const { internalModuleReadJSON } = internalBinding('fs');

const cache = new SafeMap();

/**
*
* @param {string} path
*/
function read(path) {
if (cache.has(path)) {
return cache.get(path);
}

const result = internalModuleReadJSON(path);
cache.set(path, result);
return result;
}

module.exports = { read };
1 change: 1 addition & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
'lib/internal/main/run_third_party_main.js',
'lib/internal/main/worker_thread.js',
'lib/internal/modules/run_main.js',
'lib/internal/modules/package_json_reader.js',
'lib/internal/modules/cjs/helpers.js',
'lib/internal/modules/cjs/loader.js',
'lib/internal/modules/esm/loader.js',
Expand Down
Loading

0 comments on commit 3d0ac05

Please sign in to comment.