From 133ad165679f333f6b14fc1ac75047b39312cedb Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:37:00 +0200 Subject: [PATCH 01/29] module: add `findPackageJSON` util --- lib/internal/modules/esm/resolve.js | 3 +- lib/internal/modules/package_json_reader.js | 32 ++- lib/internal/validators.js | 10 + lib/module.js | 6 +- .../packages/cjs-main-no-index/main.js | 1 + .../packages/cjs-main-no-index/other.js | 3 + .../packages/cjs-main-no-index/package.json | 5 + test/fixtures/packages/nested/package.json | 1 + .../packages/nested/sub-pkg-cjs/index.js | 4 + .../packages/nested/sub-pkg-cjs/package.json | 1 + .../packages/nested/sub-pkg-mjs/index.js | 3 + .../packages/nested/sub-pkg-mjs/package.json | 1 + .../packages/root-types-field/index.js | 0 .../packages/root-types-field/package.json | 5 + test/parallel/test-find-package-json.js | 186 ++++++++++++++++++ 15 files changed, 258 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/packages/cjs-main-no-index/main.js create mode 100644 test/fixtures/packages/cjs-main-no-index/other.js create mode 100644 test/fixtures/packages/cjs-main-no-index/package.json create mode 100644 test/fixtures/packages/nested/package.json create mode 100644 test/fixtures/packages/nested/sub-pkg-cjs/index.js create mode 100644 test/fixtures/packages/nested/sub-pkg-cjs/package.json create mode 100644 test/fixtures/packages/nested/sub-pkg-mjs/index.js create mode 100644 test/fixtures/packages/nested/sub-pkg-mjs/package.json create mode 100644 test/fixtures/packages/root-types-field/index.js create mode 100644 test/fixtures/packages/root-types-field/package.json create mode 100644 test/parallel/test-find-package-json.js diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index 61c043e35c6ce9..080a190694c6c1 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -1105,10 +1105,11 @@ module.exports = { decorateErrorWithCommonJSHints, defaultResolve, encodedSepRegEx, + legacyMainResolve, packageExportsResolve, packageImportsResolve, + packageResolve, throwIfInvalidParentURL, - legacyMainResolve, }; // cycle diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 9a9dcebb799c00..3d3643d2961faa 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -7,9 +7,14 @@ const { StringPrototypeLastIndexOf, StringPrototypeSlice, } = primordials; +const { fileURLToPath } = require('internal/url'); +const { kEmptyObject } = require('internal/util'); +const { + validateString, + validateURLString, +} = require('internal/validators'); const modulesBinding = internalBinding('modules'); const { resolve, sep } = require('path'); -const { kEmptyObject } = require('internal/util'); /** * @param {string} path @@ -154,10 +159,35 @@ function getPackageType(url) { return getPackageScopeConfig(url).type; } +const pjsonImportAttributes = { __proto__: null, type: 'json' }; +let cascadedLoader; +/** + * @param {URL['href'] | URL['pathname']} specifier The location for which to get the "root" package.json + * @param {URL['href'] | URL['pathname']} parentURL The location of the current module (ex file://tmp/foo.js). + */ +function findPackageJSON(specifier, parentURL) { + validateString(specifier, 'specifier'); + validateURLString(parentURL, 'parentURL'); + + // console.log('findPackageJSON after validation', { specifier, parentURL }) + + cascadedLoader ??= require('internal/modules/esm/loader').getOrInitializeCascadedLoader(); + + const { url: resolvedTarget } = cascadedLoader.resolve(specifier, parentURL, pjsonImportAttributes); +console.log({ resolvedTarget }) + + const pkg = getNearestParentPackageJSON(fileURLToPath(resolvedTarget)); + + console.log({ pkg }) + + return pkg?.path; +} + module.exports = { read, readPackage, getNearestParentPackageJSON, getPackageScopeConfig, getPackageType, + findPackageJSON, }; diff --git a/lib/internal/validators.js b/lib/internal/validators.js index 9798e0b5fe353c..5aaa62d44ca5e2 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -499,6 +499,15 @@ function validateUnion(value, name, union) { } } +/** + * @param {*} value + * @param {string} name + * @returns {asserts value is a parsable URL string} + */ +const validateURLString = hideStackFrames((value, name) => { + if (!URL.canParse(value)) throw new ERR_INVALID_ARG_TYPE(name, 'a URL string', value); +}); + /* The rules for the Link header field are described here: https://www.rfc-editor.org/rfc/rfc8288.html#section-3 @@ -639,6 +648,7 @@ module.exports = { validateUint32, validateUndefined, validateUnion, + validateURLString, validateAbortSignal, validateLinkHeaderValue, validateInternalField, diff --git a/lib/module.js b/lib/module.js index eb400e71ae0400..e1142224a4b168 100644 --- a/lib/module.js +++ b/lib/module.js @@ -10,6 +10,9 @@ const { flushCompileCache, getCompileCacheDir, } = require('internal/modules/helpers'); +const { + findPackageJSON, +} = require('internal/modules/package_json_reader'); const { stripTypeScriptTypes } = require('internal/modules/typescript'); Module.findSourceMap = findSourceMap; @@ -18,6 +21,7 @@ Module.SourceMap = SourceMap; Module.constants = constants; Module.enableCompileCache = enableCompileCache; Module.flushCompileCache = flushCompileCache; -Module.stripTypeScriptTypes = stripTypeScriptTypes; Module.getCompileCacheDir = getCompileCacheDir; +Module.stripTypeScriptTypes = stripTypeScriptTypes;Module.findPackageJSON = findPackageJSON; + module.exports = Module; diff --git a/test/fixtures/packages/cjs-main-no-index/main.js b/test/fixtures/packages/cjs-main-no-index/main.js new file mode 100644 index 00000000000000..888cae37af95c5 --- /dev/null +++ b/test/fixtures/packages/cjs-main-no-index/main.js @@ -0,0 +1 @@ +module.exports = 42; diff --git a/test/fixtures/packages/cjs-main-no-index/other.js b/test/fixtures/packages/cjs-main-no-index/other.js new file mode 100644 index 00000000000000..8fa48276783484 --- /dev/null +++ b/test/fixtures/packages/cjs-main-no-index/other.js @@ -0,0 +1,3 @@ +const answer = require('./'); + +module.exports = answer+1; diff --git a/test/fixtures/packages/cjs-main-no-index/package.json b/test/fixtures/packages/cjs-main-no-index/package.json new file mode 100644 index 00000000000000..897d3f99daf7a2 --- /dev/null +++ b/test/fixtures/packages/cjs-main-no-index/package.json @@ -0,0 +1,5 @@ +{ + "name": "main-no-index", + "main": "./main.js", + "type":"commonjs" +} diff --git a/test/fixtures/packages/nested/package.json b/test/fixtures/packages/nested/package.json new file mode 100644 index 00000000000000..0ce6c71db42e8c --- /dev/null +++ b/test/fixtures/packages/nested/package.json @@ -0,0 +1 @@ +{"name": "package-with-sub-package"} diff --git a/test/fixtures/packages/nested/sub-pkg-cjs/index.js b/test/fixtures/packages/nested/sub-pkg-cjs/index.js new file mode 100644 index 00000000000000..9db89b8bf068c8 --- /dev/null +++ b/test/fixtures/packages/nested/sub-pkg-cjs/index.js @@ -0,0 +1,4 @@ +const { getPackageJSON } = require('node:module'); +const { resolve } = require('node:path'); + +module.exports = getPackageJSON(resolve(__dirname, '..')); diff --git a/test/fixtures/packages/nested/sub-pkg-cjs/package.json b/test/fixtures/packages/nested/sub-pkg-cjs/package.json new file mode 100644 index 00000000000000..2dec7591cd6db8 --- /dev/null +++ b/test/fixtures/packages/nested/sub-pkg-cjs/package.json @@ -0,0 +1 @@ +{"name": "sub-package", "type": "commonjs"} diff --git a/test/fixtures/packages/nested/sub-pkg-mjs/index.js b/test/fixtures/packages/nested/sub-pkg-mjs/index.js new file mode 100644 index 00000000000000..6dd75e8525d8cf --- /dev/null +++ b/test/fixtures/packages/nested/sub-pkg-mjs/index.js @@ -0,0 +1,3 @@ +import { getPackageJSON } from 'node:module'; + +export default getPackageJSON(import.meta.resolve('..')); diff --git a/test/fixtures/packages/nested/sub-pkg-mjs/package.json b/test/fixtures/packages/nested/sub-pkg-mjs/package.json new file mode 100644 index 00000000000000..c294ec5158824a --- /dev/null +++ b/test/fixtures/packages/nested/sub-pkg-mjs/package.json @@ -0,0 +1 @@ +{"name": "sub-package", "type": "module"} diff --git a/test/fixtures/packages/root-types-field/index.js b/test/fixtures/packages/root-types-field/index.js new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/fixtures/packages/root-types-field/package.json b/test/fixtures/packages/root-types-field/package.json new file mode 100644 index 00000000000000..4af68442446dd8 --- /dev/null +++ b/test/fixtures/packages/root-types-field/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-with-unrecognised-fields", + "type": "module", + "types": "./index.d.ts" +} diff --git a/test/parallel/test-find-package-json.js b/test/parallel/test-find-package-json.js new file mode 100644 index 00000000000000..2bd4d3c3d1c5da --- /dev/null +++ b/test/parallel/test-find-package-json.js @@ -0,0 +1,186 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const tmpdir = require('../common/tmpdir') +const assert = require('assert'); +const { findPackageJSON } = require('module'); +const { describe, it } = require('node:test'); +const { pathToFileURL } = require('url'); + + +describe('findPackageJSON', () => { // Throws when no arguments are provided + it.skip('should throw when no arguments are provided', () => { + assert.throws( + () => findPackageJSON(), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + }); + + it.skip('should throw when base is invalid', () => { + for (const invalid of ['', null, undefined, {}, [], Symbol(), 1, 0]) assert.throws( + () => findPackageJSON('', invalid), + { code: 'ERR_INVALID_ARG_TYPE' } + ); + }); + + it('should accept a file URL (string), like from `import.meta.resolve()`', () => { + const importMetaUrl = `${pathToFileURL(fixtures.fixturesDir)}/` + const specifier = './packages/root-types-field/index.js'; + assert.strictEqual( + findPackageJSON(specifier, importMetaUrl), + fixtures.path(specifier, 'package.json') + ); + }); +}); + +// { // Exclude unrecognised fields when `everything` is not `true` +// const pathToDir = fixtures.path('packages/root-types-field/'); +// const pkg = findPackageJSON(pathToDir); + +// assert.deepStrictEqual(pkg, { +// path: path.join(pathToDir, 'package.json'), +// exists: true, +// data: { +// __proto__: null, +// name: pkgName, +// type: pkgType, +// } +// }); +// } + +// { // Include unrecognised fields when `everything` is `true` +// const pathToDir = fixtures.path('packages/root-types-field/'); +// const pkg = findPackageJSON(pathToDir, true); + +// assert.deepStrictEqual(pkg, { +// path: path.join(pathToDir, 'package.json'), +// exists: true, +// data: { +// __proto__: null, +// name: pkgName, +// type: pkgType, +// types: './index.d.ts', +// } +// }); +// } + +// { // Exclude unrecognised fields when `everything` is not `true` +// const pathToDir = fixtures.path('packages/nested-types-field/'); +// const pkg = findPackageJSON(pathToDir); + +// assert.deepStrictEqual(pkg, { +// path: path.join(pathToDir, 'package.json'), +// exists: true, +// data: { +// __proto__: null, +// name: pkgName, +// type: pkgType, +// exports: { +// default: './index.js', +// types: './index.d.ts', // I think this is unexpected? +// }, +// }, +// }); +// } + +// { // Include unrecognised fields when `everything` is not `true` +// const pathToDir = fixtures.path('packages/nested-types-field/'); +// const pkg = findPackageJSON(pathToDir, true); + +// assert.deepStrictEqual(pkg, { +// path: path.join(pathToDir, 'package.json'), +// exists: true, +// data: { +// __proto__: null, +// name: pkgName, +// type: pkgType, +// exports: { +// default: './index.js', +// types: './index.d.ts', // I think this is unexpected? +// }, +// unrecognised: true, +// }, +// }); +// } + +// { // Throws on unresolved location +// let err; +// try { +// findPackageJSON('..'); +// } catch (e) { +// err = e; +// } + +// assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE'); +// assert.match(err.message, /fully resolved/); +// assert.match(err.message, /relative/); +// assert.match(err.message, /import\.meta\.resolve/); +// assert.match(err.message, /path\.resolve\(__dirname/); +// } + +// { // Can crawl up (CJS) +// const pathToMod = fixtures.path('packages/nested/sub-pkg-cjs/index.js'); +// const parentPkg = require(pathToMod); + +// assert.strictEqual(parentPkg.data.name, 'package-with-sub-package'); +// const pathToParent = fixtures.path('packages/nested/package.json'); +// assert.strictEqual(parentPkg.path, pathToParent); +// } + +// { // Can crawl up (ESM) +// const pathToMod = fixtures.path('packages/nested/sub-pkg-mjs/index.js'); +// const parentPkg = require(pathToMod).default; + +// assert.strictEqual(parentPkg.data.name, 'package-with-sub-package'); +// const pathToParent = fixtures.path('packages/nested/package.json'); +// assert.strictEqual(parentPkg.path, pathToParent); +// } + +// { // Can require via package.json +// const pathToMod = fixtures.path('packages/cjs-main-no-index/other.js'); +// // require() falls back to package.json values like "main" to resolve when there is no index +// const answer = require(pathToMod); + +// assert.strictEqual(answer, 43); +// } + +// tmpdir.refresh(); + +// { +// fs.writeFileSync(tmpdir.resolve('entry.mjs'), ` +// import { findPackageJSON, createRequire } from 'node:module'; +// const require = createRequire(import.meta.url); + +// const { secretNumber1 } = findPackageJSON(import.meta.resolve('pkg'), true).data; +// const secretNumber2 = NaN; // TODO get the actual value +// const { secretNumber3 } = findPackageJSON(import.meta.resolve('pkg2'), true).data; +// const { secretNumber4 } = require('pkg2/package.json'); // TODO: is there a way to get that without relying on package.json being exported? +// console.log(secretNumber1, secretNumber2, secretNumber3, secretNumber4); +// `); + +// const secretNumber1 = Math.ceil(Math.random() * 999); +// const secretNumber2 = Math.ceil(Math.random() * 999); +// const secretNumber3 = Math.ceil(Math.random() * 999); +// const secretNumber4 = Math.ceil(Math.random() * 999); + +// fs.mkdirSync(tmpdir.resolve('node_modules/pkg/subfolder'), { recursive: true }); +// fs.writeFileSync(tmpdir.resolve('node_modules/pkg/subfolder/index.js'), ''); +// fs.writeFileSync(tmpdir.resolve('node_modules/pkg/subfolder/package.json'), JSON.stringify({ type: 'module', secretNumber1 })); +// fs.writeFileSync(tmpdir.resolve('node_modules/pkg/package.json'), JSON.stringify({ name: 'pkg', exports: './subfolder/index.js', secretNumber2 })); +// fs.mkdirSync(tmpdir.resolve('node_modules/pkg/subfolder2')); +// fs.writeFileSync(tmpdir.resolve('node_modules/pkg/subfolder2/package.json'), JSON.stringify({ type: 'module', secretNumber3 })); +// fs.writeFileSync(tmpdir.resolve('node_modules/pkg/subfolder2/index.js'), ''); +// fs.mkdirSync(tmpdir.resolve('node_modules/pkg2')); +// fs.writeFileSync(tmpdir.resolve('node_modules/pkg2/package.json'), JSON.stringify({ name: 'pkg', main: tmpdir.resolve('node_modules/pkg/subfolder2/index.js'), secretNumber4 })); + + +// common.spawnPromisified(process.execPath, [tmpdir.resolve('entry.mjs')]).then(common.mustCall((result) => { +// assert.deepStrictEqual(result, { +// stdout: `${secretNumber1} ${secretNumber2} ${secretNumber3} ${secretNumber4}\n`, +// stderr: '', +// code: 0, +// signal: null, +// }) +// })) +// } From 1c80d6d04f772bf904e3b65d114cf567d0dd6bf8 Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:55:44 +0200 Subject: [PATCH 02/29] fixup!: `DeserializedPackageConfig` --- lib/internal/modules/cjs/loader.js | 50 ++++----- lib/internal/modules/package_json_reader.js | 110 +++++++++++--------- test/parallel/test-find-package-json.js | 8 +- typings/internalBinding/modules.d.ts | 13 ++- 4 files changed, 95 insertions(+), 86 deletions(-) diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index af37c2511200da..876550bb109ca9 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -146,6 +146,7 @@ const { safeGetenv } = internalBinding('credentials'); const { getCjsConditions, initializeCjsConditions, + isUnderNodeModules, loadBuiltinModule, makeRequireFunction, setHasStartedUserCJSExecution, @@ -602,11 +603,11 @@ function trySelf(parentPath, request) { try { const { packageExportsResolve } = require('internal/modules/esm/resolve'); return finalizeEsmResolution(packageExportsResolve( - pathToFileURL(pkg.path + '/package.json'), expansion, pkg.data, + pathToFileURL(pkg.path), expansion, pkg.data, pathToFileURL(parentPath), getCjsConditions()), parentPath, pkg.path); } catch (e) { if (e.code === 'ERR_MODULE_NOT_FOUND') { - throw createEsmNotFoundErr(request, pkg.path + '/package.json'); + throw createEsmNotFoundErr(request, pkg.path); } throw e; } @@ -1201,14 +1202,15 @@ Module._resolveFilename = function(request, parent, isMain, options) { if (request[0] === '#' && (parent?.filename || parent?.id === '')) { const parentPath = parent?.filename ?? process.cwd() + path.sep; - const pkg = packageJsonReader.getNearestParentPackageJSON(parentPath) || { __proto__: null }; - if (pkg.data?.imports != null) { + const pkg = packageJsonReader.getNearestParentPackageJSON(parentPath); + if (pkg?.data.imports != null) { try { const { packageImportsResolve } = require('internal/modules/esm/resolve'); return finalizeEsmResolution( - packageImportsResolve(request, pathToFileURL(parentPath), - getCjsConditions()), parentPath, - pkg.path); + packageImportsResolve(request, pathToFileURL(parentPath), getCjsConditions()), + parentPath, + pkg.path, + ); } catch (e) { if (e.code === 'ERR_MODULE_NOT_FOUND') { throw createEsmNotFoundErr(request); @@ -1268,8 +1270,7 @@ function finalizeEsmResolution(resolved, parentPath, pkgPath) { if (actual) { return actual; } - const err = createEsmNotFoundErr(filename, - path.resolve(pkgPath, 'package.json')); + const err = createEsmNotFoundErr(filename, pkgPath); throw err; } @@ -1431,13 +1432,10 @@ function loadESMFromCJS(mod, filename) { // createRequiredModuleFacade() to `wrap` which is a ModuleWrap wrapping // over the original module. - // We don't do this to modules that are marked as CJS ESM or that - // don't have default exports to avoid the unnecessary overhead. - // If __esModule is already defined, we will also skip the extension - // to allow users to override it. - if (ObjectHasOwn(namespace, 'module.exports')) { - mod.exports = namespace['module.exports']; - } else if (!ObjectHasOwn(namespace, 'default') || ObjectHasOwn(namespace, '__esModule')) { + // We don't do this to modules that don't have default exports to avoid + // the unnecessary overhead. If __esModule is already defined, we will + // also skip the extension to allow users to override it. + if (!ObjectHasOwn(namespace, 'default') || ObjectHasOwn(namespace, '__esModule')) { mod.exports = namespace; } else { mod.exports = createRequiredModuleFacade(wrap); @@ -1453,7 +1451,7 @@ function loadESMFromCJS(mod, filename) { * @param {'commonjs'|undefined} format Intended format of the module. */ function wrapSafe(filename, content, cjsModuleInstance, format) { - assert(format !== 'module', 'ESM should be handled in loadESMFromCJS()'); + assert(format !== 'module'); // ESM should be handled in loadESMFromCJS(). const hostDefinedOptionId = vm_dynamic_import_default_internal; const importModuleDynamically = vm_dynamic_import_default_internal; if (patched) { @@ -1483,17 +1481,7 @@ function wrapSafe(filename, content, cjsModuleInstance, format) { }; } - let shouldDetectModule = false; - if (format !== 'commonjs') { - if (cjsModuleInstance?.[kIsMainSymbol]) { - // For entry points, format detection is used unless explicitly disabled. - shouldDetectModule = getOptionValue('--experimental-detect-module'); - } else { - // For modules being loaded by `require()`, if require(esm) is disabled, - // don't try to reparse to detect format and just throw for ESM syntax. - shouldDetectModule = getOptionValue('--experimental-require-module'); - } - } + const shouldDetectModule = (format !== 'commonjs' && getOptionValue('--experimental-detect-module')); const result = compileFunctionForCJSLoader(content, filename, false /* is_sea_main */, shouldDetectModule); // Cache the source map for the module if present. @@ -1523,6 +1511,8 @@ Module.prototype._compile = function(content, filename, format) { } } + // TODO(joyeecheung): when the module is the entry point, consider allowing TLA. + // Only modules being require()'d really need to avoid TLA. if (format === 'module') { // Pass the source into the .mjs extension handler indirectly through the cache. this[kModuleSource] = content; @@ -1623,7 +1613,7 @@ function loadTS(module, filename) { const parent = module[kModuleParent]; const parentPath = parent?.filename; - const packageJsonPath = path.resolve(pkg.path, 'package.json'); + const packageJsonPath = pkg.path; const usesEsm = containsModuleSyntax(content, filename); const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath, packageJsonPath); @@ -1682,7 +1672,7 @@ Module._extensions['.js'] = function(module, filename) { // This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed. const parent = module[kModuleParent]; const parentPath = parent?.filename; - const packageJsonPath = path.resolve(pkg.path, 'package.json'); + const packageJsonPath = pkg.path; const usesEsm = containsModuleSyntax(content, filename); const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath, packageJsonPath); diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 3d3643d2961faa..73f01ece08e534 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -4,8 +4,6 @@ const { ArrayIsArray, JSONParse, ObjectDefineProperty, - StringPrototypeLastIndexOf, - StringPrototypeSlice, } = primordials; const { fileURLToPath } = require('internal/url'); const { kEmptyObject } = require('internal/util'); @@ -14,24 +12,31 @@ const { validateURLString, } = require('internal/validators'); const modulesBinding = internalBinding('modules'); -const { resolve, sep } = require('path'); +const { resolve } = require('path'); /** - * @param {string} path - * @param {import('typings/internalBinding/modules').SerializedPackageConfig} contents - * @returns {import('typings/internalBinding/modules').PackageConfig} + * @typedef {import('typings/internalBinding/modules').DeserializedPackageConfig} DeserializedPackageConfig + * @typedef {import('typings/internalBinding/modules').PackageConfig} PackageConfig + * @typedef {import('typings/internalBinding/modules').SerializedPackageConfig} SerializedPackageConfig + */ + +/** + * @param {URL['pathname']} path + * @param {SerializedPackageConfig} contents + * @returns {DeserializedPackageConfig} */ function deserializePackageJSON(path, contents) { if (contents === undefined) { return { - __proto__: null, + data: { + __proto__: null, + type: 'none', // Ignore unknown types for forwards compatibility + }, exists: false, - pjsonPath: path, - type: 'none', // Ignore unknown types for forwards compatibility + path, }; } - let pjsonPath = path; const { 0: name, 1: main, @@ -41,37 +46,40 @@ function deserializePackageJSON(path, contents) { 5: optionalFilePath, } = contents; - // This is required to be used in getPackageScopeConfig. - if (optionalFilePath) { - pjsonPath = optionalFilePath; - } - - // The imports and exports fields can be either undefined or a string. - // - If it's a string, it's either plain string or a stringified JSON string. - // - If it's a stringified JSON string, it starts with either '[' or '{'. - const requiresJSONParse = (value) => (value !== undefined && (value[0] === '[' || value[0] === '{')); + const pjsonPath = optionalFilePath ?? path; return { - __proto__: null, - exists: true, - pjsonPath, - name, - main, - type, - // This getters are used to lazily parse the imports and exports fields. - get imports() { - const value = requiresJSONParse(plainImports) ? JSONParse(plainImports) : plainImports; - ObjectDefineProperty(this, 'imports', { __proto__: null, value }); - return this.imports; - }, - get exports() { - const value = requiresJSONParse(plainExports) ? JSONParse(plainExports) : plainExports; - ObjectDefineProperty(this, 'exports', { __proto__: null, value }); - return this.exports; + data: { + __proto__: null, + ...(name != null && {name }), + ...(main != null && {main }), + ...(type != null && {type }), + ...(plainImports != null && { + // This getters are used to lazily parse the imports and exports fields. + get imports() { + const value = requiresJSONParse(plainImports) ? JSONParse(plainImports) : plainImports; + ObjectDefineProperty(this, 'imports', { __proto__: null, value }); + return this.imports; + }, + }), + ...(plainExports != null && { + get exports() { + const value = requiresJSONParse(plainExports) ? JSONParse(plainExports) : plainExports; + ObjectDefineProperty(this, 'exports', { __proto__: null, value }); + return this.exports; + }, + }), }, + exists: true, + path: pjsonPath, }; } +// The imports and exports fields can be either undefined or a string. +// - If it's a string, it's either plain string or a stringified JSON string. +// - If it's a stringified JSON string, it starts with either '[' or '{'. +const requiresJSONParse = (value) => (value !== undefined && (value[0] === '[' || value[0] === '{')); + /** * Reads a package.json file and returns the parsed contents. * @param {string} jsonPath @@ -80,7 +88,7 @@ function deserializePackageJSON(path, contents) { * specifier?: URL | string, * isESM?: boolean, * }} options - * @returns {import('typings/internalBinding/modules').PackageConfig} + * @returns {PackageConfig} */ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) { // This function will be called by both CJS and ESM, so we need to make sure @@ -92,7 +100,14 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) { specifier == null ? undefined : `${specifier}`, ); - return deserializePackageJSON(jsonPath, parsed); + const result = deserializePackageJSON(jsonPath, parsed); + + return { + __proto__: null, + ...result.data, + exists: result.exists, + pjsonPath: result.path, + }; } /** @@ -109,8 +124,8 @@ function readPackage(requestPath) { /** * Get the nearest parent package.json file from a given path. * Return the package.json data and the path to the package.json file, or undefined. - * @param {string} checkPath The path to start searching from. - * @returns {undefined | {data: import('typings/internalBinding/modules').PackageConfig, path: string}} + * @param {URL['pathname']} checkPath The path to start searching from. + * @returns {undefined | DeserializedPackageConfig} */ function getNearestParentPackageJSON(checkPath) { const result = modulesBinding.getNearestParentPackageJSON(checkPath); @@ -119,13 +134,7 @@ function getNearestParentPackageJSON(checkPath) { return undefined; } - const data = deserializePackageJSON(checkPath, result); - - // Path should be the root folder of the matched package.json - // For example for ~/path/package.json, it should be ~/path - const path = StringPrototypeSlice(data.pjsonPath, 0, StringPrototypeLastIndexOf(data.pjsonPath, sep)); - - return { data, path }; + return deserializePackageJSON(checkPath, result); } /** @@ -137,7 +146,14 @@ function getPackageScopeConfig(resolved) { const result = modulesBinding.getPackageScopeConfig(`${resolved}`); if (ArrayIsArray(result)) { - return deserializePackageJSON(`${resolved}`, result); + const { data, exists, path } = deserializePackageJSON(`${resolved}`, result); + + return { + __proto__: null, + ...data, + exists, + pjsonPath: path, + }; } // This means that the response is a string diff --git a/test/parallel/test-find-package-json.js b/test/parallel/test-find-package-json.js index 2bd4d3c3d1c5da..834eb79ebd0cc6 100644 --- a/test/parallel/test-find-package-json.js +++ b/test/parallel/test-find-package-json.js @@ -25,11 +25,11 @@ describe('findPackageJSON', () => { // Throws when no arguments are provided }); it('should accept a file URL (string), like from `import.meta.resolve()`', () => { - const importMetaUrl = `${pathToFileURL(fixtures.fixturesDir)}/` - const specifier = './packages/root-types-field/index.js'; + const importMetaUrl = `${pathToFileURL(fixtures.fixturesDir)}/`; + const specifierBase = './packages/root-types-field/'; assert.strictEqual( - findPackageJSON(specifier, importMetaUrl), - fixtures.path(specifier, 'package.json') + findPackageJSON(`${specifierBase}/index.js`, importMetaUrl), + fixtures.path(specifierBase, 'package.json') ); }); }); diff --git a/typings/internalBinding/modules.d.ts b/typings/internalBinding/modules.d.ts index 8142874edfde88..7d35b864dae356 100644 --- a/typings/internalBinding/modules.d.ts +++ b/typings/internalBinding/modules.d.ts @@ -1,27 +1,30 @@ export type PackageType = 'commonjs' | 'module' | 'none' export type PackageConfig = { - pjsonPath: string - exists: boolean name?: string main?: any type: PackageType exports?: string | string[] | Record imports?: string | string[] | Record } +export type DeserializedPackageConfig = { + data: PackageConfig, + exists: boolean, + path: URL['pathname'], +} export type SerializedPackageConfig = [ PackageConfig['name'], PackageConfig['main'], PackageConfig['type'], string | undefined, // exports string | undefined, // imports - string | undefined, // raw json available for experimental policy + DeserializedPackageConfig['path'], // pjson file path ] export interface ModulesBinding { readPackageJSON(path: string): SerializedPackageConfig | undefined; - getNearestParentPackageJSON(path: string): PackageConfig | undefined + getNearestParentPackageJSON(path: string): SerializedPackageConfig | undefined + getNearestRawParentPackageJSON(origin: URL['pathname']): [ReturnType, DeserializedPackageConfig['path']] | undefined getNearestParentPackageJSONType(path: string): PackageConfig['type'] getPackageScopeConfig(path: string): SerializedPackageConfig | undefined getPackageJSONScripts(): string | undefined - flushCompileCache(keepDeserializedCache?: boolean): void } From 746a88c71a70964fb3d2f6f9645edd7bc114820f Mon Sep 17 00:00:00 2001 From: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:42:30 +0200 Subject: [PATCH 03/29] fixup!: handle CJS & relative specifiers --- doc/api/module.md | 63 +++++++++ lib/internal/modules/package_json_reader.js | 46 ++++-- .../packages/nested/sub-pkg-cjs/index.js | 5 +- .../packages/nested/sub-pkg-esm/index.js | 3 + .../{sub-pkg-mjs => sub-pkg-esm}/package.json | 0 .../packages/nested/sub-pkg-mjs/index.js | 3 - test/parallel/test-find-package-json.js | 132 +++--------------- 7 files changed, 127 insertions(+), 125 deletions(-) create mode 100644 test/fixtures/packages/nested/sub-pkg-esm/index.js rename test/fixtures/packages/nested/{sub-pkg-mjs => sub-pkg-esm}/package.json (100%) delete mode 100644 test/fixtures/packages/nested/sub-pkg-mjs/index.js diff --git a/doc/api/module.md b/doc/api/module.md index 8662cc218898d5..14a03b2c322023 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -217,6 +217,69 @@ added: v22.8.0 * Returns: {string|undefined} Path to the [module compile cache][] directory if it is enabled, or `undefined` otherwise. +### `module.findPackageJSON(startLocation, parentLocation)` + + + +> Stability: 1.1 - Active Development + +* `startLocation` {string} Where to look (relative to `parentLocation`). This can be a + relative/unresolved specifier (ex `'..'`) or a package name. +* `parentLocation` {string} The absolute location (file URL string or FS path) of the containing + module. For CJS, use `__filename` (not `__dirname`!); for ESM, use `import.meta.url`. +* Returns: {string} A file URL string. When `startLocation` is a package, the package's root + package.json; when a relative or unresolved, the closest package.json to the `startLocation`. + +> **Caveat**: Do not use this to try to determine module format. There are many things effecting +> that determination; the `type` field of package.json is the _least_ definitive (ex file extension +> superceeds it, and a loader hook superceeds that). + +``` +/path/to/project + ├ packages/ + ├ bar/ + ├ bar.js + └ package.json // name = '@foo/bar' + └ qux/ + ├ node_modules/ + └ some-package/ + └ package.json // name = 'some-package' + ├ qux.js + └ package.json // name = '@foo/qux' + ├ main.js + └ package.json // name = '@foo' +``` + +```mjs +// /path/to/project/packages/bar/bar.js +import { findPackageJSON } from 'node:module'; + +findPackageJSON('..', import.meta.url); +// 'file:///path/to/project/package.json' + +findPackageJSON('some-package', import.meta.url); +// 'file:///path/to/project/packages/bar/node_modules/some-package/package.json' + +findPackageJSON('@foo/qux', import.meta.url); +// 'file:///path/to/project/packages/qux/package.json' +``` + +```cjs +// /path/to/project/packages/bar/bar.js +const { findPackageJSON } = require('node:module'); + +findPackageJSON('..', __filename); +// 'file:///path/to/project/package.json' + +findPackageJSON('some-package', __filename); +// 'file:///path/to/project/packages/bar/node_modules/some-package/package.json' + +findPackageJSON('@foo/qux', __filename); +// 'file:///path/to/project/packages/qux/package.json' +``` + ### `module.isBuiltin(moduleName)`