ttsc
Version:
General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.
162 lines • 6.62 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resolveEmittedJavaScript = resolveEmittedJavaScript;
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const paths_1 = require("./paths");
/**
* Locate the JavaScript file emitted for a TypeScript source file.
*
* Resolution strategy:
*
* 1. Try to derive the exact output path by mirroring the source's relative
* position inside `projectRoot` into `outDir`, applying the correct JS
* extension (`.js` / `.jsx` / `.mjs` / `.cjs`). Use this path if it exists
* on disk.
* 2. Fall back to scoring each candidate in `emittedFiles` (or a recursive
* directory scan of `outDir`) by the number of trailing path-stem segments
* shared with the source file name, and pick the highest-scoring existing
* file.
*
* Returns `null` when no matching output file is found on disk.
*/
function resolveEmittedJavaScript(options) {
const exact = resolveExactEmittedFiles(options.outDir, options.projectRoot, options.sourceFile);
const emitted = new Set(options.emittedFiles?.map((file) => emittedPathKey(file)) ?? []);
for (const candidate of exact) {
if (emitted.has(emittedPathKey(candidate)) && node_fs_1.default.existsSync(candidate)) {
return candidate;
}
}
for (const candidate of exact) {
if (node_fs_1.default.existsSync(candidate)) {
return candidate;
}
}
// Score the pre-computed emit list first (cheap). When it yields nothing —
// because the list is incomplete (a native transform host such as typia emits
// without printing the `--listEmittedFiles` lines) or because the emit landed
// at a path the exact mirror did not predict (tsgo shifts every output path
// when the program pulls a raw-`.ts` dependency that sits outside `rootDir`,
// so it strips the common source root rather than `rootDir`) — fall back to a
// full recursive scan of `outDir`. Trailing-stem scoring still pins the right
// file regardless of how deep the shifted prefix is.
const primary = bestStemMatch(options.emittedFiles ?? listJavaScriptFiles(options.outDir), options.sourceFile);
if (primary !== null && node_fs_1.default.existsSync(primary)) {
return primary;
}
if (options.emittedFiles !== undefined) {
const fromDir = bestStemMatch(listJavaScriptFiles(options.outDir), options.sourceFile);
if (fromDir !== null && node_fs_1.default.existsSync(fromDir)) {
return fromDir;
}
}
return null;
}
/** Highest trailing-stem-scoring JavaScript output among `files`, or `null`. */
function bestStemMatch(files, sourceFile) {
let best = null;
let bestScore = 0;
for (const file of files) {
if (!isJavaScriptOutput(file))
continue;
const score = sharedSourceStemSegments(file, sourceFile);
if (score > bestScore) {
best = file;
bestScore = score;
}
}
return best;
}
/**
* Derive the exact output path for `sourceFile` by mirroring its position
* relative to `projectRoot` into `outDir`. Returns no candidates when the
* source is not inside the project root or when the path cannot be determined.
*/
function resolveExactEmittedFiles(outDir, projectRoot, sourceFile) {
const relative = node_path_1.default.relative(projectRoot, sourceFile);
if (relative === "" || (0, paths_1.isOutsideRelativePath)(relative)) {
return [];
}
const stem = relative.slice(0, relative.length - node_path_1.default.extname(relative).length);
return emittedJavaScriptExtensions(sourceFile).map((extension) => node_path_1.default.resolve(outDir, stem + extension));
}
/**
* Recursively enumerate every JavaScript output file under `root`. Uses an
* explicit stack instead of recursion to avoid call-stack overflow on deep
* directory trees. Non-existent roots are silently skipped.
*/
function listJavaScriptFiles(root) {
const out = [];
const stack = [root];
while (stack.length !== 0) {
const current = stack.pop();
if (!node_fs_1.default.existsSync(current))
continue;
for (const entry of node_fs_1.default.readdirSync(current, { withFileTypes: true })) {
const next = node_path_1.default.join(current, entry.name);
if (entry.isDirectory()) {
stack.push(next);
}
else if (entry.isFile() && isJavaScriptOutput(next)) {
out.push(node_path_1.default.resolve(next));
}
}
}
return out;
}
/**
* Count the number of consecutive trailing path-stem segments that `outPath`
* and `srcPath` share when both are stripped of their extensions and normalised
* to forward slashes.
*
* Example: `dist/lib/foo.js` vs `src/lib/foo.ts` → 2 (`lib`, `foo`).
*/
function sharedSourceStemSegments(outPath, srcPath) {
const stripExtAndSplit = (location) => {
const normalized = location.replace(/\\/g, "/");
return normalized
.slice(0, normalized.length - node_path_1.default.extname(normalized).length)
.split("/");
};
const a = stripExtAndSplit(outPath);
const b = stripExtAndSplit(srcPath);
const count = Math.min(a.length, b.length);
let shared = 0;
for (let i = 1; i <= count; i += 1) {
if (a[a.length - i] !== b[b.length - i])
break;
shared += 1;
}
return shared;
}
/**
* Map a source extension to every JavaScript output counterpart tsgo can use.
* JSX preserve mode writes `.tsx`/`.jsx` inputs as `.jsx`; all other JSX modes
* write `.js`.
*/
function emittedJavaScriptExtensions(filename) {
switch (node_path_1.default.extname(filename).toLowerCase()) {
case ".mts":
return [".mjs"];
case ".cts":
return [".cjs"];
case ".tsx":
case ".jsx":
return [".js", ".jsx"];
default:
return [".js"];
}
}
/** Return true when `filename` has a JavaScript output extension. */
function isJavaScriptOutput(filename) {
return /\.(?:[cm]?js|jsx)$/i.test(filename);
}
function emittedPathKey(filename) {
const resolved = node_path_1.default.resolve(filename);
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
}
//# sourceMappingURL=resolveEmittedJavaScript.js.map