almostnode 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/__sw__.js +32 -1
- package/dist/assets/{runtime-worker-D9x_Ddwz.js → runtime-worker-B8_LZkBX.js} +85 -32
- package/dist/assets/runtime-worker-B8_LZkBX.js.map +1 -0
- package/dist/frameworks/next-dev-server.d.ts +63 -0
- package/dist/frameworks/next-dev-server.d.ts.map +1 -1
- package/dist/frameworks/tailwind-config-loader.d.ts +32 -0
- package/dist/frameworks/tailwind-config-loader.d.ts.map +1 -0
- package/dist/frameworks/vite-dev-server.d.ts +1 -0
- package/dist/frameworks/vite-dev-server.d.ts.map +1 -1
- package/dist/index.cjs +995 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +975 -60
- package/dist/index.mjs.map +1 -1
- package/dist/macaly-demo.d.ts +42 -0
- package/dist/macaly-demo.d.ts.map +1 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/types/package-json.d.ts +16 -0
- package/dist/types/package-json.d.ts.map +1 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts +1 -0
- package/dist/virtual-fs.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/frameworks/next-dev-server.ts +940 -34
- package/src/frameworks/tailwind-config-loader.ts +206 -0
- package/src/frameworks/vite-dev-server.ts +25 -0
- package/src/macaly-demo.ts +172 -0
- package/src/runtime.ts +84 -25
- package/src/types/package-json.ts +15 -0
- package/src/utils/hash.ts +12 -0
- package/src/virtual-fs.ts +14 -10
- package/dist/assets/runtime-worker-D9x_Ddwz.js.map +0 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind Config Loader
|
|
3
|
+
*
|
|
4
|
+
* Parses tailwind.config.ts files and generates JavaScript to configure
|
|
5
|
+
* the Tailwind CDN at runtime via window.tailwind.config.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { VirtualFS } from '../virtual-fs';
|
|
9
|
+
|
|
10
|
+
export interface TailwindConfigResult {
|
|
11
|
+
/** JavaScript code to set window.tailwind.config (empty string if no config) */
|
|
12
|
+
configScript: string;
|
|
13
|
+
/** Whether config was successfully loaded */
|
|
14
|
+
success: boolean;
|
|
15
|
+
/** Error message if loading failed */
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Config file names to search for, in priority order */
|
|
20
|
+
const CONFIG_FILE_NAMES = [
|
|
21
|
+
'/tailwind.config.ts',
|
|
22
|
+
'/tailwind.config.js',
|
|
23
|
+
'/tailwind.config.mjs',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load and parse a Tailwind config file from VirtualFS
|
|
28
|
+
*/
|
|
29
|
+
export async function loadTailwindConfig(
|
|
30
|
+
vfs: VirtualFS,
|
|
31
|
+
root: string = '/'
|
|
32
|
+
): Promise<TailwindConfigResult> {
|
|
33
|
+
// Find config file
|
|
34
|
+
let configPath: string | null = null;
|
|
35
|
+
let configContent: string | null = null;
|
|
36
|
+
|
|
37
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
38
|
+
const fullPath = root === '/' ? fileName : `${root}${fileName}`;
|
|
39
|
+
try {
|
|
40
|
+
const content = vfs.readFileSync(fullPath);
|
|
41
|
+
configContent =
|
|
42
|
+
typeof content === 'string'
|
|
43
|
+
? content
|
|
44
|
+
: content instanceof Uint8Array
|
|
45
|
+
? new TextDecoder('utf-8').decode(content)
|
|
46
|
+
: Buffer.from(content).toString('utf-8');
|
|
47
|
+
configPath = fullPath;
|
|
48
|
+
break;
|
|
49
|
+
} catch {
|
|
50
|
+
// File not found, try next
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!configPath || configContent === null) {
|
|
56
|
+
return {
|
|
57
|
+
configScript: '',
|
|
58
|
+
success: true, // Not an error, just no config
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Strip TypeScript syntax and extract config object
|
|
64
|
+
const jsConfig = stripTypescriptSyntax(configContent);
|
|
65
|
+
const configObject = extractConfigObject(jsConfig);
|
|
66
|
+
|
|
67
|
+
if (!configObject) {
|
|
68
|
+
return {
|
|
69
|
+
configScript: '',
|
|
70
|
+
success: false,
|
|
71
|
+
error: 'Could not extract config object from tailwind.config',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Generate the script to inject
|
|
76
|
+
const configScript = generateConfigScript(configObject);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
configScript,
|
|
80
|
+
success: true,
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
configScript: '',
|
|
85
|
+
success: false,
|
|
86
|
+
error: `Failed to parse tailwind.config: ${error instanceof Error ? error.message : String(error)}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Strip TypeScript-specific syntax from config content
|
|
93
|
+
*/
|
|
94
|
+
export function stripTypescriptSyntax(content: string): string {
|
|
95
|
+
let result = content;
|
|
96
|
+
|
|
97
|
+
// Remove import type statements
|
|
98
|
+
// e.g., import type { Config } from "tailwindcss"
|
|
99
|
+
result = result.replace(/import\s+type\s+\{[^}]*\}\s+from\s+['"][^'"]*['"]\s*;?\s*/g, '');
|
|
100
|
+
|
|
101
|
+
// Remove regular import statements (Config type, etc.)
|
|
102
|
+
// e.g., import { Config } from "tailwindcss"
|
|
103
|
+
result = result.replace(/import\s+\{[^}]*\}\s+from\s+['"][^'"]*['"]\s*;?\s*/g, '');
|
|
104
|
+
|
|
105
|
+
// Remove satisfies Type assertions
|
|
106
|
+
// e.g., } satisfies Config
|
|
107
|
+
result = result.replace(/\s+satisfies\s+\w+\s*$/gm, '');
|
|
108
|
+
result = result.replace(/\s+satisfies\s+\w+\s*;?\s*$/gm, '');
|
|
109
|
+
|
|
110
|
+
// Remove type annotations on variables
|
|
111
|
+
// e.g., const config: Config = { ... }
|
|
112
|
+
result = result.replace(/:\s*Config\s*=/g, ' =');
|
|
113
|
+
|
|
114
|
+
// Remove 'as const' assertions
|
|
115
|
+
result = result.replace(/\s+as\s+const\s*/g, ' ');
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract the config object from the processed content
|
|
122
|
+
*/
|
|
123
|
+
export function extractConfigObject(content: string): string | null {
|
|
124
|
+
// Look for export default { ... }
|
|
125
|
+
// We need to find the opening brace and match it to the closing brace
|
|
126
|
+
|
|
127
|
+
// First, find "export default"
|
|
128
|
+
const exportDefaultMatch = content.match(/export\s+default\s*/);
|
|
129
|
+
if (!exportDefaultMatch || exportDefaultMatch.index === undefined) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const startIndex = exportDefaultMatch.index + exportDefaultMatch[0].length;
|
|
134
|
+
const remaining = content.substring(startIndex);
|
|
135
|
+
|
|
136
|
+
// Check if it starts with an object literal
|
|
137
|
+
const trimmedRemaining = remaining.trimStart();
|
|
138
|
+
if (!trimmedRemaining.startsWith('{')) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Find the matching closing brace
|
|
143
|
+
const objectStart = startIndex + (remaining.length - trimmedRemaining.length);
|
|
144
|
+
const objectContent = content.substring(objectStart);
|
|
145
|
+
|
|
146
|
+
let braceCount = 0;
|
|
147
|
+
let inString = false;
|
|
148
|
+
let stringChar = '';
|
|
149
|
+
let escaped = false;
|
|
150
|
+
let endIndex = -1;
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < objectContent.length; i++) {
|
|
153
|
+
const char = objectContent[i];
|
|
154
|
+
|
|
155
|
+
if (escaped) {
|
|
156
|
+
escaped = false;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (char === '\\') {
|
|
161
|
+
escaped = true;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (inString) {
|
|
166
|
+
if (char === stringChar) {
|
|
167
|
+
inString = false;
|
|
168
|
+
}
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
173
|
+
inString = true;
|
|
174
|
+
stringChar = char;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (char === '{') {
|
|
179
|
+
braceCount++;
|
|
180
|
+
} else if (char === '}') {
|
|
181
|
+
braceCount--;
|
|
182
|
+
if (braceCount === 0) {
|
|
183
|
+
endIndex = i + 1;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (endIndex === -1) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return objectContent.substring(0, endIndex);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Generate the script to inject the Tailwind config
|
|
198
|
+
*/
|
|
199
|
+
export function generateConfigScript(configObject: string): string {
|
|
200
|
+
// Wrap in a script that sets tailwind.config
|
|
201
|
+
// This must run AFTER the Tailwind CDN script loads
|
|
202
|
+
// The CDN creates the global `tailwind` object, then we configure it
|
|
203
|
+
return `<script>
|
|
204
|
+
tailwind.config = ${configObject};
|
|
205
|
+
</script>`;
|
|
206
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { DevServer, DevServerOptions, ResponseData, HMRUpdate } from '../dev-server';
|
|
7
7
|
import { VirtualFS } from '../virtual-fs';
|
|
8
8
|
import { Buffer } from '../shims/stream';
|
|
9
|
+
import { simpleHash } from '../utils/hash';
|
|
9
10
|
|
|
10
11
|
// Check if we're in a real browser environment (not jsdom or Node.js)
|
|
11
12
|
// jsdom has window but doesn't have ServiceWorker or SharedArrayBuffer
|
|
@@ -279,6 +280,7 @@ export class ViteDevServer extends DevServer {
|
|
|
279
280
|
private watcherCleanup: (() => void) | null = null;
|
|
280
281
|
private options: ViteDevServerOptions;
|
|
281
282
|
private hmrTargetWindow: Window | null = null;
|
|
283
|
+
private transformCache: Map<string, { code: string; hash: string }> = new Map();
|
|
282
284
|
|
|
283
285
|
constructor(vfs: VirtualFS, options: ViteDevServerOptions) {
|
|
284
286
|
super(vfs, options);
|
|
@@ -478,8 +480,31 @@ export class ViteDevServer extends DevServer {
|
|
|
478
480
|
private async transformAndServe(filePath: string, urlPath: string): Promise<ResponseData> {
|
|
479
481
|
try {
|
|
480
482
|
const content = this.vfs.readFileSync(filePath, 'utf8');
|
|
483
|
+
const hash = simpleHash(content);
|
|
484
|
+
|
|
485
|
+
// Check transform cache
|
|
486
|
+
const cached = this.transformCache.get(filePath);
|
|
487
|
+
if (cached && cached.hash === hash) {
|
|
488
|
+
const buffer = Buffer.from(cached.code);
|
|
489
|
+
return {
|
|
490
|
+
statusCode: 200,
|
|
491
|
+
statusMessage: 'OK',
|
|
492
|
+
headers: {
|
|
493
|
+
'Content-Type': 'application/javascript; charset=utf-8',
|
|
494
|
+
'Content-Length': String(buffer.length),
|
|
495
|
+
'Cache-Control': 'no-cache',
|
|
496
|
+
'X-Transformed': 'true',
|
|
497
|
+
'X-Cache': 'hit',
|
|
498
|
+
},
|
|
499
|
+
body: buffer,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
481
503
|
const transformed = await this.transformCode(content, urlPath);
|
|
482
504
|
|
|
505
|
+
// Cache the transform result
|
|
506
|
+
this.transformCache.set(filePath, { code: transformed, hash });
|
|
507
|
+
|
|
483
508
|
const buffer = Buffer.from(transformed);
|
|
484
509
|
return {
|
|
485
510
|
statusCode: 200,
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Macaly Demo - Load the REAL macaly-web repository into almostnode
|
|
3
|
+
* This tests almostnode's ability to run a real-world Next.js app
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { VirtualFS } from './virtual-fs';
|
|
7
|
+
import { createRuntime } from './create-runtime';
|
|
8
|
+
import type { IRuntime } from './runtime-interface';
|
|
9
|
+
import { NextDevServer } from './frameworks/next-dev-server';
|
|
10
|
+
import { getServerBridge } from './server-bridge';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Files to load from the real macaly-web repository
|
|
14
|
+
* We'll populate this dynamically, but here's the structure we need
|
|
15
|
+
*/
|
|
16
|
+
export interface MacalyFiles {
|
|
17
|
+
[path: string]: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load the real macaly-web project into VirtualFS
|
|
22
|
+
*/
|
|
23
|
+
export function loadMacalyProject(vfs: VirtualFS, files: MacalyFiles): void {
|
|
24
|
+
for (const [path, content] of Object.entries(files)) {
|
|
25
|
+
// Ensure directory exists
|
|
26
|
+
const dir = path.substring(0, path.lastIndexOf('/'));
|
|
27
|
+
if (dir) {
|
|
28
|
+
vfs.mkdirSync(dir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Handle base64-encoded binary files
|
|
32
|
+
if (content.startsWith('base64:')) {
|
|
33
|
+
const base64Data = content.slice(7); // Remove 'base64:' prefix
|
|
34
|
+
const binaryData = atob(base64Data);
|
|
35
|
+
const bytes = new Uint8Array(binaryData.length);
|
|
36
|
+
for (let i = 0; i < binaryData.length; i++) {
|
|
37
|
+
bytes[i] = binaryData.charCodeAt(i);
|
|
38
|
+
}
|
|
39
|
+
vfs.writeFileSync(path, Buffer.from(bytes));
|
|
40
|
+
} else {
|
|
41
|
+
vfs.writeFileSync(path, content);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the Macaly demo with real files
|
|
48
|
+
*/
|
|
49
|
+
export async function initMacalyDemo(
|
|
50
|
+
outputElement: HTMLElement,
|
|
51
|
+
files: MacalyFiles,
|
|
52
|
+
options: { useWorker?: boolean } = {}
|
|
53
|
+
): Promise<{ vfs: VirtualFS; runtime: IRuntime }> {
|
|
54
|
+
const { useWorker = false } = options;
|
|
55
|
+
|
|
56
|
+
const log = (message: string) => {
|
|
57
|
+
const line = document.createElement('div');
|
|
58
|
+
line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
59
|
+
outputElement.appendChild(line);
|
|
60
|
+
outputElement.scrollTop = outputElement.scrollHeight;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
log('Creating virtual file system...');
|
|
64
|
+
const vfs = new VirtualFS();
|
|
65
|
+
|
|
66
|
+
log(`Loading ${Object.keys(files).length} files from macaly-web...`);
|
|
67
|
+
loadMacalyProject(vfs, files);
|
|
68
|
+
|
|
69
|
+
log(`Initializing runtime (${useWorker ? 'Web Worker mode' : 'main thread'})...`);
|
|
70
|
+
const runtime = await createRuntime(vfs, {
|
|
71
|
+
dangerouslyAllowSameOrigin: true,
|
|
72
|
+
useWorker,
|
|
73
|
+
cwd: '/',
|
|
74
|
+
env: {
|
|
75
|
+
NODE_ENV: 'development',
|
|
76
|
+
},
|
|
77
|
+
onConsole: (method, args) => {
|
|
78
|
+
const prefix = method === 'error' ? '[ERROR]' : method === 'warn' ? '[WARN]' : '';
|
|
79
|
+
log(`${prefix} ${args.map((a) => String(a)).join(' ')}`);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (useWorker) {
|
|
84
|
+
log('Runtime is running in a Web Worker for better UI responsiveness');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
log('Setting up file watcher...');
|
|
88
|
+
vfs.watch('/app', { recursive: true }, (eventType, filename) => {
|
|
89
|
+
log(`File ${eventType}: ${filename}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
log('Macaly demo initialized!');
|
|
93
|
+
|
|
94
|
+
return { vfs, runtime };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Start the Macaly dev server
|
|
99
|
+
*/
|
|
100
|
+
export async function startMacalyDevServer(
|
|
101
|
+
vfs: VirtualFS,
|
|
102
|
+
options: {
|
|
103
|
+
port?: number;
|
|
104
|
+
log?: (message: string) => void;
|
|
105
|
+
} = {}
|
|
106
|
+
): Promise<{
|
|
107
|
+
server: NextDevServer;
|
|
108
|
+
url: string;
|
|
109
|
+
stop: () => void;
|
|
110
|
+
}> {
|
|
111
|
+
const port = options.port || 3001;
|
|
112
|
+
const log = options.log || console.log;
|
|
113
|
+
|
|
114
|
+
log('Starting Macaly dev server...');
|
|
115
|
+
|
|
116
|
+
const server = new NextDevServer(vfs, { port, root: '/' });
|
|
117
|
+
const bridge = getServerBridge();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
log('Initializing Service Worker...');
|
|
121
|
+
await bridge.initServiceWorker();
|
|
122
|
+
log('Service Worker ready');
|
|
123
|
+
} catch (error) {
|
|
124
|
+
log(`Warning: Service Worker failed to initialize: ${error}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
bridge.on('server-ready', (p: unknown, u: unknown) => {
|
|
128
|
+
log(`Server ready at ${u}`);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const httpServer = {
|
|
132
|
+
listening: true,
|
|
133
|
+
address: () => ({ port: server.getPort(), address: '0.0.0.0', family: 'IPv4' }),
|
|
134
|
+
async handleRequest(
|
|
135
|
+
method: string,
|
|
136
|
+
url: string,
|
|
137
|
+
headers: Record<string, string>,
|
|
138
|
+
body?: string | ArrayBuffer
|
|
139
|
+
) {
|
|
140
|
+
const bodyBuffer = body
|
|
141
|
+
? typeof body === 'string'
|
|
142
|
+
? Buffer.from(body)
|
|
143
|
+
: Buffer.from(body)
|
|
144
|
+
: undefined;
|
|
145
|
+
return server.handleRequest(method, url, headers, bodyBuffer);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
bridge.registerServer(httpServer as any, port);
|
|
150
|
+
server.start();
|
|
151
|
+
log('File watcher started');
|
|
152
|
+
|
|
153
|
+
server.on('hmr-update', (update: unknown) => {
|
|
154
|
+
log(`HMR update: ${JSON.stringify(update)}`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const url = bridge.getServerUrl(port);
|
|
158
|
+
log(`Macaly dev server running at: ${url}/`);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
server,
|
|
162
|
+
url: url + '/',
|
|
163
|
+
stop: () => {
|
|
164
|
+
server.stop();
|
|
165
|
+
bridge.unregisterServer(port);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Export for use in the demo page
|
|
171
|
+
export { VirtualFS, NextDevServer, createRuntime };
|
|
172
|
+
export type { IRuntime };
|
package/src/runtime.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { VirtualFS } from './virtual-fs';
|
|
9
9
|
import type { IRuntime, IExecuteResult, IRuntimeOptions } from './runtime-interface';
|
|
10
|
+
import type { PackageJson } from './types/package-json';
|
|
11
|
+
import { simpleHash } from './utils/hash';
|
|
10
12
|
import { createFsShim, FsShim } from './shims/fs';
|
|
11
13
|
import * as pathShim from './shims/path';
|
|
12
14
|
import { createProcess, Process } from './shims/process';
|
|
@@ -347,8 +349,30 @@ function createRequire(
|
|
|
347
349
|
process: Process,
|
|
348
350
|
currentDir: string,
|
|
349
351
|
moduleCache: Record<string, Module>,
|
|
350
|
-
options: RuntimeOptions
|
|
352
|
+
options: RuntimeOptions,
|
|
353
|
+
processedCodeCache?: Map<string, string>
|
|
351
354
|
): RequireFunction {
|
|
355
|
+
// Module resolution cache for faster repeated imports
|
|
356
|
+
const resolutionCache: Map<string, string | null> = new Map();
|
|
357
|
+
|
|
358
|
+
// Package.json parsing cache
|
|
359
|
+
const packageJsonCache: Map<string, PackageJson | null> = new Map();
|
|
360
|
+
|
|
361
|
+
const getParsedPackageJson = (pkgPath: string): PackageJson | null => {
|
|
362
|
+
if (packageJsonCache.has(pkgPath)) {
|
|
363
|
+
return packageJsonCache.get(pkgPath)!;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const content = vfs.readFileSync(pkgPath, 'utf8');
|
|
367
|
+
const parsed = JSON.parse(content) as PackageJson;
|
|
368
|
+
packageJsonCache.set(pkgPath, parsed);
|
|
369
|
+
return parsed;
|
|
370
|
+
} catch {
|
|
371
|
+
packageJsonCache.set(pkgPath, null);
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
352
376
|
const resolveModule = (id: string, fromDir: string): string => {
|
|
353
377
|
// Handle node: protocol prefix (Node.js 16+)
|
|
354
378
|
if (id.startsWith('node:')) {
|
|
@@ -360,6 +384,16 @@ function createRequire(
|
|
|
360
384
|
return id;
|
|
361
385
|
}
|
|
362
386
|
|
|
387
|
+
// Check resolution cache
|
|
388
|
+
const cacheKey = `${fromDir}|${id}`;
|
|
389
|
+
const cached = resolutionCache.get(cacheKey);
|
|
390
|
+
if (cached !== undefined) {
|
|
391
|
+
if (cached === null) {
|
|
392
|
+
throw new Error(`Cannot find module '${id}'`);
|
|
393
|
+
}
|
|
394
|
+
return cached;
|
|
395
|
+
}
|
|
396
|
+
|
|
363
397
|
// Relative paths
|
|
364
398
|
if (id.startsWith('./') || id.startsWith('../') || id.startsWith('/')) {
|
|
365
399
|
const resolved = id.startsWith('/')
|
|
@@ -370,11 +404,13 @@ function createRequire(
|
|
|
370
404
|
if (vfs.existsSync(resolved)) {
|
|
371
405
|
const stats = vfs.statSync(resolved);
|
|
372
406
|
if (stats.isFile()) {
|
|
407
|
+
resolutionCache.set(cacheKey, resolved);
|
|
373
408
|
return resolved;
|
|
374
409
|
}
|
|
375
410
|
// Directory - look for index.js
|
|
376
411
|
const indexPath = pathShim.join(resolved, 'index.js');
|
|
377
412
|
if (vfs.existsSync(indexPath)) {
|
|
413
|
+
resolutionCache.set(cacheKey, indexPath);
|
|
378
414
|
return indexPath;
|
|
379
415
|
}
|
|
380
416
|
}
|
|
@@ -384,10 +420,12 @@ function createRequire(
|
|
|
384
420
|
for (const ext of extensions) {
|
|
385
421
|
const withExt = resolved + ext;
|
|
386
422
|
if (vfs.existsSync(withExt)) {
|
|
423
|
+
resolutionCache.set(cacheKey, withExt);
|
|
387
424
|
return withExt;
|
|
388
425
|
}
|
|
389
426
|
}
|
|
390
427
|
|
|
428
|
+
resolutionCache.set(cacheKey, null);
|
|
391
429
|
throw new Error(`Cannot find module '${id}' from '${fromDir}'`);
|
|
392
430
|
}
|
|
393
431
|
|
|
@@ -436,10 +474,8 @@ function createRequire(
|
|
|
436
474
|
const pkgRoot = pathShim.join(nodeModulesDir, pkgName);
|
|
437
475
|
const pkgPath = pathShim.join(pkgRoot, 'package.json');
|
|
438
476
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const pkg = JSON.parse(pkgContent);
|
|
442
|
-
|
|
477
|
+
const pkg = getParsedPackageJson(pkgPath);
|
|
478
|
+
if (pkg) {
|
|
443
479
|
// Use resolve.exports to handle the exports field
|
|
444
480
|
if (pkg.exports) {
|
|
445
481
|
try {
|
|
@@ -474,15 +510,22 @@ function createRequire(
|
|
|
474
510
|
while (searchDir !== '/') {
|
|
475
511
|
const nodeModulesDir = pathShim.join(searchDir, 'node_modules');
|
|
476
512
|
const resolved = tryResolveFromNodeModules(nodeModulesDir, id);
|
|
477
|
-
if (resolved)
|
|
513
|
+
if (resolved) {
|
|
514
|
+
resolutionCache.set(cacheKey, resolved);
|
|
515
|
+
return resolved;
|
|
516
|
+
}
|
|
478
517
|
|
|
479
518
|
searchDir = pathShim.dirname(searchDir);
|
|
480
519
|
}
|
|
481
520
|
|
|
482
521
|
// Try root node_modules as last resort
|
|
483
522
|
const rootResolved = tryResolveFromNodeModules('/node_modules', id);
|
|
484
|
-
if (rootResolved)
|
|
523
|
+
if (rootResolved) {
|
|
524
|
+
resolutionCache.set(cacheKey, rootResolved);
|
|
525
|
+
return rootResolved;
|
|
526
|
+
}
|
|
485
527
|
|
|
528
|
+
resolutionCache.set(cacheKey, null);
|
|
486
529
|
throw new Error(`Cannot find module '${id}'`);
|
|
487
530
|
};
|
|
488
531
|
|
|
@@ -514,28 +557,40 @@ function createRequire(
|
|
|
514
557
|
}
|
|
515
558
|
|
|
516
559
|
// Read and execute JS file
|
|
517
|
-
|
|
560
|
+
const rawCode = vfs.readFileSync(resolvedPath, 'utf8');
|
|
518
561
|
const dirname = pathShim.dirname(resolvedPath);
|
|
519
562
|
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
563
|
+
// Check processed code cache (useful for HMR when module cache is cleared but code hasn't changed)
|
|
564
|
+
// Use a simple hash of the content for cache key to handle content changes
|
|
565
|
+
const codeCacheKey = `${resolvedPath}|${simpleHash(rawCode)}`;
|
|
566
|
+
let code = processedCodeCache?.get(codeCacheKey);
|
|
567
|
+
|
|
568
|
+
if (!code) {
|
|
569
|
+
code = rawCode;
|
|
526
570
|
|
|
527
|
-
|
|
528
|
-
|
|
571
|
+
// Transform ESM to CJS if needed (for .mjs files or ESM that wasn't pre-transformed)
|
|
572
|
+
// This handles files that weren't transformed during npm install
|
|
573
|
+
// BUT skip .cjs files and already-bundled CJS code
|
|
574
|
+
const isCjsFile = resolvedPath.endsWith('.cjs');
|
|
575
|
+
const isAlreadyBundledCjs = code.startsWith('"use strict";\nvar __') ||
|
|
576
|
+
code.startsWith("'use strict';\nvar __");
|
|
529
577
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
578
|
+
const hasEsmImport = /\bimport\s+[\w{*'"]/m.test(code);
|
|
579
|
+
const hasEsmExport = /\bexport\s+(?:default|const|let|var|function|class|{|\*)/m.test(code);
|
|
580
|
+
|
|
581
|
+
if (!isCjsFile && !isAlreadyBundledCjs) {
|
|
582
|
+
if (resolvedPath.endsWith('.mjs') || resolvedPath.includes('/esm/') || hasEsmImport || hasEsmExport) {
|
|
583
|
+
code = transformEsmToCjs(code, resolvedPath);
|
|
584
|
+
}
|
|
533
585
|
}
|
|
534
|
-
}
|
|
535
586
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
587
|
+
// Transform dynamic imports: import('x') -> __dynamicImport('x')
|
|
588
|
+
// This allows dynamic imports to work in our eval-based runtime
|
|
589
|
+
code = transformDynamicImports(code);
|
|
590
|
+
|
|
591
|
+
// Cache the processed code
|
|
592
|
+
processedCodeCache?.set(codeCacheKey, code);
|
|
593
|
+
}
|
|
539
594
|
|
|
540
595
|
// Create require for this module
|
|
541
596
|
const moduleRequire = createRequire(
|
|
@@ -544,7 +599,8 @@ function createRequire(
|
|
|
544
599
|
process,
|
|
545
600
|
dirname,
|
|
546
601
|
moduleCache,
|
|
547
|
-
options
|
|
602
|
+
options,
|
|
603
|
+
processedCodeCache
|
|
548
604
|
);
|
|
549
605
|
moduleRequire.cache = moduleCache;
|
|
550
606
|
|
|
@@ -779,6 +835,8 @@ export class Runtime {
|
|
|
779
835
|
private process: Process;
|
|
780
836
|
private moduleCache: Record<string, Module> = {};
|
|
781
837
|
private options: RuntimeOptions;
|
|
838
|
+
/** Cache for pre-processed code (after ESM transform) before eval */
|
|
839
|
+
private processedCodeCache: Map<string, string> = new Map();
|
|
782
840
|
|
|
783
841
|
constructor(vfs: VirtualFS, options: RuntimeOptions = {}) {
|
|
784
842
|
this.vfs = vfs;
|
|
@@ -907,7 +965,8 @@ export class Runtime {
|
|
|
907
965
|
this.process,
|
|
908
966
|
dirname,
|
|
909
967
|
this.moduleCache,
|
|
910
|
-
this.options
|
|
968
|
+
this.options,
|
|
969
|
+
this.processedCodeCache
|
|
911
970
|
);
|
|
912
971
|
|
|
913
972
|
// Create module object
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definition for package.json files
|
|
3
|
+
*/
|
|
4
|
+
export interface PackageJson {
|
|
5
|
+
name?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
main?: string;
|
|
8
|
+
module?: string;
|
|
9
|
+
types?: string;
|
|
10
|
+
exports?: Record<string, unknown> | string;
|
|
11
|
+
dependencies?: Record<string, string>;
|
|
12
|
+
devDependencies?: Record<string, string>;
|
|
13
|
+
peerDependencies?: Record<string, string>;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple hash function for content-based cache invalidation.
|
|
3
|
+
* Uses djb2-style hashing for fast string hashing.
|
|
4
|
+
*/
|
|
5
|
+
export function simpleHash(str: string): string {
|
|
6
|
+
let hash = 0;
|
|
7
|
+
for (let i = 0; i < str.length; i++) {
|
|
8
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
|
|
9
|
+
hash |= 0;
|
|
10
|
+
}
|
|
11
|
+
return hash.toString(36);
|
|
12
|
+
}
|