ttsc 0.4.4 → 0.5.0-dev.20260429

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 (49) hide show
  1. package/README.md +35 -3
  2. package/cmd/ttsc/main.go +2 -2
  3. package/driver/program.go +120 -15
  4. package/go.mod +3 -3
  5. package/go.sum +6 -6
  6. package/lib/api.d.ts +3 -3
  7. package/lib/api.js +275 -624
  8. package/lib/api.js.map +1 -1
  9. package/lib/cli.js +78 -2
  10. package/lib/cli.js.map +1 -1
  11. package/lib/index.d.ts +3 -3
  12. package/lib/index.js +3 -3
  13. package/lib/native.d.ts +5 -0
  14. package/lib/native.js +8 -0
  15. package/lib/native.js.map +1 -1
  16. package/lib/plugin.d.ts +8 -48
  17. package/lib/plugin.js +79 -132
  18. package/lib/plugin.js.map +1 -1
  19. package/lib/runner/cli.js +1 -0
  20. package/lib/runner/cli.js.map +1 -1
  21. package/lib/source-build.d.ts +30 -0
  22. package/lib/source-build.js +365 -0
  23. package/lib/source-build.js.map +1 -0
  24. package/package.json +9 -9
  25. package/shim/ast/go.mod +1 -1
  26. package/shim/ast/lint.go +328 -0
  27. package/shim/bundled/go.mod +1 -1
  28. package/shim/checker/go.mod +1 -1
  29. package/shim/compiler/go.mod +1 -1
  30. package/shim/core/go.mod +1 -1
  31. package/shim/core/shim.go +15 -0
  32. package/shim/diagnosticwriter/go.mod +1 -1
  33. package/shim/diagnosticwriter/lint.go +137 -0
  34. package/shim/parser/go.mod +1 -1
  35. package/shim/scanner/go.mod +1 -1
  36. package/shim/tsoptions/go.mod +1 -1
  37. package/shim/tspath/go.mod +1 -1
  38. package/shim/vfs/cachedvfs/go.mod +1 -1
  39. package/shim/vfs/go.mod +1 -1
  40. package/shim/vfs/osvfs/go.mod +1 -1
  41. package/src/api.ts +413 -771
  42. package/src/cli.ts +82 -2
  43. package/src/index.ts +3 -3
  44. package/src/native.ts +18 -0
  45. package/src/plugin.ts +107 -213
  46. package/src/runner/cli.ts +5 -0
  47. package/src/source-build.ts +383 -0
  48. package/test/native.test.ts +43 -0
  49. package/test/source-build.test.ts +219 -0
package/src/api.ts CHANGED
@@ -23,10 +23,7 @@ import * as path from "node:path";
23
23
 
24
24
  import { type ResolveOptions, resolveBinary } from "./platform";
25
25
  import {
26
- type TtscSourceTransformStage,
27
- type TtscPlugin,
28
- applyPluginSourceTransformsWithMap,
29
- applyPluginTransforms,
26
+ type LoadedNativePlugin,
30
27
  loadProjectPlugins,
31
28
  } from "./plugin";
