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

esm: improve check for ESM syntax #50127

Merged
merged 5 commits into from
Oct 18, 2023
Merged
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
7 changes: 4 additions & 3 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const {
makeContextifyScript,
runScriptInThisContext,
} = require('internal/vm');
const { containsModuleSyntax } = internalBinding('contextify');

const assert = require('internal/assert');
const fs = require('fs');
Expand All @@ -104,7 +105,6 @@ const {
const {
getCjsConditions,
initializeCjsConditions,
hasEsmSyntax,
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
Expand Down Expand Up @@ -1315,7 +1315,7 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
} catch (err) {
if (process.mainModule === cjsModuleInstance) {
const { enrichCJSError } = require('internal/modules/esm/translators');
enrichCJSError(err, content);
enrichCJSError(err, content, filename);
}
throw err;
}
Expand Down Expand Up @@ -1400,10 +1400,11 @@ Module._extensions['.js'] = function(module, filename) {
const pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null };
// Function require shouldn't be used in ES modules.
if (pkg.data?.type === 'module') {
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
const parent = moduleParentCache.get(module);
const parentPath = parent?.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
const usesEsm = hasEsmSyntax(content);
const usesEsm = containsModuleSyntax(content, filename);
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
packageJsonPath);
// Attempt to reconstruct the parent require frame.
Expand Down
10 changes: 5 additions & 5 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ function lazyTypes() {
return _TYPES = require('internal/util/types');
}

const { containsModuleSyntax } = internalBinding('contextify');
const assert = require('internal/assert');
const { readFileSync } = require('fs');
const { dirname, extname, isAbsolute } = require('path');
const {
hasEsmSyntax,
loadBuiltinModule,
stripBOM,
} = require('internal/modules/helpers');
Expand Down Expand Up @@ -166,11 +166,11 @@ translators.set('module', async function moduleStrategy(url, source, isMain) {
* Provide a more informative error for CommonJS imports.
* @param {Error | any} err
* @param {string} [content] Content of the file, if known.
* @param {string} [filename] Useful only if `content` is unknown.
* @param {string} [filename] The filename of the erroring module.
*/
function enrichCJSError(err, content, filename) {
if (err != null && ObjectGetPrototypeOf(err) === SyntaxErrorPrototype &&
hasEsmSyntax(content || readFileSync(filename, 'utf-8'))) {
containsModuleSyntax(content, filename)) {
// Emit the warning synchronously because we are in the middle of handling
// a SyntaxError that will throw and likely terminate the process before an
// asynchronous warning would be emitted.
Expand Down Expand Up @@ -217,7 +217,7 @@ function loadCJSModule(module, source, url, filename) {
importModuleDynamically, // importModuleDynamically
).function;
} catch (err) {
enrichCJSError(err, source, url);
enrichCJSError(err, source, filename);
throw err;
}

Expand Down Expand Up @@ -344,7 +344,7 @@ translators.set('commonjs', async function commonjsStrategy(url, source,
assert(module === CJSModule._cache[filename]);
CJSModule._load(filename);
} catch (err) {
enrichCJSError(err, source, url);
enrichCJSError(err, source, filename);
throw err;
}
} : loadCJSModule;
Expand Down
23 changes: 0 additions & 23 deletions lib/internal/modules/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const {
ArrayPrototypeForEach,
ArrayPrototypeJoin,
ArrayPrototypeSome,
ObjectDefineProperty,
ObjectPrototypeHasOwnProperty,
SafeMap,
Expand Down Expand Up @@ -299,32 +298,10 @@ function normalizeReferrerURL(referrer) {
return new URL(referrer).href;
}

/**
* For error messages only, check if ESM syntax is in use.
* @param {string} code
*/
function hasEsmSyntax(code) {
debug('Checking for ESM syntax');
const parser = require('internal/deps/acorn/acorn/dist/acorn').Parser;
let root;
try {
root = parser.parse(code, { sourceType: 'module', ecmaVersion: 'latest' });
} catch {
return false;
}

return ArrayPrototypeSome(root.body, (stmt) =>
stmt.type === 'ExportDefaultDeclaration' ||
stmt.type === 'ExportNamedDeclaration' ||
stmt.type === 'ImportDeclaration' ||
stmt.type === 'ExportAllDeclaration');
}

module.exports = {
addBuiltinLibsToObject,
getCjsConditions,
initializeCjsConditions,
hasEsmSyntax,
loadBuiltinModule,
makeRequireFunction,
normalizeReferrerURL,
Expand Down
208 changes: 174 additions & 34 deletions src/node_contextify.cc
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,15 @@ void ContextifyContext::CreatePerIsolateProperties(
SetMethod(isolate, target, "makeContext", MakeContext);
SetMethod(isolate, target, "isContext", IsContext);
SetMethod(isolate, target, "compileFunction", CompileFunction);
SetMethod(isolate, target, "containsModuleSyntax", ContainsModuleSyntax);
}

void ContextifyContext::RegisterExternalReferences(
ExternalReferenceRegistry* registry) {
registry->Register(MakeContext);
registry->Register(IsContext);
registry->Register(CompileFunction);
registry->Register(ContainsModuleSyntax);
registry->Register(PropertyGetterCallback);
registry->Register(PropertySetterCallback);
registry->Register(PropertyDescriptorCallback);
Expand Down Expand Up @@ -1204,33 +1206,18 @@ void ContextifyContext::CompileFunction(
data + cached_data_buf->ByteOffset(), cached_data_buf->ByteLength());
}

// Set host_defined_options
Local<PrimitiveArray> host_defined_options =
PrimitiveArray::New(isolate, loader::HostDefinedOptions::kLength);
host_defined_options->Set(
isolate, loader::HostDefinedOptions::kID, id_symbol);

ScriptOrigin origin(isolate,
filename,
line_offset, // line offset
column_offset, // column offset
true, // is cross origin
-1, // script id
Local<Value>(), // source map URL
false, // is opaque (?)
false, // is WASM
false, // is ES Module
host_defined_options);

ScriptCompiler::Source source(code, origin, cached_data);
ScriptCompiler::CompileOptions options;
if (source.GetCachedData() == nullptr) {
options = ScriptCompiler::kNoCompileOptions;
} else {
options = ScriptCompiler::kConsumeCodeCache;
}
GetHostDefinedOptions(isolate, id_symbol);
ScriptCompiler::Source source =
GetCommonJSSourceInstance(isolate,
code,
filename,
line_offset,
column_offset,
host_defined_options,
cached_data);
ScriptCompiler::CompileOptions options = GetCompileOptions(source);

TryCatchScope try_catch(env);
Context::Scope scope(parsing_context);

// Read context extensions from buffer
Expand All @@ -1255,9 +1242,83 @@ void ContextifyContext::CompileFunction(
}
}

TryCatchScope try_catch(env);
Local<Object> result = CompileFunctionAndCacheResult(env,
parsing_context,
&source,
params,
context_extensions,
options,
produce_cached_data,
id_symbol,
try_catch);

if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
try_catch.ReThrow();
return;
}

if (result.IsEmpty()) {
return;
}
args.GetReturnValue().Set(result);
}

Local<PrimitiveArray> ContextifyContext::GetHostDefinedOptions(
Isolate* isolate, Local<Symbol> id_symbol) {
Local<PrimitiveArray> host_defined_options =
PrimitiveArray::New(isolate, loader::HostDefinedOptions::kLength);
host_defined_options->Set(
isolate, loader::HostDefinedOptions::kID, id_symbol);
return host_defined_options;
}

ScriptCompiler::Source ContextifyContext::GetCommonJSSourceInstance(
Isolate* isolate,
Local<String> code,
Local<String> filename,
int line_offset,
int column_offset,
Local<PrimitiveArray> host_defined_options,
ScriptCompiler::CachedData* cached_data) {
ScriptOrigin origin(isolate,
filename,
line_offset, // line offset
column_offset, // column offset
true, // is cross origin
-1, // script id
Local<Value>(), // source map URL
false, // is opaque (?)
false, // is WASM
false, // is ES Module
host_defined_options);
return ScriptCompiler::Source(code, origin, cached_data);
}

ScriptCompiler::CompileOptions ContextifyContext::GetCompileOptions(
const ScriptCompiler::Source& source) {
ScriptCompiler::CompileOptions options;
if (source.GetCachedData() != nullptr) {
options = ScriptCompiler::kConsumeCodeCache;
} else {
options = ScriptCompiler::kNoCompileOptions;
}
return options;
}

Local<Object> ContextifyContext::CompileFunctionAndCacheResult(
Environment* env,
Local<Context> parsing_context,
ScriptCompiler::Source* source,
std::vector<Local<String>> params,
std::vector<Local<Object>> context_extensions,
ScriptCompiler::CompileOptions options,
bool produce_cached_data,
Local<Symbol> id_symbol,
const TryCatchScope& try_catch) {
MaybeLocal<Function> maybe_fn = ScriptCompiler::CompileFunction(
parsing_context,
&source,
source,
params.size(),
params.data(),
context_extensions.size(),
Expand All @@ -1269,24 +1330,26 @@ void ContextifyContext::CompileFunction(
if (!maybe_fn.ToLocal(&fn)) {
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
errors::DecorateErrorStack(env, try_catch);
try_catch.ReThrow();
return Object::New(env->isolate());
}
return;
}

Local<Context> context = env->context();
if (fn->SetPrivate(context, env->host_defined_option_symbol(), id_symbol)
.IsNothing()) {
return;
return Object::New(env->isolate());
}

Isolate* isolate = env->isolate();
Local<Object> result = Object::New(isolate);
if (result->Set(parsing_context, env->function_string(), fn).IsNothing())
return;
return Object::New(env->isolate());
if (result
->Set(parsing_context,
env->source_map_url_string(),
fn->GetScriptOrigin().SourceMapUrl())
.IsNothing())
return;
return Object::New(env->isolate());

std::unique_ptr<ScriptCompiler::CachedData> new_cached_data;
if (produce_cached_data) {
Expand All @@ -1295,14 +1358,91 @@ void ContextifyContext::CompileFunction(
if (StoreCodeCacheResult(env,
result,
options,
source,
*source,
produce_cached_data,
std::move(new_cached_data))
.IsNothing()) {
return;
return Object::New(env->isolate());
}

args.GetReturnValue().Set(result);
return result;
}

// When compiling as CommonJS source code that contains ESM syntax, the
// following error messages are returned:
// - `import` statements: "Cannot use import statement outside a module"
// - `export` statements: "Unexpected token 'export'"
// - `import.meta` references: "Cannot use 'import.meta' outside a module"
// Dynamic `import()` is permitted in CommonJS, so it does not error.
// While top-level `await` is not permitted in CommonJS, it returns the same
// error message as when `await` is used in a sync function, so we don't use it
// as a disambiguation.
constexpr std::array<std::string_view, 3> esm_syntax_error_messages = {
"Cannot use import statement outside a module", // `import` statements
"Unexpected token 'export'", // `export` statements
"Cannot use 'import.meta' outside a module"}; // `import.meta` references
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved

void ContextifyContext::ContainsModuleSyntax(
const FunctionCallbackInfo<Value>& args) {
// Argument 1: source code
CHECK(args[0]->IsString());
Local<String> code = args[0].As<String>();

// Argument 2: filename
Local<String> filename = String::Empty(args.GetIsolate());
if (!args[1]->IsUndefined()) {
CHECK(args[1]->IsString());
filename = args[1].As<String>();
}

Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
Local<Context> context = env->context();

// TODO(geoffreybooth): Centralize this rather than matching the logic in
// cjs/loader.js and translators.js
Local<String> script_id = String::Concat(
isolate, String::NewFromUtf8(isolate, "cjs:").ToLocalChecked(), filename);
Local<Symbol> id_symbol = Symbol::New(isolate, script_id);

Local<PrimitiveArray> host_defined_options =
GetHostDefinedOptions(isolate, id_symbol);
ScriptCompiler::Source source = GetCommonJSSourceInstance(
isolate, code, filename, 0, 0, host_defined_options, nullptr);
ScriptCompiler::CompileOptions options = GetCompileOptions(source);

std::vector<Local<String>> params = {
String::NewFromUtf8(isolate, "exports").ToLocalChecked(),
String::NewFromUtf8(isolate, "require").ToLocalChecked(),
String::NewFromUtf8(isolate, "module").ToLocalChecked(),
String::NewFromUtf8(isolate, "__filename").ToLocalChecked(),
String::NewFromUtf8(isolate, "__dirname").ToLocalChecked()};

TryCatchScope try_catch(env);

ContextifyContext::CompileFunctionAndCacheResult(env,
context,
&source,
params,
std::vector<Local<Object>>(),
options,
true,
id_symbol,
try_catch);

bool found_error_message_caused_by_module_syntax = false;
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
Utf8Value message_value(env->isolate(), try_catch.Message()->Get());
auto message = message_value.ToStringView();

for (const auto& error_message : esm_syntax_error_messages) {
if (message.find(error_message) != std::string_view::npos) {
found_error_message_caused_by_module_syntax = true;
break;
}
}
}
args.GetReturnValue().Set(found_error_message_caused_by_module_syntax);
}

static void StartSigintWatchdog(const FunctionCallbackInfo<Value>& args) {
Expand Down
Loading