typia
Version:
Superfast runtime validators with only one line
176 lines (158 loc) • 5.48 kB
text/typescript
import fs from "fs";
import path from "path";
import ts from "typescript";
import { ImportTransformer } from "../transformers/ImportTransformer";
import transform from "../transform";
export namespace TypiaProgrammer {
export interface ILocation {
input: string;
output: string;
project: string;
}
export const build = async (
location: TypiaProgrammer.ILocation,
): Promise<void> => {
location.input = path.resolve(location.input);
location.output = path.resolve(location.output);
if ((await is_directory(location.input)) === false)
throw new URIError(
"Error on TypiaGenerator.generate(): input path is not a directory.",
);
else if (fs.existsSync(location.output) === false)
await fs.promises.mkdir(location.output, { recursive: true });
else if ((await is_directory(location.output)) === false) {
const parent: string = path.join(location.output, "..");
if ((await is_directory(parent)) === false)
throw new URIError(
"Error on TypiaGenerator.generate(): output path is not a directory.",
);
await fs.promises.mkdir(location.output);
}
// CREATE PROGRAM
const { options: compilerOptions } = ts.parseJsonConfigFileContent(
ts.readConfigFile(location.project, ts.sys.readFile).config,
{
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
readDirectory: ts.sys.readDirectory,
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
},
path.dirname(location.project),
);
const program: ts.Program = ts.createProgram(
await (async () => {
const container: string[] = [];
await gather({
location,
container,
from: location.input,
to: location.output,
});
return container;
})(),
compilerOptions,
);
// DO TRANSFORM
const diagnostics: ts.Diagnostic[] = [];
const result: ts.TransformationResult<ts.SourceFile> = ts.transform(
program
.getSourceFiles()
.filter(
(file) =>
!file.isDeclarationFile &&
path.resolve(file.fileName).indexOf(location.input) !== -1,
),
[
ImportTransformer.transform({
from: location.input,
to: location.output,
}),
transform(
program,
((compilerOptions.plugins as any[]) ?? []).find(
(p: any) =>
p.transform === "typia/lib/transform" ||
p.transform === "../src/transform.ts",
) ?? {},
{
addDiagnostic: (diag) => diagnostics.push(diag),
},
),
],
program.getCompilerOptions(),
);
// TRACE ERRORS
for (const diag of diagnostics) {
const file: string = diag.file
? path.relative(diag.file.fileName, process.cwd())
: "(unknown file)";
const category: string =
diag.category === ts.DiagnosticCategory.Warning
? "warning"
: diag.category === ts.DiagnosticCategory.Error
? "error"
: diag.category === ts.DiagnosticCategory.Suggestion
? "suggestion"
: diag.category === ts.DiagnosticCategory.Message
? "message"
: "unknown";
const [line, pos] = diag.file
? (() => {
const lines: string[] = diag
.file!.text.substring(0, diag.start)
.split("\n");
if (lines.length === 0) return [0, 0];
return [lines.length, lines.at(-1)!.length + 1];
})()
: [0, 0];
console.error(
`${file}:${line}:${pos} - ${category} TS${diag.code}: ${diag.messageText}`,
);
}
if (diagnostics.length) process.exit(-1);
// ARCHIVE TRANSFORMED FILES
const printer: ts.Printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
});
for (const file of result.transformed) {
const to: string = path
.resolve(file.fileName)
.replace(location.input, location.output);
const content: string = printer.printFile(file);
await fs.promises.writeFile(to, content, "utf8");
}
};
const is_directory = async (current: string): Promise<boolean> => {
const stat: fs.Stats = await fs.promises.stat(current);
return stat.isDirectory();
};
const gather = async (props: {
location: ILocation;
container: string[];
from: string;
to: string;
}) => {
if (props.from === props.location.output) return;
else if (fs.existsSync(props.to) === false)
await fs.promises.mkdir(props.to);
for (const file of await fs.promises.readdir(props.from)) {
const next: string = path.join(props.from, file);
const stat: fs.Stats = await fs.promises.stat(next);
if (stat.isDirectory()) {
await gather({
location: props.location,
container: props.container,
from: next,
to: path.join(props.to, file),
});
continue;
} else if (is_supported_extension(file)) props.container.push(next);
}
};
const is_supported_extension = (filename: string): boolean => {
// avoid using look-behind assertion as it is not marked as Baseline Widely Available
return TS_PATTERN.test(filename) && !DTS_PATTERN.test(filename);
};
}
const TS_PATTERN = /\.[cm]?tsx?$/;
const DTS_PATTERN = /\.d\.[cm]?tsx?$/;