Skip to content

Commit 983c64d

Browse files
authored
Merge pull request #81 from thutasann/develop
feat: support module import
2 parents f91e160 + 570e45c commit 983c64d

6 files changed

Lines changed: 214 additions & 24 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,18 @@ const result = await go(
113113
async () => {
114114
const fs = require('node:fs');
115115
const crypto = require('node:crypto');
116+
const moment = (await import('moment')).default;
117+
118+
const moment_res = {
119+
version: moment.version,
120+
now: moment().format(),
121+
isValid: moment().isValid(),
122+
};
116123

117124
return {
118125
fileExists: fs.existsSync('/tmp/test'),
119126
randomBytes: crypto.randomBytes(4).toString('hex'),
127+
...moment_res,
120128
};
121129
},
122130
[],
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function js_sleep(duration) {
2+
return new Promise(resolve => {
3+
setTimeout(resolve, duration);
4+
});
5+
}
6+
7+
export default js_sleep;

examples/core/goroutines/moment-import-test.js

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ async function testMomentImports() {
2222
console.log('\n1. Testing basic moment import...');
2323
const result1 = await go(
2424
async () => {
25-
const moment = require('moment');
26-
const sleep = require('./helpers/sleep.cjs');
25+
const moment = (await import('moment')).default;
2726

28-
await sleep(1000);
27+
const { sleep } = await import('gonex');
28+
await sleep(300);
2929
console.log('Moment imported successfully in worker thread');
3030
return {
3131
version: moment.version,
@@ -42,8 +42,10 @@ async function testMomentImports() {
4242
console.log('\n2. Testing moment with date formatting...');
4343
const result2 = await go(
4444
async () => {
45-
// eslint-disable-next-line @typescript-eslint/no-var-requires
4645
const moment = require('moment');
46+
const sleep = (await import('./helpers/sleep.cjs')).default;
47+
48+
await sleep(300);
4749
console.log('Moment imported for date formatting');
4850

4951
const now = moment();
@@ -59,6 +61,46 @@ async function testMomentImports() {
5961
);
6062
console.log('Moment formatting result:', result2);
6163

64+
// Test 3: Lodash Import Testing
65+
console.log('\n3. Testing lodash import...');
66+
const result3 = await go(
67+
async () => {
68+
const lodash = (await import('lodash')).default;
69+
const sleep = (await import('./helpers/sleep.js')).default;
70+
71+
await sleep(300);
72+
console.log('Lodash imported successfully');
73+
return {
74+
isArray: lodash.isArray([1, 2, 3]),
75+
capitalize: lodash.capitalize('hello'),
76+
};
77+
},
78+
[],
79+
{ useWorkerThreads: true }
80+
);
81+
console.log('Lodash result:', result3);
82+
83+
// Test 4: chalk import testing
84+
console.log('\n4. Testing chalk import...');
85+
const result4 = await go(
86+
async () => {
87+
const chalk = (await import('chalk')).default;
88+
console.log('Chalk imported successfully');
89+
console.log(chalk.red('Hello, world!'));
90+
console.log(chalk.green('Hello, world!'));
91+
console.log(chalk.blue('Hello, world!'));
92+
93+
return {
94+
red: chalk.red('Hello, world!'),
95+
green: chalk.green('Hello, world!'),
96+
blue: chalk.blue('Hello, world!'),
97+
};
98+
},
99+
[],
100+
{ useWorkerThreads: true }
101+
);
102+
console.log('Chalk result:', result4);
103+
62104
console.log('\nAll moment import tests completed successfully!');
63105
} catch (error) {
64106
console.error('Test failed:', error);

examples/core/goroutines/simple-import-test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ async function testSimpleImports() {
1919
console.log('\n1. Testing function with Node.js built-in module...');
2020
const result1 = await go(
2121
async () => {
22-
const fs = await import('node:fs');
22+
const fs = (await import('node:fs')).default;
2323
console.log('FS module imported successfully in worker thread');
2424
return {
2525
existsSync: typeof fs.existsSync === 'function',
@@ -35,7 +35,7 @@ async function testSimpleImports() {
3535
console.log('\n2. Testing function with path module...');
3636
const result2 = await go(
3737
async () => {
38-
const path = await import('node:path');
38+
const path = (await import('node:path')).default;
3939
console.log('Path module imported successfully');
4040
return {
4141
join: typeof path.join === 'function',
@@ -52,7 +52,7 @@ async function testSimpleImports() {
5252
console.log('\n3. Testing function with crypto module...');
5353
const result3 = await go(
5454
async () => {
55-
const crypto = await import('node:crypto');
55+
const crypto = (await import('node:crypto')).default;
5656
console.log('Crypto module imported successfully');
5757
return {
5858
randomBytes: typeof crypto.randomBytes === 'function',
@@ -70,14 +70,14 @@ async function testSimpleImports() {
7070
const result4 = await go(
7171
async moduleName => {
7272
console.log(`Attempting to import: ${moduleName}`);
73-
const module = await import(moduleName);
73+
const module = (await import(moduleName)).default;
7474
return {
7575
moduleName,
7676
success: true,
7777
exports: Object.keys(module),
7878
};
7979
},
80-
['node:fs'],
80+
['node:fs', 'node:path', 'node:crypto'],
8181
{ useWorkerThreads: true }
8282
);
8383
console.log('Dynamic import result:', result4);

src/core/worker/helpers/execution.ts

Lines changed: 136 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,21 @@ export function createEnhancedRequire(
8585
path.resolve('${currentWorkingDir}', id),
8686
path.resolve(process.cwd(), id),
8787
path.resolve('${userProjectDir}', id),
88-
path.resolve('${userProjectDir}', 'examples', id),
89-
path.resolve('${userProjectDir}', 'examples', 'core', 'goroutines', id),
9088
path.resolve('${currentWorkingDir}', 'core', 'goroutines', id),
9189
];
9290
9391
for (const modulePath of possiblePaths) {
9492
try {
95-
return originalRequire(modulePath);
93+
const module = originalRequire(modulePath);
94+
// Apply smart proxy for .default access
95+
return new Proxy(module, {
96+
get(target, prop) {
97+
if (prop === 'default') {
98+
return target.default !== undefined ? target.default : target;
99+
}
100+
return target[prop];
101+
}
102+
});
96103
} catch (localError) {
97104
// Continue to next path
98105
}
@@ -104,16 +111,23 @@ export function createEnhancedRequire(
104111
// For npm packages, try node_modules directories in this order:
105112
const possibleNodeModulesPaths = [
106113
path.resolve('${userProjectDir}', 'node_modules', id),
107-
path.resolve('${userProjectDir}', 'examples', 'node_modules', id),
108114
path.resolve('${currentWorkingDir}', 'node_modules', id),
109115
path.resolve('${currentWorkingDir}', '..', 'node_modules', id),
110116
path.resolve('${currentWorkingDir}', '..', '..', 'node_modules', id),
111-
path.resolve('${currentWorkingDir}', '..', '..', 'examples', 'node_modules', id),
112117
];
113118
114119
for (const modulePath of possibleNodeModulesPaths) {
115120
try {
116-
return originalRequire(modulePath);
121+
const module = originalRequire(modulePath);
122+
// Apply smart proxy for .default access
123+
return new Proxy(module, {
124+
get(target, prop) {
125+
if (prop === 'default') {
126+
return target.default !== undefined ? target.default : target;
127+
}
128+
return target[prop];
129+
}
130+
});
117131
} catch (moduleError) {
118132
// Continue to next path
119133
}
@@ -125,6 +139,24 @@ export function createEnhancedRequire(
125139
`;
126140
}
127141

142+
/**
143+
* Transform import() calls to use our custom import function
144+
* This preserves the await import() syntax while making it work in worker threads
145+
*/
146+
function transformImportCalls(userFunction: string): string {
147+
// Replace await import('module') with await customImport('module')
148+
// This regex matches: await import('module') or await import("module")
149+
let transformed = userFunction;
150+
151+
// Replace import() calls with customImport() calls
152+
transformed = transformed.replace(
153+
/await\s+import\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g,
154+
'await customImport("$1")'
155+
);
156+
157+
return transformed;
158+
}
159+
128160
/**
129161
* Create execution environment code
130162
*/
@@ -133,12 +165,11 @@ export function createExecutionEnvironment(
133165
userProjectDir: string,
134166
userFunction: string
135167
): string {
136-
const enhancedRequire = createEnhancedRequire(
137-
currentWorkingDir,
138-
userProjectDir
139-
);
140168
const contextProxy = createContextProxy();
141169

170+
// Transform import() calls to require() calls
171+
const transformedFunction = transformImportCalls(userFunction);
172+
142173
return `
143174
(async function(...args) {
144175
// Ensure essential globals are available
@@ -311,14 +342,105 @@ export function createExecutionEnvironment(
311342
});
312343
};
313344
314-
// Set up module resolution
315-
${enhancedRequire}
345+
346+
347+
// Create a custom import function that works in worker threads
348+
// This mimics the behavior of ES module dynamic imports
349+
var customImport = async function(id) {
350+
const path = require('path');
351+
352+
// Handle Node.js built-in modules (node:fs, node:path, etc.)
353+
if (id.startsWith('node:')) {
354+
const moduleName = id.substring(5); // Remove 'node:' prefix
355+
try {
356+
const module = require(moduleName);
357+
// For built-in modules, return { default: module, ...module }
358+
return { default: module, ...module };
359+
} catch (error) {
360+
throw new Error('Built-in module not found: ' + moduleName);
361+
}
362+
}
363+
364+
// Handle local files (relative paths)
365+
if (id.startsWith('./') || id.startsWith('../') || id.endsWith('.js')) {
366+
const possiblePaths = [
367+
path.resolve('${currentWorkingDir}', id),
368+
path.resolve(process.cwd(), id),
369+
path.resolve('${userProjectDir}', id),
370+
path.resolve('${currentWorkingDir}', 'core', 'goroutines', id),
371+
];
372+
373+
for (const modulePath of possiblePaths) {
374+
try {
375+
// For .js files, try to use dynamic import first (ES modules)
376+
if (id.endsWith('.js')) {
377+
try {
378+
// Use dynamic import for ES modules
379+
const module = await import(modulePath);
380+
return module;
381+
} catch (importError) {
382+
// If dynamic import fails, fall back to require (CommonJS)
383+
const module = require(modulePath);
384+
if (module.default === undefined) {
385+
return { default: module, ...module };
386+
} else {
387+
return { default: module.default, ...module };
388+
}
389+
}
390+
} else {
391+
// For .cjs files, use require (CommonJS)
392+
const module = require(modulePath);
393+
if (module.default === undefined) {
394+
return { default: module, ...module };
395+
} else {
396+
return { default: module.default, ...module };
397+
}
398+
}
399+
} catch (localError) {
400+
// Continue to next path
401+
}
402+
}
403+
404+
throw new Error('Module not found: ' + id);
405+
}
406+
407+
// For npm packages, try node_modules directories in this order:
408+
const possibleNodeModulesPaths = [
409+
path.resolve('${userProjectDir}', 'node_modules', id),
410+
path.resolve('${currentWorkingDir}', 'node_modules', id),
411+
path.resolve('${currentWorkingDir}', '..', 'node_modules', id),
412+
path.resolve('${currentWorkingDir}', '..', '..', 'node_modules', id),
413+
];
414+
415+
for (const modulePath of possibleNodeModulesPaths) {
416+
try {
417+
const module = require(modulePath);
418+
// Always return an object with default property for consistency
419+
// This supports both: await import('moment') and (await import('moment')).default
420+
if (module.default === undefined) {
421+
// CommonJS module - return { default: module, ...module }
422+
return { default: module, ...module };
423+
} else {
424+
// ES module - return { default: module.default, ...module }
425+
return { default: module.default, ...module };
426+
}
427+
} catch (moduleError) {
428+
// Continue to next path
429+
}
430+
}
431+
432+
throw new Error('Package not found: ' + id);
433+
};
434+
435+
// Make the custom import function available globally
436+
// Users can call customImport('moment') instead of import('moment')
437+
globalThis.customImport = customImport;
316438
317439
// Resolve function arguments and handle context objects
318440
${contextProxy}
319441
320-
// Create the user function
321-
${userFunction}
442+
// Create the user function (with transformed import calls)
443+
${transformedFunction}
322444
323445
// Execute the function with the resolved arguments
324446
return await fn(...resolvedArgs);

src/core/worker/worker-thread-manager.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os from 'os';
2-
import { join } from 'path';
2+
import path, { join } from 'path';
33
import { Worker } from 'worker_threads';
44
import type {
55
MessageHandlers,
@@ -318,9 +318,11 @@ export class WorkerThreadManager {
318318
*/
319319
private async createWorker(workerId: number): Promise<void> {
320320
// Get the user's project directory (where the go() function is called from)
321+
// Use process.cwd() to get the actual working directory where the user ran the code
321322
const userProjectDir = process.cwd();
322323

323324
// Get the current working directory for resolving relative imports
325+
// This should be the user's project directory, not the dist directory
324326
const currentWorkingDir = process.cwd();
325327

326328
const worker = new Worker(join(__dirname, './worker.js'), {
@@ -329,6 +331,15 @@ export class WorkerThreadManager {
329331
userProjectDir, // Pass the user's project directory
330332
currentWorkingDir, // Pass the current working directory
331333
},
334+
env: {
335+
...process.env,
336+
['NODE_PATH']: [
337+
path.join(userProjectDir, 'node_modules'),
338+
process.env['NODE_PATH'],
339+
]
340+
.filter(Boolean)
341+
.join(path.delimiter),
342+
},
332343
});
333344

334345
// Set higher max listeners to prevent warnings

0 commit comments

Comments
 (0)