@nimbus-sh/core 0.7.0 → 0.8.0
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/dist/_shared/tarball.d.ts +2 -1
- package/dist/_shared/tarball.d.ts.map +1 -1
- package/dist/_shared/tarball.js +5 -1
- package/dist/runtime/comment-strip.d.ts +38 -0
- package/dist/runtime/comment-strip.d.ts.map +1 -1
- package/dist/runtime/comment-strip.js +270 -76
- package/dist/runtime/cpython-runner.d.ts +1 -0
- package/dist/runtime/cpython-runner.d.ts.map +1 -1
- package/dist/runtime/cpython-runner.js +1 -1
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +10 -195
- package/dist/runtime/ruby-runner.d.ts +1 -0
- package/dist/runtime/ruby-runner.d.ts.map +1 -1
- package/dist/runtime/ruby-runner.js +1 -0
- package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/npm.js +3 -4
- package/dist/workspace/nimbus-workspace.d.ts +7 -1
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +1 -2
- package/dist/workspace/supervisor-op.d.ts +88 -4
- package/dist/workspace/supervisor-op.d.ts.map +1 -1
- package/dist/workspace/supervisor-op.js +146 -14
- package/package.json +2 -2
- package/src/_shared/tarball.ts +5 -1
- package/src/runtime/comment-strip.ts +216 -69
- package/src/runtime/cpython-runner.ts +2 -1
- package/src/runtime/esbuild-service.ts +10 -159
- package/src/runtime/ruby-runner.ts +2 -0
- package/src/substrate/lifo/commands/system/npm.ts +3 -6
- package/src/workspace/nimbus-workspace.ts +6 -3
- package/src/workspace/supervisor-op.ts +212 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus-sh/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Backend-agnostic half of Nimbus — the durable filesystem, the shell, and the OS/process contracts, over a host-supplied SQL port.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"typecheck": "tsc --noEmit"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@nimbus-sh/platform": "^0.
|
|
58
|
+
"@nimbus-sh/platform": "^0.3.0",
|
|
59
59
|
"@renovatebot/pep440": "^5.0.0",
|
|
60
60
|
"acorn": "^8.15.0",
|
|
61
61
|
"acorn-walk": "^8.3.5",
|
package/src/_shared/tarball.ts
CHANGED
|
@@ -35,7 +35,8 @@ const PACKAGE_MANIFEST = 'package.json';
|
|
|
35
35
|
* Stream a gzipped npm archive into a package directory. Entry names are
|
|
36
36
|
* already canonical and prefix-stripped by streamTarEntries. Hold only the
|
|
37
37
|
* current entry and the manifest; write the manifest last so a failed install
|
|
38
|
-
* is not mistaken for a complete package on retry.
|
|
38
|
+
* is not mistaken for a complete package on retry. A second manifest in one
|
|
39
|
+
* archive is a malformed package, not an overwrite. Filesystem failures reject.
|
|
39
40
|
*/
|
|
40
41
|
export async function writeTarballStream(
|
|
41
42
|
body: ReadableStream<Uint8Array>,
|
|
@@ -54,6 +55,9 @@ export async function writeTarballStream(
|
|
|
54
55
|
);
|
|
55
56
|
for await (const entry of entries) {
|
|
56
57
|
if (entry.name === PACKAGE_MANIFEST) {
|
|
58
|
+
if (manifest !== null) {
|
|
59
|
+
throw new Error(`tarball for ${targetDir} carried two ${PACKAGE_MANIFEST} entries`);
|
|
60
|
+
}
|
|
57
61
|
manifest = entry.data;
|
|
58
62
|
continue;
|
|
59
63
|
}
|
|
@@ -1,92 +1,239 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* One JavaScript source scanner shared by the two comment-stripping
|
|
3
|
+
* call sites — prefetch's import detection and the esbuild transform
|
|
4
|
+
* pipeline's classifiers.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* comment
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
* pipeline).
|
|
6
|
+
* `scanJsSource(src, literals)` walks `src` once and returns a
|
|
7
|
+
* byte-aligned copy in which `//` and `/* … *\/` comments are blanked
|
|
8
|
+
* (each comment becomes a space; newlines inside a block comment are
|
|
9
|
+
* preserved so line numbers still match the input). String, template
|
|
10
|
+
* and regex literals are what `literals` decides:
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* - `'blank'`: literal content is replaced too — templates keep their
|
|
13
|
+
* `${…}` interpolation expression as code so a depth-tracked caller
|
|
14
|
+
* sees `await` etc. inside it. This is the transform pipeline's
|
|
15
|
+
* classification view: nothing quoted can read as an `import`.
|
|
16
|
+
* - `'keep'`: literals are copied verbatim. This is prefetch's
|
|
17
|
+
* import-detection view: IMPORT_RE/REQUIRE_RE must see the
|
|
18
|
+
* specifier string, and a `//` or `/*` inside a literal must NOT
|
|
19
|
+
* open a comment that swallows a following real import.
|
|
17
20
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* String-literal handling is intentionally OUT of scope. A literal
|
|
25
|
-
* `import x from 'y'` inside a JavaScript string would still produce
|
|
26
|
-
* a false-positive prefetch attempt. The resolver no-ops on misses,
|
|
27
|
-
* so it's a minor wasted-work cost — see IMPORT_RE header comment in
|
|
28
|
-
* `require-resolver.ts:84-86`.
|
|
29
|
-
*
|
|
30
|
-
* Memory: the output is assembled from slices of the input — one per
|
|
31
|
-
* comment-free span — and joined once. It used to be built one character
|
|
32
|
-
* at a time with `out += c`, which every JS engine represents as a rope
|
|
33
|
-
* of one node per append: ~30 bytes of heap per character of output, all
|
|
34
|
-
* of it live until the string is first read. The require walk runs this
|
|
35
|
-
* over every file in the closure, and on typescript's 6.15 MB `lib/_tsc.js`
|
|
36
|
-
* that rope measured 174 MB (V8) / 172 MB (JSC) against a 128 MB isolate —
|
|
21
|
+
* The output is assembled from input slices and joined once. It used to
|
|
22
|
+
* be built one character at a time (`stripped += c`), which V8 keeps as
|
|
23
|
+
* a rope of one node per append — ~30 bytes of heap per character, all
|
|
24
|
+
* live until the string is first read. On typescript's 6.15 MB
|
|
25
|
+
* `lib/_tsc.js` that rope measured 174 MB against a 128 MB isolate —
|
|
37
26
|
* the session Durable Object was killed inside `tsc`'s spawn before the
|
|
38
|
-
* facet existed.
|
|
39
|
-
* short array of slices, and nothing else.
|
|
27
|
+
* facet existed. Spans hold the input, the output and a short array.
|
|
40
28
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* same logical pass over comments; only the string/regex-literal
|
|
45
|
-
* handling differs.
|
|
29
|
+
* Serialized by name: `generateEsbuildTransformRuntimeSource` embeds
|
|
30
|
+
* this function's `.toString()` in the transform runtime, so every
|
|
31
|
+
* constant it reads is declared inside the body.
|
|
46
32
|
*/
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
33
|
+
export function scanJsSource(src: string, literals: 'keep' | 'blank'): string {
|
|
34
|
+
const NEWLINE = 0x0a;
|
|
35
|
+
// Identifier-suffix detection: `/` after one of these is division;
|
|
36
|
+
// after anything else (operators, punctuators, keywords, start-of-file)
|
|
37
|
+
// it opens a regex literal. Word chars + `)` + `]` cover `foo()/x` and
|
|
38
|
+
// `a[i]/y`; the quote characters cover `'…' /re/` and `` `…` /re/ ``.
|
|
39
|
+
const DIVISION_AFTER = /[A-Za-z0-9_$\)\]'\"`]/;
|
|
50
40
|
|
|
51
|
-
export function stripCommentsForImports(src: string): string {
|
|
52
|
-
const N = src.length;
|
|
53
41
|
const parts: string[] = [];
|
|
54
|
-
// Start of the
|
|
42
|
+
// Start of the span not yet copied to `parts`.
|
|
55
43
|
let spanStart = 0;
|
|
56
44
|
let i = 0;
|
|
45
|
+
const N = src.length;
|
|
46
|
+
let lastNonWs = '';
|
|
47
|
+
const record = (ch: string) => {
|
|
48
|
+
if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r') lastNonWs = ch;
|
|
49
|
+
};
|
|
50
|
+
// The division/regex discriminator needs the last non-ws char the loop
|
|
51
|
+
// has passed — stepped chars record themselves; spans already landed in
|
|
52
|
+
// `parts` only need their final char re-derived.
|
|
53
|
+
const step = () => { record(src[i]); i++; };
|
|
54
|
+
const copy = (end: number) => {
|
|
55
|
+
if (end > spanStart) parts.push(src.slice(spanStart, end));
|
|
56
|
+
};
|
|
57
|
+
const emit = (ch: string) => { parts.push(ch); };
|
|
58
|
+
|
|
59
|
+
// A shebang is the file's first line and never a comment or a regex —
|
|
60
|
+
// `#!/` at offset 0 is copied whole before the loop sees the `/`.
|
|
61
|
+
if (src.charCodeAt(0) === 0x23 && src.charCodeAt(1) === 0x21) {
|
|
62
|
+
i = 2;
|
|
63
|
+
while (i < N && src.charCodeAt(i) !== NEWLINE) i++;
|
|
64
|
+
record('!');
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
while (i < N) {
|
|
58
|
-
if (src.charCodeAt(i)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
if (src.charCodeAt(i) === 0x2f /* / */) {
|
|
69
|
+
const next = src.charCodeAt(i + 1);
|
|
70
|
+
if (next === 0x2f) {
|
|
71
|
+
// Line comment: to end-of-line, one space. The newline itself is
|
|
72
|
+
// not part of the comment and stays in the next span.
|
|
73
|
+
copy(i);
|
|
74
|
+
i += 2;
|
|
75
|
+
while (i < N && src.charCodeAt(i) !== NEWLINE) i++;
|
|
76
|
+
emit(' ');
|
|
77
|
+
spanStart = i;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (next === 0x2a) {
|
|
81
|
+
// Block comment: replaced by its own newlines plus one space; an
|
|
82
|
+
// unterminated one runs to the end of input and gets no space.
|
|
83
|
+
copy(i);
|
|
84
|
+
i += 2;
|
|
85
|
+
let newlines = 0;
|
|
86
|
+
let closed = false;
|
|
87
|
+
while (i < N) {
|
|
88
|
+
const bc = src.charCodeAt(i);
|
|
89
|
+
if (bc === NEWLINE) newlines++;
|
|
90
|
+
else if (bc === 0x2a && src.charCodeAt(i + 1) === 0x2f) { i += 2; closed = true; break; }
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
if (newlines > 0) parts.push('\n'.repeat(newlines));
|
|
94
|
+
if (closed) emit(' ');
|
|
95
|
+
spanStart = i;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Regex literal when the previous non-ws char can't end a value.
|
|
99
|
+
if (!DIVISION_AFTER.test(lastNonWs)) {
|
|
100
|
+
// A regex needs its closing `/` on the same line; without one the
|
|
101
|
+
// slash is division (or a lone `/`), and treating the rest of the
|
|
102
|
+
// line as a literal would drop real code — imports included.
|
|
103
|
+
let j = i + 1;
|
|
104
|
+
let closes = false;
|
|
105
|
+
while (j < N) {
|
|
106
|
+
const rc = src.charCodeAt(j);
|
|
107
|
+
if (rc === 0x5c) { j += 2; continue; }
|
|
108
|
+
if (rc === 0x5b) {
|
|
109
|
+
j++;
|
|
110
|
+
while (j < N && src.charCodeAt(j) !== 0x5d) {
|
|
111
|
+
if (src.charCodeAt(j) === 0x5c) { j += 2; continue; }
|
|
112
|
+
j++;
|
|
113
|
+
}
|
|
114
|
+
if (j < N) j++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (rc === 0x2f) { closes = true; break; }
|
|
118
|
+
if (rc === NEWLINE) break;
|
|
119
|
+
j++;
|
|
120
|
+
}
|
|
121
|
+
if (!closes) { step(); continue; }
|
|
122
|
+
copy(i);
|
|
123
|
+
emit(' ');
|
|
124
|
+
i++;
|
|
125
|
+
while (i < N) {
|
|
126
|
+
const rc = src.charCodeAt(i);
|
|
127
|
+
if (rc === 0x5c) { i += 2; continue; }
|
|
128
|
+
if (rc === 0x5b) {
|
|
129
|
+
// Character class: `/` inside it is content.
|
|
130
|
+
i++;
|
|
131
|
+
while (i < N && src.charCodeAt(i) !== 0x5d) {
|
|
132
|
+
if (src.charCodeAt(i) === 0x5c) { i += 2; continue; }
|
|
133
|
+
i++;
|
|
134
|
+
}
|
|
135
|
+
if (i < N) i++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (rc === 0x2f) { i++; break; }
|
|
139
|
+
i++;
|
|
140
|
+
}
|
|
141
|
+
// Flags: g, i, m, s, u, y, d.
|
|
142
|
+
while (i < N && /[gimsuyd]/.test(src[i])) i++;
|
|
143
|
+
record('/');
|
|
144
|
+
spanStart = i;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Division or a lone slash — plain source.
|
|
148
|
+
step();
|
|
67
149
|
continue;
|
|
68
150
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
151
|
+
|
|
152
|
+
const ch = src[i];
|
|
153
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
154
|
+
if (literals === 'keep') {
|
|
155
|
+
i++;
|
|
156
|
+
while (i < N) {
|
|
157
|
+
const cc = src[i];
|
|
158
|
+
if (cc === '\\') { i += 2; continue; }
|
|
159
|
+
if (cc === ch) { i++; break; }
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
// The literal ends a value: a following `/` is division.
|
|
163
|
+
record(ch);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
// blank: the delimiters and content become one space; a template's
|
|
167
|
+
// `${…}` interpolation keeps its expression as code.
|
|
168
|
+
copy(i);
|
|
169
|
+
emit(' ');
|
|
170
|
+
i++;
|
|
77
171
|
while (i < N) {
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
172
|
+
const cc = src[i];
|
|
173
|
+
if (cc === '\\') { i += 2; continue; }
|
|
174
|
+
if (cc === ch) { i++; break; }
|
|
175
|
+
if (ch === '`' && cc === '$' && src[i + 1] === '{') {
|
|
176
|
+
emit('${');
|
|
177
|
+
i += 2;
|
|
178
|
+
let depth = 1;
|
|
179
|
+
while (i < N && depth > 0) {
|
|
180
|
+
const ic = src[i];
|
|
181
|
+
// Nested string inside the interpolation: blank its content
|
|
182
|
+
// so a `}` inside it doesn't drop the depth early.
|
|
183
|
+
if (ic === '"' || ic === "'" || ic === '`') {
|
|
184
|
+
const iq = ic;
|
|
185
|
+
emit(' ');
|
|
186
|
+
i++;
|
|
187
|
+
while (i < N) {
|
|
188
|
+
const icc = src[i];
|
|
189
|
+
if (icc === '\\') { i += 2; continue; }
|
|
190
|
+
if (icc === iq) { i++; break; }
|
|
191
|
+
// A nested template's own ${…} recurses once more —
|
|
192
|
+
// deeper nesting falls back to brace counting.
|
|
193
|
+
if (iq === '`' && icc === '$' && src[i + 1] === '{') {
|
|
194
|
+
emit('${');
|
|
195
|
+
i += 2;
|
|
196
|
+
let d2 = 1;
|
|
197
|
+
while (i < N && d2 > 0) {
|
|
198
|
+
const i2 = src[i];
|
|
199
|
+
if (i2 === '{') d2++;
|
|
200
|
+
else if (i2 === '}') d2--;
|
|
201
|
+
if (d2 > 0) emit(i2);
|
|
202
|
+
i++;
|
|
203
|
+
}
|
|
204
|
+
emit('}');
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (icc === '\n') emit('\n');
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (ic === '{') depth++;
|
|
213
|
+
else if (ic === '}') depth--;
|
|
214
|
+
if (depth > 0) emit(ic);
|
|
215
|
+
i++;
|
|
216
|
+
}
|
|
217
|
+
emit('}');
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (cc === '\n') emit('\n');
|
|
81
221
|
i++;
|
|
82
222
|
}
|
|
83
|
-
if (newlines > 0) parts.push('\n'.repeat(newlines));
|
|
84
|
-
if (closed) parts.push(' ');
|
|
85
223
|
spanStart = i;
|
|
86
224
|
continue;
|
|
87
225
|
}
|
|
88
|
-
|
|
226
|
+
step();
|
|
89
227
|
}
|
|
90
|
-
|
|
228
|
+
copy(N);
|
|
91
229
|
return parts.join('');
|
|
92
230
|
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Strip comments for import/require detection. Literals are kept: the
|
|
234
|
+
* specifier the regexes extract lives inside a string, and a comment
|
|
235
|
+
* marker inside a string must not swallow the code that follows it.
|
|
236
|
+
*/
|
|
237
|
+
export function stripCommentsForImports(src: string): string {
|
|
238
|
+
return scanJsSource(src, 'keep');
|
|
239
|
+
}
|
|
@@ -230,6 +230,7 @@ async function cpythonRunFacetFn(
|
|
|
230
230
|
* that has none does not get a degraded version — it gets none, and says so.
|
|
231
231
|
*/
|
|
232
232
|
export type CPythonResidentStart = (spawn: {
|
|
233
|
+
argv: string[];
|
|
233
234
|
/** VFS path of the interpreter. By path, not by value: it is 10.6 MiB. */
|
|
234
235
|
wasmVfsPath: string;
|
|
235
236
|
startArgs: Record<string, unknown>;
|
|
@@ -474,7 +475,7 @@ export function makeCPythonRunnerFactory(deps: {
|
|
|
474
475
|
const command = [binName, ...argv].map((part) =>
|
|
475
476
|
(/^[A-Za-z0-9_./:=@+-]+$/.test(part) ? part : JSON.stringify(part))).join(' ');
|
|
476
477
|
const spawnResult = await deps.startResident(
|
|
477
|
-
{ wasmVfsPath: wasmVfs, startArgs: facetArgs, cwd, command });
|
|
478
|
+
{ wasmVfsPath: wasmVfs, startArgs: facetArgs, cwd, command, argv: [binName, ...argv] });
|
|
478
479
|
if (spawnResult.stdout) ctx.stdout.write(spawnResult.stdout);
|
|
479
480
|
if (spawnResult.stderr) ctx.stderr.write(spawnResult.stderr);
|
|
480
481
|
return spawnResult.exitCode;
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
nodeProp,
|
|
30
30
|
parseJavaScriptModule,
|
|
31
31
|
} from './javascript-ast.js';
|
|
32
|
+
import { scanJsSource } from './comment-strip.js';
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Bundler version tag. BUMP THIS whenever bundling semantics change —
|
|
@@ -203,167 +204,14 @@ function hasEsmExports(src: string): boolean {
|
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
/**
|
|
206
|
-
* Strip `//` and `/*
|
|
207
|
-
* source
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* for the small entry points that reach the true-TLA fallback.
|
|
207
|
+
* Strip `//` and `/* *\/` comments and string / template literals from
|
|
208
|
+
* source for the import/export classifiers — one scanner shared with
|
|
209
|
+
* prefetch's import detection in `comment-strip.ts`. The result is
|
|
210
|
+
* byte-aligned with the input per line (comment and literal newlines are
|
|
211
|
+
* preserved), so error line numbers still match the original.
|
|
212
212
|
*/
|
|
213
213
|
function stripCommentsAndStrings(src: string): string {
|
|
214
|
-
|
|
215
|
-
let i = 0;
|
|
216
|
-
const N = src.length;
|
|
217
|
-
// Track the last non-whitespace non-comment output character so we
|
|
218
|
-
// can disambiguate `/` as division (after identifier/literal/`)`/`]`)
|
|
219
|
-
// vs regex-literal opener (after operator / punctuator / keyword /
|
|
220
|
-
// start-of-file). Pre-fix the stripper had no regex awareness, so
|
|
221
|
-
// patterns like `var X = /^(?:'…)/` contained an unmatched `'` that
|
|
222
|
-
// bit it as a string opener that didn't close until many lines
|
|
223
|
-
// later — corrupting the import/export classifiers. Real-world bite:
|
|
224
|
-
// sv-utils@0.0.3 index.mjs has dozens of these regex literals.
|
|
225
|
-
let lastNonWsChar = '';
|
|
226
|
-
const recordOut = (ch: string) => {
|
|
227
|
-
if (ch !== ' ' && ch !== '\t' && ch !== '\n' && ch !== '\r') {
|
|
228
|
-
lastNonWsChar = ch;
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
// Identifier-suffix detection on lastNonWsChar: any of these means
|
|
232
|
-
// `/` is division. Anything else means `/` opens a regex.
|
|
233
|
-
// Includes ascii word chars + `)` `]` to cover `foo()/x` `a[i]/y`.
|
|
234
|
-
const DIVISION_AFTER = /[A-Za-z0-9_$\)\]]/;
|
|
235
|
-
while (i < N) {
|
|
236
|
-
const c = src[i];
|
|
237
|
-
if (c === '/' && src[i + 1] === '/') {
|
|
238
|
-
while (i < N && src[i] !== '\n') i++;
|
|
239
|
-
stripped += ' ';
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (c === '/' && src[i + 1] === '*') {
|
|
243
|
-
i += 2;
|
|
244
|
-
while (i < N && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
245
|
-
// Preserve newlines so line numbers stay aligned.
|
|
246
|
-
if (src[i] === '\n') stripped += '\n';
|
|
247
|
-
i++;
|
|
248
|
-
}
|
|
249
|
-
i += 2;
|
|
250
|
-
stripped += ' ';
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
// Regex literal: `/` not followed by `/` or `*` (already handled
|
|
254
|
-
// above), AND preceded by something that makes regex valid here.
|
|
255
|
-
// The classifier looks at lastNonWsChar — for `(`, `,`, `=`, `;`,
|
|
256
|
-
// `{`, `[`, `:`, `!`, `&`, `|`, `?`, operators, or start-of-file,
|
|
257
|
-
// a slash is a regex opener.
|
|
258
|
-
if (c === '/' && !DIVISION_AFTER.test(lastNonWsChar)) {
|
|
259
|
-
stripped += ' ';
|
|
260
|
-
i++;
|
|
261
|
-
while (i < N) {
|
|
262
|
-
const rc = src[i];
|
|
263
|
-
if (rc === '\\') { i += 2; continue; }
|
|
264
|
-
// Regex character class [...]: `/` inside it is content.
|
|
265
|
-
if (rc === '[') {
|
|
266
|
-
i++;
|
|
267
|
-
while (i < N && src[i] !== ']') {
|
|
268
|
-
if (src[i] === '\\') { i += 2; continue; }
|
|
269
|
-
if (src[i] === '\n') { stripped += '\n'; }
|
|
270
|
-
i++;
|
|
271
|
-
}
|
|
272
|
-
if (i < N) i++; // consume ']'
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
if (rc === '/') { i++; break; }
|
|
276
|
-
if (rc === '\n') {
|
|
277
|
-
// Regex literals can't span newlines. If we hit one without
|
|
278
|
-
// finding the closing `/`, this was probably NOT a regex —
|
|
279
|
-
// bail out gracefully (rare in practice; better than getting
|
|
280
|
-
// stuck in a wrong state).
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
i++;
|
|
284
|
-
}
|
|
285
|
-
// Skip regex flags (g, i, m, s, u, y, d).
|
|
286
|
-
while (i < N && /[gimsuyd]/.test(src[i])) i++;
|
|
287
|
-
recordOut('/');
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
if (c === '"' || c === "'" || c === '`') {
|
|
291
|
-
const q = c;
|
|
292
|
-
stripped += ' ';
|
|
293
|
-
i++;
|
|
294
|
-
while (i < N) {
|
|
295
|
-
const cc = src[i];
|
|
296
|
-
if (cc === '\\') { i += 2; continue; }
|
|
297
|
-
if (cc === q) { i++; break; }
|
|
298
|
-
// Template interpolation: ${...} — preserve the inner
|
|
299
|
-
// expression as raw code so the caller's depth-tracker can
|
|
300
|
-
// see `await` etc. inside it. Strip the wrapping `${`/`}`
|
|
301
|
-
// (those don't affect brace-depth from the caller's POV).
|
|
302
|
-
//
|
|
303
|
-
// CRITICAL: inside the interpolation expression, we must
|
|
304
|
-
// recognise NESTED strings (`'}'`, `"}"`, `` `${...}` ``) so
|
|
305
|
-
// their internal `}` characters don't decrement the depth
|
|
306
|
-
// counter — otherwise we exit the interpolation early and
|
|
307
|
-
// start consuming code as string content, eventually parsing
|
|
308
|
-
// the rest of the file as one massive unclosed string. This
|
|
309
|
-
// bit sv-utils/dist/index.mjs: multi-line backticks with
|
|
310
|
-
// `${url.replace('}', '_')}`-style interpolations corrupted
|
|
311
|
-
// line classification downstream. Recursive-by-loop here.
|
|
312
|
-
if (q === '`' && cc === '$' && src[i + 1] === '{') {
|
|
313
|
-
stripped += '${';
|
|
314
|
-
i += 2;
|
|
315
|
-
let depth = 1;
|
|
316
|
-
while (i < N && depth > 0) {
|
|
317
|
-
const ic = src[i];
|
|
318
|
-
// Nested string inside interpolation: skip its content.
|
|
319
|
-
if (ic === '"' || ic === "'" || ic === '`') {
|
|
320
|
-
const iq = ic;
|
|
321
|
-
stripped += ' ';
|
|
322
|
-
i++;
|
|
323
|
-
while (i < N) {
|
|
324
|
-
const icc = src[i];
|
|
325
|
-
if (icc === '\\') { i += 2; continue; }
|
|
326
|
-
if (icc === iq) { i++; break; }
|
|
327
|
-
// Nested template inside interpolation can also have its
|
|
328
|
-
// own ${...} — recurse once more (the practical depth
|
|
329
|
-
// observed in real code never exceeds 2; deeper nesting
|
|
330
|
-
// falls back to brace counting which is a best-effort).
|
|
331
|
-
if (iq === '`' && icc === '$' && src[i + 1] === '{') {
|
|
332
|
-
stripped += '${'; i += 2;
|
|
333
|
-
let d2 = 1;
|
|
334
|
-
while (i < N && d2 > 0) {
|
|
335
|
-
const i2 = src[i];
|
|
336
|
-
if (i2 === '{') d2++;
|
|
337
|
-
else if (i2 === '}') d2--;
|
|
338
|
-
if (d2 > 0) stripped += i2;
|
|
339
|
-
i++;
|
|
340
|
-
}
|
|
341
|
-
stripped += '}';
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
if (icc === '\n') stripped += '\n';
|
|
345
|
-
i++;
|
|
346
|
-
}
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
if (ic === '{') depth++;
|
|
350
|
-
else if (ic === '}') depth--;
|
|
351
|
-
if (depth > 0) stripped += ic;
|
|
352
|
-
i++;
|
|
353
|
-
}
|
|
354
|
-
stripped += '}';
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if (cc === '\n') stripped += '\n';
|
|
358
|
-
i++;
|
|
359
|
-
}
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
stripped += c;
|
|
363
|
-
recordOut(c);
|
|
364
|
-
i++;
|
|
365
|
-
}
|
|
366
|
-
return stripped;
|
|
214
|
+
return scanJsSource(src, 'blank');
|
|
367
215
|
}
|
|
368
216
|
|
|
369
217
|
/**
|
|
@@ -1245,6 +1093,9 @@ async function transformWithEsbuild(
|
|
|
1245
1093
|
/** Source needed by the slim Worker Loader transform isolate. */
|
|
1246
1094
|
export function generateEsbuildTransformRuntimeSource(): string {
|
|
1247
1095
|
return [
|
|
1096
|
+
// scanJsSource is self-contained — its constants live in the body —
|
|
1097
|
+
// so this serialized copy carries the whole scanner.
|
|
1098
|
+
scanJsSource.toString(),
|
|
1248
1099
|
stripCommentsAndStrings.toString(),
|
|
1249
1100
|
hasEsmImports.toString(),
|
|
1250
1101
|
hasEsmExports.toString(),
|
|
@@ -260,6 +260,7 @@ export function makeRubyRunnerFactory(deps: {
|
|
|
260
260
|
startArgs: toRubyCallArgs(facetArgs),
|
|
261
261
|
cwd,
|
|
262
262
|
command: formatRubyCommand(binName, argv),
|
|
263
|
+
argv: [binName, ...argv],
|
|
263
264
|
});
|
|
264
265
|
} else {
|
|
265
266
|
result = await dispatchRubyFacet(deps.facets, vfs, facetArgs, ctx.pid);
|
|
@@ -619,6 +620,7 @@ export interface RubyFacetResult {
|
|
|
619
620
|
* no degraded version — it gets none, and says so.
|
|
620
621
|
*/
|
|
621
622
|
export type RubyResidentStart = (spawn: {
|
|
623
|
+
argv: string[];
|
|
622
624
|
/** VFS path of the interpreter. By path, not by value: it is 34.3 MiB. */
|
|
623
625
|
wasmVfsPath: string;
|
|
624
626
|
startArgs: RubyFacetCallArgs;
|
|
@@ -289,12 +289,9 @@ async function installSinglePackage(
|
|
|
289
289
|
|
|
290
290
|
const info = await fetchPackageInfo(npmRegistry, name, version, signal);
|
|
291
291
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
`${name}: extraction wrote ${written.files} files and left no package.json in ${targetDir}`,
|
|
296
|
-
);
|
|
297
|
-
}
|
|
292
|
+
// writeTarballStream throws when the archive carried no manifest and writes
|
|
293
|
+
// package.json last, so a return here is a complete package on disk.
|
|
294
|
+
await fetchAndStreamPackage(info.dist.tarball, targetDir, vfs, signal);
|
|
298
295
|
|
|
299
296
|
let installed = 1;
|
|
300
297
|
|
|
@@ -60,8 +60,12 @@ export interface NimbusWorkspaceOptions {
|
|
|
60
60
|
* Every atomic write in the filesystem rests on this being a real
|
|
61
61
|
* transaction. An implementation that merely calls the callback converts
|
|
62
62
|
* each one into a torn write that reports success.
|
|
63
|
+
*
|
|
64
|
+
* `NimbusWorkspace.create` is a first-write-wins composition root beside
|
|
65
|
+
* `worker/index.ts` and `loom/actor.ts`: when `ctxExports` is absent the
|
|
66
|
+
* host's ctx `exports` bag is adopted as the isolate's.
|
|
63
67
|
*/
|
|
64
|
-
readonly transactions?: TransactionHost;
|
|
68
|
+
readonly transactions?: TransactionHost & { readonly exports?: CtxExports };
|
|
65
69
|
/**
|
|
66
70
|
* The filesystem already open over `sql`, for a host that has one.
|
|
67
71
|
*
|
|
@@ -195,8 +199,7 @@ export class NimbusWorkspace {
|
|
|
195
199
|
|
|
196
200
|
static async create(options: NimbusWorkspaceOptions): Promise<NimbusWorkspace> {
|
|
197
201
|
if (options.fabric) composeFabric(options.fabric);
|
|
198
|
-
const exports = options.ctxExports
|
|
199
|
-
?? (options.transactions as (TransactionHost & { exports?: CtxExports }) | undefined)?.exports;
|
|
202
|
+
const exports = options.ctxExports ?? options.transactions?.exports;
|
|
200
203
|
if (exports) adoptCtxExports(exports);
|
|
201
204
|
const vfs = options.vfs ?? openFilesystem(options);
|
|
202
205
|
const mounts = options.mounts ?? DEFAULT_MOUNT_POINTS;
|