ttsc 0.4.3 → 0.4.4-dev.20260426

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api.ts CHANGED
@@ -23,7 +23,9 @@ import * as path from "node:path";
23
23
 
24
24
  import { type ResolveOptions, resolveBinary } from "./platform";
25
25
  import {
26
+ type TtscSourceTransformStage,
26
27
  type TtscPlugin,
28
+ applyPluginSourceTransformsWithMap,
27
29
  applyPluginTransforms,
28
30
  loadProjectPlugins,
29
31
  } from "./plugin";
@@ -88,6 +90,8 @@ export interface BuildOptions extends CommonOptions {
88
90
  quiet?: boolean;
89
91
  /** @internal Caller already ran diagnostics and accepts responsibility. */
90
92
  skipDiagnosticsCheck?: boolean;
93
+ /** @internal Force `tsgo --listEmittedFiles` even when no output plugin runs. */
94
+ forceListEmittedFiles?: boolean;
91
95
  }
92
96
 
93
97
  /** Options for `check()`. */
@@ -168,6 +172,9 @@ export function transform(options: TransformOptions): string {
168
172
  ? options.file
169
173
  : path.resolve(options.cwd ?? process.cwd(), options.file),
170
174
  );
175
+ if (execution.sourcePlugins.length > 0) {
176
+ return transformWithSourceTransforms(options, execution, sourceFile);
177
+ }
171
178
  if (execution.nativeBinary) {
172
179
  return transformWithNativeBinary(options, execution, sourceFile);
173
180
  }
@@ -199,9 +206,10 @@ export function transform(options: TransformOptions): string {
199
206
  throw new Error(`ttsc.transform: no output produced for ${sourceFile}`);
200
207
  }
