Skip to content

Commit

Permalink
module: look for node_ceiling
Browse files Browse the repository at this point in the history
Stop searching for package.json or node_modules when a node_ceiling
file is found.

Refs: nodejs#43192
Refs: nodejs#43368
  • Loading branch information
arhart committed Jul 13, 2022
1 parent 8536460 commit 0006156
Show file tree
Hide file tree
Showing 29 changed files with 157 additions and 19 deletions.
21 changes: 14 additions & 7 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -1307,18 +1307,21 @@ The resolver can throw the following errors:
> 11. While _parentURL_ is not the file system root,
> 1. Let _packageURL_ be the URL resolution of _"node\_modules/"_
> concatenated with _packageSpecifier_, relative to _parentURL_.
> 2. Set _parentURL_ to the parent folder URL of _parentURL_.
> 3. If the folder at _packageURL_ does not exist, then
> 1. Continue the next loop iteration.
> 4. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
> 5. If _pjson_ is not **null** and _pjson_._exports_ is not **null** or
> 2. Let _ceilingURL_ be the URL resolution of _"node\_ceiling"_,
> relative to _parentURL_.
> 3. Set _parentURL_ to the parent folder URL of _parentURL_.
> 4. If the folder at _packageURL_ does not exist, then
> 1. If the file at _ceilingURL_ exists, then break out of the loop.
> 2. Otherwise, continue the next loop iteration.
> 5. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
> 6. If _pjson_ is not **null** and _pjson_._exports_ is not **null** or
> **undefined**, then
> 1. Return the result of **PACKAGE\_EXPORTS\_RESOLVE**(_packageURL_,
> _packageSubpath_, _pjson.exports_, _defaultConditions_).
> 6. Otherwise, if _packageSubpath_ is equal to _"."_, then
> 7. Otherwise, if _packageSubpath_ is equal to _"."_, then
> 1. If _pjson.main_ is a string, then
> 1. Return the URL resolution of _main_ in _packageURL_.
> 7. Otherwise,
> 8. Otherwise,
> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
> 12. Throw a _Module Not Found_ error.
Expand Down Expand Up @@ -1503,6 +1506,10 @@ _internal_, _conditions_)
> _scopeURL_.
> 4. if the file at _pjsonURL_ exists, then
> 1. Return _scopeURL_.
> 5. Let _ceilingURL_ be the resolution of _"node\_ceiling"_ within
> _scopeURL_.
> 6. if the file at _ceilingURL_ exists, then
> 1. Return **null**.
> 3. Return **null**.
**READ\_PACKAGE\_JSON**(_packageURL_)
Expand Down
18 changes: 13 additions & 5 deletions doc/api/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,16 @@ LOAD_AS_DIRECTORY(X)

LOAD_NODE_MODULES(X, START)
1. let DIRS = NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_PACKAGE_EXPORTS(X, DIR)
b. LOAD_AS_FILE(DIR/X)
c. LOAD_AS_DIRECTORY(DIR/X)
2. let FOUND_ROOT = false
3. let FOUND_CEILING = false
4. for each DIR in DIRS:
a. If FOUND_ROOT is false
1. If path resolve(DIR, "../../node_modules") = DIR, let FOUND_ROOT = true
2. If FOUND_CEILING is true, CONTINUE
b. LOAD_PACKAGE_EXPORTS(X, DIR)
c. LOAD_AS_FILE(DIR/X)
d. LOAD_AS_DIRECTORY(DIR/X)
e. If FOUND_ROOT is false && FOUND_CEILING is false && path resolve(DIR, "../node_ceiling") is a file, let FOUND_CEILING = true

NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
Expand Down Expand Up @@ -493,7 +499,9 @@ Node.js will not append `node_modules` to a path already ending in
`node_modules`.

If it is not found there, then it moves to the parent directory, and so
on, until the root of the file system is reached.
on, until the root of the file system is reached. If a `node_ceiling`
file is found in a directory, Node.js stops moving to the parent directory
before the root of the file system is reached.

For example, if the file at `'/home/ry/projects/foo.js'` called
`require('bar.js')`, then Node.js would look in the following locations, in
Expand Down
8 changes: 5 additions & 3 deletions doc/api/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ changes:
A package is a folder tree described by a `package.json` file. The package
consists of the folder containing the `package.json` file and all subfolders
until the next folder containing another `package.json` file, or a folder
named `node_modules`.
named `node_modules`, or a folder containing a `node_ceiling` file.

This page provides guidance for package authors writing `package.json` files
along with a reference for the [`package.json`][] fields defined by Node.js.
Expand Down Expand Up @@ -1201,7 +1201,8 @@ Files ending with `.js` are loaded as ES modules when the nearest parent

