@ttsc/playground 0.18.3 → 0.19.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/README.md +1 -1
- package/lib/src/compiler/createWorkerCompiler.js +7 -191
- package/lib/src/compiler/createWorkerCompiler.js.map +1 -1
- package/lib/src/compiler/internal/createWorkerCompilerService.d.ts +18 -0
- package/lib/src/compiler/internal/createWorkerCompilerService.js +272 -0
- package/lib/src/compiler/internal/createWorkerCompilerService.js.map +1 -0
- package/lib/src/npm/collectExternalPackageNames.js +253 -9
- package/lib/src/npm/collectExternalPackageNames.js.map +1 -1
- package/lib/src/sandbox/createSandboxRequire.js +34 -9
- package/lib/src/sandbox/createSandboxRequire.js.map +1 -1
- package/package.json +3 -3
- package/src/compiler/createWorkerCompiler.ts +8 -236
- package/src/compiler/internal/createWorkerCompilerService.ts +358 -0
- package/src/npm/collectExternalPackageNames.ts +262 -9
- package/src/sandbox/createSandboxRequire.ts +32 -9
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { BUILT_IN_PLAYGROUND_PACKAGES } from "./BUILT_IN_PLAYGROUND_PACKAGES";
|
|
2
2
|
import { packageNameFromSpecifier } from "./packageNameFromSpecifier";
|
|
3
3
|
|
|
4
|
-
const MODULE_SPECIFIER_REGEXP =
|
|
5
|
-
/\b(?:import|export)\s+(?:type\s+)?(?:[^"'()]*?\s+from\s*)?["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|require\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
6
|
-
|
|
7
4
|
/**
|
|
8
5
|
* Scan `source` for `import` / `require` specifiers and return the unique
|
|
9
6
|
* sorted list of bare npm package names that are not in `ignoredPackages`.
|
|
@@ -21,14 +18,270 @@ export function collectExternalPackageNames(
|
|
|
21
18
|
return [...found].sort();
|
|
22
19
|
}
|
|
23
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Collect the string specifiers of every executable module-loading construct in
|
|
23
|
+
* `source`: `import`/`export ... from`, side-effect `import "x"`, dynamic
|
|
24
|
+
* `import("x")`, and `require("x")` calls.
|
|
25
|
+
*
|
|
26
|
+
* The scan tokenizes `source` first so import/export/require lookalikes that
|
|
27
|
+
* live inside comments, string or template contents, or regular-expression
|
|
28
|
+
* literals never become package requests — only real code-level string
|
|
29
|
+
* arguments are returned. Specifiers that cannot be resolved statically (a
|
|
30
|
+
* template-literal or computed argument) are intentionally skipped so inert
|
|
31
|
+
* text cannot drive a network install.
|
|
32
|
+
*/
|
|
24
33
|
function collectModuleSpecifiers(source: string): string[] {
|
|
34
|
+
const tokens = tokenize(source);
|
|
25
35
|
const out: string[] = [];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
const asString = (token: Token | undefined): string | null =>
|
|
37
|
+
token && token.kind === "string" ? token.value : null;
|
|
38
|
+
const isOpenParen = (token: Token | undefined): boolean =>
|
|
39
|
+
token !== undefined && token.kind === "punct" && token.value === "(";
|
|
40
|
+
const isMemberAccess = (token: Token | undefined): boolean =>
|
|
41
|
+
token !== undefined && token.kind === "punct" && token.value === ".";
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
44
|
+
const token = tokens[i];
|
|
45
|
+
if (!token || token.kind !== "word") continue;
|
|
46
|
+
|
|
47
|
+
if (token.value === "require") {
|
|
48
|
+
// `obj.require(...)` is an unrelated method call, not CommonJS require.
|
|
49
|
+
if (isMemberAccess(tokens[i - 1])) continue;
|
|
50
|
+
if (isOpenParen(tokens[i + 1])) {
|
|
51
|
+
const spec = asString(tokens[i + 2]);
|
|
52
|
+
if (spec !== null) out.push(spec);
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (token.value === "import" || token.value === "export") {
|
|
58
|
+
// `foo.import(...)` / `import.meta` are not module-loading imports.
|
|
59
|
+
if (token.value === "import" && isMemberAccess(tokens[i - 1])) continue;
|
|
60
|
+
// Dynamic `import("x")`.
|
|
61
|
+
if (token.value === "import" && isOpenParen(tokens[i + 1])) {
|
|
62
|
+
const spec = asString(tokens[i + 2]);
|
|
63
|
+
if (spec !== null) out.push(spec);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
// Side-effect `import "x"`.
|
|
67
|
+
if (token.value === "import") {
|
|
68
|
+
const bare = asString(tokens[i + 1]);
|
|
69
|
+
if (bare !== null) {
|
|
70
|
+
out.push(bare);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// `import ... from "x"` / `export ... from "x"`.
|
|
75
|
+
const spec = findFromSpecifier(tokens, i + 1);
|
|
76
|
+
if (spec !== null) out.push(spec);
|
|
77
|
+
}
|
|
32
78
|
}
|
|
33
79
|
return out;
|
|
34
80
|
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* From token index `start`, find the specifier of a `... from "x"` clause,
|
|
84
|
+
* bounded to the current statement. Stops at a `;` terminator or the start of
|
|
85
|
+
* another `import`/`export` so a local `export const x = ...` never borrows a
|
|
86
|
+
* later statement's `from`.
|
|
87
|
+
*/
|
|
88
|
+
function findFromSpecifier(tokens: Token[], start: number): string | null {
|
|
89
|
+
for (let i = start; i < tokens.length; i++) {
|
|
90
|
+
const token = tokens[i];
|
|
91
|
+
if (!token) break;
|
|
92
|
+
if (token.kind === "punct" && token.value === ";") return null;
|
|
93
|
+
if (
|
|
94
|
+
token.kind === "word" &&
|
|
95
|
+
(token.value === "import" || token.value === "export")
|
|
96
|
+
)
|
|
97
|
+
return null;
|
|
98
|
+
if (token.kind === "word" && token.value === "from") {
|
|
99
|
+
const next = tokens[i + 1];
|
|
100
|
+
return next && next.kind === "string" ? next.value : null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type Token =
|
|
107
|
+
// An identifier or keyword.
|
|
108
|
+
| { kind: "word"; value: string }
|
|
109
|
+
// A single- or double-quoted string literal, with escapes decoded to their
|
|
110
|
+
// literal characters so a specifier survives unchanged.
|
|
111
|
+
| { kind: "string"; value: string }
|
|
112
|
+
// A single punctuation character.
|
|
113
|
+
| { kind: "punct"; value: string }
|
|
114
|
+
// An opaque value token — number, template literal, or regular-expression
|
|
115
|
+
// literal — whose contents can never be a static specifier.
|
|
116
|
+
| { kind: "other" };
|
|
117
|
+
|
|
118
|
+
// Keywords after which a `/` begins a regular-expression literal rather than a
|
|
119
|
+
// division operator. After any other word (an identifier or value keyword such
|
|
120
|
+
// as `this`), `/` is division.
|
|
121
|
+
const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
122
|
+
"return",
|
|
123
|
+
"typeof",
|
|
124
|
+
"instanceof",
|
|
125
|
+
"in",
|
|
126
|
+
"of",
|
|
127
|
+
"new",
|
|
128
|
+
"delete",
|
|
129
|
+
"void",
|
|
130
|
+
"do",
|
|
131
|
+
"else",
|
|
132
|
+
"yield",
|
|
133
|
+
"await",
|
|
134
|
+
"case",
|
|
135
|
+
"throw",
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Lexically tokenize `source` into the coarse token stream the specifier
|
|
140
|
+
* collector needs. Comments are dropped; strings, templates, regex literals,
|
|
141
|
+
* and numbers become single tokens so their contents cannot leak into the
|
|
142
|
+
* grammar match.
|
|
143
|
+
*/
|
|
144
|
+
function tokenize(source: string): Token[] {
|
|
145
|
+
const tokens: Token[] = [];
|
|
146
|
+
const n = source.length;
|
|
147
|
+
const isIdStart = (c: string): boolean =>
|
|
148
|
+
(c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c === "_" || c === "$";
|
|
149
|
+
const isIdPart = (c: string): boolean =>
|
|
150
|
+
isIdStart(c) || (c >= "0" && c <= "9");
|
|
151
|
+
const isDigit = (c: string): boolean => c >= "0" && c <= "9";
|
|
152
|
+
|
|
153
|
+
// A `/` opens a regex only in operator/statement position — never right after
|
|
154
|
+
// a value (identifier, number, string, template, regex, `)` or `]`).
|
|
155
|
+
const regexAllowed = (): boolean => {
|
|
156
|
+
const prev = tokens[tokens.length - 1];
|
|
157
|
+
if (!prev) return true;
|
|
158
|
+
if (prev.kind === "string" || prev.kind === "other") return false;
|
|
159
|
+
if (prev.kind === "word") return REGEX_PRECEDING_KEYWORDS.has(prev.value);
|
|
160
|
+
return prev.value !== ")" && prev.value !== "]";
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
let i = 0;
|
|
164
|
+
while (i < n) {
|
|
165
|
+
const c = source[i]!;
|
|
166
|
+
// Whitespace.
|
|
167
|
+
if (
|
|
168
|
+
c === " " ||
|
|
169
|
+
c === "\t" ||
|
|
170
|
+
c === "\r" ||
|
|
171
|
+
c === "\n" ||
|
|
172
|
+
c === "\f" ||
|
|
173
|
+
c === "\v"
|
|
174
|
+
) {
|
|
175
|
+
i++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// Line comment.
|
|
179
|
+
if (c === "/" && source[i + 1] === "/") {
|
|
180
|
+
i += 2;
|
|
181
|
+
while (i < n && source[i] !== "\n") i++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Block comment.
|
|
185
|
+
if (c === "/" && source[i + 1] === "*") {
|
|
186
|
+
i += 2;
|
|
187
|
+
while (i < n && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
188
|
+
i += 2;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// Regular-expression literal.
|
|
192
|
+
if (c === "/" && regexAllowed()) {
|
|
193
|
+
i++;
|
|
194
|
+
let inClass = false;
|
|
195
|
+
while (i < n) {
|
|
196
|
+
const d = source[i];
|
|
197
|
+
if (d === "\\") {
|
|
198
|
+
i += 2;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (d === "\n") break;
|
|
202
|
+
if (d === "[") inClass = true;
|
|
203
|
+
else if (d === "]") inClass = false;
|
|
204
|
+
else if (d === "/" && !inClass) {
|
|
205
|
+
i++;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
i++;
|
|
209
|
+
}
|
|
210
|
+
while (i < n && isIdPart(source[i]!)) i++; // flags
|
|
211
|
+
tokens.push({ kind: "other" });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// String literal.
|
|
215
|
+
if (c === '"' || c === "'") {
|
|
216
|
+
i++;
|
|
217
|
+
let value = "";
|
|
218
|
+
while (i < n) {
|
|
219
|
+
const d = source[i]!;
|
|
220
|
+
if (d === "\\") {
|
|
221
|
+
value += source[i + 1] ?? "";
|
|
222
|
+
i += 2;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (d === c) {
|
|
226
|
+
i++;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (d === "\n") break; // unterminated single-line string
|
|
230
|
+
value += d;
|
|
231
|
+
i++;
|
|
232
|
+
}
|
|
233
|
+
tokens.push({ kind: "string", value });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// Template literal. Contents (including `${...}` expressions) are treated as
|
|
237
|
+
// opaque: a template specifier is not statically resolvable.
|
|
238
|
+
if (c === "`") {
|
|
239
|
+
i++;
|
|
240
|
+
let depth = 0;
|
|
241
|
+
while (i < n) {
|
|
242
|
+
const d = source[i];
|
|
243
|
+
if (d === "\\") {
|
|
244
|
+
i += 2;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (depth === 0 && d === "`") {
|
|
248
|
+
i++;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
if (d === "$" && source[i + 1] === "{") {
|
|
252
|
+
depth++;
|
|
253
|
+
i += 2;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (depth > 0 && d === "}") {
|
|
257
|
+
depth--;
|
|
258
|
+
i++;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
i++;
|
|
262
|
+
}
|
|
263
|
+
tokens.push({ kind: "other" });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
// Identifier / keyword.
|
|
267
|
+
if (isIdStart(c)) {
|
|
268
|
+
let j = i + 1;
|
|
269
|
+
while (j < n && isIdPart(source[j]!)) j++;
|
|
270
|
+
tokens.push({ kind: "word", value: source.slice(i, j) });
|
|
271
|
+
i = j;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
// Numeric literal.
|
|
275
|
+
if (isDigit(c) || (c === "." && isDigit(source[i + 1] ?? ""))) {
|
|
276
|
+
let j = i + 1;
|
|
277
|
+
while (j < n && /[0-9a-fA-FxXoObBeE._]/.test(source[j]!)) j++;
|
|
278
|
+
tokens.push({ kind: "other" });
|
|
279
|
+
i = j;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Single punctuation character.
|
|
283
|
+
tokens.push({ kind: "punct", value: c });
|
|
284
|
+
i++;
|
|
285
|
+
}
|
|
286
|
+
return tokens;
|
|
287
|
+
}
|
|
@@ -64,15 +64,13 @@ export function createSandboxRequire(
|
|
|
64
64
|
return null;
|
|
65
65
|
}
|
|
66
66
|
if (subpath === null) {
|
|
67
|
-
// bare "name": honor exports
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return has(resolved) ? resolved : null;
|
|
75
|
-
}
|
|
67
|
+
// bare "name": honor the root `exports` entry (string, subpath table
|
|
68
|
+
// "." key, or bare condition map) → CJS target, else main, else index.
|
|
69
|
+
const root = rootExportTarget(pj.exports);
|
|
70
|
+
const r = pickConditionalExport(root);
|
|
71
|
+
if (r) {
|
|
72
|
+
const resolved = `${pkg}/${stripDotSlash(r)}`;
|
|
73
|
+
return has(resolved) ? resolved : null;
|
|
76
74
|
}
|
|
77
75
|
if (typeof pj.main === "string") {
|
|
78
76
|
return tryPaths(
|
|
@@ -220,6 +218,31 @@ export function createSandboxRequire(
|
|
|
220
218
|
};
|
|
221
219
|
}
|
|
222
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Reduce a `package.json` `exports` field to the value describing its ROOT
|
|
223
|
+
* (".") entry, ready for {@link pickConditionalExport}. Node accepts three valid
|
|
224
|
+
* root shapes and they must all resolve consistently:
|
|
225
|
+
*
|
|
226
|
+
* - A bare string target — `"exports": "./index.cjs"`;
|
|
227
|
+
* - A subpath table keyed by "." — `{ ".": <target>, "./sub": ... }`;
|
|
228
|
+
* - A bare condition map whose keys are all conditions, not subpaths — `{
|
|
229
|
+
* "require": "./index.cjs", "default": "./index.cjs" }`.
|
|
230
|
+
*
|
|
231
|
+
* Node forbids mixing subpath keys with condition keys, so the presence of any
|
|
232
|
+
* "."-prefixed key decides the interpretation: a subpath table exposes its root
|
|
233
|
+
* as `exports["."]`, while a condition map is itself the root target. Returns
|
|
234
|
+
* null when `exports` is absent, empty, or describes no root entry.
|
|
235
|
+
*/
|
|
236
|
+
function rootExportTarget(exports: unknown): unknown {
|
|
237
|
+
if (typeof exports === "string") return exports;
|
|
238
|
+
if (!exports || typeof exports !== "object") return null;
|
|
239
|
+
const obj = exports as Record<string, unknown>;
|
|
240
|
+
const keys = Object.keys(obj);
|
|
241
|
+
if (keys.length === 0) return null;
|
|
242
|
+
const hasSubpathKey = keys.some((k) => k === "." || k.startsWith("./"));
|
|
243
|
+
return hasSubpathKey ? obj["."] : obj;
|
|
244
|
+
}
|
|
245
|
+
|
|
223
246
|
function pickConditionalExport(value: unknown): string | null {
|
|
224
247
|
if (typeof value === "string") return value;
|
|
225
248
|
if (value && typeof value === "object") {
|