201
208
  const transformed = finalizeTransformText(
202
- execution.plugins,
209
+ execution.outputPlugins,
203
210
  {
204
211
  command: "transform",
212
+ compilerOptions: execution.compilerOptions,
205
213
  cwd: execution.cwd,
206
214
  projectRoot: execution.projectRoot,
207
215
  sourceFile,
@@ -250,9 +258,10 @@ function transformWithNativeBinary(
250
258
  );
251
259
  }
252
260
  const transformed = finalizeTransformText(
253
- execution.plugins,
261
+ execution.outputPlugins,
254
262
  {
255
263
  command: "transform",
264
+ compilerOptions: execution.compilerOptions,
256
265
  cwd: execution.cwd,
257
266
  projectRoot: execution.projectRoot,
258
267
  sourceFile,
@@ -267,6 +276,83 @@ function transformWithNativeBinary(
267
276
  return transformed;
268
277
  }
269
278
 
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");
295
+ 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,
325
+ sourceFile,
326
+ 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",
344
+ );
345
+ transformed = rewriteSourceMapReference(transformed, path.basename(outMap));
346
+ }
347
+ fs.mkdirSync(path.dirname(options.out), { recursive: true });
348
+ fs.writeFileSync(options.out, transformed, "utf8");
349
+ }
350
+ return transformed;
351
+ } finally {
352
+ fs.rmSync(materialized.root, { recursive: true, force: true });
353
+ }
354
+ }
355
+
270
356
  function realpathIfExists(file: string): string {
271
357
  try {
272
358
  return fs.realpathSync(file);
@@ -277,6 +363,7 @@ function realpathIfExists(file: string): string {
277
363
 
278
364
  /** Result of `build()`. Non-zero `status` means the build failed. */
279
365
  export interface BuildResult {
366
+ emittedFiles?: string[];
280
367
  status: number;
281
368
  stdout: string;
282
369
  stderr: string;
@@ -289,6 +376,9 @@ export interface BuildResult {
289
376
  */
290
377
  export function build(options: BuildOptions = {}): BuildResult {
291
378
  const execution = resolveExecutionContext(options);
379
+ if (execution.sourcePlugins.length > 0) {
380
+ return buildWithSourceTransforms(options, execution);
381
+ }
292
382
  if (execution.nativeBinary) {
293
383
  return buildWithNativeBinary(options, execution);
294
384
  }
@@ -310,7 +400,9 @@ export function build(options: BuildOptions = {}): BuildResult {
310
400
  }
311
401
 
312
402
  const args = createTsgoBuildArgs(execution, options, {
313
- listEmittedFiles: options.emit !== false && execution.plugins.length > 0,
403
+ listEmittedFiles:
404
+ options.emit !== false &&
405
+ (execution.outputPlugins.length > 0 || options.forceListEmittedFiles === true),
314
406
  });
315
407
  const res = spawnBinary(execution.tsgo.binary, args, {
316
408
  cwd: execution.projectRoot,
@@ -334,10 +426,10 @@ export function build(options: BuildOptions = {}): BuildResult {
334
426
  if (emittedFiles.length !== 0) {
335
427
  result.stdout = stripEmittedFileLines(result.stdout);
336
428
  }
337
- if (result.status === 0 && options.emit !== false && execution.plugins.length > 0) {
338
- applyBuildPlugins(execution.plugins, execution, emittedFiles);
429
+ if (result.status === 0 && options.emit !== false && execution.outputPlugins.length > 0) {
430
+ applyBuildPlugins(execution.outputPlugins, execution, emittedFiles);
339
431
  }
340
- return normalizeFailedDiagnostics(result);
432
+ return normalizeFailedDiagnostics({ ...result, emittedFiles });
341
433
  }
342
434
 
343
435
  function buildWithNativeBinary(
@@ -365,6 +457,42 @@ function buildWithNativeBinary(
365
457
  });
366
458
  }
367
459
 
460
+ function buildWithSourceTransforms(
461
+ options: BuildOptions,
462
+ execution: ExecutionContext,
463
+ ): 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
+ }
494
+ }
495
+
368
496
  /**
369
497
  * Run `ttsc check` (build without emit) — CI gate / pre-commit hook use.
370
498
  * Resolves with an exit-code record; does not throw.
@@ -469,11 +597,13 @@ export function transformAsync(options: TransformOptions): Promise<string> {
469
597
  }
470
598
 
471
599
  interface ExecutionContext {
600
+ compilerOptions: Record<string, unknown>;
472
601
  cwd: string;
473
602
  nativeBinary: string | null;
474
603
  nativeMode: string;
475
- plugins: readonly TtscPlugin[];
604
+ outputPlugins: readonly TtscPlugin[];
476
605
  projectRoot: string;
606
+ sourcePlugins: readonly TtscPlugin[];
477
607
  tsgo: ResolvedTsgo;
478
608
  tsconfig: string;
479
609
  }
@@ -496,11 +626,13 @@ function resolveExecutionContext(
496
626
  tsconfig,
497
627
  });
498
628
  return {
629
+ compilerOptions: loaded.project.compilerOptions,
499
630
  cwd,
500
631
  nativeBinary: loaded.nativeBinary ?? null,
501
632
  nativeMode: options.rewriteMode ?? loaded.nativeMode,
502
- plugins: loaded.plugins.filter((plugin) => plugin.transformOutput),
633
+ outputPlugins: loaded.plugins.filter((plugin) => plugin.transformOutput),
503
634
  projectRoot,
635
+ sourcePlugins: loaded.plugins.filter((plugin) => plugin.transformSource),
504
636
  tsgo,
505
637
  tsconfig,
506
638
  };
@@ -536,6 +668,7 @@ function applyBuildPlugins(
536
668
  const next = applyPluginTransforms(plugins, {
537
669
  code: current,
538
670
  command: "build",
671
+ compilerOptions: execution.compilerOptions,
539
672
  cwd: execution.cwd,
540
673
  outputFile: file,
541
674
  projectRoot: execution.projectRoot,
@@ -578,6 +711,7 @@ function normalizeFailedDiagnostics(result: BuildResult): BuildResult {
578
711
  return result;
579
712
  }
580
713
  return {
714
+ emittedFiles: result.emittedFiles,
581
715
  status: result.status,
582
716
  stdout: "",
583
717
  stderr: result.stdout,
@@ -654,3 +788,490 @@ function readOwnPackageVersion(): string {
654
788
  return "0.0.0";
655
789
  }
656
790
  }
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
+ }