UNPKG

typia

Version:

Superfast runtime validators with only one line

1,134 lines (1,053 loc) 36.3 kB
import { createCommand } from "commander"; import fs from "fs"; import inquirer from "inquirer"; import { createRequire } from "module"; import os from "os"; import path from "path"; import { glob, isDynamicPattern } from "tinyglobby"; import type { ITtscCompilerDiagnostic, ITtscCompilerTransformation, } from "ttsc"; import { FileSystemIdentity } from "./FileSystemIdentity"; export namespace TypiaGenerateWizard { export async function generate(): Promise<void> { console.log("----------------------------------------"); console.log(" Typia Generate Wizard"); console.log("----------------------------------------"); const options: IArguments = await parseArguments(); await build(options); } async function parseArguments(): Promise<IArguments> { const command = createCommand("typia generate"); command.usage("[options] [files...]"); command.argument("[files...]", "input TypeScript source files or globs"); command.option("--input <path>", "input directory"); command.option("--output <directory>", "output directory"); command.option( "--project <project>", "tsconfig.json/jsconfig.json file or directory", ); const questioned = { value: false }; const prompt = inquirer.createPromptModule; const input = (name: string) => async (message: string) => { questioned.value = true; const result = await prompt()({ type: "input", name, message, default: "", }); return result[name] as string; }; const configure = async (): Promise<string> => { const file: string | null = findProjectConfigFile(process.cwd()); if (file === null) { throw new URIError( `Unable to find "tsconfig.json" or "jsconfig.json" file.`, ); } return file; }; return new Promise<IArguments>((resolve, reject) => { command.action(async (files: string[], options: Partial<IArguments>) => { try { if (files.length !== 0 && options.input !== undefined) { throw new URIError( "Error on TypiaGenerateWizard.generate(): file arguments cannot be combined with --input.", ); } if (files.length === 0) { options.input ??= await input("input")("input directory"); } if (files.length !== 0 && options.output === undefined) { throw new URIError( "Error on TypiaGenerateWizard.generate(): output directory is required when file arguments are used.", ); } const output: string = options.output ?? (await input("output")("output directory")); const project: string = options.project ?? (await configure()); if (questioned.value) console.log(""); resolve({ input: options.input, output, project, files, }); } catch (exp) { reject(exp); } }); command.parseAsync(process.argv.slice(3), { from: "user" }).catch(reject); }); } export interface IArguments { input?: string; output: string; project: string; files: string[]; } async function build(location: IArguments): Promise<void> { location.output = path.resolve(location.output); location.project = resolveProjectConfigFile(location.project); const policy = new FileSystemIdentity.Policy(); const outputProbe: string = await nearestExistingAncestor(location.output); await ensureExistingDirectoryPath({ label: "output parent path", directory: outputProbe, }); policy.observe( await FileSystemIdentity.probeDirectory(outputProbe), outputProbe, ); policy.observe( await FileSystemIdentity.inspectDirectory(path.dirname(location.project)), path.dirname(location.project), ); const entries: IInputFile[] = location.files.length === 0 ? await prepareDirectoryInput(location, policy) : await prepareFileInputs(location, policy); const identity: FileSystemIdentity.IIdentity = policy.get(); await inspectTargetDirectories({ identity, output: location.output, targets: entries.map((entry) => entry.target), }); const binary = resolveTsgoBinary(); const cwd = path.dirname(location.project); const temporaryProject: ITemporaryProject = await createTemporaryProject({ entries, project: location.project, }); let transformed: Record<string, string>; try { transformed = transformProject({ binary, cwd, projectRoot: cwd, tsconfig: temporaryProject.config, }); } finally { await fs.promises.rm(temporaryProject.directory, { force: true, recursive: true, }); } const outputByKey: Map<string, string> = indexTransformedOutputs( transformed, identity, ); const outputs: IOutputFile[] = entries.map((entry) => { const output = getTransformedOutput({ cwd, entry, identity, outputByKey, }); if (output === undefined) { throw new URIError( `Error on TypiaGenerateWizard.generate(): no transformed output for ${entry.file}. Check that --project includes the file.`, ); } return { entry, output }; }); await ensureOutputDirectory(location.output); await ensureTargetDirectories({ identity, output: location.output, targets: outputs.map(({ entry }) => entry.target), }); await ensurePhysicalTargets({ identity, output: location.output, entries: outputs.map(({ entry }) => entry), }); await ensureTargetFiles( outputs.map(({ entry }) => entry), identity, ); for (const { entry, output } of outputs) { await fs.promises.writeFile(entry.target, formatOutput(output), "utf8"); } } interface IInputFile { file: string; target: string; } interface IOutputFile { entry: IInputFile; output: string; } interface ITraversalEntry { file: string; name: string; stat: fs.Stats; } interface ITemporaryProject { config: string; directory: string; } async function createTemporaryProject(props: { entries: IInputFile[]; project: string; }): Promise<ITemporaryProject> { const directory: string = await fs.promises.mkdtemp( path.join(os.tmpdir(), "typia-generate-project-"), ); const config: string = path.join(directory, "tsconfig.json"); try { await fs.promises.writeFile( config, JSON.stringify({ extends: props.project, exclude: [], files: props.entries.map((entry) => compilerInputPath(entry.file)), include: [], }), "utf8", ); return { config, directory }; } catch (error) { await fs.promises.rm(directory, { force: true, recursive: true }); throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to prepare the bounded input project: ${formatUnknownError(error)}`, ); } } async function ensureOutputDirectory(output: string): Promise<void> { if (fs.existsSync(output) === false) { await ensureCreatableDirectory(output); await fs.promises.mkdir(output, { recursive: true }); } else { await ensureExistingDirectory({ label: "output path", directory: output, }); } } async function ensureTargetDirectories(props: { identity: FileSystemIdentity.IIdentity; output: string; targets: string[]; }): Promise<void> { await inspectTargetDirectories(props); const directories: Map<string, string> = targetDirectories(props); for (const directory of directories.values()) { try { await fs.promises.mkdir(directory, { recursive: true }); } catch (exp) { throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to create output parent directory ${directory}: ${formatUnknownError(exp)}`, ); } await ensureExistingDirectory({ label: "output parent path", directory, }); } } async function inspectTargetDirectories(props: { identity: FileSystemIdentity.IIdentity; output: string; targets: string[]; }): Promise<void> { const directories: Map<string, string> = targetDirectories(props); for (const directory of directories.values()) { await ensureOutputAncestorDirectories({ identity: props.identity, output: props.output, directory, }); if (fs.existsSync(directory)) { await ensureExistingDirectory({ label: "output parent path", directory, }); } } } function targetDirectories(props: { identity: FileSystemIdentity.IIdentity; targets: string[]; }): Map<string, string> { const directories: Map<string, string> = new Map(); for (const target of props.targets) { const directory: string = path.dirname(target); directories.set(props.identity.filesystemKey(directory), directory); } return directories; } async function ensureCreatableDirectory(directory: string): Promise<void> { const parent: string = await nearestExistingAncestor(directory); await ensureExistingDirectoryPath({ label: "output parent path", directory: parent, }); } async function nearestExistingAncestor(directory: string): Promise<string> { let current: string = path.resolve(directory); while (fs.existsSync(current) === false) { const parent: string = path.dirname(current); if (parent === current) { throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to find existing output parent path: ${directory}`, ); } current = parent; } return current; } async function ensureOutputAncestorDirectories(props: { identity: FileSystemIdentity.IIdentity; output: string; directory: string; }): Promise<void> { const output: string = path.resolve(props.output); const directory: string = path.resolve(props.directory); if (props.identity.contains(directory, output) === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): output parent path escapes output directory: ${props.directory}`, ); } const relative: string = path.relative(output, directory); if (relative === "") { return; } let current: string = output; for (const segment of relative.split(path.sep)) { current = path.join(current, segment); let stat: fs.Stats; try { stat = await fs.promises.lstat(current); } catch (exp) { if (isMissingFileError(exp)) { return; } throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to inspect output parent path ${current}: ${formatUnknownError(exp)}`, ); } if (stat.isSymbolicLink()) { throw new URIError( `Error on TypiaGenerateWizard.generate(): output parent path contains a symbolic link: ${current}`, ); } if (stat.isDirectory() === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): output parent path is not a directory: ${current}`, ); } } } async function ensureExistingDirectory(props: { label: string; directory: string; }): Promise<void> { await ensureExistingDirectoryPath(props); } async function ensureExistingDirectoryPath(props: { label: string; directory: string; }): Promise<void> { const directory: string = path.resolve(props.directory); const parsed: path.ParsedPath = path.parse(directory); const relative: string = path.relative(parsed.root, directory); let current: string = parsed.root; for (const segment of relative === "" ? [] : relative.split(path.sep)) { current = path.join(current, segment); await ensureExistingDirectorySegment({ label: path.normalize(current) === path.normalize(directory) ? props.label : `${props.label} ancestor`, directory: current, }); } } async function ensureExistingDirectorySegment(props: { label: string; directory: string; }): Promise<void> { const stat: fs.Stats = await fs.promises.lstat(props.directory); if (stat.isSymbolicLink()) { throw new URIError( `Error on TypiaGenerateWizard.generate(): ${props.label} is a symbolic link: ${props.directory}`, ); } if (stat.isDirectory() === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): ${props.label} is not a directory: ${props.directory}`, ); } } async function ensurePhysicalTargets(props: { identity: FileSystemIdentity.IIdentity; output: string; entries: IInputFile[]; }): Promise<void> { const output: string = await fs.promises.realpath(props.output); const inputs: Set<string> = new Set(); for (const entry of props.entries) { inputs.add( props.identity.filesystemKey(await fs.promises.realpath(entry.file)), ); } for (const entry of props.entries) { const parent: string = path.dirname(entry.target); const directory: string = await fs.promises.realpath(parent); if (props.identity.contains(directory, output) === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): output parent path escapes output directory through a symbolic link: ${parent}`, ); } const target: string = path.join(directory, path.basename(entry.target)); if (inputs.has(props.identity.filesystemKey(target))) { throw new URIError( `Error on TypiaGenerateWizard.generate(): output file would overwrite input file through a symbolic link: ${entry.target}`, ); } } } async function ensureTargetFiles( entries: IInputFile[], identity: FileSystemIdentity.IIdentity, ): Promise<void> { const inputs: Set<string> = new Set(); const files: Map<string, IInputFile> = new Map(); for (const entry of entries) { inputs.add( fileIdentityKey( await fs.promises.stat(entry.file, { bigint: true }), await fs.promises.realpath(entry.file), ), ); files.set(identity.filesystemKey(entry.target), entry); } for (const entry of files.values()) { let stat: fs.BigIntStats; try { stat = await fs.promises.lstat(entry.target, { bigint: true }); } catch (exp) { if (isMissingFileError(exp)) { continue; } throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to inspect output file ${entry.target}: ${formatUnknownError(exp)}`, ); } if (stat.isFile() === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): output file path is not a regular file: ${entry.target}`, ); } if ( inputs.has( fileIdentityKey(stat, await fs.promises.realpath(entry.target)), ) ) { throw new URIError( `Error on TypiaGenerateWizard.generate(): output file would overwrite input file through a physical file alias: ${entry.target}`, ); } if (stat.nlink > BigInt(1)) { throw new URIError( `Error on TypiaGenerateWizard.generate(): output file has multiple hard links: ${entry.target}`, ); } } } async function prepareDirectoryInput( location: IArguments, policy: FileSystemIdentity.Policy, ): Promise<IInputFile[]> { if (location.input === undefined) { throw new URIError( "Error on TypiaGenerateWizard.generate(): input path is required.", ); } const input = path.resolve(location.input); if (fs.existsSync(input) === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): input path does not exist: ${input}`, ); } if ((await isDirectory(input)) === false) { throw new URIError( "Error on TypiaGenerateWizard.generate(): input path is not a directory.", ); } const inputReal: string = await fs.promises.realpath(input); const outputReal: string | undefined = await optionalRealPath( location.output, ); const files: string[] = []; await gather({ container: files, from: input, inputReal, outputReal, policy, visitedDirectories: new Set(), visitedFiles: new Set(), }); return files.map((file) => ({ file, target: path.join(location.output, path.relative(input, file)), })); } async function prepareFileInputs( location: IArguments, policy: FileSystemIdentity.Policy, ): Promise<IInputFile[]> { const targets: Set<string> = new Set(); const output: IInputFile[] = []; for (const input of await expandFileInputs( location.files, location.output, policy, )) { const file: string = path.resolve(input); policy.observe( await FileSystemIdentity.inspectDirectory(path.dirname(file)), path.dirname(file), ); const identity: FileSystemIdentity.IIdentity = policy.get(); if (fs.existsSync(file) === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): input file does not exist: ${input}`, ); } else if ((await isFile(file)) === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): input path is not a file: ${input}`, ); } else if (identity.isDeclarationFile(file)) { continue; } else if (identity.isSupportedExtension(file) === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): input file is not a supported TypeScript source: ${input}`, ); } const target: string = path.join(location.output, path.basename(file)); if (identity.isSamePath(file, target)) { throw new URIError( `Error on TypiaGenerateWizard.generate(): output file would overwrite input file: ${input}`, ); } const key: string = identity.filesystemKey(target); if (targets.has(key)) { throw new URIError( `Error on TypiaGenerateWizard.generate(): duplicate output filename for ${target}`, ); } targets.add(key); output.push({ file, target }); } if (output.length === 0) { throw new URIError( "Error on TypiaGenerateWizard.generate(): input files do not include any supported TypeScript source files outside the output directory.", ); } return output; } async function expandFileInputs( inputs: string[], directory: string, policy: FileSystemIdentity.Policy, ): Promise<string[]> { const output: string[] = []; for (const input of inputs) { const pattern: string = toGlobPattern(input); if (isDynamicPattern(pattern, { caseSensitiveMatch: true })) { const searchDirectory: string = await globSearchDirectory(input); const caseSensitive: boolean | undefined = await FileSystemIdentity.inspectDirectory(searchDirectory); if (caseSensitive === undefined) { throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to determine filesystem case behavior for input pattern base ${searchDirectory}.`, ); } policy.observe(caseSensitive, searchDirectory); const identity: FileSystemIdentity.IIdentity = policy.get(); const matches: string[] = await glob(pattern, { absolute: true, caseSensitiveMatch: identity.caseSensitive, cwd: process.cwd(), onlyFiles: true, }); if (matches.length === 0) { throw new URIError( `Error on TypiaGenerateWizard.generate(): input pattern does not match any files: ${input}`, ); } output.push( ...excludeOutputFiles(matches, directory, identity).filter((file) => identity.isSupportedExtension(file), ), ); } else { const file: string = path.resolve(input); policy.observe( await FileSystemIdentity.inspectDirectory(path.dirname(file)), path.dirname(file), ); if (policy.get().contains(file, directory) === false) { output.push(file); } } } return output; } function excludeOutputFiles( files: string[], directory: string, identity: FileSystemIdentity.IIdentity, ): string[] { return files.filter((file) => identity.contains(file, directory) === false); } async function globSearchDirectory(input: string): Promise<string> { let current: string = path.resolve(input); while ( isDynamicPattern(toGlobPattern(current), { caseSensitiveMatch: true }) ) { const parent: string = path.dirname(current); if (parent === current) break; current = parent; } if (fs.existsSync(current) && (await isDirectory(current))) return current; return nearestExistingAncestor(path.dirname(current)); } function toGlobPattern(input: string): string { return input.replace(/\\/g, "/"); } function transformProject(props: { binary: string; cwd: string; projectRoot: string; tsconfig: string; }): Record<string, string> { const TtscCompiler = loadTtscCompiler(); const result: ITtscCompilerTransformation = new TtscCompiler({ binary: props.binary, cwd: props.cwd, projectRoot: props.projectRoot, tsconfig: props.tsconfig, }).transform(); if (result.type === "success") { return result.typescript; } if (result.type === "failure") { throw new URIError( `Error on TypiaGenerateWizard.generate(): ${formatDiagnostics(result.diagnostics)}`, ); } throw new URIError( `Error on TypiaGenerateWizard.generate(): ${formatUnknownError(result.error)}`, ); } function resolveProjectConfigFile(project: string): string { const resolved: string = path.resolve(project); if (fs.existsSync(resolved) === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): project path does not exist: ${resolved}`, ); } const stat: fs.Stats = fs.statSync(resolved); if (stat.isDirectory()) { for (const filename of ["tsconfig.json", "jsconfig.json"]) { const candidate: string = path.join(resolved, filename); if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { return resolveRealPath(candidate); } } throw new URIError( `Error on TypiaGenerateWizard.generate(): project directory has no tsconfig.json or jsconfig.json: ${resolved}`, ); } if (stat.isFile() === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): project path is not a file: ${resolved}`, ); } return resolveRealPath(resolved); } function findProjectConfigFile(directory: string): string | null { let current: string = path.resolve(directory); while (true) { for (const filename of ["tsconfig.json", "jsconfig.json"]) { const candidate: string = path.join(current, filename); if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) { return resolveRealPath(candidate); } } const parent: string = path.dirname(current); if (parent === current) { return null; } current = parent; } } function loadTtscCompiler(): typeof import("ttsc").TtscCompiler { const packageRoot: string = resolveTypiaPackageRoot(); const resolved: string | null = resolveFromRoots( "ttsc", resolveRuntimeRoots(packageRoot), ); if (resolved === null) { throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to resolve ttsc from the current project, typia package, or workspace root. Run "npm i -D ttsc typescript" before.`, ); } const imported = createRequire(resolved)(resolved) as typeof import("ttsc"); return imported.TtscCompiler; } function resolveTsgoBinary(): string { const explicit: string | undefined = process.env.TTSC_TSGO_BINARY; if (explicit !== undefined && explicit.length !== 0) { if (path.isAbsolute(explicit) && fs.existsSync(explicit)) { return explicit; } throw new URIError( `Error on TypiaGenerateWizard.generate(): TTSC_TSGO_BINARY must be an existing absolute path: ${explicit}`, ); } const packageRoot: string = resolveTypiaPackageRoot(); const manifest: string | null = resolveFromRoots( "typescript/package.json", resolveRuntimeRoots(packageRoot), ); if (manifest === null) { throw new URIError( "Error on TypiaGenerateWizard.generate(): unable to resolve typescript from the current project, typia package, or workspace root.", ); } const platform: string = `@typescript/typescript-${process.platform}-${process.arch}`; const platformManifest: string = createRequire(manifest).resolve( `${platform}/package.json`, ); const binary: string = path.join( path.dirname(platformManifest), "lib", process.platform === "win32" ? "tsc.exe" : "tsc", ); if (fs.existsSync(binary) === false) { throw new URIError( `Error on TypiaGenerateWizard.generate(): TypeScript-Go executable not found: ${binary}`, ); } return binary; } function resolveTypiaPackageRoot(): string { // The CLI entrypoint (`lib/executable/typia.js`) lives in the same // directory as this module, so its `process.argv[1]` path anchors the // walk-up identically in both the CJS and ESM builds — `__dirname` does // not exist in the transcoded `.mjs`. const current: string = path.dirname(path.resolve(process.argv[1] ?? "")); for (const directory of [ path.resolve(current, "..", ".."), path.resolve(current, ".."), ]) { const file: string = path.join(directory, "package.json"); if (fs.existsSync(file) === false) { continue; } try { const pack = JSON.parse(fs.readFileSync(file, "utf8")) as Partial< Record<"name", unknown> >; if (pack.name === "typia") { return directory; } } catch { continue; } } const resolved: string | null = resolveFromRoots("typia/package.json", [ process.cwd(), current, ]); if (resolved === null) { throw new URIError( "Error on TypiaGenerateWizard.generate(): unable to resolve typia package root.", ); } return path.dirname(resolved); } function resolveRuntimeRoots(packageRoot: string): string[] { return [process.cwd(), packageRoot, path.resolve(packageRoot, "..", "..")]; } function resolveFromRoots(request: string, roots: string[]): string | null { for (const root of roots) { try { return createRequire(path.join(root, "package.json")).resolve(request); } catch { continue; } } return null; } async function isDirectory(current: string): Promise<boolean> { const stat: fs.Stats = await fs.promises.stat(current); return stat.isDirectory(); } async function isFile(current: string): Promise<boolean> { const stat: fs.Stats = await fs.promises.stat(current); return stat.isFile(); } async function gather(props: { container: string[]; from: string; inputReal: string; outputReal: string | undefined; policy: FileSystemIdentity.Policy; visitedDirectories: Set<string>; visitedFiles: Set<string>; }): Promise<void> { const currentReal: string = await resolveTraversalPath(props.from); if ( props.outputReal !== undefined && isPhysicalSameOrChildPath(currentReal, props.outputReal) ) return; ensurePhysicalInputContainment({ file: props.from, input: props.inputReal, real: currentReal, }); const currentStat: fs.BigIntStats = await fs.promises.stat(props.from, { bigint: true, }); const directoryIdentity: string = fileIdentityKey(currentStat, currentReal); if (props.visitedDirectories.has(directoryIdentity)) { const lexicalStat: fs.Stats = await fs.promises.lstat(props.from); if (lexicalStat.isSymbolicLink()) { throw new URIError( `Error on TypiaGenerateWizard.generate(): input directory link revisits a physical directory: ${props.from}.`, ); } return; } props.visitedDirectories.add(directoryIdentity); props.policy.observe( await FileSystemIdentity.inspectDirectory(props.from), props.from, ); const identity: FileSystemIdentity.IIdentity = props.policy.get(); const entries: ITraversalEntry[] = await Promise.all( (await fs.promises.readdir(props.from)).map(async (name) => { const file: string = path.join(props.from, name); try { return { file, name, stat: await fs.promises.lstat(file) }; } catch (error) { throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to inspect input path ${file}: ${formatUnknownError(error)}`, ); } }), ); entries.sort((x, y) => { const linkOrder: number = Number(x.stat.isSymbolicLink()) - Number(y.stat.isSymbolicLink()); return linkOrder !== 0 ? linkOrder : Buffer.compare(Buffer.from(x.name), Buffer.from(y.name)); }); for (const entry of entries) { let stat: fs.BigIntStats; let real: string; try { stat = await fs.promises.stat(entry.file, { bigint: true }); real = await fs.promises.realpath(entry.file); } catch (error) { throw new URIError( `Error on TypiaGenerateWizard.generate(): input link target is missing or unreadable: ${entry.file}: ${formatUnknownError(error)}`, ); } if ( props.outputReal !== undefined && isPhysicalSameOrChildPath(real, props.outputReal) ) continue; ensurePhysicalInputContainment({ file: entry.file, input: props.inputReal, real, }); if (stat.isDirectory()) { await gather({ ...props, from: entry.file }); continue; } if ( stat.isFile() === false || identity.isSupportedExtension(entry.name) === false ) continue; const fileIdentity: string = fileIdentityKey(stat, real); if (props.visitedFiles.has(fileIdentity)) continue; props.visitedFiles.add(fileIdentity); props.container.push(entry.file); } } function formatOutput(output: string): string { return output.startsWith("// @ts-nocheck") ? output : `// @ts-nocheck\n${output}`; } function indexTransformedOutputs( outputs: Record<string, string>, identity: FileSystemIdentity.IIdentity, ): Map<string, string> { const map: Map<string, string> = new Map(); for (const [file, output] of Object.entries(outputs)) { const key: string = identity.projectFileKey(file); if (map.has(key)) { throw new URIError( `Error on TypiaGenerateWizard.generate(): transformed outputs have ambiguous filesystem identities: ${file}.`, ); } map.set(key, output); } return map; } function getTransformedOutput(props: { cwd: string; entry: IInputFile; identity: FileSystemIdentity.IIdentity; outputByKey: Map<string, string>; }): string | undefined { const output = props.outputByKey.get( props.identity.projectFileKey(projectKey(props.cwd, props.entry.file)), ); if (output !== undefined) { return output; } const compilerFile: string = compilerInputPath(props.entry.file); if ( props.identity.isSamePath(compilerFile, props.entry.file) === false && props.identity.contains(compilerFile, props.cwd) ) { const compiled: string | undefined = props.outputByKey.get( props.identity.projectFileKey(projectKey(props.cwd, compilerFile)), ); if (compiled !== undefined) return compiled; } const real: string = resolveRealPath(props.entry.file); if ( props.identity.isSamePath(real, props.entry.file) || props.identity.contains(real, props.cwd) === false ) { return undefined; } return props.outputByKey.get( props.identity.projectFileKey(projectKey(props.cwd, real)), ); } function projectKey(root: string, file: string): string { return path.relative(root, file).replace(/\\/g, "/"); } function resolveRealPath(file: string): string { try { return fs.realpathSync(file); } catch { return file; } } function compilerInputPath(file: string): string { try { if (fs.lstatSync(file).isSymbolicLink()) { return path.join( resolveRealPath(path.dirname(file)), path.basename(file), ); } } catch { return file; } return resolveRealPath(file); } async function optionalRealPath(file: string): Promise<string | undefined> { try { return await fs.promises.realpath(file); } catch (error) { if (isMissingFileError(error)) return undefined; throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to resolve path ${file}: ${formatUnknownError(error)}`, ); } } async function resolveTraversalPath(file: string): Promise<string> { try { return await fs.promises.realpath(file); } catch (error) { throw new URIError( `Error on TypiaGenerateWizard.generate(): unable to resolve input path ${file}: ${formatUnknownError(error)}`, ); } } function ensurePhysicalInputContainment(props: { file: string; input: string; real: string; }): void { if (isPhysicalSameOrChildPath(props.real, props.input)) return; throw new URIError( `Error on TypiaGenerateWizard.generate(): input path resolves outside the input directory: ${props.file}.`, ); } function isPhysicalSameOrChildPath(file: string, directory: string): boolean { const relative: string = path.relative(directory, file); return ( relative === "" || (relative !== ".." && relative.startsWith(`..${path.sep}`) === false && path.isAbsolute(relative) === false) ); } function isMissingFileError(exp: unknown): boolean { return ( typeof exp === "object" && exp !== null && "code" in exp && exp.code === "ENOENT" ); } /** * Delegates to {@link FileSystemIdentity.identityKey}, which owns the rule and * carries the reasoning for reading the identity as a `bigint`. */ function fileIdentityKey(stat: fs.BigIntStats, realpath: string): string { return FileSystemIdentity.identityKey(stat, realpath); } function formatDiagnostics(diagnostics: ITtscCompilerDiagnostic[]): string { return diagnostics.length === 0 ? "transformation failed" : diagnostics .map((diag) => [ diag.file ?? "ttsc", diag.line === undefined ? undefined : `${diag.line}:${diag.character ?? 1}`, diag.messageText, ] .filter((part) => part !== undefined && part !== "") .join(": "), ) .join("\n"); } function formatUnknownError(error: unknown): string { if (error instanceof Error) { return error.message; } if ( typeof error === "object" && error !== null && "message" in error && typeof error.message === "string" ) { return error.message; } return String(error); } }