77#include < filesystem>
88#include < fstream>
99#include < sstream>
10+ #include < unordered_map>
1011#include < unordered_set>
1112
1213#include " runtime/RuntimeConfig.h"
1314
1415namespace v8impl {
1516
17+ namespace {
18+
19+ // Cache for package.json "type" field lookups
20+ std::unordered_map<std::string, bool > g_packageTypeCache;
21+
22+ // Strip shebang line from source code (e.g., #!/usr/bin/env node)
23+ std::string StripShebang (const std::string& source) {
24+ if (source.size () >= 2 && source[0 ] == ' #' && source[1 ] == ' !' ) {
25+ size_t lineEnd = source.find (' \n ' );
26+ if (lineEnd != std::string::npos) {
27+ return source.substr (lineEnd + 1 );
28+ }
29+ return " " ; // Entire file is just a shebang
30+ }
31+ return source;
32+ }
33+
34+ // Check if path has .cjs extension (explicitly CommonJS)
35+ bool IsCJSModule (const std::string& path) {
36+ return path.size () >= 4 && path.compare (path.size () - 4 , 4 , " .cjs" ) == 0 ;
37+ }
38+
39+ // Find nearest package.json by walking up from directory
40+ std::string FindPackageJson (const std::filesystem::path& startDir) {
41+ std::filesystem::path current = startDir;
42+
43+ while (!current.empty () && current != current.root_path ()) {
44+ std::filesystem::path packagePath = current / " package.json" ;
45+ std::error_code ec;
46+ if (std::filesystem::exists (packagePath, ec) && !ec) {
47+ return packagePath.string ();
48+ }
49+ current = current.parent_path ();
50+ }
51+
52+ return " " ;
53+ }
54+
55+ // Check if package.json has "type": "module"
56+ bool IsPackageTypeModule (const std::string& packageJsonPath) {
57+ auto cacheIt = g_packageTypeCache.find (packageJsonPath);
58+ if (cacheIt != g_packageTypeCache.end ()) {
59+ return cacheIt->second ;
60+ }
61+
62+ bool isModule = false ;
63+
64+ std::ifstream file (packageJsonPath);
65+ if (file.is_open ()) {
66+ std::string content ((std::istreambuf_iterator<char >(file)),
67+ std::istreambuf_iterator<char >());
68+ file.close ();
69+
70+ // Simple JSON parsing for "type": "module"
71+ // Look for "type" followed by : and "module"
72+ size_t typePos = content.find (" \" type\" " );
73+ if (typePos != std::string::npos) {
74+ size_t colonPos = content.find (' :' , typePos + 6 );
75+ if (colonPos != std::string::npos) {
76+ size_t valueStart = content.find (' "' , colonPos + 1 );
77+ if (valueStart != std::string::npos) {
78+ size_t valueEnd = content.find (' "' , valueStart + 1 );
79+ if (valueEnd != std::string::npos) {
80+ std::string typeValue =
81+ content.substr (valueStart + 1 , valueEnd - valueStart - 1 );
82+ isModule = (typeValue == " module" );
83+ }
84+ }
85+ }
86+ }
87+ }
88+
89+ g_packageTypeCache[packageJsonPath] = isModule;
90+ return isModule;
91+ }
92+
93+ // Determine if a .js file should be treated as ESM based on nearest
94+ // package.json
95+ bool ShouldTreatAsESModule (const std::string& path) {
96+ std::filesystem::path filePath (path);
97+ std::string packageJson = FindPackageJson (filePath.parent_path ());
98+
99+ if (!packageJson.empty ()) {
100+ return IsPackageTypeModule (packageJson);
101+ }
102+
103+ return false ; // Default to CommonJS
104+ }
105+
106+ } // namespace
107+
16108// Global registry for ES modules
17109std::unordered_map<std::string, v8::Global<v8::Module>> g_moduleRegistry;
18110
@@ -59,9 +151,8 @@ std::string ModulePathToURL(const std::string& modulePath) {
59151
60152bool IsNodeBuiltinSpecifier (const std::string& specifier) {
61153 static const std::unordered_set<std::string> kBuiltins = {
62- " url" , " node:url" , " fs" , " node:fs" ,
63- " fs/promises" , " node:fs/promises" , " web" , " node:web" ,
64- " stream/web" , " node:stream/web" };
154+ " url" , " node:url" , " fs" , " node:fs" , " fs/promises" , " node:fs/promises" ,
155+ " path" , " node:path" , " web" , " node:web" , " stream/web" , " node:stream/web" };
65156 return kBuiltins .contains (specifier);
66157}
67158
@@ -149,6 +240,38 @@ export default __fsp;
149240)" ;
150241 }
151242
243+ if (builtinName == " path" ) {
244+ return R"(
245+ const __load = (name) => {
246+ if (typeof globalThis.require === "function") {
247+ return globalThis.require(name);
248+ }
249+ if (typeof globalThis.__nativeRequire === "function") {
250+ const dir = typeof globalThis.__approot === "string" ? `${globalThis.__approot}/app` : "";
251+ return globalThis.__nativeRequire(name, dir);
252+ }
253+ throw new Error(`Cannot load builtin module '${name}'`);
254+ };
255+ const __path = __load("node:path");
256+ export const basename = __path.basename;
257+ export const dirname = __path.dirname;
258+ export const extname = __path.extname;
259+ export const isAbsolute = __path.isAbsolute;
260+ export const join = __path.join;
261+ export const normalize = __path.normalize;
262+ export const parse = __path.parse;
263+ export const format = __path.format;
264+ export const relative = __path.relative;
265+ export const resolve = __path.resolve;
266+ export const toNamespacedPath = __path.toNamespacedPath;
267+ export const sep = __path.sep;
268+ export const delimiter = __path.delimiter;
269+ export const posix = __path.posix;
270+ export const win32 = __path.win32;
271+ export default __path;
272+ )" ;
273+ }
274+
152275 if (builtinName == " web" ) {
153276 return R"(
154277const __load = (name) => {
@@ -201,7 +324,22 @@ export default __streamWeb;
201324}
202325
203326bool IsESModule (const std::string& path) {
204- return path.size () >= 4 && path.compare (path.size () - 4 , 4 , " .mjs" ) == 0 ;
327+ // .mjs is always ESM
328+ if (path.size () >= 4 && path.compare (path.size () - 4 , 4 , " .mjs" ) == 0 ) {
329+ return true ;
330+ }
331+
332+ // .cjs is always CommonJS
333+ if (IsCJSModule (path)) {
334+ return false ;
335+ }
336+
337+ // .js files: check package.json "type" field
338+ if (path.size () >= 3 && path.compare (path.size () - 3 , 3 , " .js" ) == 0 ) {
339+ return ShouldTreatAsESModule (path);
340+ }
341+
342+ return false ;
205343}
206344
207345bool IsJSONModule (const std::string& path) {
@@ -216,7 +354,7 @@ std::string ReadFileContent(const std::string& path) {
216354
217355 std::stringstream buffer;
218356 buffer << file.rdbuf ();
219- return buffer.str ();
357+ return StripShebang ( buffer.str () );
220358}
221359
222360v8::Local<v8::String> WrapModuleContent (v8::Isolate* isolate,
@@ -278,6 +416,13 @@ std::string ResolveESModulePath(v8::Isolate* isolate,
278416 return NormalizeModulePath (jsPath);
279417 }
280418
419+ // Try with .cjs extension (explicit CommonJS)
420+ std::filesystem::path cjsPath = fullPath.string () + " .cjs" ;
421+ if (std::filesystem::exists (cjsPath) &&
422+ std::filesystem::is_regular_file (cjsPath)) {
423+ return NormalizeModulePath (cjsPath);
424+ }
425+
281426 // Try directory with index.mjs
282427 if (std::filesystem::exists (fullPath) &&
283428 std::filesystem::is_directory (fullPath)) {
@@ -293,6 +438,13 @@ std::string ResolveESModulePath(v8::Isolate* isolate,
293438 std::filesystem::is_regular_file (indexJs)) {
294439 return NormalizeModulePath (indexJs);
295440 }
441+
442+ // Try directory with index.cjs
443+ std::filesystem::path indexCjs = fullPath / " index.cjs" ;
444+ if (std::filesystem::exists (indexCjs) &&
445+ std::filesystem::is_regular_file (indexCjs)) {
446+ return NormalizeModulePath (indexCjs);
447+ }
296448 }
297449
298450 throw std::runtime_error (" Module not found: " + moduleName);
@@ -644,6 +796,9 @@ void CleanupESModuleSystem(v8::Isolate* isolate) {
644796
645797 // Clear the registry
646798 g_moduleRegistry.clear ();
799+
800+ // Clear the package.json type cache
801+ g_packageTypeCache.clear ();
647802}
648803
649804} // namespace v8impl
0 commit comments