The nearest parent `package.json` is defined as the first `package.json` found
when searching in the current folder, that folder's parent, and so on up
until a node\_modules folder or the volume root is reached.
until a node\_modules folder or a folder containing a `node_ceiling` file or
the volume root is reached.

```json
// package.json
Expand All @@ -1216,7 +1217,8 @@ node my-app.js # Runs as ES module
```

If the nearest parent `package.json` lacks a `"type"` field, or contains
`"type": "commonjs"`, `.js` files are treated as [CommonJS][]. If the volume
`"type": "commonjs"`, `.js` files are treated as [CommonJS][]. If a folder
containing a `node_ceiling` file is reached or the volume
root is reached and no `package.json` is found, `.js` files are treated as
[CommonJS][].

Expand Down
38 changes: 37 additions & 1 deletion lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,12 @@ function readPackageScope(checkPath) {
data: pjson,
path: checkPath,
};
const ceilingPath = checkPath + sep + 'node_ceiling';
const rc = stat(ceilingPath);
if (rc === 0) { // File.
debug('found node_ceiling at', ceilingPath);
return false;
}
} while (separatorIndex > rootSeparatorIndex);
return false;
}
Expand Down Expand Up @@ -501,11 +507,25 @@ function resolveExports(nmPath, request) {
}
}

function lookForCeiling(foundCeiling, foundRoot, curPath) {
if (!foundCeiling && !foundRoot) {
const ceilingPath = path.resolve(curPath, '../node_ceiling');
const rc = stat(ceilingPath);
if (rc === 0) { // File.
debug('found node_ceiling at', ceilingPath);
return true;
}
}
return foundCeiling;
}
const trailingSlashRegex = /(?:^|\/)\.?\.$/;
Module._findPath = function(request, paths, isMain) {
let foundRoot = false;
let foundCeiling = false;
const absoluteRequest = path.isAbsolute(request);
if (absoluteRequest) {
paths = [''];
foundRoot = true;
} else if (!paths || paths.length === 0) {
return false;
}
Expand All @@ -527,7 +547,21 @@ Module._findPath = function(request, paths, isMain) {
for (let i = 0; i < paths.length; i++) {
// Don't search further if path doesn't exist
const curPath = paths[i];
if (curPath && stat(curPath) < 1) continue;

if (!foundRoot) {
foundRoot = curPath === path.resolve(curPath, '../../node_modules');
if (foundRoot) {
debug('foundRoot', curPath);
}
if (foundCeiling) {
continue;
}
}

if (curPath && stat(curPath) < 1) {
foundCeiling = lookForCeiling(foundCeiling, foundRoot, curPath);
continue;
}

if (!absoluteRequest) {
const exportsResolved = resolveExports(curPath, request);
Expand Down Expand Up @@ -581,6 +615,8 @@ Module._findPath = function(request, paths, isMain) {
Module._pathCache[cacheKey] = filename;
return filename;
}

foundCeiling = lookForCeiling(foundCeiling, foundRoot, curPath);
}

return false;
Expand Down
17 changes: 15 additions & 2 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const {
statSync,
Stats,
} = require('fs');
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
debug = fn;
});
const { getOptionValue } = require('internal/options');
// Do not eagerly grab .manifest, it may be in TDZ
const policy = getOptionValue('--experimental-policy') ?
Expand Down Expand Up @@ -235,6 +238,12 @@ function getPackageScopeConfig(resolved) {
resolved);
if (packageConfig.exists) return packageConfig;

const ceilingURL = new URL('./node_ceiling', packageJSONUrl);
if (fileExists(ceilingURL)) {
debug('found node_ceiling at', ceilingURL.href);
break;
}

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

Expand Down Expand Up @@ -878,6 +887,12 @@ function packageResolve(specifier, base, conditions) {
const stat = tryStatSync(StringPrototypeSlice(packageJSONPath, 0,
packageJSONPath.length - 13));
if (!stat.isDirectory()) {
const ceilingURL = new URL(isScoped ? '../../../node_ceiling' : '../../node_ceiling', packageJSONUrl);
if (fileExists(ceilingURL)) {
debug('found node_ceiling at', ceilingURL.href);
break;
}

lastPath = packageJSONPath;
packageJSONUrl = new URL((isScoped ?
'../../../../node_modules/' : '../../../node_modules/') +
Expand All @@ -904,8 +919,6 @@ function packageResolve(specifier, base, conditions) {
// Cross-platform root check.
} while (packageJSONPath.length !== lastPath.length);

// eslint can't handle the above code.
// eslint-disable-next-line no-unreachable
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base));
}

Expand Down
3 changes: 2 additions & 1 deletion test/common/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,8 @@ spawn(...common.pwdCommand, { stdio: ['pipe'] });
* `dir` [\<string>][<string>] default = \_\_dirname

Throws an `AssertionError` if a `package.json` file exists in any ancestor
directory above `dir`. Such files may interfere with proper test functionality.
directory above `dir` but stops searching if a `node_ceiling` file is found
first. Such `package.json` files may interfere with proper test functionality.

### `runWithInvalidFD(func)`

Expand Down
4 changes: 4 additions & 0 deletions test/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,10 @@ function requireNoPackageJSONAbove(dir = __dirname) {
'This test shouldn\'t load properties from a package.json above ' +
`its file location. Found package.json at ${possiblePackage}.`);
}
const ceilingPath = path.join(possiblePackage, '../node_ceiling');
if (fs.statSync(ceilingPath, { throwIfNoEntry: false })?.isFile() ?? false) {
break;
}
lastPackage = possiblePackage;
possiblePackage = path.join(possiblePackage, '..', '..', 'package.json');
}
Expand Down
13 changes: 13 additions & 0 deletions test/es-module/test-esm-node_ceiling.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as common from '../common/index.mjs';
import * as fixtures from '../common/fixtures.mjs';
import assert from 'node:assert';
import path from 'node:path';

