@ttsc/playground 0.19.3 → 0.20.1
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 +6 -0
- package/lib/src/npm/collectExternalPackageNames.js +436 -73
- package/lib/src/npm/collectExternalPackageNames.js.map +1 -1
- package/lib/src/npm/installPlaygroundDependencies.d.ts +5 -3
- package/lib/src/npm/installPlaygroundDependencies.js +230 -27
- package/lib/src/npm/installPlaygroundDependencies.js.map +1 -1
- package/lib/src/npm/internal/npmRegistry.d.ts +13 -2
- package/lib/src/npm/internal/npmRegistry.js +73 -50
- package/lib/src/npm/internal/npmRegistry.js.map +1 -1
- package/lib/src/npm/packageNameFromSpecifier.js +17 -4
- package/lib/src/npm/packageNameFromSpecifier.js.map +1 -1
- package/lib/src/react/PlaygroundShell.js +68 -61
- package/lib/src/react/PlaygroundShell.js.map +1 -1
- package/lib/src/react/createCompilerClient.d.ts +3 -3
- package/lib/src/react/createCompilerClient.js +32 -54
- package/lib/src/react/createCompilerClient.js.map +1 -1
- package/lib/src/sandbox/createSandboxRequire.js +117 -54
- package/lib/src/sandbox/createSandboxRequire.js.map +1 -1
- package/lib/src/structures/IPlaygroundDependencyInstallOptions.d.ts +13 -1
- package/lib/src/structures/IPlaygroundDependencyInstallResult.d.ts +4 -0
- package/lib/src/structures/IPlaygroundDependencyPackage.d.ts +2 -0
- package/lib/src/structures/IPlaygroundDependencyRequest.d.ts +9 -0
- package/lib/src/structures/IPlaygroundDependencyRequest.js +3 -0
- package/lib/src/structures/IPlaygroundDependencyRequest.js.map +1 -0
- package/lib/src/structures/IPlaygroundInstalledDependency.d.ts +12 -0
- package/lib/src/structures/IPlaygroundInstalledDependency.js +3 -0
- package/lib/src/structures/IPlaygroundInstalledDependency.js.map +1 -0
- package/lib/src/structures/index.d.ts +2 -0
- package/lib/src/structures/index.js +2 -0
- package/lib/src/structures/index.js.map +1 -1
- package/package.json +6 -4
- package/src/npm/collectExternalPackageNames.ts +503 -68
- package/src/npm/installPlaygroundDependencies.ts +288 -29
- package/src/npm/internal/npmRegistry.ts +99 -38
- package/src/npm/packageNameFromSpecifier.ts +16 -3
- package/src/react/PlaygroundShell.tsx +86 -72
- package/src/react/createCompilerClient.ts +37 -51
- package/src/sandbox/createSandboxRequire.ts +140 -49
- package/src/structures/IPlaygroundDependencyInstallOptions.ts +13 -1
- package/src/structures/IPlaygroundDependencyInstallResult.ts +4 -0
- package/src/structures/IPlaygroundDependencyPackage.ts +2 -0
- package/src/structures/IPlaygroundDependencyRequest.ts +9 -0
- package/src/structures/IPlaygroundInstalledDependency.ts +13 -0
- package/src/structures/index.ts +2 -0
|
@@ -21,7 +21,7 @@ export function collectExternalPackageNames(
|
|
|
21
21
|
/**
|
|
22
22
|
* Collect the string specifiers of every executable module-loading construct in
|
|
23
23
|
* `source`: `import`/`export ... from`, side-effect `import "x"`, dynamic
|
|
24
|
-
* `import("x")`, and `require("x")` calls.
|
|
24
|
+
* `import("x")`, `require("x")`, and `require?.("x")` calls.
|
|
25
25
|
*
|
|
26
26
|
* The scan tokenizes `source` first so import/export/require lookalikes that
|
|
27
27
|
* live inside comments, string or template contents, or regular-expression
|
|
@@ -38,7 +38,11 @@ function collectModuleSpecifiers(source: string): string[] {
|
|
|
38
38
|
const isOpenParen = (token: Token | undefined): boolean =>
|
|
39
39
|
token !== undefined && token.kind === "punct" && token.value === "(";
|
|
40
40
|
const isMemberAccess = (token: Token | undefined): boolean =>
|
|
41
|
-
token !== undefined &&
|
|
41
|
+
token !== undefined &&
|
|
42
|
+
token.kind === "punct" &&
|
|
43
|
+
(token.value === "." || token.value === "?.");
|
|
44
|
+
const isOptionalChain = (token: Token | undefined): boolean =>
|
|
45
|
+
token !== undefined && token.kind === "punct" && token.value === "?.";
|
|
42
46
|
|
|
43
47
|
for (let i = 0; i < tokens.length; i++) {
|
|
44
48
|
const token = tokens[i];
|
|
@@ -47,8 +51,10 @@ function collectModuleSpecifiers(source: string): string[] {
|
|
|
47
51
|
if (token.value === "require") {
|
|
48
52
|
// `obj.require(...)` is an unrelated method call, not CommonJS require.
|
|
49
53
|
if (isMemberAccess(tokens[i - 1])) continue;
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
const optional =
|
|
55
|
+
isOptionalChain(tokens[i + 1]) && isOpenParen(tokens[i + 2]);
|
|
56
|
+
if (isOpenParen(tokens[i + 1]) || optional) {
|
|
57
|
+
const spec = asString(tokens[i + (optional ? 3 : 2)]);
|
|
52
58
|
if (spec !== null) out.push(spec);
|
|
53
59
|
}
|
|
54
60
|
continue;
|
|
@@ -109,8 +115,16 @@ type Token =
|
|
|
109
115
|
// A single- or double-quoted string literal, with escapes decoded to their
|
|
110
116
|
// literal characters so a specifier survives unchanged.
|
|
111
117
|
| { kind: "string"; value: string }
|
|
112
|
-
// A
|
|
113
|
-
|
|
118
|
+
// A punctuation token; compound forms are retained where lexical state or
|
|
119
|
+
// module-call recognition depends on them.
|
|
120
|
+
| {
|
|
121
|
+
kind: "punct";
|
|
122
|
+
value: string;
|
|
123
|
+
/** Whether a slash after this closing delimiter begins a regex literal. */
|
|
124
|
+
regexAllowedAfter?: boolean;
|
|
125
|
+
/** Whether this closes a function parameter list for an expression. */
|
|
126
|
+
functionBodyIsExpression?: boolean;
|
|
127
|
+
}
|
|
114
128
|
// An opaque value token — number, template literal, or regular-expression
|
|
115
129
|
// literal — whose contents can never be a static specifier.
|
|
116
130
|
| { kind: "other" };
|
|
@@ -129,12 +143,189 @@ const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
|
129
143
|
"void",
|
|
130
144
|
"do",
|
|
131
145
|
"else",
|
|
146
|
+
"extends",
|
|
132
147
|
"yield",
|
|
133
148
|
"await",
|
|
134
149
|
"case",
|
|
135
150
|
"throw",
|
|
136
151
|
]);
|
|
137
152
|
|
|
153
|
+
const CONTROL_HEADER_KEYWORDS = new Set([
|
|
154
|
+
"if",
|
|
155
|
+
"while",
|
|
156
|
+
"for",
|
|
157
|
+
"with",
|
|
158
|
+
"switch",
|
|
159
|
+
"catch",
|
|
160
|
+
]);
|
|
161
|
+
const BLOCK_PRECEDING_KEYWORDS = new Set(["else", "try", "finally", "do"]);
|
|
162
|
+
|
|
163
|
+
interface IParenContext {
|
|
164
|
+
kind: "control" | "function" | "normal";
|
|
165
|
+
functionIsExpression?: boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Retain just enough delimiter context to distinguish a regex statement after a
|
|
170
|
+
* control header or block from division after an expression value. The
|
|
171
|
+
* collector is not a parser: this state only decides whether a following slash
|
|
172
|
+
* is opaque regex text or executable code worth scanning for module calls.
|
|
173
|
+
*/
|
|
174
|
+
class LexicalContext {
|
|
175
|
+
public readonly tokens: Token[] = [];
|
|
176
|
+
|
|
177
|
+
private readonly parens: IParenContext[] = [];
|
|
178
|
+
private readonly braces: boolean[] = [];
|
|
179
|
+
private pendingFunctionExpression: boolean | undefined;
|
|
180
|
+
private readonly pendingClassExpressions: boolean[] = [];
|
|
181
|
+
|
|
182
|
+
public isRegexAllowed(): boolean {
|
|
183
|
+
return isRegexAllowedAfter(this.tokens[this.tokens.length - 1]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
public pushWord(value: string): void {
|
|
187
|
+
if (
|
|
188
|
+
value === "function" &&
|
|
189
|
+
!isMemberAccess(this.tokens[this.tokens.length - 1])
|
|
190
|
+
)
|
|
191
|
+
this.pendingFunctionExpression = isExpressionPosition(this.tokens);
|
|
192
|
+
else if (
|
|
193
|
+
value === "class" &&
|
|
194
|
+
!isMemberAccess(this.tokens[this.tokens.length - 1])
|
|
195
|
+
)
|
|
196
|
+
this.pendingClassExpressions.push(isExpressionPosition(this.tokens));
|
|
197
|
+
this.tokens.push({ kind: "word", value });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
public pushOther(): void {
|
|
201
|
+
this.tokens.push({ kind: "other" });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public pushPunct(value: string): void {
|
|
205
|
+
if (value === ":") {
|
|
206
|
+
const previous = this.tokens[this.tokens.length - 1];
|
|
207
|
+
if (
|
|
208
|
+
previous?.kind === "word" &&
|
|
209
|
+
!isMemberAccess(this.tokens[this.tokens.length - 2])
|
|
210
|
+
) {
|
|
211
|
+
if (previous.value === "function")
|
|
212
|
+
this.pendingFunctionExpression = undefined;
|
|
213
|
+
else if (previous.value === "class") this.pendingClassExpressions.pop();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (value === "(") {
|
|
217
|
+
const functionIsExpression = this.pendingFunctionExpression;
|
|
218
|
+
this.pendingFunctionExpression = undefined;
|
|
219
|
+
this.parens.push(
|
|
220
|
+
functionIsExpression === undefined
|
|
221
|
+
? {
|
|
222
|
+
kind: isControlHeader(this.tokens) ? "control" : "normal",
|
|
223
|
+
}
|
|
224
|
+
: { kind: "function", functionIsExpression },
|
|
225
|
+
);
|
|
226
|
+
this.tokens.push({ kind: "punct", value });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (value === ")") {
|
|
230
|
+
const context = this.parens.pop();
|
|
231
|
+
this.tokens.push({
|
|
232
|
+
kind: "punct",
|
|
233
|
+
value,
|
|
234
|
+
regexAllowedAfter: context?.kind === "control",
|
|
235
|
+
functionBodyIsExpression:
|
|
236
|
+
context?.kind === "function"
|
|
237
|
+
? context.functionIsExpression
|
|
238
|
+
: undefined,
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (value === "{") {
|
|
243
|
+
this.braces.push(this.braceAllowsRegexAfter());
|
|
244
|
+
this.tokens.push({ kind: "punct", value });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (value === "}") {
|
|
248
|
+
this.tokens.push({
|
|
249
|
+
kind: "punct",
|
|
250
|
+
value,
|
|
251
|
+
regexAllowedAfter: this.braces.pop() ?? false,
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.tokens.push({ kind: "punct", value });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private braceAllowsRegexAfter(): boolean {
|
|
259
|
+
const previous = this.tokens[this.tokens.length - 1];
|
|
260
|
+
if (
|
|
261
|
+
previous?.kind === "punct" &&
|
|
262
|
+
previous.value === ")" &&
|
|
263
|
+
previous.functionBodyIsExpression !== undefined
|
|
264
|
+
)
|
|
265
|
+
return !previous.functionBodyIsExpression;
|
|
266
|
+
if (this.classBodyPrecedes(previous))
|
|
267
|
+
return !this.pendingClassExpressions.pop()!;
|
|
268
|
+
if (!previous) return true;
|
|
269
|
+
if (previous.kind === "word")
|
|
270
|
+
return BLOCK_PRECEDING_KEYWORDS.has(previous.value);
|
|
271
|
+
if (previous.kind !== "punct") return false;
|
|
272
|
+
if (previous.value === ")") return previous.regexAllowedAfter === true;
|
|
273
|
+
return (
|
|
274
|
+
previous.value === ";" || previous.value === "{" || previous.value === "}"
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private classBodyPrecedes(previous: Token | undefined): boolean {
|
|
279
|
+
if (this.pendingClassExpressions.length === 0 || this.parens.length !== 0)
|
|
280
|
+
return false;
|
|
281
|
+
if (!previous) return false;
|
|
282
|
+
if (previous.kind === "word" || previous.kind === "other") return true;
|
|
283
|
+
return (
|
|
284
|
+
previous.kind === "punct" &&
|
|
285
|
+
(previous.value === ")" ||
|
|
286
|
+
previous.value === "]" ||
|
|
287
|
+
previous.value === "}")
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function isControlHeader(tokens: readonly Token[]): boolean {
|
|
293
|
+
const previous = tokens[tokens.length - 1];
|
|
294
|
+
const beforePrevious = tokens[tokens.length - 2];
|
|
295
|
+
if (
|
|
296
|
+
previous?.kind === "word" &&
|
|
297
|
+
CONTROL_HEADER_KEYWORDS.has(previous.value) &&
|
|
298
|
+
!isMemberAccess(beforePrevious)
|
|
299
|
+
)
|
|
300
|
+
return true;
|
|
301
|
+
return (
|
|
302
|
+
previous?.kind === "word" &&
|
|
303
|
+
previous.value === "await" &&
|
|
304
|
+
beforePrevious?.kind === "word" &&
|
|
305
|
+
beforePrevious.value === "for"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isMemberAccess(token: Token | undefined): boolean {
|
|
310
|
+
return (
|
|
311
|
+
token?.kind === "punct" && (token.value === "." || token.value === "?.")
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isExpressionPosition(tokens: readonly Token[]): boolean {
|
|
316
|
+
let index = tokens.length - 1;
|
|
317
|
+
// `async function` inherits the context before `async`: it is a declaration
|
|
318
|
+
// at a statement boundary and an expression after an assignment or return.
|
|
319
|
+
const latest = tokens[index];
|
|
320
|
+
if (latest?.kind === "word" && latest.value === "async") index--;
|
|
321
|
+
const previous = tokens[index];
|
|
322
|
+
if (!previous) return false;
|
|
323
|
+
if (previous.kind === "word")
|
|
324
|
+
return REGEX_PRECEDING_KEYWORDS.has(previous.value);
|
|
325
|
+
if (previous.kind !== "punct") return false;
|
|
326
|
+
return ![")", "]", "}", ";", "{", "++", "--"].includes(previous.value);
|
|
327
|
+
}
|
|
328
|
+
|
|
138
329
|
/**
|
|
139
330
|
* Lexically tokenize `source` into the coarse token stream the specifier
|
|
140
331
|
* collector needs. Comments are dropped; strings, templates, regex literals,
|
|
@@ -142,7 +333,8 @@ const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
|
142
333
|
* grammar match.
|
|
143
334
|
*/
|
|
144
335
|
function tokenize(source: string): Token[] {
|
|
145
|
-
const
|
|
336
|
+
const context = new LexicalContext();
|
|
337
|
+
const tokens = context.tokens;
|
|
146
338
|
const n = source.length;
|
|
147
339
|
const isIdStart = (c: string): boolean =>
|
|
148
340
|
(c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c === "_" || c === "$";
|
|
@@ -152,14 +344,6 @@ function tokenize(source: string): Token[] {
|
|
|
152
344
|
|
|
153
345
|
// A `/` opens a regex only in operator/statement position — never right after
|
|
154
346
|
// 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
347
|
let i = 0;
|
|
164
348
|
while (i < n) {
|
|
165
349
|
const c = source[i]!;
|
|
@@ -189,85 +373,52 @@ function tokenize(source: string): Token[] {
|
|
|
189
373
|
continue;
|
|
190
374
|
}
|
|
191
375
|
// Regular-expression literal.
|
|
192
|
-
if (c === "/" &&
|
|
193
|
-
i
|
|
194
|
-
|
|
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" });
|
|
376
|
+
if (c === "/" && context.isRegexAllowed()) {
|
|
377
|
+
i = skipRegularExpression(source, i, isIdPart);
|
|
378
|
+
context.pushOther();
|
|
212
379
|
continue;
|
|
213
380
|
}
|
|
214
381
|
// String literal.
|
|
215
382
|
if (c === '"' || c === "'") {
|
|
216
|
-
i
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 });
|
|
383
|
+
const quoted = readQuotedString(source, i, c);
|
|
384
|
+
i = quoted.end;
|
|
385
|
+
if (quoted.value === null) context.pushOther();
|
|
386
|
+
else tokens.push({ kind: "string", value: quoted.value });
|
|
234
387
|
continue;
|
|
235
388
|
}
|
|
236
|
-
// Template
|
|
237
|
-
//
|
|
389
|
+
// Template quasis are opaque, but `${...}` substitutions are executable
|
|
390
|
+
// JavaScript and must receive the same lexical treatment as top-level code.
|
|
238
391
|
if (c === "`") {
|
|
239
392
|
i++;
|
|
240
|
-
let depth = 0;
|
|
241
393
|
while (i < n) {
|
|
242
394
|
const d = source[i];
|
|
243
395
|
if (d === "\\") {
|
|
244
396
|
i += 2;
|
|
245
397
|
continue;
|
|
246
398
|
}
|
|
247
|
-
if (
|
|
399
|
+
if (d === "`") {
|
|
248
400
|
i++;
|
|
249
401
|
break;
|
|
250
402
|
}
|
|
251
403
|
if (d === "$" && source[i + 1] === "{") {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
i++;
|
|
404
|
+
const start = i + 2;
|
|
405
|
+
const end = findTemplateSubstitutionEnd(source, start);
|
|
406
|
+
context.pushOther();
|
|
407
|
+
tokens.push(...tokenize(source.slice(start, end)));
|
|
408
|
+
context.pushOther();
|
|
409
|
+
i = end < n ? end + 1 : end;
|
|
259
410
|
continue;
|
|
260
411
|
}
|
|
261
412
|
i++;
|
|
262
413
|
}
|
|
263
|
-
|
|
414
|
+
context.pushOther();
|
|
264
415
|
continue;
|
|
265
416
|
}
|
|
266
417
|
// Identifier / keyword.
|
|
267
418
|
if (isIdStart(c)) {
|
|
268
419
|
let j = i + 1;
|
|
269
420
|
while (j < n && isIdPart(source[j]!)) j++;
|
|
270
|
-
|
|
421
|
+
context.pushWord(source.slice(i, j));
|
|
271
422
|
i = j;
|
|
272
423
|
continue;
|
|
273
424
|
}
|
|
@@ -275,13 +426,297 @@ function tokenize(source: string): Token[] {
|
|
|
275
426
|
if (isDigit(c) || (c === "." && isDigit(source[i + 1] ?? ""))) {
|
|
276
427
|
let j = i + 1;
|
|
277
428
|
while (j < n && /[0-9a-fA-FxXoObBeE._]/.test(source[j]!)) j++;
|
|
278
|
-
|
|
429
|
+
context.pushOther();
|
|
279
430
|
i = j;
|
|
280
431
|
continue;
|
|
281
432
|
}
|
|
433
|
+
// Compound punctuation whose identity affects the following slash or
|
|
434
|
+
// direct optional-call recognition.
|
|
435
|
+
if ((c === "+" || c === "-") && source[i + 1] === c) {
|
|
436
|
+
context.pushPunct(c + c);
|
|
437
|
+
i += 2;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (c === "?" && source[i + 1] === "." && !isDigit(source[i + 2] ?? "")) {
|
|
441
|
+
context.pushPunct("?.");
|
|
442
|
+
i += 2;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
282
445
|
// Single punctuation character.
|
|
283
|
-
|
|
446
|
+
context.pushPunct(c);
|
|
284
447
|
i++;
|
|
285
448
|
}
|
|
286
449
|
return tokens;
|
|
287
450
|
}
|
|
451
|
+
|
|
452
|
+
function findTemplateSubstitutionEnd(source: string, start: number): number {
|
|
453
|
+
let depth = 1;
|
|
454
|
+
const context = new LexicalContext();
|
|
455
|
+
const isIdentifierStart = (character: string): boolean =>
|
|
456
|
+
/[A-Za-z_$]/.test(character);
|
|
457
|
+
const isIdentifierPart = (character: string): boolean =>
|
|
458
|
+
/[A-Za-z0-9_$]/.test(character);
|
|
459
|
+
for (let i = start; i < source.length; i++) {
|
|
460
|
+
const c = source[i]!;
|
|
461
|
+
if (/\s/.test(c)) continue;
|
|
462
|
+
if (c === "\\") {
|
|
463
|
+
i++;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (c === "'" || c === '"') {
|
|
467
|
+
i = skipQuoted(source, i, c);
|
|
468
|
+
context.tokens.push({ kind: "string", value: "" });
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (c === "`") {
|
|
472
|
+
i = skipTemplate(source, i);
|
|
473
|
+
context.pushOther();
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (c === "/" && source[i + 1] === "/") {
|
|
477
|
+
i += 2;
|
|
478
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (c === "/" && source[i + 1] === "*") {
|
|
482
|
+
i += 2;
|
|
483
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/"))
|
|
484
|
+
i++;
|
|
485
|
+
i++;
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (c === "/" && context.isRegexAllowed()) {
|
|
489
|
+
i = skipRegularExpression(source, i, (character) =>
|
|
490
|
+
/[A-Za-z0-9_$]/.test(character),
|
|
491
|
+
);
|
|
492
|
+
i--;
|
|
493
|
+
context.pushOther();
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (isIdentifierStart(c)) {
|
|
497
|
+
let end = i + 1;
|
|
498
|
+
while (end < source.length && isIdentifierPart(source[end]!)) end++;
|
|
499
|
+
context.pushWord(source.slice(i, end));
|
|
500
|
+
i = end - 1;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (c >= "0" && c <= "9") {
|
|
504
|
+
let end = i + 1;
|
|
505
|
+
while (end < source.length && /[0-9a-fA-FxXoObBeE._]/.test(source[end]!))
|
|
506
|
+
end++;
|
|
507
|
+
context.pushOther();
|
|
508
|
+
i = end - 1;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if ((c === "+" || c === "-") && source[i + 1] === c) {
|
|
512
|
+
context.pushPunct(c + c);
|
|
513
|
+
i++;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (
|
|
517
|
+
c === "?" &&
|
|
518
|
+
source[i + 1] === "." &&
|
|
519
|
+
!/[0-9]/.test(source[i + 2] ?? "")
|
|
520
|
+
) {
|
|
521
|
+
context.pushPunct("?.");
|
|
522
|
+
i++;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (c === "{") depth++;
|
|
526
|
+
else if (c === "}" && --depth === 0) return i;
|
|
527
|
+
context.pushPunct(c);
|
|
528
|
+
}
|
|
529
|
+
return source.length;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function isRegexAllowedAfter(previous: Token | undefined): boolean {
|
|
533
|
+
if (!previous) return true;
|
|
534
|
+
if (previous.kind === "string" || previous.kind === "other") return false;
|
|
535
|
+
if (previous.kind === "word")
|
|
536
|
+
return REGEX_PRECEDING_KEYWORDS.has(previous.value);
|
|
537
|
+
if (previous.value === ")" || previous.value === "}")
|
|
538
|
+
return previous.regexAllowedAfter === true;
|
|
539
|
+
return (
|
|
540
|
+
previous.value !== "]" && previous.value !== "++" && previous.value !== "--"
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
interface IQuotedStringResult {
|
|
545
|
+
end: number;
|
|
546
|
+
value: string | null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Read one quoted literal and compute its JavaScript StringValue. */
|
|
550
|
+
function readQuotedString(
|
|
551
|
+
source: string,
|
|
552
|
+
start: number,
|
|
553
|
+
quote: string,
|
|
554
|
+
): IQuotedStringResult {
|
|
555
|
+
let value = "";
|
|
556
|
+
for (let index = start + 1; index < source.length; ) {
|
|
557
|
+
const character = source[index]!;
|
|
558
|
+
if (character === quote) {
|
|
559
|
+
return { end: index + 1, value };
|
|
560
|
+
}
|
|
561
|
+
if (isLineTerminator(character)) {
|
|
562
|
+
return { end: index, value: null };
|
|
563
|
+
}
|
|
564
|
+
if (character !== "\\") {
|
|
565
|
+
value += character;
|
|
566
|
+
index++;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const escaped = source[index + 1];
|
|
571
|
+
if (escaped === undefined) {
|
|
572
|
+
return { end: source.length, value: null };
|
|
573
|
+
}
|
|
574
|
+
if (isLineTerminator(escaped)) {
|
|
575
|
+
index += escaped === "\r" && source[index + 2] === "\n" ? 3 : 2;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const simple = SIMPLE_STRING_ESCAPES[escaped];
|
|
580
|
+
if (simple !== undefined) {
|
|
581
|
+
value += simple;
|
|
582
|
+
index += 2;
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (escaped === "0") {
|
|
586
|
+
if (/[0-9]/.test(source[index + 2] ?? "")) {
|
|
587
|
+
return invalidQuotedString(source, start, quote);
|
|
588
|
+
}
|
|
589
|
+
value += "\0";
|
|
590
|
+
index += 2;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (escaped >= "1" && escaped <= "9") {
|
|
594
|
+
return invalidQuotedString(source, start, quote);
|
|
595
|
+
}
|
|
596
|
+
if (escaped === "x") {
|
|
597
|
+
const digits = source.slice(index + 2, index + 4);
|
|
598
|
+
if (digits.length !== 2 || !/^[0-9a-fA-F]{2}$/.test(digits)) {
|
|
599
|
+
return invalidQuotedString(source, start, quote);
|
|
600
|
+
}
|
|
601
|
+
value += String.fromCharCode(Number.parseInt(digits, 16));
|
|
602
|
+
index += 4;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
if (escaped === "u") {
|
|
606
|
+
if (source[index + 2] === "{") {
|
|
607
|
+
const close = source.indexOf("}", index + 3);
|
|
608
|
+
const digits = close < 0 ? "" : source.slice(index + 3, close);
|
|
609
|
+
const significantDigits = digits.replace(/^0+/, "") || "0";
|
|
610
|
+
const codePoint = Number.parseInt(digits, 16);
|
|
611
|
+
if (
|
|
612
|
+
close < 0 ||
|
|
613
|
+
!/^[0-9a-fA-F]+$/.test(digits) ||
|
|
614
|
+
significantDigits.length > 6 ||
|
|
615
|
+
codePoint > 0x10ffff
|
|
616
|
+
) {
|
|
617
|
+
return invalidQuotedString(source, start, quote);
|
|
618
|
+
}
|
|
619
|
+
value += String.fromCodePoint(codePoint);
|
|
620
|
+
index = close + 1;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const digits = source.slice(index + 2, index + 6);
|
|
624
|
+
if (digits.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(digits)) {
|
|
625
|
+
return invalidQuotedString(source, start, quote);
|
|
626
|
+
}
|
|
627
|
+
value += String.fromCharCode(Number.parseInt(digits, 16));
|
|
628
|
+
index += 6;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// IdentityEscape / NonEscapeCharacter: the slash is discarded and the
|
|
633
|
+
// escaped source character contributes directly to the StringValue.
|
|
634
|
+
value += escaped;
|
|
635
|
+
index += 2;
|
|
636
|
+
}
|
|
637
|
+
return { end: source.length, value: null };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const SIMPLE_STRING_ESCAPES: Readonly<Record<string, string>> = {
|
|
641
|
+
"'": "'",
|
|
642
|
+
'"': '"',
|
|
643
|
+
"\\": "\\",
|
|
644
|
+
b: "\b",
|
|
645
|
+
f: "\f",
|
|
646
|
+
n: "\n",
|
|
647
|
+
r: "\r",
|
|
648
|
+
t: "\t",
|
|
649
|
+
v: "\v",
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
function invalidQuotedString(
|
|
653
|
+
source: string,
|
|
654
|
+
start: number,
|
|
655
|
+
quote: string,
|
|
656
|
+
): IQuotedStringResult {
|
|
657
|
+
const end = skipQuoted(source, start, quote);
|
|
658
|
+
return {
|
|
659
|
+
end: end < source.length && source[end] === quote ? end + 1 : end,
|
|
660
|
+
value: null,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function isLineTerminator(character: string): boolean {
|
|
665
|
+
return (
|
|
666
|
+
character === "\n" ||
|
|
667
|
+
character === "\r" ||
|
|
668
|
+
character === "\u2028" ||
|
|
669
|
+
character === "\u2029"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function skipRegularExpression(
|
|
674
|
+
source: string,
|
|
675
|
+
start: number,
|
|
676
|
+
isIdentifierPart: (character: string) => boolean,
|
|
677
|
+
): number {
|
|
678
|
+
let index = start + 1;
|
|
679
|
+
let inClass = false;
|
|
680
|
+
while (index < source.length) {
|
|
681
|
+
const character = source[index];
|
|
682
|
+
if (character === "\\") {
|
|
683
|
+
index += 2;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (character === "\n") break;
|
|
687
|
+
if (character === "[") inClass = true;
|
|
688
|
+
else if (character === "]") inClass = false;
|
|
689
|
+
else if (character === "/" && !inClass) {
|
|
690
|
+
index++;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
index++;
|
|
694
|
+
}
|
|
695
|
+
while (index < source.length && isIdentifierPart(source[index]!)) index++;
|
|
696
|
+
return index;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function skipQuoted(source: string, start: number, quote: string): number {
|
|
700
|
+
for (let i = start + 1; i < source.length; i++) {
|
|
701
|
+
if (source[i] === "\\") {
|
|
702
|
+
i++;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (source[i] === quote || isLineTerminator(source[i]!)) return i;
|
|
706
|
+
}
|
|
707
|
+
return source.length;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function skipTemplate(source: string, start: number): number {
|
|
711
|
+
for (let i = start + 1; i < source.length; i++) {
|
|
712
|
+
if (source[i] === "\\") {
|
|
713
|
+
i++;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if (source[i] === "`") return i;
|
|
717
|
+
if (source[i] === "$" && source[i + 1] === "{") {
|
|
718
|
+
i = findTemplateSubstitutionEnd(source, i + 2);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return source.length;
|
|
722
|
+
}
|