singularity-forge/src/web/ts-subprocess-flags.ts
2026-05-08 03:01:20 +02:00

101 lines
3.5 KiB
TypeScript

import { existsSync as defaultExistsSync } from "node:fs";
import { join } from "node:path";
/**
* Returns the correct Node.js type-stripping flag for subprocess spawning.
*
* Node v26 enforces ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING for files
* resolved under `node_modules/`. When SF is installed globally via npm,
* all source files live under `node_modules/sf-run/src/...`, so
* `--experimental-strip-types` fails deterministically.
*
* `--experimental-transform-types` applies a full TypeScript transform that
* works regardless of whether the file is under `node_modules/`. SF requires
* Node 26+, so this flag is always available.
*/
export function resolveTypeStrippingFlag(packageRoot: string): string {
return isUnderNodeModules(packageRoot)
? "--experimental-transform-types"
: "--experimental-strip-types";
}
/**
* Returns true when the given path sits inside a `node_modules/` directory.
* Handles both Unix and Windows path separators.
*/
export function isUnderNodeModules(filePath: string): boolean {
const normalized = filePath.replace(/\\/g, "/");
return normalized.includes("/node_modules/");
}
export interface SubprocessModuleResolution {
/** Absolute path to the module file (either src/.ts or dist/.js). */
modulePath: string;
/** When true the module is pre-compiled JS — skip TS flags and loader. */
useCompiledJs: boolean;
}
/**
* Resolves a subprocess module path, preferring compiled `dist/*.js` when the
* package root is under `node_modules/`.
*
* Node v26 unconditionally refuses `.ts` files under `node_modules/` — even
* with `--experimental-transform-types`. When SF is installed globally via
* npm, every subprocess that loads a `.ts` extension module crashes with
* `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`.
*
* The compiled JS files already ship in the npm package (`dist/` is in the
* `files` array in package.json) and are the correct artefacts to use when
* running from a packaged install.
*
* @param packageRoot Absolute path to the SF package root.
* @param relPath Path relative to `src/`, e.g.
* `"resources/extensions/sf/workspace-index.ts"`.
* @param checkExists Optional `existsSync` override (for testing).
*/
export function resolveSubprocessModule(
packageRoot: string,
relPath: string,
checkExists: (path: string) => boolean = defaultExistsSync,
): SubprocessModuleResolution {
const jsRelPath = relPath.replace(/\.ts$/, ".js");
const distPath = join(packageRoot, "dist", jsRelPath);
if (checkExists(distPath)) {
return { modulePath: distPath, useCompiledJs: true };
}
const sourceJsPath = join(packageRoot, "src", jsRelPath);
if (checkExists(sourceJsPath)) {
return { modulePath: sourceJsPath, useCompiledJs: true };
}
return {
modulePath: join(packageRoot, "src", relPath),
useCompiledJs: false,
};
}
/**
* Builds the Node.js subprocess prefix args for running a SF extension module.
*
* When the module resolved to compiled JS (`useCompiledJs === true`), returns
* only `["--input-type=module"]` — no TS loader, no TS stripping flag.
*
* When the module is TypeScript source, returns the full prefix:
* `["--import", <loaderHref>, <tsFlag>, "--input-type=module"]`.
*/
export function buildSubprocessPrefixArgs(
packageRoot: string,
resolution: SubprocessModuleResolution,
tsLoaderHref: string,
): string[] {
if (resolution.useCompiledJs) {
return ["--input-type=module"];
}
return [
"--import",
tsLoaderHref,
resolveTypeStrippingFlag(packageRoot),
"--input-type=module",
];
}