// should not reject
await import(path.resolve(fixtures.path('/es-module-node_ceiling/nested-without-node_ceiling/find-dep.mjs')));
await import(path.resolve(fixtures.path('/es-module-node_ceiling/find-dep.mjs')));

await assert.rejects(
import(path.resolve(fixtures.path('/es-module-node_ceiling/nested-with-node_ceiling/dep-not-found.mjs'))),
{ code: 'ERR_MODULE_NOT_FOUND' },
);
1 change: 1 addition & 0 deletions test/es-module/test-esm-resolve-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ try {
*/
[
[ '/es-modules/package-type-module/index.js', 'module' ],
[ '/es-modules/package-type-module/nested-with-node_ceiling/index.js', 'commonjs' ],
[ '/es-modules/package-type-commonjs/index.js', 'commonjs' ],
[ '/es-modules/package-without-type/index.js', 'commonjs' ],
[ '/es-modules/package-without-pjson/index.js', 'commonjs' ],
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/cjs-loader-node_ceiling/find-dep.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('dep');
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('dep');
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('dep');
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('#dep');
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"imports": {
"#dep": {
"node": "renamed"
}
}
}
1 change: 1 addition & 0 deletions test/fixtures/es-module-node_ceiling/find-dep.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import dep from 'dep';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import dep from 'dep';
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import dep from 'dep';
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// No package.json -> should still be CommonJS as there is node_ceiling
module.exports = 42;
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict';
console.log(require('foo').string);
27 changes: 27 additions & 0 deletions test/parallel/test-cjs-loader-node_ceiling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict';

require('../common');
const fixtures = require('../common/fixtures');
const path = require('node:path');
const assert = require('node:assert');

// should not throw
require(path.resolve(fixtures.path('/cjs-loader-node_ceiling/nested-without-node_ceiling/find-dep.js')));
require(path.resolve(fixtures.path('/cjs-loader-node_ceiling/find-dep.js')));

assert.throws(
() => {
require(path.resolve(fixtures.path('/cjs-loader-node_ceiling/nested-with-node_ceiling/dep-not-found.js')));
}, {
code: 'MODULE_NOT_FOUND',
}
);

assert.throws(
() => {
require(path.resolve(fixtures.path('/cjs-loader-node_ceiling/package-not-found-due-to-node_ceiling/dir-with-node_ceiling/dep-not-found.js')));
}, {
code: 'MODULE_NOT_FOUND',
}
);

8 changes: 8 additions & 0 deletions test/parallel/test-module-loading-globalpaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,12 @@ if (process.argv[2] === 'child') {
[ path.join(localDir, 'test.js') ],
{ encoding: 'utf8', env: env });
assert.strictEqual(child.trim(), 'local');

// Test module in local folder above node_ceiling is not loaded but NODE_PATH is still loaded
env.HOME = env.USERPROFILE = bothHomeDir;
env.NODE_PATH = path.join(testFixturesDir, 'node_path');
const child2 = child_process.execFileSync(testExecPath,
[ path.join(localDir, 'nested-with-node_ceiling', 'test.js') ],
{ encoding: 'utf8', env: env });
assert.strictEqual(child2.trim(), '$NODE_PATH');
}

0 comments on commit 0006156

Please sign in to comment.