src: implement IsInsideNodeModules() in C++

This previously compiles a script and run it in a new context
to avoid global pollution, which is more complex than necessary
and can be too slow for it to be reused in other cases. The
new implementation just checks the frames in C++ which is safe
from global pollution, faster and simpler.

The previous implementation also had a bug when the call site
is in a ESM, because ESM have URLs as their script names,
which don't start with '/' or '\' and will be skipped. The new
implementation removes the skipping to fix it for ESM.

PR-URL: https://github.com/nodejs/node/pull/55286
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Joyee Cheung 2024-10-08 12:19:46 +02:00 committed by GitHub
parent 8dbca2d35b
commit 14353387eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 108 additions and 31 deletions

View File

@ -79,10 +79,10 @@ const {
ONLY_ENUMERABLE, ONLY_ENUMERABLE,
}, },
getOwnNonIndexProperties, getOwnNonIndexProperties,
isInsideNodeModules,
} = internalBinding('util'); } = internalBinding('util');
const { const {
customInspectSymbol, customInspectSymbol,
isInsideNodeModules,
lazyDOMException, lazyDOMException,
normalizeEncoding, normalizeEncoding,
kIsEncodingSymbol, kIsEncodingSymbol,
@ -178,13 +178,15 @@ function showFlaggedDeprecation() {
if (bufferWarningAlreadyEmitted || if (bufferWarningAlreadyEmitted ||
++nodeModulesCheckCounter > 10000 || ++nodeModulesCheckCounter > 10000 ||
(!require('internal/options').getOptionValue('--pending-deprecation') && (!require('internal/options').getOptionValue('--pending-deprecation') &&
isInsideNodeModules())) { isInsideNodeModules(100, true))) {
// We don't emit a warning, because we either: // We don't emit a warning, because we either:
// - Already did so, or // - Already did so, or
// - Already checked too many times whether a call is coming // - Already checked too many times whether a call is coming
// from node_modules and want to stop slowing down things, or // from node_modules and want to stop slowing down things, or
// - We aren't running with `--pending-deprecation` enabled, // - We aren't running with `--pending-deprecation` enabled,
// and the code is inside `node_modules`. // and the code is inside `node_modules`.
// - We found node_modules in up to the topmost 100 frames, or
// there are more than 100 frames and we don't want to search anymore.
return; return;
} }

View File

@ -2,7 +2,6 @@
const { const {
ArrayFrom, ArrayFrom,
ArrayIsArray,
ArrayPrototypePush, ArrayPrototypePush,
ArrayPrototypeSlice, ArrayPrototypeSlice,
ArrayPrototypeSort, ArrayPrototypeSort,
@ -34,9 +33,7 @@ const {
SafeSet, SafeSet,
SafeWeakMap, SafeWeakMap,
SafeWeakRef, SafeWeakRef,
StringPrototypeIncludes,
StringPrototypeReplace, StringPrototypeReplace,
StringPrototypeStartsWith,
StringPrototypeToLowerCase, StringPrototypeToLowerCase,
StringPrototypeToUpperCase, StringPrototypeToUpperCase,
Symbol, Symbol,
@ -516,31 +513,6 @@ function getStructuredStack() {
return getStructuredStackImpl(); return getStructuredStackImpl();
} }
function isInsideNodeModules() {
const stack = getStructuredStack();
// Iterate over all stack frames and look for the first one not coming
// from inside Node.js itself:
if (ArrayIsArray(stack)) {
for (const frame of stack) {
const filename = frame.getFileName();
if (
filename == null ||
StringPrototypeStartsWith(filename, 'node:') === true ||
(
filename[0] !== '/' &&
StringPrototypeIncludes(filename, '\\') === false
)
) {
continue;
}
return isUnderNodeModules(filename);
}
}
return false;
}
function once(callback, { preserveReturnValue = false } = kEmptyObject) { function once(callback, { preserveReturnValue = false } = kEmptyObject) {
let called = false; let called = false;
let returnValue; let returnValue;
@ -914,7 +886,6 @@ module.exports = {
getSystemErrorName, getSystemErrorName,
guessHandleType, guessHandleType,
isError, isError,
isInsideNodeModules,
isUnderNodeModules, isUnderNodeModules,
isMacOS, isMacOS,
isWindows, isWindows,

View File

@ -298,6 +298,48 @@ static void GetCallSite(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(callsites); args.GetReturnValue().Set(callsites);
} }
static void IsInsideNodeModules(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
CHECK_EQ(args.Length(), 2);
CHECK(args[0]->IsInt32()); // frame_limit
// The second argument is the default value.
int frames_limit = args[0].As<v8::Int32>()->Value();
Local<StackTrace> stack =
StackTrace::CurrentStackTrace(isolate, frames_limit);
int frame_count = stack->GetFrameCount();
// If the search requires looking into more than |frames_limit| frames, give
// up and return the specified default value.
if (frame_count == frames_limit) {
return args.GetReturnValue().Set(args[1]);
}
bool result = false;
for (int i = 0; i < frame_count; ++i) {
Local<StackFrame> stack_frame = stack->GetFrame(isolate, i);
Local<String> script_name = stack_frame->GetScriptName();
if (script_name.IsEmpty() || script_name->Length() == 0) {
continue;
}
Utf8Value script_name_utf8(isolate, script_name);
std::string_view script_name_str = script_name_utf8.ToStringView();
if (script_name_str.starts_with("node:")) {
continue;
}
if (script_name_str.find("/node_modules/") != std::string::npos ||
script_name_str.find("\\node_modules\\") != std::string::npos ||
script_name_str.find("/node_modules\\") != std::string::npos ||
script_name_str.find("\\node_modules/") != std::string::npos) {
result = true;
break;
}
}
args.GetReturnValue().Set(result);
}
void RegisterExternalReferences(ExternalReferenceRegistry* registry) { void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetPromiseDetails); registry->Register(GetPromiseDetails);
registry->Register(GetProxyDetails); registry->Register(GetProxyDetails);
@ -313,6 +355,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(FastGuessHandleType); registry->Register(FastGuessHandleType);
registry->Register(fast_guess_handle_type_.GetTypeInfo()); registry->Register(fast_guess_handle_type_.GetTypeInfo());
registry->Register(ParseEnv); registry->Register(ParseEnv);
registry->Register(IsInsideNodeModules);
} }
void Initialize(Local<Object> target, void Initialize(Local<Object> target,
@ -396,6 +439,7 @@ void Initialize(Local<Object> target,
target->Set(context, env->constants_string(), constants).Check(); target->Set(context, env->constants_string(), constants).Check();
} }
SetMethod(context, target, "isInsideNodeModules", IsInsideNodeModules);
SetMethodNoSideEffect( SetMethodNoSideEffect(
context, target, "getPromiseDetails", GetPromiseDetails); context, target, "getPromiseDetails", GetPromiseDetails);
SetMethodNoSideEffect(context, target, "getProxyDetails", GetProxyDetails); SetMethodNoSideEffect(context, target, "getProxyDetails", GetProxyDetails);

1
test/fixtures/warning_node_modules/new-buffer-cjs.js generated vendored Normal file
View File

@ -0,0 +1 @@
require('new-buffer-cjs');

View File

@ -0,0 +1 @@
import 'new-buffer-esm'

View File

@ -0,0 +1 @@
new Buffer(10);

View File

@ -0,0 +1,3 @@
{
"main": "index.js"
}

View File

@ -0,0 +1,2 @@
import { Buffer } from 'node:buffer';
new Buffer(10);

View File

@ -0,0 +1,4 @@
{
"main": "index.js",
"type": "module"
}

View File

@ -0,0 +1,48 @@
'use strict';
const common = require('../common');
const fixtures = require('../common/fixtures');
const { spawnSyncAndAssert } = require('../common/child_process');
if (process.env.NODE_PENDING_DEPRECATION)
common.skip('test does not work when NODE_PENDING_DEPRECATION is set');
spawnSyncAndAssert(
process.execPath,
[ fixtures.path('warning_node_modules', 'new-buffer-cjs.js') ],
{
trim: true,
stderr: '',
}
);
spawnSyncAndAssert(
process.execPath,
[ fixtures.path('warning_node_modules', 'new-buffer-esm.mjs') ],
{
trim: true,
stderr: '',
}
);
spawnSyncAndAssert(
process.execPath,
[
'--pending-deprecation',
fixtures.path('warning_node_modules', 'new-buffer-cjs.js'),
],
{
stderr: /DEP0005/
}
);
spawnSyncAndAssert(
process.execPath,
[
'--pending-deprecation',
fixtures.path('warning_node_modules', 'new-buffer-esm.mjs'),
],
{
stderr: /DEP0005/
}
);