32
29
  import {
@@ -90,7 +87,7 @@ export interface BuildOptions extends CommonOptions {
90
87
  quiet?: boolean;
91
88
  /** @internal Caller already ran diagnostics and accepts responsibility. */
92
89
  skipDiagnosticsCheck?: boolean;
93
- /** @internal Force `tsgo --listEmittedFiles` even when no output plugin runs. */
90
+ /** @internal Force `tsgo --listEmittedFiles` for caller-side output discovery. */
94
91
  forceListEmittedFiles?: boolean;
95
92
  }
96
93
 
@@ -155,6 +152,47 @@ function outputText(value: string | Buffer | null | undefined): string {
155
152
  return typeof value === "string" ? value : value.toString("utf8");
156
153
  }
157
154
 
155
+ function hasCapability(
156
+ plugin: LoadedNativePlugin,
157
+ capability: string,
158
+ ): boolean {
159
+ return plugin.backend.capabilities?.includes(capability) === true;
160
+ }
161
+
162
+ function isOutputPlugin(plugin: LoadedNativePlugin): boolean {
163
+ return hasCapability(plugin, "output");
164
+ }
165
+
166
+ function isCheckOnlyPlugin(plugin: LoadedNativePlugin): boolean {
167
+ const capabilities = plugin.backend.capabilities ?? [];
168
+ return (
169
+ capabilities.length > 0 &&
170
+ capabilities.every((capability) => capability === "check")
171
+ );
172
+ }
173
+
174
+ function isCompilerPlugin(plugin: LoadedNativePlugin): boolean {
175
+ return !isOutputPlugin(plugin) && !isCheckOnlyPlugin(plugin);
176
+ }
177
+
178
+ function outputPlugins(
179
+ plugins: readonly LoadedNativePlugin[],
180
+ ): LoadedNativePlugin[] {
181
+ return plugins.filter(isOutputPlugin);
182
+ }
183
+
184
+ function checkOnlyPlugins(
185
+ plugins: readonly LoadedNativePlugin[],
186
+ ): LoadedNativePlugin[] {
187
+ return plugins.filter(isCheckOnlyPlugin);
188
+ }
189
+
190
+ function compilerPlugins(
191
+ plugins: readonly LoadedNativePlugin[],
192
+ ): LoadedNativePlugin[] {
193
+ return plugins.filter(isCompilerPlugin);
194
+ }
195
+
158
196
  /**
159
197
  * Transform a single .ts file and return the rewritten JS as a string.
160
198
  *
@@ -172,16 +210,8 @@ export function transform(options: TransformOptions): string {
172
210
  ? options.file
173
211
  : path.resolve(options.cwd ?? process.cwd(), options.file),
174
212
  );
175
- if (execution.sourcePlugins.length > 0) {
176
- return transformWithSourceTransforms(options, execution, sourceFile);
177
- }
178
- if (execution.nativeBinary) {
179
- return transformWithNativeBinary(options, execution, sourceFile);
180
- }
181
- if (execution.nativeMode !== "none") {
182
- throw new Error(
183
- `ttsc.transform: native rewrite mode "${execution.nativeMode}" requires a plugin-provided version-matched binary`,
184
- );
213
+ if (execution.nativePlugins.length > 0) {
214
+ return transformWithNativePlugins(options, execution, sourceFile);
185
215
  }
186
216
 
187
217
  const tempOutDir = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-transform-"));
@@ -205,18 +235,7 @@ export function transform(options: TransformOptions): string {
205
235
  if (!emitted) {
206
236
  throw new Error(`ttsc.transform: no output produced for ${sourceFile}`);
207
237
  }
208
- const transformed = finalizeTransformText(
209
- execution.outputPlugins,
210
- {
211
- command: "transform",
212
- compilerOptions: execution.compilerOptions,
213
- cwd: execution.cwd,
214
- projectRoot: execution.projectRoot,
215
- sourceFile,
216
- tsconfig: execution.tsconfig,
217
- },
218
- fs.readFileSync(emitted, "utf8"),
219
- );
238
+ const transformed = fs.readFileSync(emitted, "utf8");
220
239
  if (options.out) {
221
240
  fs.mkdirSync(path.dirname(options.out), { recursive: true });
222
241
  fs.writeFileSync(options.out, transformed, "utf8");
@@ -227,129 +246,84 @@ export function transform(options: TransformOptions): string {
227
246
  }
228
247
  }
229
248
 
230
- function transformWithNativeBinary(
249
+ function transformWithNativePlugins(
231
250
  options: TransformOptions,
232
251
  execution: ExecutionContext,
233
252
  sourceFile: string,
234
253
  ): string {
235
- const args = [
236
- "transform",
237
- "--file=" + sourceFile,
238
- "--tsconfig=" + execution.tsconfig,
239
- "--rewrite-mode=" + execution.nativeMode,
240
- ];
241
-
242
- const res = spawnBinary(execution.nativeBinary!, args, {
243
- cwd: options.cwd,
244
- env: mergeEnv(options.env),
245
- encoding: "utf8",
246
- });
247
- if (res.error) {
248
- throw new Error(
249
- "ttsc.transform: failed to spawn " +
250
- execution.nativeBinary +
251
- ": " +
252
- res.error.message,
253
- );
254
- }
255
- if (res.status !== 0) {
254
+ const checked = runNativeCheckPlugins(options, execution);
255
+ if (checked.status !== 0) {
256
256
  throw new Error(
257
- "ttsc.transform exited " + res.status + "\n" + (res.stderr || ""),
257
+ "ttsc.transform exited " + checked.status + "\n" + checked.stderr,
258
258
  );
259
259
  }
260
- const transformed = finalizeTransformText(
261
- execution.outputPlugins,
262
- {
263
- command: "transform",
264
- compilerOptions: execution.compilerOptions,
265
- cwd: execution.cwd,
266
- projectRoot: execution.projectRoot,
267
- sourceFile,
268
- tsconfig: execution.tsconfig,
269
- },
270
- outputText(res.stdout),
271
- );
272
- if (options.out) {
273
- fs.mkdirSync(path.dirname(options.out), { recursive: true });
274
- fs.writeFileSync(options.out, transformed, "utf8");
275
- }
276
- return transformed;
277
- }
278
260
 
279
- function transformWithSourceTransforms(
280
- options: TransformOptions,
281
- execution: ExecutionContext,
282
- sourceFile: string,
283
- ): string {
284
- if (execution.nativeBinary || execution.nativeMode !== "none") {
285
- throw new Error(
286
- "ttsc.transform: transformSource cannot be combined with native rewrite mode yet",
287
- );
288
- }
289
- const materialized = materializeSourceTransformedProject(
290
- execution,
291
- "transform",
292
- sourceFile,
293
- );
294
- const tempOutDir = path.join(materialized.root, ".ttsc-transform-out");
261
+ const compilers = compilerPlugins(execution.nativePlugins);
262
+ const outputs = outputPlugins(execution.nativePlugins);
263
+ const tempOutDir = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-transform-"));
295
264
  try {
296
- const tempSourceFile = toMaterializedPath(sourceFile, materialized);
297
- const result = build({
298
- ...options,
299
- cwd: materialized.root,
300
- emit: true,
301
- forceListEmittedFiles: true,
302
- outDir: tempOutDir,
303
- plugins: false,
304
- tsconfig: materialized.tsconfig,
305
- });
306
- if (result.status !== 0) {
307
- throw new Error(
308
- "ttsc.transform exited " +
309
- result.status +
310
- "\n" +
311
- (result.stderr || result.stdout),
312
- );
313
- }
314
- const emitted = findEmittedFile(tempOutDir, materialized.root, tempSourceFile);
315
- if (!emitted) {
316
- throw new Error(`ttsc.transform: no output produced for ${sourceFile}`);
317
- }
318
- let transformed = finalizeTransformText(
319
- execution.outputPlugins,
320
- {
321
- command: "transform",
322
- compilerOptions: execution.compilerOptions,
323
- cwd: execution.cwd,
324
- projectRoot: execution.projectRoot,
265
+ let emitted: string | null = null;
266
+ if (compilers.length !== 0) {
267
+ assertSingleCompilerHost(compilers);
268
+ emitted = path.join(tempOutDir, "transform.js");
269
+ const transformed = runNativeCompilerTransform(
270
+ options,
271
+ execution,
325
272
  sourceFile,
273
+ emitted,
274
+ compilers,
275
+ );
276
+ if (transformed.status !== 0) {
277
+ throw new Error(
278
+ "ttsc.transform exited " +
279
+ transformed.status +
280
+ "\n" +
281
+ transformed.stderr,
282
+ );
283
+ }
284
+ } else {
285
+ const result = build({
286
+ ...options,
287
+ emit: true,
288
+ outDir: tempOutDir,
289
+ plugins: false,
290
+ skipDiagnosticsCheck: checkOnlyPlugins(execution.nativePlugins).length !== 0,
326
291
  tsconfig: execution.tsconfig,
327
- },
328
- fs.readFileSync(emitted, "utf8"),
329
- );
330
- if (options.out) {
331
- const mapFile = `${emitted}.map`;
332
- if (fs.existsSync(mapFile)) {
333
- const outMap = `${options.out}.map`;
334
- fs.mkdirSync(path.dirname(outMap), { recursive: true });
335
- fs.writeFileSync(
336
- outMap,
337
- patchSourceMapText(
338
- fs.readFileSync(mapFile, "utf8"),
339
- materialized,
340
- outMap,
341
- mapFile,
342
- ),
343
- "utf8",
292
+ });
293
+ if (result.status !== 0) {
294
+ throw new Error(
295
+ "ttsc.transform exited " +
296
+ result.status +
297
+ "\n" +
298
+ (result.stderr || result.stdout),
299
+ );
300
+ }
301
+ emitted = findEmittedFile(tempOutDir, execution.projectRoot, sourceFile);
302
+ if (!emitted) {
303
+ throw new Error(`ttsc.transform: no output produced for ${sourceFile}`);
304
+ }
305
+ }
306
+
307
+ for (const plugin of outputs) {
308
+ const result = runNativeOutputPlugin(options, execution, plugin, emitted);
309
+ if (result.status !== 0) {
310
+ throw new Error(
311
+ "ttsc.transform exited " +
312
+ result.status +
313
+ "\n" +
314
+ (result.stderr || result.stdout),
344
315
  );
345
- transformed = rewriteSourceMapReference(transformed, path.basename(outMap));
346
316
  }
317
+ }
318
+
319
+ const transformed = fs.readFileSync(emitted, "utf8");
320
+ if (options.out) {
347
321
  fs.mkdirSync(path.dirname(options.out), { recursive: true });
348
322
  fs.writeFileSync(options.out, transformed, "utf8");
349
323
  }
350
324
  return transformed;
351
325
  } finally {
352
- fs.rmSync(materialized.root, { recursive: true, force: true });
326
+ fs.rmSync(tempOutDir, { recursive: true, force: true });
353
327
  }
354
328
  }
355
329
 
@@ -376,20 +350,61 @@ export interface BuildResult {
376
350
  */
377
351
  export function build(options: BuildOptions = {}): BuildResult {
378
352
  const execution = resolveExecutionContext(options);
379
- if (execution.sourcePlugins.length > 0) {
380
- return buildWithSourceTransforms(options, execution);
381
- }
382
- if (execution.nativeBinary) {
383
- return buildWithNativeBinary(options, execution);
384
- }
385
- if (execution.nativeMode !== "none") {
386
- return {
387
- status: 2,
388
- stdout: "",
389
- stderr:
390
- `ttsc.build: native rewrite mode "${execution.nativeMode}" requires ` +
391
- "a version-matched sidecar build; project builds use the consuming project's @typescript/native-preview by default\n",
392
- };
353
+ if (execution.nativePlugins.length > 0) {
354
+ const compilers = compilerPlugins(execution.nativePlugins);
355
+ const outputs = outputPlugins(execution.nativePlugins);
356
+ const checked = runNativeCheckPlugins(options, execution);
357
+ if (checked.status !== 0) {
358
+ return checked;
359
+ }
360
+
361
+ if (options.emit === false) {
362
+ if (compilers.length !== 0) {
363
+ assertSingleCompilerHost(compilers);
364
+ return appendBuildOutput(
365
+ checked,
366
+ buildWithNativeCompilerPlugins(options, execution, compilers),
367
+ );
368
+ }
369
+ if (checked.stdout !== "" || checked.stderr !== "") {
370
+ return checked;
371
+ }
372
+ return runTsgo(execution, ["--noEmit"], options);
373
+ }
374
+
375
+ let result: BuildResult;
376
+ if (compilers.length !== 0) {
377
+ assertSingleCompilerHost(compilers);
378
+ result = appendBuildOutput(
379
+ checked,
380
+ buildWithNativeCompilerPlugins(options, execution, compilers),
381
+ );
382
+ } else {
383
+ if (
384
+ checked.stdout === "" &&
385
+ checked.stderr === "" &&
386
+ options.skipDiagnosticsCheck !== true
387
+ ) {
388
+ const tsgoChecked = runTsgo(execution, ["--noEmit"], options);
389
+ if (tsgoChecked.status !== 0) {
390
+ return tsgoChecked;
391
+ }
392
+ }
393
+ const args = createTsgoBuildArgs(execution, options, {
394
+ listEmittedFiles:
395
+ outputs.length !== 0 || options.forceListEmittedFiles === true,
396
+ });
397
+ const emitted = runTsgoBuild(execution, options, args);
398
+ result = appendBuildOutput(checked, emitted);
399
+ }
400
+
401
+ if (result.status !== 0 || outputs.length === 0) {
402
+ return result;
403
+ }
404
+ return appendBuildOutput(
405
+ result,
406
+ applyOutputPlugins(options, execution, result.emittedFiles ?? [], outputs),
407
+ );
393
408
  }
394
409
 
395
410
  if (options.emit !== false && options.skipDiagnosticsCheck !== true) {
@@ -401,96 +416,23 @@ export function build(options: BuildOptions = {}): BuildResult {
401
416
 
402
417
  const args = createTsgoBuildArgs(execution, options, {
403
418
  listEmittedFiles:
404
- options.emit !== false &&
405
- (execution.outputPlugins.length > 0 || options.forceListEmittedFiles === true),
406
- });
407
- const res = spawnBinary(execution.tsgo.binary, args, {
408
- cwd: execution.projectRoot,
409
- env: mergeEnv(options.env),
410
- encoding: "utf8",
411
- });
412
- if (res.error) {
413
- throw new Error(
414
- "ttsc.build: failed to spawn " +
415
- execution.tsgo.binary +
416
- ": " +
417
- res.error.message,
418
- );
419
- }
420
- const result = {
421
- status: res.status ?? 1,
422
- stdout: outputText(res.stdout),
423
- stderr: outputText(res.stderr),
424
- };
425
- const emittedFiles = parseEmittedFiles(result.stdout);
426
- if (emittedFiles.length !== 0) {
427
- result.stdout = stripEmittedFileLines(result.stdout);
428
- }
429
- if (result.status === 0 && options.emit !== false && execution.outputPlugins.length > 0) {
430
- applyBuildPlugins(execution.outputPlugins, execution, emittedFiles);
431
- }
432
- return normalizeFailedDiagnostics({ ...result, emittedFiles });
433
- }
434
-
435
- function buildWithNativeBinary(
436
- options: BuildOptions,
437
- execution: ExecutionContext,
438
- ): BuildResult {
439
- const args = createNativeBuildArgs(execution, options);
440
- const res = spawnBinary(execution.nativeBinary!, args, {
441
- cwd: execution.projectRoot,
442
- env: mergeEnv(options.env),
443
- encoding: "utf8",
444
- });
445
- if (res.error) {
446
- throw new Error(
447
- "ttsc.build: failed to spawn " +
448
- execution.nativeBinary +
449
- ": " +
450
- res.error.message,
451
- );
452
- }
453
- return normalizeFailedDiagnostics({
454
- status: res.status ?? 1,
455
- stdout: outputText(res.stdout),
456
- stderr: outputText(res.stderr),
419
+ options.emit !== false && options.forceListEmittedFiles === true,
457
420
  });
421
+ return runTsgoBuild(execution, options, args);
458
422
  }
459
423
 
460
- function buildWithSourceTransforms(
424
+ function buildWithNativeCompilerPlugins(
461
425
  options: BuildOptions,
462
426
  execution: ExecutionContext,
427
+ plugins: readonly LoadedNativePlugin[],
463
428
  ): BuildResult {
464
- if (execution.nativeBinary || execution.nativeMode !== "none") {
465
- return {
466
- status: 2,
467
- stdout: "",
468
- stderr:
469
- "ttsc.build: transformSource cannot be combined with native rewrite mode yet\n",
470
- };
471
- }
472
- const materialized = materializeSourceTransformedProject(execution, "build");
473
- try {
474
- const outDir = mapOutDirToMaterializedProject(options.outDir, execution, materialized);
475
- const result = build({
476
- ...options,
477
- cwd: materialized.root,
478
- forceListEmittedFiles: true,
479
- outDir,
480
- plugins: false,
481
- tsconfig: materialized.tsconfig,
482
- });
483
- if (result.status === 0 && options.emit !== false) {
484
- const copied = copyMaterializedEmittedFiles(materialized, result.emittedFiles ?? []);
485
- result.emittedFiles = copied;
486
- if (execution.outputPlugins.length > 0) {
487
- applyBuildPlugins(execution.outputPlugins, execution, copied);
488
- }
489
- }
490
- return result;
491
- } finally {
492
- fs.rmSync(materialized.root, { recursive: true, force: true });
493
- }
429
+ return runNativePluginCommand(
430
+ plugins[0]!,
431
+ createNativeBuildArgs(execution, options, plugins),
432
+ options,
433
+ execution,
434
+ "ttsc.build",
435
+ );
494
436
  }
495
437
 
496
438
  /**
@@ -527,6 +469,36 @@ function runTsgo(
527
469
  });
528
470
  }
529
471
 
472
+ function runTsgoBuild(
473
+ execution: ExecutionContext,
474
+ options: BuildOptions,
475
+ args: readonly string[],
476
+ ): BuildResult {
477
+ const res = spawnBinary(execution.tsgo.binary, args, {
478
+ cwd: execution.projectRoot,
479
+ env: mergeEnv(options.env),
480
+ encoding: "utf8",
481
+ });
482
+ if (res.error) {
483
+ throw new Error(
484
+ "ttsc.build: failed to spawn " +
485
+ execution.tsgo.binary +
486
+ ": " +
487
+ res.error.message,
488
+ );
489
+ }
490
+ const result = {
491
+ status: res.status ?? 1,
492
+ stdout: outputText(res.stdout),
493
+ stderr: outputText(res.stderr),
494
+ };
495
+ const emittedFiles = parseEmittedFiles(result.stdout);
496
+ if (emittedFiles.length !== 0) {
497
+ result.stdout = stripEmittedFileLines(result.stdout);
498
+ }
499
+ return normalizeFailedDiagnostics({ ...result, emittedFiles });
500
+ }
501
+
530
502
  function createTsgoBuildArgs(
531
503
  execution: ExecutionContext,
532
504
  options: BuildOptions,
@@ -550,11 +522,13 @@ function createTsgoBuildArgs(
550
522
  function createNativeBuildArgs(
551
523
  execution: ExecutionContext,
552
524
  options: BuildOptions,
525
+ plugins: readonly LoadedNativePlugin[],
553
526
  ): string[] {
554
527
  const args = [
555
528
  options.emit === false ? "check" : "build",
556
529
  "--tsconfig=" + execution.tsconfig,
557
- "--rewrite-mode=" + execution.nativeMode,
530
+ "--rewrite-mode=" + (options.rewriteMode ?? plugins[0]?.backend.mode ?? "none"),
531
+ "--plugins-json=" + serializeNativePlugins(plugins),
558
532
  "--cwd=" + execution.projectRoot,
559
533
  ];
560
534
  if (options.emit === true) {
@@ -573,6 +547,209 @@ function createNativeBuildArgs(
573
547
  return args;
574
548
  }
575
549
 
550
+ function createNativeCheckArgs(
551
+ execution: ExecutionContext,
552
+ options: BuildOptions | TransformOptions,
553
+ plugin: LoadedNativePlugin,
554
+ ): string[] {
555
+ const args = [
556
+ "check",
557
+ "--tsconfig=" + execution.tsconfig,
558
+ "--rewrite-mode=" + (options.rewriteMode ?? plugin.backend.mode),
559
+ "--plugins-json=" + serializeNativePlugins(execution.nativePlugins),
560
+ "--cwd=" + execution.projectRoot,
561
+ ];
562
+ if ("outDir" in options && options.outDir) {
563
+ args.push("--outDir=" + path.resolve(execution.cwd, options.outDir));
564
+ }
565
+ if ("quiet" in options && options.quiet === false) {
566
+ args.push("--verbose");
567
+ } else if ("quiet" in options && options.quiet === true) {
568
+ args.push("--quiet");
569
+ }
570
+ return args;
571
+ }
572
+
573
+ function createNativeOutputArgs(
574
+ execution: ExecutionContext,
575
+ options: BuildOptions | TransformOptions,
576
+ plugin: LoadedNativePlugin,
577
+ file: string,
578
+ ): string[] {
579
+ const args = [
580
+ "output",
581
+ "--file=" + file,
582
+ "--tsconfig=" + execution.tsconfig,
583
+ "--rewrite-mode=" + (options.rewriteMode ?? plugin.backend.mode),
584
+ "--plugins-json=" + serializeNativePlugins(execution.nativePlugins),
585
+ "--cwd=" + execution.projectRoot,
586
+ ];
587
+ if ("outDir" in options && options.outDir) {
588
+ args.push("--outDir=" + path.resolve(execution.cwd, options.outDir));
589
+ }
590
+ return args;
591
+ }
592
+
593
+ function createNativeTransformArgs(
594
+ execution: ExecutionContext,
595
+ options: TransformOptions,
596
+ sourceFile: string,
597
+ out: string,
598
+ plugins: readonly LoadedNativePlugin[],
599
+ ): string[] {
600
+ return [
601
+ "transform",
602
+ "--file=" + sourceFile,
603
+ "--out=" + out,
604
+ "--tsconfig=" + execution.tsconfig,
605
+ "--rewrite-mode=" + (options.rewriteMode ?? plugins[0]?.backend.mode ?? "none"),
606
+ "--plugins-json=" + serializeNativePlugins(plugins),
607
+ ];
608
+ }
609
+
610
+ function serializeNativePlugins(plugins: readonly LoadedNativePlugin[]): string {
611
+ return JSON.stringify(
612
+ plugins.map((plugin) => ({
613
+ config: plugin.config,
614
+ contractVersion: plugin.backend.contractVersion,
615
+ mode: plugin.backend.mode,
616
+ name: plugin.name,
617
+ })),
618
+ );
619
+ }
620
+
621
+ function runNativeCheckPlugins(
622
+ options: BuildOptions | TransformOptions,
623
+ execution: ExecutionContext,
624
+ ): BuildResult {
625
+ let out: BuildResult = { status: 0, stdout: "", stderr: "" };
626
+ for (const plugin of checkOnlyPlugins(execution.nativePlugins)) {
627
+ const result = runNativePluginCommand(
628
+ plugin,
629
+ createNativeCheckArgs(execution, options, plugin),
630
+ options,
631
+ execution,
632
+ "ttsc.check",
633
+ );
634
+ out = appendBuildOutput(out, result);
635
+ if (result.status !== 0) {
636
+ return out;
637
+ }
638
+ }
639
+ return out;
640
+ }
641
+
642
+ function runNativeCompilerTransform(
643
+ options: TransformOptions,
644
+ execution: ExecutionContext,
645
+ sourceFile: string,
646
+ out: string,
647
+ plugins: readonly LoadedNativePlugin[],
648
+ ): BuildResult {
649
+ return runNativePluginCommand(
650
+ plugins[0]!,
651
+ createNativeTransformArgs(execution, options, sourceFile, out, plugins),
652
+ options,
653
+ execution,
654
+ "ttsc.transform",
655
+ );
656
+ }
657
+
658
+ function applyOutputPlugins(
659
+ options: BuildOptions,
660
+ execution: ExecutionContext,
661
+ emittedFiles: readonly string[],
662
+ plugins: readonly LoadedNativePlugin[],
663
+ ): BuildResult {
664
+ let out: BuildResult = { status: 0, stdout: "", stderr: "" };
665
+ for (const plugin of plugins) {
666
+ for (const file of emittedFiles) {
667
+ if (!fs.existsSync(file)) {
668
+ continue;
669
+ }
670
+ const result = runNativeOutputPlugin(options, execution, plugin, file);
671
+ out = appendBuildOutput(out, result);
672
+ if (result.status !== 0) {
673
+ return out;
674
+ }
675
+ }
676
+ }
677
+ return out;
678
+ }
679
+
680
+ function runNativeOutputPlugin(
681
+ options: BuildOptions | TransformOptions,
682
+ execution: ExecutionContext,
683
+ plugin: LoadedNativePlugin,
684
+ file: string,
685
+ ): BuildResult {
686
+ return runNativePluginCommand(
687
+ plugin,
688
+ createNativeOutputArgs(execution, options, plugin, file),
689
+ options,
690
+ execution,
691
+ "ttsc.output",
692
+ );
693
+ }
694
+
695
+ function runNativePluginCommand(
696
+ plugin: LoadedNativePlugin,
697
+ args: readonly string[],
698
+ options: BuildOptions | TransformOptions,
699
+ execution: ExecutionContext,
700
+ label: string,
701
+ ): BuildResult {
702
+ const binary = plugin.backend.binary;
703
+ if (!binary) {
704
+ return {
705
+ status: 2,
706
+ stdout: "",
707
+ stderr: `${label}: plugin "${plugin.name}" requires a version-matched binary\n`,
708
+ };
709
+ }
710
+ const res = spawnBinary(binary, args, {
711
+ cwd: execution.projectRoot,
712
+ env: mergeEnv(options.env),
713
+ encoding: "utf8",
714
+ });
715
+ if (res.error) {
716
+ throw new Error(
717
+ `${label}: failed to spawn ${binary}: ${res.error.message}`,
718
+ );
719
+ }
720
+ return normalizeFailedDiagnostics({
721
+ status: res.status ?? 1,
722
+ stdout: outputText(res.stdout),
723
+ stderr: outputText(res.stderr),
724
+ });
725
+ }
726
+
727
+ function appendBuildOutput(left: BuildResult, right: BuildResult): BuildResult {
728
+ return normalizeFailedDiagnostics({
729
+ emittedFiles:
730
+ right.emittedFiles !== undefined ? right.emittedFiles : left.emittedFiles,
731
+ status: right.status !== 0 ? right.status : left.status,
732
+ stdout: left.stdout + right.stdout,
733
+ stderr: left.stderr + right.stderr,
734
+ });
735
+ }
736
+
737
+ function assertSingleCompilerHost(plugins: readonly LoadedNativePlugin[]): void {
738
+ const binaries = [
739
+ ...new Set(
740
+ plugins
741
+ .map((plugin) => plugin.backend.binary)
742
+ .filter((binary): binary is string => typeof binary === "string"),
743
+ ),
744
+ ];
745
+ if (binaries.length > 1) {
746
+ throw new Error(
747
+ "ttsc: multiple compiler native backends cannot share one emit pass; " +
748
+ "use output-capability plugins for post-emit transforms",
749
+ );
750
+ }
751
+ }
752
+
576
753
  /** Ask the binary for its version banner. Handy for user-agent strings. */
577
754
  export function version(options: CommonOptions = {}): string {
578
755
  const tsgo = resolveTsgo(options);
@@ -588,9 +765,9 @@ export function version(options: CommonOptions = {}): string {
588
765
  }
589
766
 
590
767
  /**
591
- * Promise-facing variant of `transform()`. The plugin host keeps the transform
592
- * pipeline synchronous so plugin modules can stay dependency-free, but many
593
- * adapter surfaces still prefer a Promise-returning function.
768
+ * Promise-facing variant of `transform()`. The host path stays synchronous so
769
+ * plugin descriptors can stay dependency-free, but many adapter surfaces still
770
+ * prefer a Promise-returning function.
594
771
  */
595
772
  export function transformAsync(options: TransformOptions): Promise<string> {
596
773
  return Promise.resolve().then(() => transform(options));
@@ -599,11 +776,8 @@ export function transformAsync(options: TransformOptions): Promise<string> {
599
776
  interface ExecutionContext {
600
777
  compilerOptions: Record<string, unknown>;
601
778
  cwd: string;
602
- nativeBinary: string | null;
603
- nativeMode: string;
604
- outputPlugins: readonly TtscPlugin[];
779
+ nativePlugins: readonly LoadedNativePlugin[];
605
780
  projectRoot: string;
606
- sourcePlugins: readonly TtscPlugin[];
607
781
  tsgo: ResolvedTsgo;
608
782
  tsconfig: string;
609
783
  }
@@ -628,58 +802,13 @@ function resolveExecutionContext(
628
802
  return {
629
803
  compilerOptions: loaded.project.compilerOptions,
630
804
  cwd,
631
- nativeBinary: loaded.nativeBinary ?? null,
632
- nativeMode: options.rewriteMode ?? loaded.nativeMode,
633
- outputPlugins: loaded.plugins.filter((plugin) => plugin.transformOutput),
805
+ nativePlugins: loaded.nativePlugins,
634
806
  projectRoot,
635
- sourcePlugins: loaded.plugins.filter((plugin) => plugin.transformSource),
636
807
  tsgo,
637
808
  tsconfig,
638
809
  };
639
810
  }
640
811
 
641
- function finalizeTransformText(
642
- plugins: readonly TtscPlugin[],
643
- context: Omit<Parameters<typeof applyPluginTransforms>[1], "code">,
644
- text: string,
645
- ): string {
646
- if (plugins.length === 0) {
647
- return text;
648
- }
649
- return applyPluginTransforms(plugins, {
650
- ...context,
651
- code: text,
652
- });
653
- }
654
-
655
- function applyBuildPlugins(
656
- plugins: readonly TtscPlugin[],
657
- execution: ExecutionContext,
658
- emittedFiles: readonly string[],
659
- ): void {
660
- if (plugins.length === 0) {
661
- return;
662
- }
663
- for (const file of emittedFiles) {
664
- if (!isJavaScriptOutput(file) || !fs.existsSync(file)) {
665
- continue;
666
- }
667
- const current = fs.readFileSync(file, "utf8");
668
- const next = applyPluginTransforms(plugins, {
669
- code: current,
670
- command: "build",
671
- compilerOptions: execution.compilerOptions,
672
- cwd: execution.cwd,
673
- outputFile: file,
674
- projectRoot: execution.projectRoot,
675
- tsconfig: execution.tsconfig,
676
- });
677
- if (next !== current) {
678
- fs.writeFileSync(file, next, "utf8");
679
- }
680
- }
681
- }
682
-
683
812
  function isJavaScriptOutput(file: string): boolean {
684
813
  return /\.(?:[cm]?js)$/i.test(file);
685
814
  }
@@ -788,490 +917,3 @@ function readOwnPackageVersion(): string {
788
917
  return "0.0.0";
789
918
  }
790
919
  }
791
-
792
- interface MaterializedProject {
793
- mappers: Map<string, SourcePositionMapper>;
794
- originalRoot: string;
795
- root: string;
796
- tsconfig: string;
797
- }
798
-
799
- interface SourcePositionMapper {
800
- finalCode: string;
801
- originalCode: string;
802
- stages: readonly TtscSourceTransformStage[];
803
- }
804
-
805
- function materializeSourceTransformedProject(
806
- execution: ExecutionContext,
807
- command: "build" | "transform",
808
- onlySourceFile?: string,
809
- ): MaterializedProject {
810
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "ttsc-source-"));
811
- const materialized: MaterializedProject = {
812
- mappers: new Map(),
813
- originalRoot: execution.projectRoot,
814
- root,
815
- tsconfig: path.join(root, path.relative(execution.projectRoot, execution.tsconfig)),
816
- };
817
- copyProjectForSourceTransforms(execution.projectRoot, root, execution.compilerOptions);
818
- const files = onlySourceFile
819
- ? [toMaterializedPath(onlySourceFile, materialized)]
820
- : listSourceTransformFiles(root);
821
- for (const tempFile of files) {
822
- if (!fs.existsSync(tempFile) || !fs.statSync(tempFile).isFile()) {
823
- continue;
824
- }
825
- const originalFile = fromMaterializedPath(tempFile, materialized);
826
- const current = fs.readFileSync(tempFile, "utf8");
827
- const transformed = applyPluginSourceTransformsWithMap(execution.sourcePlugins, {
828
- code: current,
829
- command,
830
- compilerOptions: execution.compilerOptions,
831
- cwd: execution.cwd,
832
- projectRoot: execution.projectRoot,
833
- sourceFile: originalFile,
834
- tsconfig: execution.tsconfig,
835
- });
836
- const next = transformed.code;
837
- if (next !== current) {
838
- fs.writeFileSync(tempFile, next, "utf8");
839
- }
840
- if (transformed.stages.length > 0) {
841
- materialized.mappers.set(filepathKey(tempFile), {
842
- finalCode: next,
843
- originalCode: current,
844
- stages: transformed.stages,
845
- });
846
- }
847
- }
848
- return materialized;
849
- }
850
-
851
- function copyProjectForSourceTransforms(
852
- source: string,
853
- target: string,
854
- compilerOptions: Record<string, unknown>,
855
- ): void {
856
- const excluded = new Set([".git", "node_modules"]);
857
- const outDir = typeof compilerOptions.outDir === "string" ? compilerOptions.outDir : "";
858
- const resolvedOutDir = outDir ? path.resolve(outDir) : "";
859
- copyDirectoryFiltered(source, target, (entry) => {
860
- if (excluded.has(path.basename(entry))) {
861
- return false;
862
- }
863
- if (resolvedOutDir && isPathInside(entry, resolvedOutDir)) {
864
- return false;
865
- }
866
- return true;
867
- });
868
- const nodeModules = path.join(source, "node_modules");
869
- if (fs.existsSync(nodeModules)) {
870
- const targetNodeModules = path.join(target, "node_modules");
871
- try {
872
- fs.symlinkSync(nodeModules, targetNodeModules, "junction");
873
- } catch {
874
- copyDirectoryFiltered(nodeModules, targetNodeModules, () => true);
875
- }
876
- }
877
- }
878
-
879
- function copyDirectoryFiltered(
880
- source: string,
881
- target: string,
882
- shouldCopy: (entry: string) => boolean,
883
- ): void {
884
- if (!shouldCopy(source)) {
885
- return;
886
- }
887
- const stat = fs.lstatSync(source);
888
- if (stat.isSymbolicLink()) {
889
- const link = fs.readlinkSync(source);
890
- fs.symlinkSync(link, target);
891
- return;
892
- }
893
- if (stat.isDirectory()) {
894
- fs.mkdirSync(target, { recursive: true });
895
- for (const entry of fs.readdirSync(source)) {
896
- copyDirectoryFiltered(path.join(source, entry), path.join(target, entry), shouldCopy);
897
- }
898
- return;
899
- }
900
- if (stat.isFile()) {
901
- fs.mkdirSync(path.dirname(target), { recursive: true });
902
- fs.copyFileSync(source, target);
903
- }
904
- }
905
-
906
- function listSourceTransformFiles(root: string): string[] {
907
- const out: string[] = [];
908
- const stack = [root];
909
- while (stack.length !== 0) {
910
- const current = stack.pop()!;
911
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
912
- if (entry.name === "node_modules" || entry.name === ".git") {
913
- continue;
914
- }
915
- const next = path.join(current, entry.name);
916
- if (entry.isDirectory()) {
917
- stack.push(next);
918
- } else if (entry.isFile() && isTransformableSource(next)) {
919
- out.push(next);
920
- }
921
- }
922
- }
923
- return out;
924
- }
925
-
926
- function isTransformableSource(file: string): boolean {
927
- return /\.(?:[cm]?tsx?)$/i.test(file) && !/\.d\.[cm]?ts$/i.test(file);
928
- }
929
-
930
- function mapOutDirToMaterializedProject(
931
- outDir: string | undefined,
932
- execution: ExecutionContext,
933
- materialized: MaterializedProject,
934
- ): string | undefined {
935
- if (!outDir) {
936
- return undefined;
937
- }
938
- const resolved = path.resolve(execution.cwd, outDir);
939
- if (isPathInside(resolved, execution.projectRoot)) {
940
- return toMaterializedPath(resolved, materialized);
941
- }
942
- return path.join(materialized.root, ".ttsc-out");
943
- }
944
-
945
- function copyMaterializedEmittedFiles(
946
- materialized: MaterializedProject,
947
- emittedFiles: readonly string[],
948
- ): string[] {
949
- const copied: string[] = [];
950
- for (const emitted of emittedFiles) {
951
- if (!isPathInside(emitted, materialized.root)) {
952
- continue;
953
- }
954
- const target = fromMaterializedPath(emitted, materialized);
955
- fs.mkdirSync(path.dirname(target), { recursive: true });
956
- if (/\.map$/i.test(emitted)) {
957
- fs.writeFileSync(
958
- target,
959
- patchSourceMapText(fs.readFileSync(emitted, "utf8"), materialized, target),
960
- "utf8",
961
- );
962
- } else {
963
- fs.copyFileSync(emitted, target);
964
- }
965
- copied.push(target);
966
- }
967
- return copied;
968
- }
969
-
970
- function toMaterializedPath(file: string, materialized: MaterializedProject): string {
971
- const relative = path.relative(materialized.originalRoot, file);
972
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
973
- return file;
974
- }
975
- return path.join(materialized.root, relative);
976
- }
977
-
978
- function fromMaterializedPath(file: string, materialized: MaterializedProject): string {
979
- const relative = path.relative(materialized.root, file);
980
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
981
- return file;
982
- }
983
- return path.join(materialized.originalRoot, relative);
984
- }
985
-
986
- function isPathInside(file: string, parent: string): boolean {
987
- const relative = path.relative(parent, file);
988
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
989
- }
990
-
991
- function filepathKey(file: string): string {
992
- return path.resolve(file).replace(/\\/g, "/");
993
- }
994
-
995
- function patchSourceMapText(
996
- text: string,
997
- materialized: MaterializedProject,
998
- outputFile: string,
999
- tempOutputFile = toMaterializedPath(outputFile, materialized),
1000
- ): string {
1001
- let map: {
1002
- sourceRoot?: string;
1003
- sources?: string[];
1004
- sourcesContent?: string[];
1005
- [key: string]: unknown;
1006
- };
1007
- try {
1008
- map = JSON.parse(text);
1009
- } catch {
1010
- return text;
1011
- }
1012
- if (!Array.isArray(map.sources)) {
1013
- return text;
1014
- }
1015
- const tempMapDir = path.dirname(tempOutputFile);
1016
- const originalMapDir = path.dirname(outputFile);
1017
- const nextSources: string[] = [];
1018
- const nextSourcesContent = Array.isArray(map.sourcesContent)
1019
- ? [...map.sourcesContent]
1020
- : undefined;
1021
- const sourceMappings = new Map<number, SourcePositionMapper>();
1022
- map.sources.forEach((source, index) => {
1023
- const tempSource = path.isAbsolute(source)
1024
- ? source
1025
- : path.resolve(tempMapDir, source);
1026
- if (isPathInside(tempSource, materialized.root)) {
1027
- const originalSource = fromMaterializedPath(tempSource, materialized);
1028
- const mapper = materialized.mappers.get(filepathKey(tempSource));
1029
- if (mapper) {
1030
- sourceMappings.set(index, mapper);
1031
- }
1032
- nextSources.push(relativeSourceMapPath(originalMapDir, originalSource));
1033
- if (nextSourcesContent) {
1034
- try {
1035
- nextSourcesContent[index] = fs.readFileSync(originalSource, "utf8");
1036
- } catch {
1037
- /* keep emitted sourcesContent */
1038
- }
1039
- }
1040
- } else {
1041
- nextSources.push(source);
1042
- }
1043
- });
1044
- if (sourceMappings.size > 0 && typeof map.mappings === "string") {
1045
- map.mappings = remapSourceMapMappings(map.mappings, sourceMappings);
1046
- }
1047
- map.sources = nextSources;
1048
- if (nextSourcesContent) {
1049
- map.sourcesContent = nextSourcesContent;
1050
- }
1051
- delete map.sourceRoot;
1052
- return JSON.stringify(map);
1053
- }
1054
-
1055
- function relativeSourceMapPath(fromDir: string, file: string): string {
1056
- const relative = path.relative(fromDir, file).replace(/\\/g, "/");
1057
- return relative.startsWith(".") ? relative : `./${relative}`;
1058
- }
1059
-
1060
- function rewriteSourceMapReference(code: string, mapFileName: string): string {
1061
- const reference = `//# sourceMappingURL=${mapFileName}`;
1062
- if (/\/\/# sourceMappingURL=.*(?:\r?\n)?$/m.test(code)) {
1063
- return code.replace(/\/\/# sourceMappingURL=.*(?:\r?\n)?$/m, `${reference}\n`);
1064
- }
1065
- return code.endsWith("\n") ? `${code}${reference}\n` : `${code}\n${reference}\n`;
1066
- }
1067
-
1068
- function remapSourceMapMappings(
1069
- mappings: string,
1070
- sourceMappings: Map<number, SourcePositionMapper>,
1071
- ): string {
1072
- const decoded = decodeMappings(mappings);
1073
- for (const line of decoded) {
1074
- for (const segment of line) {
1075
- if (segment.length < 4) {
1076
- continue;
1077
- }
1078
- const mapper = sourceMappings.get(segment[1]!);
1079
- if (!mapper) {
1080
- continue;
1081
- }
1082
- const mapped = mapTransformedPositionToOriginal(mapper, {
1083
- column: segment[3]!,
1084
- line: segment[2]!,
1085
- });
1086
- segment[2] = mapped.line;
1087
- segment[3] = mapped.column;
1088
- }
1089
- }
1090
- return encodeMappings(decoded);
1091
- }
1092
-
1093
- function mapTransformedPositionToOriginal(
1094
- mapper: SourcePositionMapper,
1095
- position: { line: number; column: number },
1096
- ): { line: number; column: number } {
1097
- let offset = lineColumnToOffset(mapper.finalCode, position.line, position.column);
1098
- for (let index = mapper.stages.length - 1; index >= 0; index -= 1) {
1099
- offset = mapOffsetToPreviousStage(offset, mapper.stages[index]!);
1100
- }
1101
- return offsetToLineColumn(mapper.originalCode, offset);
1102
- }
1103
-
1104
- function mapOffsetToPreviousStage(
1105
- offset: number,
1106
- stage: TtscSourceTransformStage,
1107
- ): number {
1108
- let delta = 0;
1109
- for (const edit of stage.edits) {
1110
- if (offset < edit.newStart) {
1111
- return clampOffset(offset - delta, stage.before.length);
1112
- }
1113
- if (offset < edit.newEnd) {
1114
- return edit.start;
1115
- }
1116
- delta += edit.code.length - (edit.end - edit.start);
1117
- }
1118
- return clampOffset(offset - delta, stage.before.length);
1119
- }
1120
-
1121
- function lineColumnToOffset(source: string, line: number, column: number): number {
1122
- const starts = lineStarts(source);
1123
- const start = starts[Math.min(Math.max(line, 0), starts.length - 1)] ?? 0;
1124
- return clampOffset(start + Math.max(column, 0), source.length);
1125
- }
1126
-
1127
- function offsetToLineColumn(
1128
- source: string,
1129
- offset: number,
1130
- ): { line: number; column: number } {
1131
- const starts = lineStarts(source);
1132
- const clamped = clampOffset(offset, source.length);
1133
- let low = 0;
1134
- let high = starts.length - 1;
1135
- while (low <= high) {
1136
- const mid = (low + high) >> 1;
1137
- const start = starts[mid]!;
1138
- const next = starts[mid + 1] ?? Number.POSITIVE_INFINITY;
1139
- if (clamped < start) {
1140
- high = mid - 1;
1141
- } else if (clamped >= next) {
1142
- low = mid + 1;
1143
- } else {
1144
- return { column: clamped - start, line: mid };
1145
- }
1146
- }
1147
- const last = starts.length - 1;
1148
- return { column: clamped - (starts[last] ?? 0), line: last };
1149
- }
1150
-
1151
- function lineStarts(source: string): number[] {
1152
- const starts = [0];
1153
- for (let index = 0; index < source.length; index += 1) {
1154
- const ch = source.charCodeAt(index);
1155
- if (ch === 13 /* \r */) {
1156
- if (source.charCodeAt(index + 1) === 10 /* \n */) {
1157
- index += 1;
1158
- }
1159
- starts.push(index + 1);
1160
- } else if (ch === 10 /* \n */) {
1161
- starts.push(index + 1);
1162
- }
1163
- }
1164
- return starts;
1165
- }
1166
-
1167
- function clampOffset(offset: number, length: number): number {
1168
- return Math.min(Math.max(offset, 0), length);
1169
- }
1170
-
1171
- type SourceMapSegment = number[];
1172
- type DecodedMappings = SourceMapSegment[][];
1173
-
1174
- const BASE64_CHARS =
1175
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1176
- const BASE64_VALUES = new Map(
1177
- [...BASE64_CHARS].map((char, index) => [char, index] as const),
1178
- );
1179
-
1180
- function decodeMappings(mappings: string): DecodedMappings {
1181
- const lines: DecodedMappings = [];
1182
- let line: SourceMapSegment[] = [];
1183
- let segment: SourceMapSegment = [];
1184
- let index = 0;
1185
- const state = [0, 0, 0, 0, 0];
1186
- while (index < mappings.length) {
1187
- const char = mappings[index]!;
1188
- if (char === ";") {
1189
- if (segment.length > 0) {
1190
- line.push(segment);
1191
- segment = [];
1192
- }
1193
- lines.push(line);
1194
- line = [];
1195
- state[0] = 0;
1196
- index += 1;
1197
- continue;
1198
- }
1199
- if (char === ",") {
1200
- if (segment.length > 0) {
1201
- line.push(segment);
1202
- segment = [];
1203
- }
1204
- index += 1;
1205
- continue;
1206
- }
1207
- const read = readVlq(mappings, index);
1208
- index = read.next;
1209
- const field = segment.length;
1210
- state[field] = (state[field] ?? 0) + read.value;
1211
- segment.push(state[field]!);
1212
- }
1213
- if (segment.length > 0) {
1214
- line.push(segment);
1215
- }
1216
- lines.push(line);
1217
- return lines;
1218
- }
1219
-
1220
- function encodeMappings(lines: DecodedMappings): string {
1221
- const state = [0, 0, 0, 0, 0];
1222
- return lines
1223
- .map((line) => {
1224
- state[0] = 0;
1225
- return line
1226
- .map((segment) =>
1227
- segment
1228
- .map((value, index) => {
1229
- const relative = value - (state[index] ?? 0);
1230
- state[index] = value;
1231
- return writeVlq(relative);
1232
- })
1233
- .join(""),
1234
- )
1235
- .join(",");
1236
- })
1237
- .join(";");
1238
- }
1239
-
1240
- function readVlq(input: string, start: number): { next: number; value: number } {
1241
- let index = start;
1242
- let shift = 0;
1243
- let value = 0;
1244
- while (index < input.length) {
1245
- const digit = BASE64_VALUES.get(input[index]!);
1246
- if (digit === undefined) {
1247
- throw new Error(`ttsc: invalid source map VLQ digit ${input[index]}`);
1248
- }
1249
- index += 1;
1250
- const continuation = (digit & 32) !== 0;
1251
- value += (digit & 31) << shift;
1252
- shift += 5;
1253
- if (!continuation) {
1254
- const negative = (value & 1) === 1;
1255
- const decoded = value >> 1;
1256
- return { next: index, value: negative ? -decoded : decoded };
1257
- }
1258
- }
1259
- throw new Error("ttsc: unterminated source map VLQ segment");
1260
- }
1261
-
1262
- function writeVlq(value: number): string {
1263
- let vlq = Math.abs(value) << 1;
1264
- if (value < 0) {
1265
- vlq |= 1;
1266
- }
1267
- let out = "";
1268
- do {
1269
- let digit = vlq & 31;
1270
- vlq >>>= 5;
1271
- if (vlq > 0) {
1272
- digit |= 32;
1273
- }
1274
- out += BASE64_CHARS[digit]!;
1275
- } while (vlq > 0);
1276
- return out;
1277
- }