ttsc 0.4.3 → 0.4.4
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/lib/api.d.ts +1 -0
- package/lib/api.js +508 -7
- package/lib/api.js.map +1 -1
- package/lib/plugin.d.ts +34 -0
- package/lib/plugin.js +96 -0
- package/lib/plugin.js.map +1 -1
- package/lib/project.d.ts +1 -1
- package/lib/project.js +7 -1
- package/lib/project.js.map +1 -1
- package/package.json +8 -8
- package/src/api.ts +629 -8
- package/src/plugin.ts +155 -0
- package/src/project.ts +10 -3
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.
|
|
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.
|
|
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:
|
|
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.
|
|
338
|
-
applyBuildPlugins(execution.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|