Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Look for node_ceiling #43828

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
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
20 changes: 14 additions & 6 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 All @@ -238,7 +244,7 @@ NODE_MODULES_PATHS(START)
4. while I >= 0,
a. if PARTS[I] = "node_modules" CONTINUE
b. DIR = path join(PARTS[0 .. I] + "node_modules")
c. DIRS = DIR + DIRS
c. DIRS = DIRS + DIR
d. let I = I - 1
5. return DIRS + GLOBAL_FOLDERS

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 '../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');
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';
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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
'use strict';
console.log(require('foo').string);
28 changes: 28 additions & 0 deletions test/parallel/test-cjs-loader-node_ceiling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'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');
}