@ttsc/playground 0.19.2 → 0.20.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.
Files changed (44) hide show
  1. package/README.md +6 -0
  2. package/lib/src/npm/collectExternalPackageNames.js +436 -73
  3. package/lib/src/npm/collectExternalPackageNames.js.map +1 -1
  4. package/lib/src/npm/installPlaygroundDependencies.d.ts +5 -3
  5. package/lib/src/npm/installPlaygroundDependencies.js +230 -27
  6. package/lib/src/npm/installPlaygroundDependencies.js.map +1 -1
  7. package/lib/src/npm/internal/npmRegistry.d.ts +13 -2
  8. package/lib/src/npm/internal/npmRegistry.js +73 -50
  9. package/lib/src/npm/internal/npmRegistry.js.map +1 -1
  10. package/lib/src/npm/packageNameFromSpecifier.js +17 -4
  11. package/lib/src/npm/packageNameFromSpecifier.js.map +1 -1
  12. package/lib/src/react/PlaygroundShell.js +68 -61
  13. package/lib/src/react/PlaygroundShell.js.map +1 -1
  14. package/lib/src/react/createCompilerClient.d.ts +3 -3
  15. package/lib/src/react/createCompilerClient.js +32 -54
  16. package/lib/src/react/createCompilerClient.js.map +1 -1
  17. package/lib/src/sandbox/createSandboxRequire.js +117 -54
  18. package/lib/src/sandbox/createSandboxRequire.js.map +1 -1
  19. package/lib/src/structures/IPlaygroundDependencyInstallOptions.d.ts +13 -1
  20. package/lib/src/structures/IPlaygroundDependencyInstallResult.d.ts +4 -0
  21. package/lib/src/structures/IPlaygroundDependencyPackage.d.ts +2 -0
  22. package/lib/src/structures/IPlaygroundDependencyRequest.d.ts +9 -0
  23. package/lib/src/structures/IPlaygroundDependencyRequest.js +3 -0
  24. package/lib/src/structures/IPlaygroundDependencyRequest.js.map +1 -0
  25. package/lib/src/structures/IPlaygroundInstalledDependency.d.ts +12 -0
  26. package/lib/src/structures/IPlaygroundInstalledDependency.js +3 -0
  27. package/lib/src/structures/IPlaygroundInstalledDependency.js.map +1 -0
  28. package/lib/src/structures/index.d.ts +2 -0
  29. package/lib/src/structures/index.js +2 -0
  30. package/lib/src/structures/index.js.map +1 -1
  31. package/package.json +6 -4
  32. package/src/npm/collectExternalPackageNames.ts +503 -68
  33. package/src/npm/installPlaygroundDependencies.ts +288 -29
  34. package/src/npm/internal/npmRegistry.ts +99 -38
  35. package/src/npm/packageNameFromSpecifier.ts +16 -3
  36. package/src/react/PlaygroundShell.tsx +86 -72
  37. package/src/react/createCompilerClient.ts +37 -51
  38. package/src/sandbox/createSandboxRequire.ts +140 -49
  39. package/src/structures/IPlaygroundDependencyInstallOptions.ts +13 -1
  40. package/src/structures/IPlaygroundDependencyInstallResult.ts +4 -0
  41. package/src/structures/IPlaygroundDependencyPackage.ts +2 -0
  42. package/src/structures/IPlaygroundDependencyRequest.ts +9 -0
  43. package/src/structures/IPlaygroundInstalledDependency.ts +13 -0
  44. 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 && token.kind === "punct" && token.value === ".";
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
- if (isOpenParen(tokens[i + 1])) {
51
- const spec = asString(tokens[i + 2]);
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 single punctuation character.
113
- | { kind: "punct"; value: string }
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 tokens: Token[] = [];
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 === "/" && 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" });
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
- 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 });
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 literal. Contents (including `${...}` expressions) are treated as
237
- // opaque: a template specifier is not statically resolvable.
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 (depth === 0 && d === "`") {
399
+ if (d === "`") {
248
400
  i++;
249
401
  break;
250
402
  }
251
403
  if (d === "$" && source[i + 1] === "{") {
252
- depth++;
253
- i += 2;
254
- continue;
255
- }
256
- if (depth > 0 && d === "}") {
257
- depth--;
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
- tokens.push({ kind: "other" });
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
- tokens.push({ kind: "word", value: source.slice(i, j) });
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
- tokens.push({ kind: "other" });
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
- tokens.push({ kind: "punct", value: c });
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
+ }