ttsc 0.8.1 → 0.9.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 (84) hide show
  1. package/README.md +39 -68
  2. package/cmd/ttsc/api_compile.go +5 -1
  3. package/driver/program.go +23 -1
  4. package/driver/rewrite.go +126 -9
  5. package/driver/rewrite_test.go +52 -0
  6. package/lib/TtscCompiler.js +28 -46
  7. package/lib/TtscCompiler.js.map +1 -1
  8. package/lib/compiler/internal/buildNativeCompiler.js +8 -38
  9. package/lib/compiler/internal/buildNativeCompiler.js.map +1 -1
  10. package/lib/compiler/internal/compileProjectInMemory.js +36 -63
  11. package/lib/compiler/internal/compileProjectInMemory.js.map +1 -1
  12. package/lib/compiler/internal/project/readProjectConfig.js +108 -60
  13. package/lib/compiler/internal/project/readProjectConfig.js.map +1 -1
  14. package/lib/compiler/internal/project/resolveProjectConfig.js +15 -45
  15. package/lib/compiler/internal/project/resolveProjectConfig.js.map +1 -1
  16. package/lib/compiler/internal/resolveBinary.js +10 -40
  17. package/lib/compiler/internal/resolveBinary.js.map +1 -1
  18. package/lib/compiler/internal/resolveEmittedJavaScript.js +21 -48
  19. package/lib/compiler/internal/resolveEmittedJavaScript.js.map +1 -1
  20. package/lib/compiler/internal/resolveTsgo.js +14 -44
  21. package/lib/compiler/internal/resolveTsgo.js.map +1 -1
  22. package/lib/compiler/internal/runBuild.d.ts +7 -0
  23. package/lib/compiler/internal/runBuild.js +47 -66
  24. package/lib/compiler/internal/runBuild.js.map +1 -1
  25. package/lib/compiler/internal/runSingleFileEmit.js +18 -48
  26. package/lib/compiler/internal/runSingleFileEmit.js.map +1 -1
  27. package/lib/compiler/internal/transformProjectInMemory.js +41 -75
  28. package/lib/compiler/internal/transformProjectInMemory.js.map +1 -1
  29. package/lib/launcher/internal/getCompilerVersionText.js +7 -37
  30. package/lib/launcher/internal/getCompilerVersionText.js.map +1 -1
  31. package/lib/launcher/internal/prepareExecution.js +48 -69
  32. package/lib/launcher/internal/prepareExecution.js.map +1 -1
  33. package/lib/launcher/internal/runTtsc.js +53 -64
  34. package/lib/launcher/internal/runTtsc.js.map +1 -1
  35. package/lib/launcher/internal/runTtsx.js +328 -57
  36. package/lib/launcher/internal/runTtsx.js.map +1 -1
  37. package/lib/plugin/internal/buildSourcePlugin.d.ts +8 -0
  38. package/lib/plugin/internal/buildSourcePlugin.js +179 -125
  39. package/lib/plugin/internal/buildSourcePlugin.js.map +1 -1
  40. package/lib/plugin/internal/loadProjectPlugins.d.ts +2 -0
  41. package/lib/plugin/internal/loadProjectPlugins.js +197 -9
  42. package/lib/plugin/internal/loadProjectPlugins.js.map +1 -1
  43. package/lib/structures/ITtscCompilerContext.d.ts +16 -3
  44. package/lib/structures/ITtscCompilerResult.d.ts +4 -0
  45. package/lib/structures/ITtscCompilerTransformation.d.ts +2 -0
  46. package/lib/structures/ITtscPluginFactoryContext.d.ts +6 -3
  47. package/lib/structures/ITtscProjectPluginConfig.d.ts +8 -7
  48. package/lib/structures/internal/ITtscParsedProjectConfig.d.ts +2 -0
  49. package/lib/structures/internal/ITtscProjectLocatorOptions.d.ts +2 -0
  50. package/lib/structures/internal/TtscCommonOptions.d.ts +2 -0
  51. package/package.json +9 -14
  52. package/shim/diagnosticwriter/lint.go +8 -0
  53. package/src/TtscCompiler.ts +20 -5
  54. package/src/compiler/internal/buildNativeCompiler.ts +2 -2
  55. package/src/compiler/internal/compileProjectInMemory.ts +21 -16
  56. package/src/compiler/internal/project/readProjectConfig.ts +134 -35
  57. package/src/compiler/internal/project/resolveProjectConfig.ts +2 -2
  58. package/src/compiler/internal/resolveBinary.ts +2 -2
  59. package/src/compiler/internal/resolveEmittedJavaScript.ts +11 -7
  60. package/src/compiler/internal/resolveTsgo.ts +2 -2
  61. package/src/compiler/internal/runBuild.ts +34 -21
  62. package/src/compiler/internal/runSingleFileEmit.ts +3 -3
  63. package/src/compiler/internal/transformProjectInMemory.ts +37 -37
  64. package/src/launcher/internal/getCompilerVersionText.ts +2 -2
  65. package/src/launcher/internal/prepareExecution.ts +14 -4
  66. package/src/launcher/internal/runTtsc.ts +30 -4
  67. package/src/launcher/internal/runTtsx.ts +377 -26
  68. package/src/plugin/internal/buildSourcePlugin.ts +127 -32
  69. package/src/plugin/internal/loadProjectPlugins.ts +266 -10
  70. package/src/structures/ITtscCompilerContext.ts +17 -3
  71. package/src/structures/ITtscCompilerResult.ts +5 -0
  72. package/src/structures/ITtscCompilerTransformation.ts +3 -0
  73. package/src/structures/ITtscPluginFactoryContext.ts +6 -3
  74. package/src/structures/ITtscProjectPluginConfig.ts +8 -7
  75. package/src/structures/internal/ITtscParsedProjectConfig.ts +2 -0
  76. package/src/structures/internal/ITtscProjectLocatorOptions.ts +2 -0
  77. package/src/structures/internal/TtscCommonOptions.ts +2 -0
  78. package/utility/host.go +610 -18
  79. package/utility/host_test.go +162 -0
  80. package/test/compiler.test.ts +0 -422
  81. package/test/platform.test.ts +0 -12
  82. package/test/project.test.ts +0 -197
  83. package/test/source-build.test.ts +0 -45
  84. package/test/tsgo.test.ts +0 -69
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
4
 
