ttsc
Version:
General-purpose TypeScript-Go compiler, runtime, plugin host, and LSP host.
368 lines • 16.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.transformProjectInMemory = transformProjectInMemory;
const node_path_1 = __importDefault(require("node:path"));
const loadProjectPlugins_1 = require("../../plugin/internal/loadProjectPlugins");
const buildNativeCompiler_1 = require("./buildNativeCompiler");
const paths_1 = require("./paths");
const createNativeProjectContextArgs_1 = require("./project/createNativeProjectContextArgs");
const readProjectConfig_1 = require("./project/readProjectConfig");
const resolveBinary_1 = require("./resolveBinary");
const resolveTsgo_1 = require("./resolveTsgo");
const runBuild_1 = require("./runBuild");
const sharedHostHelpers_1 = require("./sharedHostHelpers");
const spawnNative_1 = require("./spawnNative");
/**
* Transform a project and capture TypeScript source output in memory.
*
* When no plugins are configured the fast path spawns the native ttsc compiler
* host (`cmd/ttsc api-transform`) which returns a JSON map of transformed
* TypeScript sources. When plugins are present:
*
* 1. Check-stage plugins run first and abort on failure.
* 2. If there are no transform-stage plugins the host is used as the transformer.
* 3. If transform plugins exist they are dispatched through the shared-host binary
* with linked plugins passed via `TTSC_LINKED_PLUGINS_JSON`.
*
* @returns A `{ result, typescript }` pair where `typescript` maps output paths
* to their transformed TypeScript source text.
*/
function transformProjectInMemory(options) {
const cwd = node_path_1.default.resolve(options.cwd ?? process.cwd());
const project = (0, readProjectConfig_1.readProjectConfig)({
cwd,
projectRoot: options.projectRoot,
tsconfig: options.tsconfig,
});
if (hasConfiguredPlugins(options, project)) {
return transformProjectWithPlugins(options, cwd, project);
}
return transformProjectWithNativeHost(options, project);
}
/** Return true when the project or the call-level options declare any plugins. */
function hasConfiguredPlugins(options, project) {
return (0, loadProjectPlugins_1.hasProjectPluginEntries)(project, options.plugins);
}
/**
* Transform via the built-in native compiler host (`cmd/ttsc api-transform`).
* Used when no user plugins are configured, or as the fallback transformer when
* check-stage plugins pass and no transform-stage plugins are declared.
*/
function transformProjectWithNativeHost(options, project) {
const binary = (0, buildNativeCompiler_1.buildNativeCompiler)({
cacheBaseDir: project.root,
cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
packageRoot: (0, paths_1.packageRootDir)(),
});
const res = (0, spawnNative_1.spawnNative)(binary, ["api-transform", "--cwd", project.root, "--tsconfig", project.path], {
cwd: project.root,
env: { ...process.env, ...options.env },
});
if (res.error) {
throw new Error(`ttsc: failed to spawn native compiler host ${binary}: ${res.error.message}`);
}
const output = parseNativeTransformOutput((0, spawnNative_1.outputText)(res.stdout), (0, spawnNative_1.outputText)(res.stderr));
return {
...envelopeSideChannels(output),
result: {
diagnostics: output.diagnostics,
status: res.status ?? 1,
stdout: "",
stderr: (0, spawnNative_1.outputText)(res.stderr),
},
typescript: output.typescript,
};
}
function transformProjectWithPlugins(options, cwd, project) {
const loaded = (0, loadProjectPlugins_1.loadProjectPlugins)({
binary: (0, resolveBinary_1.resolveBinary)(options) ?? "",
cacheDir: options.cacheDir ?? options.env?.TTSC_CACHE_DIR,
cwd,
entries: options.plugins,
env: { ...process.env, ...options.env },
pluginConfigDir: options.pluginConfigDir,
projectRoot: options.projectRoot,
tsconfig: project.path,
});
const checks = loaded.nativePlugins.filter((plugin) => plugin.stage === "check");
const transformers = loaded.nativePlugins.filter((plugin) => plugin.stage === "transform");
const tsgoBinary = loaded.nativePlugins.length === 0
? ""
: (0, resolveTsgo_1.resolveTsgo)({ ...options, cwd: project.root }).binary;
const checked = runNativeChecks(options, project, tsgoBinary, loaded.nativePlugins, checks);
if (checked.status !== 0) {
return {
result: checked,
typescript: {},
};
}
if (transformers.length === 0) {
const transformed = transformProjectWithNativeHost(options, project);
return {
...envelopeSideChannels(transformed),
result: (0, runBuild_1.appendBuildOutput)(checked, transformed.result),
typescript: transformed.typescript,
};
}
(0, sharedHostHelpers_1.assertSharedHostCompatibility)(transformers, "source-to-source");
const plugin = (0, sharedHostHelpers_1.selectSharedHostPlugin)(transformers);
const res = (0, spawnNative_1.spawnNative)(plugin.binary, createNativeTransformArgs(project, transformers, (0, sharedHostHelpers_1.resolvePluginConfigDir)(options)), {
cwd: project.root,
env: nativePluginEnv(options, tsgoBinary, loaded.nativePlugins, plugin),
});
if (res.error) {
throw new Error(`ttsc.transform: failed to spawn ${plugin.binary}: ${res.error.message}`);
}
const output = parseNativeTransformOutput((0, spawnNative_1.outputText)(res.stdout), (0, spawnNative_1.outputText)(res.stderr));
const result = {
diagnostics: output.diagnostics,
status: res.status ?? 1,
stdout: "",
stderr: (0, spawnNative_1.outputText)(res.stderr),
};
return {
...envelopeSideChannels(output),
result: (0, runBuild_1.appendBuildOutput)(checked, result),
typescript: output.typescript,
};
}
/**
* Collect the optional advisory envelope fields (`dependencies`,
* `dependenciesComplete`, `graph`, `volatile`) into a spreadable object,
* omitting absent fields so downstream result shapes stay free of `undefined`
* keys.
*/
function envelopeSideChannels(output) {
return {
...(output.dependencies === undefined
? {}
: { dependencies: output.dependencies }),
...(output.dependenciesComplete === undefined
? {}
: { dependenciesComplete: output.dependenciesComplete }),
...(output.graph === undefined ? {} : { graph: output.graph }),
...(output.volatile === undefined ? {} : { volatile: output.volatile }),
};
}
/**
* Run every check-stage plugin in sequence, short-circuiting on the first
* failure. Returns the aggregated `TtscBuildResult` (status 0 when all pass).
*/
function runNativeChecks(options, project, tsgoBinary, nativePlugins, checks) {
let result = {
diagnostics: [],
status: 0,
stdout: "",
stderr: "",
};
for (const plugin of checks) {
const res = (0, spawnNative_1.spawnNative)(plugin.binary, createNativeCheckArgs(project, nativePlugins, plugin, (0, sharedHostHelpers_1.resolvePluginConfigDir)(options)), {
cwd: project.root,
env: nativePluginEnv(options, tsgoBinary, nativePlugins, plugin),
});
if (res.error) {
throw new Error(`ttsc.transform.check: failed to spawn ${plugin.binary}: ${res.error.message}`);
}
result = (0, runBuild_1.appendBuildOutput)(result, (0, runBuild_1.normalizeBuildOutput)({
status: res.status ?? 1,
stdout: (0, spawnNative_1.outputText)(res.stdout),
stderr: (0, spawnNative_1.outputText)(res.stderr),
}, project.root));
if (result.status !== 0) {
return result;
}
}
return result;
}
/** Build the CLI argument list for the `transform` subcommand. */
function createNativeTransformArgs(project, plugins, pluginConfigOrigin) {
const args = [
"transform",
"--tsconfig=" + project.path,
"--plugins-json=" + serializeNativePlugins(plugins),
"--cwd=" + project.root,
];
if ((0, sharedHostHelpers_1.selectSharedHostPlugin)(plugins).capabilities?.projectContextArgs === true) {
args.push(...(0, createNativeProjectContextArgs_1.createNativeProjectContextArgs)(project, pluginConfigOrigin));
}
return args;
}
/** Build the CLI argument list for the `check` subcommand. */
function createNativeCheckArgs(project, plugins, plugin, pluginConfigOrigin) {
const args = [
"check",
"--tsconfig=" + project.path,
"--plugins-json=" + serializeNativePlugins(plugins),
"--cwd=" + project.root,
];
if (plugin.capabilities?.projectContextArgs === true) {
args.push(...(0, createNativeProjectContextArgs_1.createNativeProjectContextArgs)(project, pluginConfigOrigin));
}
return args;
}
/**
* Serialize the plugin list to a JSON string for `--plugins-json=`. Only the
* fields the native binary needs are included to keep the arg short.
*/
function serializeNativePlugins(plugins) {
return JSON.stringify(plugins.map((plugin) => ({
config: plugin.config,
name: plugin.name,
stage: plugin.stage,
})));
}
/**
* Build the environment for a native plugin spawn. Injects `TTSC_NODE_BINARY`,
* `TTSC_TSGO_BINARY`, and `TTSC_TTSX_BINARY` so the sidecar can re-invoke
* Node.js or tsgo without searching PATH, plus `TTSC_PLUGIN_CONFIG_DIR` when
* the caller declared a plugin config anchor (an embedder compiling through a
* generated wrapper tsconfig) so config-file discovery walks the real project
* instead of the wrapper's temp-dir ancestry. For transform plugins, also
* passes `TTSC_LINKED_PLUGINS_JSON` when linked sources are present.
*/
function nativePluginEnv(options, tsgoBinary, nativePlugins, plugin) {
const pluginConfigDir = (0, sharedHostHelpers_1.resolvePluginConfigDir)(options);
const env = {
...process.env,
TTSC_NODE_BINARY: process.env.TTSC_NODE_BINARY ?? process.execPath,
...(pluginConfigDir === undefined
? {}
: { TTSC_PLUGIN_CONFIG_DIR: pluginConfigDir }),
TTSC_TSGO_BINARY: process.env.TTSC_TSGO_BINARY ?? tsgoBinary,
TTSC_TTSX_BINARY: process.env.TTSC_TTSX_BINARY ??
node_path_1.default.join(__dirname, "..", "..", "launcher", "ttsx.js"),
...options.env,
};
// The anchor is per-invocation state owned by this host: when this run
// declared none (and the caller's env does not name one), drop any value
// inherited from an ancestor ttsc process so a nested build never
// mis-anchors its plugins at the outer project.
if (pluginConfigDir === undefined &&
options.env?.TTSC_PLUGIN_CONFIG_DIR === undefined) {
delete env.TTSC_PLUGIN_CONFIG_DIR;
}
if (plugin?.stage === "transform") {
const linked = (0, sharedHostHelpers_1.linkedTransformPlugins)(nativePlugins ?? []);
if (linked.length !== 0) {
env.TTSC_LINKED_PLUGINS_JSON = serializeNativePlugins(linked);
}
}
return env;
}
/**
* Parse the JSON envelope written by the native transform host to stdout.
*
* The `typescript` field must be a `Record<string, string>`. Any other shape is
* treated as a protocol error and throws with the stderr/stdout context. JSON
* parse errors are also wrapped with the same context message.
*
* The optional `dependencies`, `dependenciesComplete`, `graph`, and `volatile`
* fields (see `ITtscCompilerTransformation`) are forwarded when well-formed;
* entries that do not match the expected shape are dropped rather than failing
* the transform — the fields are advisory invalidation metadata, not output.
*
* Dropping a malformed `dependenciesComplete` member is the safe direction on
* purpose: an unlisted file keeps the sound host-owned bound, so a garbled
* declaration costs over-invalidation, never a stale output.
*/
function parseNativeTransformOutput(stdout, stderr) {
try {
const parsed = JSON.parse(stdout);
if (!isTextRecord(parsed.typescript)) {
throw new Error("ttsc: native transform host did not return a TypeScript source map");
}
const dependencies = parseDependencyLists(parsed.dependencies);
const dependenciesComplete = parseFileList(parsed.dependenciesComplete);
const graph = parseReferenceGraph(parsed.graph);
const volatile = parseFileList(parsed.volatile);
return {
...(dependencies === undefined ? {} : { dependencies }),
...(dependenciesComplete === undefined ? {} : { dependenciesComplete }),
...(graph === undefined ? {} : { graph }),
...(volatile === undefined ? {} : { volatile }),
diagnostics: Array.isArray(parsed.diagnostics) ? parsed.diagnostics : [],
typescript: parsed.typescript,
};
}
catch (error) {
if (error instanceof Error && !(error instanceof SyntaxError)) {
throw error;
}
throw new Error((stderr || stdout).trim() ||
"ttsc: native transform host returned no output");
}
}
/**
* Normalize the optional `dependencies` envelope field into a record of string
* arrays, or `undefined` when absent or carrying nothing usable.
*/
function parseDependencyLists(value) {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return undefined;
}
const output = {};
for (const [key, entries] of Object.entries(value)) {
if (!Array.isArray(entries)) {
continue;
}
const files = entries.filter((entry) => typeof entry === "string");
if (files.length !== 0) {
output[key] = files;
}
}
return Object.keys(output).length === 0 ? undefined : output;
}
/**
* Normalize the optional `graph` envelope section with the same tolerance as
* `dependencies`: non-object sections are dropped, edge entries that are not
* string arrays are dropped, and non-string list members are filtered. A
* section carrying nothing usable collapses to `undefined`.
*
* `candidates` is the one optional member, so an empty one is left off the
* result instead of being materialized as `{}`. The host omits the key when it
* has no superseding candidate to report, and a consumer that narrows on the
* declared optional type must see the same shape whether it reads the decoded
* envelope or the wire.
*/
function parseReferenceGraph(value) {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return undefined;
}
const section = value;
const candidates = parseDependencyLists(section.candidates) ?? {};
const edges = parseDependencyLists(section.edges) ?? {};
const globals = parseFileList(section.globals) ?? [];
const configs = parseFileList(section.configs) ?? [];
if (Object.keys(candidates).length === 0 &&
Object.keys(edges).length === 0 &&
globals.length === 0 &&
configs.length === 0) {
return undefined;
}
return Object.keys(candidates).length === 0
? { configs, edges, globals }
: { candidates, configs, edges, globals };
}
/**
* Normalize an optional string-list envelope field (`dependenciesComplete`,
* `volatile`, and the `globals`/`configs` graph sections), or `undefined` when
* absent or carrying nothing usable.
*/
function parseFileList(value) {
if (!Array.isArray(value)) {
return undefined;
}
const files = value.filter((entry) => typeof entry === "string" && entry.length !== 0);
return files.length === 0 ? undefined : files;
}
/** Type guard: true when `value` is a non-null, non-array object of strings. */
function isTextRecord(value) {
return (typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
Object.values(value).every((entry) => typeof entry === "string"));
}
//# sourceMappingURL=transformProjectInMemory.js.map