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", , , "--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", ]; }