5
5
  import { TtscCompiler } from "../../TtscCompiler";
6
6
  import { resolveBinary } from "../../compiler/internal/resolveBinary";
@@ -72,7 +72,7 @@ function runCompatibleBuild(
72
72
  argv: readonly string[],
73
73
  checkOnly: boolean,
74
74
  ): number {
75
- const options = parseBuildArgs(argv, checkOnly);
75
+ const options = normalizeBuildOptions(parseBuildArgs(argv, checkOnly));
76
76
  if (options.watch) {
77
77
  return runWatch(options, checkOnly);
78
78
  }
@@ -85,6 +85,17 @@ function runCompatibleBuild(
85
85
  return result.status;
86
86
  }
87
87
 
88
+ function normalizeBuildOptions(
89
+ options: ReturnType<typeof parseBuildArgs>,
90
+ ): ReturnType<typeof parseBuildArgs> {
91
+ const cwd = path.resolve(options.cwd ?? process.cwd());
92
+ return {
93
+ ...options,
94
+ cacheDir: resolveCacheDir(cwd, options.cacheDir),
95
+ cwd,
96
+ };
97
+ }
98
+
88
99
  function runPrepare(argv: readonly string[]): number {
89
100
  const options = parseProjectArgs(argv);
90
101
  const cwd = path.resolve(options.cwd ?? process.cwd());
@@ -153,12 +164,20 @@ function resolveCleanProjectRoot(cwd: string, tsconfig?: string): string {
153
164
 
154
165
  function formatProjectPath(cwd: string, target: string): string {
155
166
  const relative = path.relative(cwd, target);
156
- if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
167
+ if (!relative || isOutsideRelativePath(relative)) {
157
168
  return target;
158
169
  }
159
170
  return relative;
160
171
  }
161
172
 
173
+ function isOutsideRelativePath(relative: string): boolean {
174
+ return (
175
+ relative === ".." ||
176
+ relative.startsWith(`..${path.sep}`) ||
177
+ path.isAbsolute(relative)
178
+ );
179
+ }
180
+
162
181
  function delegateToNative(argv: readonly string[]): number {
163
182
  const bin = resolveBinary();
164
183
  if (!bin) {
@@ -498,6 +517,13 @@ function runWatch(
498
517
  return 0;
499
518
  }
500
519
 
520
+ function resolveCacheDir(cwd: string, cacheDir?: string): string | undefined {
521
+ if (!cacheDir) {
522
+ return undefined;
523
+ }
524
+ return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
525
+ }
526
+
501
527
  function collectWatchDirectories(root: string): string[] {
502
528
  const out: string[] = [];
503
529
  const stack: string[] = [root];
@@ -1,6 +1,6 @@
1
- import * as fs from "node:fs";
1
+ import fs from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
- import * as path from "node:path";
3
+ import path from "node:path";
4
4
 
5
5
  import { getCompilerVersionText } from "./getCompilerVersionText";
6
6
  import { prepareExecution } from "./prepareExecution";
@@ -38,7 +38,7 @@ function run(argv: readonly string[]): number {
38
38
 
39
39
  const prepared = prepareExecution(entry, {
40
40
  binary: parsed.binary,
41
- cacheDir: parsed.cacheDir,
41
+ cacheDir: resolveCacheDir(cwd, parsed.cacheDir),
42
42
  cwd,
43
43
  project: parsed.project,
44
44
  });
@@ -140,9 +140,9 @@ function printHelp(): void {
140
140
  "Options:",
141
141
  " -P, --project <file> Use an explicit tsconfig.json",
142
142
  " --cwd <dir> Resolve entry/project relative to this directory",
143
- " --cache-dir <dir> Override the compiled JS cache directory",
143
+ " --cache-dir <dir> Override the runner and source-plugin cache root",
144
144
  " --binary <path> Use an explicit tsgo binary",
145
- " -r, --require <file> Preload a module before the entrypoint",
145
+ " -r, --require <module> Preload a module before the entrypoint",
146
146
  " -h, --help Show this help",
147
147
  " -v, --version Print the runner version",
148
148
  "",
@@ -155,16 +155,30 @@ function printHelp(): void {
155
155
  }
156
156
 
157
157
  function resolvePreload(cwd: string, preload: string): string {
158
- if (
159
- preload.startsWith(".") ||
160
- preload.startsWith("/") ||
161
- preload.includes(path.sep)
162
- ) {
158
+ if (path.isAbsolute(preload) || isRelativeSpecifier(preload)) {
163
159
  return path.resolve(cwd, preload);
164
160
  }
165
161
  return preload;
166
162
  }
167
163
 
164
+ function isRelativeSpecifier(specifier: string): boolean {
165
+ return (
166
+ specifier === "." ||
167
+ specifier === ".." ||
168
+ specifier.startsWith("./") ||
169
+ specifier.startsWith("../") ||
170
+ specifier.startsWith(".\\") ||
171
+ specifier.startsWith("..\\")
172
+ );
173
+ }
174
+
175
+ function resolveCacheDir(cwd: string, cacheDir?: string): string | undefined {
176
+ if (!cacheDir) {
177
+ return undefined;
178
+ }
179
+ return path.isAbsolute(cacheDir) ? cacheDir : path.resolve(cwd, cacheDir);
180
+ }
181
+
168
182
  function takeValue(flag: string, rest: string[]): string {
169
183
  const value = rest.shift();
170
184
  if (!value) {
@@ -196,6 +210,7 @@ function runPreparedEntry(
196
210
  ...parsed.passthrough,
197
211
  ];
198
212
  const result = spawnSync(process.execPath, args, {
213
+ cwd,
199
214
  stdio: "inherit",
200
215
  env: process.env,
201
216
  windowsHide: true,
@@ -221,22 +236,7 @@ function rewriteEsmSpecifiers(root: string): void {
221
236
  continue;
222
237
  }
223
238
  const before = fs.readFileSync(next, "utf8");
224
- const after = before
225
- .replace(
226
- /\bfrom\s+(['"])(\.[^'"]+)\1/g,
227
- (_, quote: string, specifier: string) =>
228
- `from ${quote}${withResolvableExtension(next, specifier)}${quote}`,
229
- )
230
- .replace(
231
- /\bimport\s+(['"])(\.[^'"]+)\1/g,
232
- (_, quote: string, specifier: string) =>
233
- `import ${quote}${withResolvableExtension(next, specifier)}${quote}`,
234
- )
235
- .replace(
236
- /\bimport\(\s*(['"])(\.[^'"]+)\1\s*\)/g,
237
- (_, quote: string, specifier: string) =>
238
- `import(${quote}${withResolvableExtension(next, specifier)}${quote})`,
239
- );
239
+ const after = rewriteEsmSpecifiersInText(next, before);
240
240
  if (after !== before) {
241
241
  fs.writeFileSync(next, after, "utf8");
242
242
  }
@@ -244,6 +244,193 @@ function rewriteEsmSpecifiers(root: string): void {
244
244
  }
245
245
  }
246
246
 
247
+ function rewriteEsmSpecifiersInText(fromFile: string, input: string): string {
248
+ const replacements: Array<{ start: number; end: number; text: string }> = [];
249
+ scanEsmSpecifiers(fromFile, input, 0, input.length, replacements);
250
+ if (replacements.length === 0) {
251
+ return input;
252
+ }
253
+ let output = "";
254
+ let last = 0;
255
+ for (const replacement of replacements) {
256
+ output += input.slice(last, replacement.start);
257
+ output += replacement.text;
258
+ last = replacement.end;
259
+ }
260
+ return output + input.slice(last);
261
+ }
262
+
263
+ function scanEsmSpecifiers(
264
+ fromFile: string,
265
+ input: string,
266
+ start: number,
267
+ end: number,
268
+ replacements: Array<{ start: number; end: number; text: string }>,
269
+ ): void {
270
+ let i = start;
271
+ while (i < end) {
272
+ if (input[i] === "`") {
273
+ i = scanTemplateExpressions(fromFile, input, i, replacements);
274
+ continue;
275
+ }
276
+ const skipped = skipNonCode(input, i);
277
+ if (skipped !== i) {
278
+ i = skipped;
279
+ continue;
280
+ }
281
+ if (isKeywordAt(input, i, "import")) {
282
+ const next = skipWhitespace(input, i + "import".length);
283
+ if (input[next] === "(") {
284
+ rewriteDynamicImportSpecifier(fromFile, input, next, replacements);
285
+ } else if (isQuote(input[next])) {
286
+ rewriteStringSpecifier(fromFile, input, next, replacements);
287
+ } else {
288
+ rewriteFromSpecifier(fromFile, input, next, replacements);
289
+ }
290
+ i += "import".length;
291
+ continue;
292
+ }
293
+ if (isKeywordAt(input, i, "export")) {
294
+ rewriteFromSpecifier(fromFile, input, i + "export".length, replacements);
295
+ i += "export".length;
296
+ continue;
297
+ }
298
+ i += 1;
299
+ }
300
+ }
301
+
302
+ function rewriteDynamicImportSpecifier(
303
+ fromFile: string,
304
+ input: string,
305
+ openParen: number,
306
+ replacements: Array<{ start: number; end: number; text: string }>,
307
+ ): void {
308
+ const specifier = skipWhitespace(input, openParen + 1);
309
+ if (isQuote(input[specifier])) {
310
+ rewriteStringSpecifier(fromFile, input, specifier, replacements);
311
+ }
312
+ }
313
+
314
+ function rewriteFromSpecifier(
315
+ fromFile: string,
316
+ input: string,
317
+ start: number,
318
+ replacements: Array<{ start: number; end: number; text: string }>,
319
+ ): void {
320
+ let i = start;
321
+ let depth = 0;
322
+ while (i < input.length) {
323
+ const skipped = skipNonCode(input, i);
324
+ if (skipped !== i) {
325
+ i = skipped;
326
+ continue;
327
+ }
328
+ const current = input[i]!;
329
+ if (depth === 0 && current === ";") {
330
+ return;
331
+ }
332
+ if (current === "{" || current === "[" || current === "(") {
333
+ depth += 1;
334
+ i += 1;
335
+ continue;
336
+ }
337
+ if (current === "}" || current === "]" || current === ")") {
338
+ depth = Math.max(0, depth - 1);
339
+ i += 1;
340
+ continue;
341
+ }
342
+ if (depth === 0 && isKeywordAt(input, i, "from")) {
343
+ const specifier = skipWhitespace(input, i + "from".length);
344
+ if (isQuote(input[specifier])) {
345
+ rewriteStringSpecifier(fromFile, input, specifier, replacements);
346
+ }
347
+ return;
348
+ }
349
+ i += 1;
350
+ }
351
+ }
352
+
353
+ function rewriteStringSpecifier(
354
+ fromFile: string,
355
+ input: string,
356
+ quoteIndex: number,
357
+ replacements: Array<{ start: number; end: number; text: string }>,
358
+ ): void {
359
+ const literal = readSimpleStringLiteral(input, quoteIndex);
360
+ if (literal === null) {
361
+ return;
362
+ }
363
+ const next = withResolvableExtension(fromFile, literal.value);
364
+ if (next === literal.value) {
365
+ return;
366
+ }
367
+ replacements.push({
368
+ start: literal.start,
369
+ end: literal.end,
370
+ text: next,
371
+ });
372
+ }
373
+
374
+ function scanTemplateExpressions(
375
+ fromFile: string,
376
+ input: string,
377
+ start: number,
378
+ replacements: Array<{ start: number; end: number; text: string }>,
379
+ ): number {
380
+ for (let i = start + 1; i < input.length; i += 1) {
381
+ const current = input[i]!;
382
+ if (current === "\\") {
383
+ i += 1;
384
+ continue;
385
+ }
386
+ if (current === "`") {
387
+ return i + 1;
388
+ }
389
+ if (current === "$" && input[i + 1] === "{") {
390
+ const expressionStart = i + 2;
391
+ const expressionEnd = findTemplateExpressionEnd(input, expressionStart);
392
+ if (expressionEnd === null) {
393
+ return input.length;
394
+ }
395
+ scanEsmSpecifiers(
396
+ fromFile,
397
+ input,
398
+ expressionStart,
399
+ expressionEnd,
400
+ replacements,
401
+ );
402
+ i = expressionEnd;
403
+ }
404
+ }
405
+ return input.length;
406
+ }
407
+
408
+ function findTemplateExpressionEnd(
409
+ input: string,
410
+ expressionStart: number,
411
+ ): number | null {
412
+ let depth = 1;
413
+ for (let i = expressionStart; i < input.length; i += 1) {
414
+ const skipped = skipNonCode(input, i);
415
+ if (skipped !== i) {
416
+ i = skipped - 1;
417
+ continue;
418
+ }
419
+ const current = input[i]!;
420
+ if (current === "{") {
421
+ depth += 1;
422
+ continue;
423
+ }
424
+ if (current === "}") {
425
+ depth -= 1;
426
+ if (depth === 0) {
427
+ return i;
428
+ }
429
+ }
430
+ }
431
+ return null;
432
+ }
433
+
247
434
  function withResolvableExtension(fromFile: string, specifier: string): string {
248
435
  if (!specifier.startsWith(".")) {
249
436
  return specifier;
@@ -258,6 +445,12 @@ function withResolvableExtension(fromFile: string, specifier: string): string {
258
445
  return pathname + extension + suffix;
259
446
  }
260
447
  }
448
+ const directory = pathname.replace(/\/+$/, "");
449
+ for (const extension of [".js", ".mjs", ".cjs"]) {
450
+ if (fs.existsSync(path.resolve(fromDir, directory, "index" + extension))) {
451
+ return `${directory}/index${extension}${suffix}`;
452
+ }
453
+ }
261
454
  return `${specifier}.js`;
262
455
  }
263
456
 
@@ -272,3 +465,161 @@ function splitSpecifierSuffix(specifier: string): [string, string?] {
272
465
  function isJavaScriptOutput(filename: string): boolean {
273
466
  return /\.(?:[cm]?js)$/i.test(filename);
274
467
  }
468
+
469
+ function skipNonCode(input: string, start: number): number {
470
+ const current = input[start];
471
+ const next = input[start + 1];
472
+ if (current === '"' || current === "'") {
473
+ return skipString(input, start);
474
+ }
475
+ if (current === "`") {
476
+ return skipTemplate(input, start);
477
+ }
478
+ if (current === "/" && next === "/") {
479
+ return skipLineComment(input, start);
480
+ }
481
+ if (current === "/" && next === "*") {
482
+ return skipBlockComment(input, start);
483
+ }
484
+ if (current === "/" && looksLikeRegexStart(input, start)) {
485
+ return skipRegex(input, start);
486
+ }
487
+ return start;
488
+ }
489
+
490
+ function skipString(input: string, start: number): number {
491
+ const quote = input[start]!;
492
+ for (let i = start + 1; i < input.length; i += 1) {
493
+ const current = input[i]!;
494
+ if (current === "\\") {
495
+ i += 1;
496
+ continue;
497
+ }
498
+ if (current === quote) {
499
+ return i + 1;
500
+ }
501
+ }
502
+ return input.length;
503
+ }
504
+
505
+ function skipTemplate(input: string, start: number): number {
506
+ for (let i = start + 1; i < input.length; i += 1) {
507
+ const current = input[i]!;
508
+ if (current === "\\") {
509
+ i += 1;
510
+ continue;
511
+ }
512
+ if (current === "`") {
513
+ return i + 1;
514
+ }
515
+ }
516
+ return input.length;
517
+ }
518
+
519
+ function skipLineComment(input: string, start: number): number {
520
+ const end = input.indexOf("\n", start + 2);
521
+ return end === -1 ? input.length : end + 1;
522
+ }
523
+
524
+ function skipBlockComment(input: string, start: number): number {
525
+ const end = input.indexOf("*/", start + 2);
526
+ return end === -1 ? input.length : end + 2;
527
+ }
528
+
529
+ function skipRegex(input: string, start: number): number {
530
+ let inClass = false;
531
+ for (let i = start + 1; i < input.length; i += 1) {
532
+ const current = input[i]!;
533
+ if (current === "\\") {
534
+ i += 1;
535
+ continue;
536
+ }
537
+ if (current === "[") {
538
+ inClass = true;
539
+ continue;
540
+ }
541
+ if (current === "]") {
542
+ inClass = false;
543
+ continue;
544
+ }
545
+ if (current === "/" && !inClass) {
546
+ i += 1;
547
+ while (/[A-Za-z]/.test(input[i] ?? "")) {
548
+ i += 1;
549
+ }
550
+ return i;
551
+ }
552
+ }
553
+ return input.length;
554
+ }
555
+
556
+ function looksLikeRegexStart(input: string, start: number): boolean {
557
+ let i = start - 1;
558
+ while (i >= 0 && /\s/.test(input[i]!)) {
559
+ i -= 1;
560
+ }
561
+ if (i < 0) {
562
+ return true;
563
+ }
564
+ const previous = input[i]!;
565
+ if ("([{=,:;!?&|+-*~^<>".includes(previous)) {
566
+ return true;
567
+ }
568
+ const word = readPreviousWord(input, i);
569
+ return /^(?:return|throw|case|delete|void|typeof|instanceof|in|of|yield|await)$/.test(
570
+ word,
571
+ );
572
+ }
573
+
574
+ function readPreviousWord(input: string, end: number): string {
575
+ let start = end;
576
+ while (start >= 0 && isIdentifierPart(input[start]!)) {
577
+ start -= 1;
578
+ }
579
+ return input.slice(start + 1, end + 1);
580
+ }
581
+
582
+ function readSimpleStringLiteral(
583
+ input: string,
584
+ quoteIndex: number,
585
+ ): { start: number; end: number; value: string } | null {
586
+ const quote = input[quoteIndex]!;
587
+ for (let i = quoteIndex + 1; i < input.length; i += 1) {
588
+ const current = input[i]!;
589
+ if (current === "\\") {
590
+ return null;
591
+ }
592
+ if (current === quote) {
593
+ return {
594
+ start: quoteIndex + 1,
595
+ end: i,
596
+ value: input.slice(quoteIndex + 1, i),
597
+ };
598
+ }
599
+ }
600
+ return null;
601
+ }
602
+
603
+ function skipWhitespace(input: string, start: number): number {
604
+ let i = start;
605
+ while (i < input.length && /\s/.test(input[i]!)) {
606
+ i += 1;
607
+ }
608
+ return i;
609
+ }
610
+
611
+ function isKeywordAt(input: string, start: number, keyword: string): boolean {
612
+ return (
613
+ input.startsWith(keyword, start) &&
614
+ !isIdentifierPart(input[start - 1] ?? "") &&
615
+ !isIdentifierPart(input[start + keyword.length] ?? "")
616
+ );
617
+ }
618
+
619
+ function isIdentifierPart(value: string): boolean {
620
+ return /[$\p{ID_Continue}]/u.test(value);
621
+ }
622
+
623
+ function isQuote(value: string | undefined): value is '"' | "'" {
624
+ return value === '"' || value === "'";
625
+ }