Make all the codegen we do incremental (#64331)

This commit is contained in:
Wesley Wigham authored and GitHub committed 2026-09-21 22:25:50 +00:00
1 parent 36482c8dbb
commit 1ac2d5aed0
35 files changed
+1525 -245

No files matched your search

-6
View File
@@ -294,12 +294,6 @@ jobs:
- uses: ./.github/actions/setup-go
- run: npm ci
- run: npx hereby generate
- run: npx hereby generate:enums
- run: npx hereby generate:ast
- run: npx hereby generate:vendor
- run: node ./tsc/internal/lsp/lsproto/_generate/fetchModel.mts
- run: node ./tsc/internal/lsp/lsproto/_generate/generate.mts
- run: npx hereby generate:extension
- run: npx hereby check:scripts
- run: git add .
- run: git diff --staged --exit-code --stat
+9 -1
View File
@@ -103,12 +103,20 @@ npx hereby build # Build the native compiler into built/local/tsc
npx hereby test # Run compiler and language-service Go tests
npx hereby test:all # Also run benchmarks, tools, and API tests
npx hereby lint # Run custom golangci-lint for both Go modules
npx hereby generate # Regenerate compiler sources and bundled assets
npx hereby generate:go # Run the generators also exposed through go generate
npx hereby generate # Regenerate all sources, assets, localization, and vendored files
npx hereby format # Format Go, TypeScript, JSON, and YAML
npx hereby check:format # Check formatting without changing files
npx hereby tidy # Tidy both modules and synchronize go.work
```
Generation is owned by `Herebyfile.mjs`; Go directives forward to the same
`generate:*` tasks for compatibility. Use a subtask such as
`npx hereby generate:diagnostics` to run one generator group, and pass `--force`
to bypass incremental caches. `generate` includes every generator and fetches
the pinned LSP model when its local cache is missing or stale. Use
`generate:go` for the narrower scope of `go generate ./...` in the `tsc` module.
Package-specific commands:
```bash
+345 -76
View File
@@ -14,10 +14,8 @@ import {
styleText,
} from "node:util";
import * as tar from "tar";
import {
x,
xSync,
} from "tinyexec";
import { xSync } from "tinyexec";
import { run } from "./tools/scripts/gen/utils.mts";
if (process.platform === "win32") {
process.chdir(fs.realpathSync.native(process.cwd()));
@@ -30,39 +28,7 @@ const isCI = !!process.env.CI || !!process.env.TF_BUILD;
const stableThreeComponentVersionPatternSource = String.raw`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`;
const stableThreeComponentVersionPattern = new RegExp(stableThreeComponentVersionPatternSource);
/**
* @typedef {{
* captureOutput?: boolean;
* cwd?: string;
* env?: NodeJS.ProcessEnv;
* signal?: AbortSignal;
* }} RunOptions
*/
/**
* @param {string} arg
*/
function formatCommandArg(arg) {
return arg && /^[\w@%+=:,./-]+$/.test(arg) ? arg : JSON.stringify(arg);
}
/**
* @param {string} command
* @param {readonly string[]} [args]
* @param {RunOptions} [options]
*/
function run(command, args = [], options = {}) {
console.log("$ " + [command, ...args].map(formatCommandArg).join(" "));
return x(command, args, {
throwOnError: true,
...(options.signal ? { signal: options.signal } : {}),
nodeOptions: {
cwd: options.cwd,
env: options.env ? { ...process.env, ...options.env } : undefined,
stdio: options.captureOutput ? "pipe" : "inherit",
},
});
}
/** @typedef {import("./tools/scripts/gen/utils.mts").RunOptions} RunOptions */
/**
* @param {string} command
@@ -105,6 +71,7 @@ const { values: rawOptions } = parseArgs({
options: {
tests: { type: "string", short: "t" },
fix: { type: "boolean" },
force: { type: "boolean", default: parseEnvBoolean("FORCE") },
api: { type: "boolean" },
all: { type: "boolean" },
debug: { type: "boolean" },
@@ -368,14 +335,203 @@ export const cleanBuilt = task({
run: () => rimraf("built"),
});
async function runGenerate() {
return await run("go", ["generate", "-v", "./..."], { cwd: "./tsc" });
/** @type {(() => Promise<void>)[]} */
const goGenerateActions = [];
async function runGenerateGo() {
for (const generate of goGenerateActions) {
await generate();
}
}
export const generate = task({
name: "generate",
description: "Runs go generate on the project.",
run: runGenerate,
export const generateGo = task({
name: "generate:go",
description: "Runs the project's Go code generators directly. Pass --force to regenerate unchanged files.",
run: runGenerateGo,
});
const getGoGenerateEnvironment = memoize(async () => {
const { stdout } = await runOutput("go", ["env", "-json", "GOOS", "GOARCH", "GOROOT"], { cwd: "./tsc" });
return /** @type {{ GOOS: string; GOARCH: string; GOROOT: string }} */ (JSON.parse(stdout));
});
/** @typedef {import("./tools/scripts/gen/cache.mts").CacheOptions & { file: string }} GoGenerator */
/**
* @param {string} name
* @param {GoGenerator} generator
*/
async function runGoGenerator(name, { file, ...spec }) {
const { default: cache } = await import("./tools/scripts/gen/cache.mts");
const sourcePath = path.resolve(__dirname, file);
const source = fs.readFileSync(sourcePath, "utf8");
const packageName = /^package\s+(\w+)/m.exec(source)?.[1];
const line = source.split(/\r?\n/).findIndex(line => line.trimEnd() === `//go:generate npx hereby ${name}`) + 1;
assert(packageName && line, `Missing package or generation directive in ${file}`);
const goEnv = await getGoGenerateEnvironment();
const pathKey = process.platform === "win32" ? Object.keys(process.env).find(key => key.toUpperCase() === "PATH") ?? "PATH" : "PATH";
const goBin = path.join(goEnv.GOROOT, "bin");
const searchPath = (process.env[pathKey] ?? "").split(path.delimiter).filter(entry => path.resolve(entry) !== goBin);
await cache({
...spec,
cwd: spec.cwd ?? path.dirname(sourcePath),
inputs: [__filename, sourcePath, ...spec.inputs],
envInputs: [
...(spec.envInputs ?? ["GOOS", "GOARCH", "GOFLAGS", "GOTOOLCHAIN", "GOEXPERIMENT", "CGO_ENABLED", "GOWORK"]),
"GOROOT",
"GOFILE",
"GOLINE",
"GOPACKAGE",
"DOLLAR",
],
env: {
...goEnv,
GOFILE: path.basename(sourcePath),
GOLINE: String(line),
GOPACKAGE: packageName,
DOLLAR: "$",
[pathKey]: [goBin, ...searchPath].join(path.delimiter),
},
force: !!options.force,
});
}
/**
* @param {string} name
* @param {GoGenerator[] | (() => Promise<void>)} generators
*/
function goGenerateTask(name, generators) {
const run = typeof generators === "function" ? generators : async () => {
for (const generator of generators) await runGoGenerator(name, generator);
};
goGenerateActions.push(run);
return task({
name,
description: `Generates ${name.slice("generate:".length)} files. Pass --force to regenerate unchanged files.`,
run,
});
}
/**
* @param {string} file
* @param {string} type
* @param {string} output
* @param {string} [trimPrefix]
* @returns {GoGenerator}
*/
function stringerGenerator(file, type, output, trimPrefix) {
return {
file,
inputs: [],
outputs: [output],
commands: [
["go", "tool", "golang.org/x/tools/cmd/stringer", `-type=${type}`, ...(trimPrefix ? [`-trimprefix=${trimPrefix}`] : []), `-output=${output}`],
["dprint", "fmt", output],
],
};
}
/**
* @param {string} file
* @param {string} type
* @param {string} output
* @param {{ source?: string; packageName: string; inputs?: string[]; stub?: boolean }} options
* @returns {GoGenerator}
*/
function moqGenerator(file, type, output, { source = ".", packageName, inputs = [], stub = false }) {
return {
file,
inputs,
outputs: [output],
commands: [
["go", "tool", "github.com/matryer/moq", ...(stub ? ["-stub"] : []), "-fmt", "goimports", "-pkg", packageName, "-out", output, source, type],
["dprint", "fmt", output],
],
};
}
async function runGenerateASTStringer() {
await runGoGenerator("generate:ast-stringer", stringerGenerator("tsc/internal/ast/kind_generated.go", "Kind", "kind_stringer_generated.go"));
}
export const generateASTStringer = goGenerateTask("generate:ast-stringer", runGenerateASTStringer);
export const generateBundled = goGenerateTask("generate:bundled", [{
file: "tsc/internal/bundled/bundled.go",
inputs: ["generate.go", "CopyrightNotice.txt", "libs/*"],
outputs: ["libs_generated.go", "embed_generated.go"],
commands: [["go", "run", "generate.go"]],
}]);
export const generateChecker = goGenerateTask("generate:checker", [
stringerGenerator("tsc/internal/checker/types.go", "SignatureKind", "stringer_generated.go"),
]);
export const generateCompilerOptions = goGenerateTask("generate:compileroptions", [
stringerGenerator("tsc/internal/core/compileroptions.go", "ModuleKind", "modulekind_stringer_generated.go", "ModuleKind"),
stringerGenerator("tsc/internal/core/compileroptions.go", "ScriptTarget", "scripttarget_stringer_generated.go", "ScriptTarget"),
]);
export const generateLanguageVariant = goGenerateTask("generate:languagevariant", [
stringerGenerator("tsc/internal/core/languagevariant.go", "LanguageVariant", "languagevariant_stringer_generated.go"),
]);
export const generateScriptKind = goGenerateTask("generate:scriptkind", [
stringerGenerator("tsc/internal/core/scriptkind.go", "ScriptKind", "scriptkind_stringer_generated.go"),
]);
export const generateTristate = goGenerateTask("generate:tristate", [
stringerGenerator("tsc/internal/core/tristate.go", "Tristate", "tristate_stringer_generated.go"),
]);
export const generateDiagnostics = goGenerateTask("generate:diagnostics", [
{
file: "tsc/internal/diagnostics/diagnostics.go",
inputs: ["generate.go", "diagnosticMessages.json", "extraDiagnosticMessages.json", "../{collections,json}/*.go", "../locale/lcl/*/diagnosticMessages/diagnosticMessages.generated.json.lcl"],
exclude: ["**/*_test.go"],
outputs: ["diagnostics_generated.go", "loc_generated.go", "loc/*.json.gz"],
commands: [
["go", "run", "generate.go", "-diagnostics", "diagnostics_generated.go", "-loc", "loc_generated.go", "-locdir", "loc"],
["dprint", "fmt", "diagnostics_generated.go", "loc_generated.go"],
],
},
stringerGenerator("tsc/internal/diagnostics/diagnostics.go", "Category", "stringer_generated.go"),
]);
export const generateAutoImport = goGenerateTask("generate:autoimport", [
stringerGenerator("tsc/internal/ls/autoimport/export.go", "ExportSyntax", "export_stringer_generated.go"),
]);
export const generateProject = goGenerateTask("generate:project", [
stringerGenerator("tsc/internal/project/project.go", "Kind", "project_stringer_generated.go", "Kind"),
]);
export const generateProjectTestUtil = goGenerateTask("generate:projecttestutil", [
moqGenerator("tsc/internal/testutil/projecttestutil/projecttestutil.go", "Client", "clientmock_generated.go", {
source: "../../project",
packageName: "projecttestutil",
inputs: ["../../project/client.go"],
stub: true,
}),
moqGenerator("tsc/internal/testutil/projecttestutil/projecttestutil.go", "NpmExecutor", "npmexecutormock_generated.go", {
source: "../../project/ata",
packageName: "projecttestutil",
inputs: ["../../project/ata/ata.go"],
stub: true,
}),
]);
export const generateVFS = goGenerateTask("generate:vfs", [
moqGenerator("tsc/internal/vfs/vfs.go", "FS", "vfsmock/mock_generated.go", { packageName: "vfsmock" }),
]);
export const generateVFSMatch = goGenerateTask("generate:vfsmatch", [
stringerGenerator("tsc/internal/vfs/vfsmatch/vfsmatch.go", "Usage", "stringer_generated.go", "Usage"),
]);
export const generateUnicode = goGenerateTask("generate:unicode", async () => {
const { default: generate } = await import("./tsc/internal/stringutil/_scripts/generate-unicode-data.mts");
await generate(!!options.force);
});
async function runGenerateExtension() {
@@ -388,6 +544,48 @@ export const generateExtension = task({
run: runGenerateExtension,
});
async function runGenerateExtensionTest() {
await run("npm", ["run", "-w", "native-preview", "generateLocTest"]);
}
export const generateExtensionTest = task({
name: "generate:extension-test",
description: "Generates pseudo-localized extension resources.",
dependencies: [generateExtension],
run: runGenerateExtensionTest,
});
async function runGenerateLSP() {
const { GeneratedFile } = await import("./tools/scripts/gen/generatedFile.mts");
const directory = path.join(__dirname, "tsc/internal/lsp/lsproto/_generate");
const modelFiles = ["metaModel.json", "metaModelSchema.mts"].map(file => new GeneratedFile(path.join(directory, file), [path.join(directory, "fetchModel.mts"), path.join(__dirname, "package-lock.json")]));
if (!modelFiles.every(file => file.isCurrent(!!options.force))) {
for (const file of modelFiles) file.invalidate();
const { default: fetchModel } = await import("./tsc/internal/lsp/lsproto/_generate/fetchModel.mts");
await fetchModel();
for (const file of modelFiles) file.markCurrent();
}
const output = new GeneratedFile(path.join(directory, "../lsp_generated.go"), [
__filename,
path.join(directory, "generate.mts"),
...modelFiles.map(file => file.fileName),
]);
if (output.isCurrent(!!options.force)) {
console.log("LSP bindings are up to date.");
return;
}
output.invalidate();
const { default: generate } = await import("./tsc/internal/lsp/lsproto/_generate/generate.mts");
await generate();
output.markCurrent();
}
export const generateLSP = task({
name: "generate:lsp",
description: "Generates LSP bindings from the pinned protocol model. Pass --force to regenerate unchanged files.",
run: runGenerateLSP,
});
// ── Enum generation from Go source ──────────────────────────────
/**
@@ -775,9 +973,10 @@ const enumValuesGeneratedGoPath = "tsc/internal/api/enum_values_generated.go";
* because it already imports (nearly) every package enums are sourced from.
*
* @param {GeneratedEnum[]} generatedEnums
* @param {import("./tools/scripts/gen/generatedFile.mts").GeneratedFile} generatedGoFile
* @returns {Promise<Record<string, Record<string, number>>>} enum def name -> (memberName -> Go value)
*/
async function computeGoGroundTruth(generatedEnums) {
async function computeGoGroundTruth(generatedEnums, generatedGoFile) {
/** @type {Map<string, {importPath: string, pkgName: string}>} */
const packagesByDir = new Map();
/**
@@ -849,7 +1048,7 @@ func toInt32[T ~int8 | ~int16 | ~int32 | ~int | ~uint8 | ~uint16 | ~uint32](v T)
`;
fs.writeFileSync(enumValuesGeneratedGoPath, goSource);
generatedGoFile.write(goSource);
await run("dprint", ["fmt", enumValuesGeneratedGoPath]);
const { stdout } = await runOutput("go", ["run", enumValuesGeneratedGoPath]);
@@ -873,6 +1072,26 @@ async function evaluateEnumMembers(enumSource, enumName) {
}
async function runGenerateEnums() {
const { GeneratedFile } = await import("./tools/scripts/gen/generatedFile.mts");
const inputs = [
__filename,
...fs.globSync(["go.work", "go.work.sum", "{tsc,tools}/go.{mod,sum}"]),
];
const enumFiles = enumDefs.map(def => {
const camelName = def.name.charAt(0).toLowerCase() + def.name.slice(1);
return {
def,
camelName,
typeFile: new GeneratedFile(path.join(def.outDir, `${camelName}.enum.ts`), [...inputs, def.goFile]),
runtimeFile: new GeneratedFile(path.join(def.outDir, `${camelName}.ts`), [...inputs, def.goFile]),
};
});
const generatedGoFile = new GeneratedFile(enumValuesGeneratedGoPath, [...inputs, ...enumDefs.map(def => def.goFile)]);
const generatedFiles = [generatedGoFile, ...enumFiles.flatMap(({ typeFile, runtimeFile }) => [typeFile, runtimeFile])];
if (generatedFiles.every(file => file.isCurrent(!!options.force))) {
console.log("Enums are up to date.");
return;
}
const ts = /** @type {typeof import("typescript")} */ (await import("typescript"));
/**
@@ -903,34 +1122,29 @@ async function runGenerateEnums() {
console.log("Generating enums from Go source...");
/** @type {Array<GeneratedEnum>} */
const generatedEnums = [];
for (const def of enumDefs) {
for (const { def, camelName, typeFile, runtimeFile } of enumFiles) {
const members = parseGoEnum(def);
const camelName = def.name.charAt(0).toLowerCase() + def.name.slice(1);
fs.mkdirSync(def.outDir, { recursive: true });
// Generate .enum.ts (TypeScript enum — used for types)
const enumTS = renderEnumTS(def, members);
const enumPath = path.join(def.outDir, `${camelName}.enum.ts`);
fs.writeFileSync(enumPath, enumTS);
typeFile.write(enumTS);
// Generate .ts (IIFE — used at runtime)
const enumJsCode = transpile(enumTS, def.name);
const iifeSource = convertEnumToTs(enumJsCode, def.name);
const iifePath = path.join(def.outDir, `${camelName}.ts`);
fs.writeFileSync(iifePath, iifeSource);
runtimeFile.write(iifeSource);
generatedEnums.push({
code: enumJsCode,
def,
members,
fileNames: [enumPath, iifePath],
fileNames: [typeFile.fileName, runtimeFile.fileName],
});
console.log(` ${def.name}: ${members.length} members → ${camelName}.enum.ts, ${camelName}.ts`);
}
console.log("Getting values from go");
const goValuesByEnum = await computeGoGroundTruth(generatedEnums);
const goValuesByEnum = await computeGoGroundTruth(generatedEnums, generatedGoFile);
/** @type {string[]} */
const mismatches = [];
for (const { def, members, code } of generatedEnums) {
@@ -958,31 +1172,63 @@ async function runGenerateEnums() {
console.log("All generated values match Go.");
await run("dprint", ["fmt", ...generatedEnums.flatMap(e => e.fileNames)]);
for (const file of generatedFiles) file.markCurrent();
console.log("Done.");
}
export const generateEnums = task({
name: "generate:enums",
description: "Generates TypeScript enum files from Go source.",
description: "Generates TypeScript enum files from Go source. Pass --force to regenerate unchanged files.",
run: runGenerateEnums,
});
async function runGenerateAST() {
const { default: generate } = await import("./tools/scripts/tsc/generate.ts");
generate(!!options.force);
await runGenerateASTStringer();
}
export const generateAST = task({
name: "generate:ast",
description: "Generates AST and encoder files from ast.json.",
run: () => run("node", ["./tools/scripts/tsc/generate.ts"]),
description: "Generates AST, kind stringer, and encoder files from ast.json. Pass --force to regenerate unchanged files.",
run: runGenerateAST,
});
async function runGenerateSync() {
const { generateSync } = await import("./packages/typescript/scripts/generateSync.ts");
generateSync(!!options.force);
}
export const generateSync = task({
name: "generate:sync",
description: "Generates synchronous and generator APIs and tests. Pass --force to regenerate unchanged files.",
run: runGenerateSync,
});
async function runGenerateAPI() {
await run("go", ["-C", "./tools", "run", "./gen-proto", "../tsc/internal/api/proto.go", "../packages/typescript/src/api/proto.generated.ts"]);
await run("npx", ["dprint", "fmt", "packages/typescript/src/api/proto.generated.ts"]);
await runGoGenerator("generate:api", {
file: "tsc/internal/api/proto.go",
cwd: __dirname,
inputs: [
"tsc/internal/api/*.go",
"tsc/internal/api/requestfilesystem/*.go",
"tsc/internal/core/*.go",
"tsc/internal/checker/types.go",
"tsc/internal/diagnostics/diagnostics.go",
"tsc/internal/tspath/path.go",
"tools/gen-proto/*.go",
],
exclude: ["**/*_test.go", "**/*_generated.go"],
envInputs: [],
outputs: ["packages/typescript/src/api/proto.generated.ts"],
commands: [
["go", "-C", "./tools", "run", "./gen-proto", "../tsc/internal/api/proto.go", "../packages/typescript/src/api/proto.generated.ts"],
["dprint", "fmt", "packages/typescript/src/api/proto.generated.ts"],
],
});
}
export const generateAPI = task({
name: "generate:api",
description: "Generates API files from internal/api/proto.go and internal/api/session.go.",
run: runGenerateAPI,
});
export const generateAPI = goGenerateTask("generate:api", runGenerateAPI);
// ── Vendored npm dependencies ───────────────────────────────────
@@ -1012,6 +1258,22 @@ export const generateVendor = task({
run: runGenerateVendor,
});
const generateCompiler = task({
name: "generate:compiler",
hiddenFromTaskList: true,
dependencies: [generateAST, generateLSP],
run: async () => {
await runGenerateGo();
await runGenerateEnums();
},
});
export const generate = task({
name: "generate",
description: "Runs all code generation, including AST, LSP, APIs, extension localization, and vendored dependencies.",
dependencies: [generateCompiler, generateSync, generateExtensionTest, generateVendor],
});
const coverageDir = path.join(__dirname, "coverage");
const ensureCoverageDirExists = memoize(() => {
@@ -1241,6 +1503,12 @@ export const testTools = task({
run: runTestTools,
});
export const testCodegen = task({
name: "test:codegen",
description: "Runs opt-in incremental codegen tests; excluded from validate and test:all. Because this runs asserts on build codegen, it takes awhile and is somewhat redundant.",
run: () => run("node", ["--test", "./tools/scripts/gen/*.test.mts"]),
});
export const buildAPI = task({
name: "build:api",
description: "Builds @typescript/typescript JS API.",
@@ -1249,8 +1517,8 @@ export const buildAPI = task({
},
});
async function runBuildAPITests() {
await run("npm", ["run", "-w", "@typescript/typescript", "generate:sync"]);
async function runBuildAPITests(generateSources = true) {
if (generateSources) await runGenerateSync();
await run("npm", ["run", "-w", "@typescript/typescript", "build:test"]);
}
@@ -1277,7 +1545,7 @@ export const testAPIBenchmarks = task({
export const testAll = task({
name: "test:all",
description: "Runs ALL tests in the repo, including benchmarks, tools, and the API tests.",
description: "Runs compiler, extension, benchmark, tools, and API tests. Codegen tests are opt-in via test:codegen.",
dependencies: [tsgo, buildAPITests],
run: async () => {
// Prevent interleaving by running these directly instead of in parallel.
@@ -1415,9 +1683,12 @@ async function runFormat() {
export const validate = task({
name: "validate",
description: "Builds, tests, lints, and formats the repo. Pass --api to include API tests, or --all to include ancillary repository tests. Benchmarks are separate: test:benchmarks and test:benchmarks:api.",
dependencies: [build],
description: "Generates, builds, tests, lints, and formats the repo. Pass --api to include API tests, or --all to include all code generation and ancillary repository tests Benchmarks are separate: test:benchmarks and test:benchmarks:api.",
dependencies: [options.all ? generate : generateGo],
run: async () => {
await generateLibs(builtLocal);
await buildTsc({ extraFlags: options.release ? getReleaseBuildFlags() : [] });
/** @type {{ name: string; error: unknown }[]} */
const failures = [];
/** @param {string} name @param {() => Promise<void>} action */
@@ -1431,18 +1702,14 @@ export const validate = task({
}
};
await runGenerate();
await runValidation("test:tsc", runTests);
await runValidation("test:extension", runTestExtension);
if (options.api || options.all) {
await runGenerateEnums(); // prereqs for test:api not included in `validate` deps
await runGenerateAPI();
await runBuildAPITests();
if (!options.all) await runGenerateEnums();
await runBuildAPITests(!options.all);
await runValidation("test:api", runTestAPI);
}
if (options.all) {
await runGenerateExtension();
await runGenerateVendor();
await runValidation("test:tools", runTestTools);
await runValidation("test:smoke", runSmokeTest); // in CI this is run with `--race`
}
@@ -1486,6 +1753,7 @@ export const checkHerebyfile = task({
"./node_modules/typescript/bin/tsc",
"--noEmit",
"--allowJs",
"--allowImportingTsExtensions",
"--checkJs",
"--target",
"es2022",
@@ -1540,6 +1808,7 @@ export const checkVsceVersion = task({
});
const scriptTsconfigs = [
"./tools/scripts/gen/tsconfig.json",
"./tools/scripts/tsc/tsconfig.json",
"./tsc/internal/lsp/lsproto/_generate/tsconfig.json",
];
+1 -1
View File
@@ -97,7 +97,7 @@
},
"scripts": {
"node": "node --conditions @typescript/source",
"generate": "npm run generate:ast && npm run generate:encoder && npm run generate:sync",
"generate": "npm run node -- scripts/generate.ts",
"generate:ast": "node ../../tools/scripts/tsc/generate-ts-ast.ts",
"generate:encoder": "npm run node -- ../../tools/scripts/tsc/generate-encoder.ts",
"generate:sync": "npm run node -- scripts/generateSync.ts",
+9
View File
@@ -0,0 +1,9 @@
import { parseGeneratorArgs } from "../../../tools/scripts/gen/utils.mts";
import generateEncoder from "../../../tools/scripts/tsc/generate-encoder.ts";
import generateAST from "../../../tools/scripts/tsc/generate-ts-ast.ts";
import { generateSync } from "./generateSync.ts";
const { force } = parseGeneratorArgs({});
generateAST(force);
generateEncoder(force);
generateSync(force);
+25 -19
View File
@@ -21,18 +21,17 @@
* node generateSync.ts
*/
import { readFileSync } from "node:fs";
import {
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import {
dirname,
join,
relative,
} from "node:path";
import { xSync } from "tinyexec";
import ts from "typescript";
import { GeneratedFile } from "../../../tools/scripts/gen/generatedFile.mts";
import {
formatFilesSync,
parseGeneratorArgs,
} from "../../../tools/scripts/gen/utils.mts";
function generatedHeader(asyncSourceRelPath: string): string {
return [
@@ -59,8 +58,11 @@ function generateSyncFile(
srcPath: string,
destPath: string,
transform: SourceTransform,
force: boolean,
variant: SyncVariant = "sync",
): string {
): GeneratedFile | undefined {
const generated = new GeneratedFile(destPath, [import.meta.filename, srcPath]);
if (generated.isCurrent(force)) return;
const source = readFileSync(srcPath, "utf-8");
// Normalize line endings to LF
@@ -77,12 +79,11 @@ function generateSyncFile(
const srcRelPath = relative(ROOT, srcPath).replaceAll("\\", "/");
result = generatedHeader(srcRelPath) + result;
mkdirSync(dirname(destPath), { recursive: true });
writeFileSync(destPath, result);
generated.write(result);
const label = relative(ROOT, srcPath).replaceAll("\\", "/");
const destLabel = relative(ROOT, destPath).replaceAll("\\", "/");
console.log(` ${label} → ${destLabel}`);
return destPath;
return generated;
}
// ── Directive processing ─────────────────────────────────────────
@@ -626,15 +627,11 @@ function getIndent(source: string, position: number): string {
// ── Formatting ───────────────────────────────────────────────────
function formatFiles(paths: string[]): void {
xSync("dprint", ["fmt", ...paths], { throwOnError: true });
}
// ── Main ─────────────────────────────────────────────────────────
export function generateSync(): void {
export function generateSync(force = false): void {
console.log("Generating sync API from async source...");
const generatedFiles: string[] = [];
const generatedFiles: (GeneratedFile | undefined)[] = [];
// Source files
for (const relPath of ["types.ts", "api.ts"]) {
@@ -655,6 +652,7 @@ export function generateSync(): void {
"",
transformAsyncSource(source, fileName, true),
].join("\n"),
force,
));
}
@@ -664,6 +662,7 @@ export function generateSync(): void {
join(TEST, "async", relPath),
join(TEST, "sync", relPath),
(source, fileName) => transformAsyncSource(source, fileName, false),
force,
));
}
@@ -671,14 +670,21 @@ export function generateSync(): void {
join(TEST, "async", "api.bench.ts"),
join(TEST, "generators", "api.bench.ts"),
(source, fileName) => transformAsyncSource(source, fileName, false),
force,
"generators",
));
const changedFiles = generatedFiles.filter(file => file !== undefined);
if (!changedFiles.length) {
console.log("Sync API is up to date.");
return;
}
console.log("Formatting...");
formatFiles(generatedFiles);
formatFilesSync(changedFiles.map(file => file.fileName));
for (const file of changedFiles) file.markCurrent();
console.log("Done.");
}
if (process.argv[1] === import.meta.filename) {
generateSync();
generateSync(parseGeneratorArgs({}).force);
}
+102
View File
@@ -0,0 +1,102 @@
import { createHash } from "node:crypto";
import * as fs from "node:fs";
import path from "node:path";
import { GeneratedFile } from "./generatedFile.mts";
import {
formatCommandArg,
goInputs,
repoRoot,
run,
} from "./utils.mts";
export interface CacheOptions {
inputs: string[];
outputs: string[];
commands: string[][];
cwd?: string;
exclude?: string[];
envInputs?: string[];
env?: NodeJS.ProcessEnv;
force?: boolean;
}
function expand(patterns: string[], cwd: string, exclude: string[] = []) {
return [...new Set(fs.globSync(patterns, { cwd, exclude }).map(file => path.resolve(cwd, file)))].sort();
}
export default async function cache({
inputs,
outputs,
commands,
cwd = process.cwd(),
exclude = [],
envInputs = ["GOOS", "GOARCH", "GOFLAGS", "GOTOOLCHAIN", "GOEXPERIMENT", "CGO_ENABLED", "GOWORK"],
env,
force = false,
}: CacheOptions): Promise<boolean> {
if (!outputs.length || !commands.length || commands.some(command => !command.length)) {
throw new Error("Cached generation requires outputs and nonempty commands.");
}
cwd = path.resolve(cwd);
const environment = { ...process.env, ...env };
const snapshotInputs = () => {
const entries = [
...new Set([
import.meta.filename,
path.join(import.meta.dirname, "generatedFile.mts"),
path.join(repoRoot, "package-lock.json"),
path.join(repoRoot, ".dprint.jsonc"),
...goInputs(),
...inputs.flatMap(pattern => {
const matches = expand([pattern], cwd, exclude);
if (!matches.length) throw new Error(`No inputs matched: ${pattern}`);
return matches;
}),
]),
].sort();
const files: string[] = [];
const hash = createHash("sha256");
for (const file of entries) {
if (fs.statSync(file).isDirectory()) {
hash.update(JSON.stringify([file, "directory"]));
}
else {
files.push(file);
hash.update(JSON.stringify([file, createHash("sha256").update(fs.readFileSync(file)).digest("hex")]));
}
}
return { files, hash: hash.digest("hex") };
};
const before = snapshotInputs();
const outputFiles = () => expand(outputs, cwd).filter(file => fs.statSync(file).isFile());
const artifacts = (files: string[]) =>
files.map(file =>
new GeneratedFile(file, before.files, undefined, {
cwd,
commands,
inputs,
outputs,
exclude,
inputHash: before.hash,
outputFiles: files,
environment: envInputs.map(name => [name, environment[name]]),
})
);
const previous = artifacts(outputFiles());
const complete = () => outputs.every(pattern => expand([pattern], cwd).some(file => fs.statSync(file).isFile()));
const commandText = commands.map(command => command.map(formatCommandArg).join(" ")).join("; ");
if (complete() && previous.every(file => file.isCurrent(force))) {
console.log(`skipped ${commandText}: codegen outputs are already up to date`);
return true;
}
for (const file of previous) file.invalidate();
for (const [command, ...args] of commands) {
await run(command, args, { cwd, env: environment });
}
if (!complete()) throw new Error(`Generation did not produce all declared outputs: ${outputs.join(", ")}`);
if (snapshotInputs().hash === before.hash) {
for (const file of artifacts(outputFiles())) file.markCurrent();
}
return false;
}
+152
View File
@@ -0,0 +1,152 @@
import assert from "node:assert/strict";
import * as fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import cache from "./cache.mts";
test("cache commands use and track an explicit environment without changing the parent", async context => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-cache-env-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const parentValue = process.env.GOFILE;
const options = {
cwd: directory,
inputs: [],
outputs: ["output.json"],
envInputs: ["GOFILE", "GOPACKAGE"],
env: { GOFILE: "source.go", GOPACKAGE: "example" },
commands: [[process.execPath, "-e", "require('node:fs').writeFileSync('output.json', JSON.stringify({ file: process.env.GOFILE, pkg: process.env.GOPACKAGE, cwd: process.cwd() }))"]],
};
assert.equal(await cache(options), false);
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(directory, "output.json"), "utf8")), {
file: "source.go",
pkg: "example",
cwd: directory,
});
assert.equal(await cache(options), true);
assert.equal(await cache({ ...options, env: { ...options.env, GOFILE: "other.go" } }), false);
assert.equal(process.env.GOFILE, parentValue);
});
test("cache commands preserve the working directory and arguments", async context => {
const root = path.resolve(import.meta.dirname, "../../..");
const directory = fs.mkdtempSync(path.join(root, "tsc/internal/_cache probe-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true, maxRetries: 10 }));
fs.writeFileSync(path.join(directory, "input.txt"), "input");
fs.writeFileSync(path.join(directory, "generate.cjs"), "const fs = require('node:fs'); fs.writeFileSync('output.txt', fs.readFileSync('input.txt') + JSON.stringify(process.argv.slice(2)));\n");
fs.writeFileSync(path.join(directory, "finish.cjs"), "require('node:fs').appendFileSync('output.txt', ' finished');\n");
const generate = (force = false, failure = false) =>
cache({
cwd: directory,
inputs: ["input.txt", "*.cjs"],
outputs: ["output.txt"],
commands: [
[process.execPath, "generate.cjs", "--help", "--force", "space value"],
[process.execPath, failure ? "missing.cjs" : "finish.cjs"],
],
force,
});
const output = path.join(directory, "output.txt");
assert.equal(await generate(), false);
assert.equal(fs.readFileSync(output, "utf8"), 'input["--help","--force","space value"] finished');
const timestamp = fs.statSync(output).mtimeMs;
assert.equal(await generate(), true);
assert.equal(fs.statSync(output).mtimeMs, timestamp);
assert.equal(await generate(true), false);
await assert.rejects(generate(false, true));
assert.equal(await generate(), false);
assert.equal(await generate(), true);
});
test("cache commands track inputs, outputs, arguments, and failures", async context => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-cache-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true, maxRetries: 10 }));
const input = path.join(directory, "input.txt");
const output = path.join(directory, "output.txt");
fs.writeFileSync(input, "first");
fs.writeFileSync(path.join(directory, "generate.cjs"), "const fs = require('node:fs'); fs.writeFileSync('output.txt', fs.readFileSync('input.txt'));\n");
fs.writeFileSync(path.join(directory, "finish.cjs"), "const fs = require('node:fs'); fs.appendFileSync('output.txt', process.argv[2]); fs.writeFileSync('extra.txt', 'extra');\n");
const generate = (suffix = " done", force = false, failure = false, env: NodeJS.ProcessEnv = {}) =>
cache({
cwd: directory,
inputs: ["input*.txt", "*.cjs"],
outputs: ["output.txt", "extra*.txt"],
envInputs: ["TSGO_CACHE_PROBE"],
env,
commands: [
[process.execPath, "generate.cjs"],
[process.execPath, failure ? "missing.cjs" : "finish.cjs", suffix],
],
force,
});
await generate();
assert.equal(fs.readFileSync(output, "utf8"), "first done");
const timestamp = fs.statSync(output).mtimeMs;
assert.equal(await generate(), true);
assert.equal(fs.statSync(output).mtimeMs, timestamp);
fs.writeFileSync(input, "second");
await generate();
assert.equal(fs.readFileSync(output, "utf8"), "second done");
const addedInput = path.join(directory, "input-added.txt");
fs.writeFileSync(addedInput, "added");
assert.equal(await generate(), false);
fs.rmSync(addedInput);
assert.equal(await generate(), false);
fs.rmSync(path.join(directory, "extra.txt"));
assert.equal(await generate(), false);
fs.writeFileSync(path.join(directory, "extra-added.txt"), "extra");
assert.equal(await generate(), false);
fs.writeFileSync(output, "edited");
assert.equal(await generate(), false);
assert.equal(await generate(" changed"), false);
assert.equal(fs.readFileSync(output, "utf8"), "second changed");
assert.equal(await generate(" changed"), true);
assert.equal(await generate(" changed", true), false);
await assert.rejects(generate(" changed", true, true));
assert.equal(await generate(" changed"), false);
assert.equal(await generate(" changed"), true);
const environment = { TSGO_CACHE_PROBE: "changed" };
assert.equal(await generate(" changed", false, false, environment), false);
assert.equal(await generate(" changed", false, false, environment), true);
assert.equal(await generate("--force"), false);
assert.equal(fs.readFileSync(output, "utf8"), "second--force");
assert.equal(await generate("--force"), true);
});
test("cache commands reject missing inputs, outputs, and empty commands", async context => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-cache-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const options = {
cwd: directory,
inputs: ["input.txt"],
outputs: ["output.txt"],
commands: [[process.execPath, "-e", "require('node:fs').writeFileSync('output.txt', 'generated')"]],
};
await assert.rejects(cache(options), /No inputs matched/);
assert.equal(fs.existsSync(path.join(directory, "output.txt")), false);
fs.writeFileSync(path.join(directory, "input.txt"), "input");
await assert.rejects(cache({ ...options, inputs: ["missing*.txt"] }), /No inputs matched/);
await assert.rejects(cache({ ...options, commands: [] }), /nonempty commands/);
await assert.rejects(cache({ ...options, commands: [[]] }), /nonempty commands/);
await assert.rejects(cache({ ...options, outputs: [] }), /requires outputs/);
await assert.rejects(cache({ ...options, outputs: ["output.txt", "missing.txt"] }), /did not produce all declared outputs/);
assert.equal(await cache(options), false);
assert.equal(await cache(options), true);
});
test("cache commands do not cache inputs changed during execution", async context => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-cache-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
for (const change of ["fs.writeFileSync('input.txt', 'changed')", "fs.writeFileSync('input-added.txt', 'added')"]) {
fs.writeFileSync(path.join(directory, "input.txt"), "initial");
const options = {
cwd: directory,
inputs: ["input*.txt"],
outputs: ["output.txt"],
commands: [[process.execPath, "-e", `const fs = require('node:fs'); fs.writeFileSync('output.txt', 'generated'); ${change};`]],
};
assert.equal(await cache(options), false);
assert.equal(await cache(options), false);
assert.equal(await cache(options), true);
}
});
+78
View File
@@ -0,0 +1,78 @@
import { createHash } from "node:crypto";
import * as fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { repoRoot } from "./utils.mts";
const commonInputs = [
import.meta.filename,
path.join(import.meta.dirname, "utils.mts"),
path.join(repoRoot, "package-lock.json"),
path.join(repoRoot, ".dprint.jsonc"),
];
function digest(value: string | Buffer): string {
return createHash("sha256").update(value).digest("hex");
}
function readIfExists(file: string): Buffer | undefined {
try {
return fs.readFileSync(file);
}
catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
}
export class GeneratedFile {
readonly fileName: string;
private readonly inputs: readonly string[];
private readonly inputHash: string;
private readonly cacheFile: string;
private readonly key: unknown;
constructor(fileName: string, inputs: readonly string[], cacheDirectory = path.join(os.tmpdir(), "typescript-codegen"), key?: unknown) {
this.fileName = path.resolve(fileName);
this.inputs = [...new Set([...commonInputs, ...inputs].map(file => path.resolve(file)))].sort();
this.key = key;
this.inputHash = this.hashInputs();
this.cacheFile = path.join(cacheDirectory, digest(this.fileName) + ".json");
}
private hashInputs(): string {
return digest(JSON.stringify([
process.version,
process.platform,
process.arch,
this.key,
this.inputs.map(file => [file, digest(fs.readFileSync(file))]),
]));
}
private state(output: Buffer): string {
return JSON.stringify([this.inputHash, digest(output)]);
}
isCurrent(force = false): boolean {
if (force) return false;
const output = readIfExists(this.fileName);
return output !== undefined && readIfExists(this.cacheFile)?.toString() === this.state(output);
}
invalidate(): void {
fs.rmSync(this.cacheFile, { force: true });
}
write(content: string): void {
this.invalidate();
fs.mkdirSync(path.dirname(this.fileName), { recursive: true });
fs.writeFileSync(this.fileName, content);
}
markCurrent(): void {
if (this.inputHash !== this.hashInputs()) return;
const state = this.state(fs.readFileSync(this.fileName));
fs.mkdirSync(path.dirname(this.cacheFile), { recursive: true });
fs.writeFileSync(this.cacheFile, state);
}
}
+584
View File
@@ -0,0 +1,584 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import * as fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { test } from "node:test";
import { stripVTControlCharacters } from "node:util";
import { runInNewContext } from "node:vm";
import { x } from "tinyexec";
import ts from "typescript";
import cache from "./cache.mts";
import { GeneratedFile } from "./generatedFile.mts";
test("validate generates before building and selects the generation scope", async () => {
const fileName = path.resolve(import.meta.dirname, "../../../Herebyfile.mjs");
const source = ts.createSourceFile(fileName, fs.readFileSync(fileName, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
const declaration = source.statements.filter(ts.isVariableStatement)
.flatMap(statement => statement.declarationList.declarations)
.find(declaration => declaration.name.getText(source) === "validate");
assert.ok(declaration?.initializer);
for (const options of [{}, { api: true }, { all: true }]) {
const calls: string[] = [];
const action = (name: string) => async () => {
calls.push(name);
};
const generate = { run: action("generate") };
const generateGo = { run: action("generate:go") };
const build = { run: action("build") };
const validation = runInNewContext(declaration.initializer.getText(source), {
task: (spec: unknown) => spec,
options,
generate,
generateGo,
build,
builtLocal: "./built/local",
generateLibs: action("lib"),
buildTsc: action("build"),
getReleaseBuildFlags: () => [],
runGenerateGo: action("generate:go"),
runGenerateEnums: action("generate:enums"),
runGenerateAPI: action("generate:api"),
runGenerateExtension: action("generate:extension"),
runGenerateVendor: action("generate:vendor"),
runBuildAPITests: async (generate = true) => {
if (generate) calls.push("generate:sync");
calls.push("build:api:test");
},
runTests: action("test:tsc"),
runTestExtension: action("test:extension"),
runTestAPI: action("test:api"),
runTestBenchmarks: action("test:benchmarks"),
runTestTools: action("test:tools"),
runSmokeTest: action("test:smoke"),
runLint: action("lint"),
runFormat: action("format"),
}) as { dependencies: { run: () => Promise<void>; }[]; run: () => Promise<void>; };
await Promise.all(validation.dependencies.map(dependency => dependency.run()));
await validation.run();
assert.equal(calls[0], "all" in options ? "generate" : "generate:go");
assert.ok(calls.indexOf("build") > 0);
assert.ok(calls.indexOf("build") < calls.indexOf("test:tsc"));
assert.equal(calls.includes("test:api"), "api" in options || "all" in options);
assert.equal(calls.includes("test:tools"), "all" in options);
if ("all" in options) {
assert.deepEqual(calls.filter(name => name.startsWith("generate")), ["generate"]);
}
}
});
test("generator options preserve repeated inputs and force overrides", async () => {
const { parseGeneratorArgs, resolveForce } = await import("./utils.mts");
const options = { type: { type: "string" }, input: { type: "string", multiple: true } } as const;
const args = ["--type", "Kind", "--input=kind.go", "--input", "extra.go", "--force"];
const parsed = parseGeneratorArgs(options, args);
assert.equal(parsed.values.type, "Kind");
assert.deepEqual(parsed.values.input, ["kind.go", "extra.go"]);
assert.equal(parsed.force, true);
assert.equal(resolveForce(undefined, "ON"), true);
assert.equal(resolveForce(undefined, "0"), false);
assert.equal(resolveForce(false, "1"), false);
assert.equal(parseGeneratorArgs({}, ["--no-force"]).force, false);
});
test("generated files are current only while their inputs and formatted output match", context => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-codegen-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const input = path.join(directory, "input.ts");
const output = path.join(directory, "output.ts");
const cache = path.join(directory, "cache");
fs.writeFileSync(input, "input");
const generated = () => new GeneratedFile(output, [input], cache);
let file = generated();
assert.equal(file.isCurrent(), false);
file.write("unformatted");
fs.writeFileSync(output, "formatted");
file.markCurrent();
assert.equal(generated().isCurrent(), true);
assert.equal(generated().isCurrent(true), false);
file.invalidate();
assert.equal(generated().isCurrent(), false);
file.write("formatted");
file.markCurrent();
assert.equal(generated().isCurrent(), true);
fs.utimesSync(input, new Date(), new Date());
assert.equal(generated().isCurrent(), true);
fs.writeFileSync(input, "changed");
assert.equal(generated().isCurrent(), false);
fs.writeFileSync(input, "input");
assert.equal(generated().isCurrent(), true);
fs.writeFileSync(output, "edited");
assert.equal(generated().isCurrent(), false);
file = generated();
file.write("formatted");
assert.equal(generated().isCurrent(), false);
file.markCurrent();
assert.equal(generated().isCurrent(), true);
const dependency = path.join(directory, "generator.ts");
fs.writeFileSync(dependency, "generator");
assert.equal(new GeneratedFile(output, [input, dependency], cache).isCurrent(), false);
fs.rmSync(dependency);
assert.throws(() => new GeneratedFile(output, [input, dependency], cache), { code: "ENOENT" });
fs.rmSync(output);
assert.equal(generated().isCurrent(), false);
});
test("changing inputs during generation does not mark stale output current", context => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-codegen-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const input = path.join(directory, "input.ts");
const output = path.join(directory, "output.ts");
const cache = path.join(directory, "cache");
fs.writeFileSync(input, "before");
const file = new GeneratedFile(output, [input], cache);
file.write("generated");
fs.writeFileSync(input, "after");
file.markCurrent();
assert.equal(new GeneratedFile(output, [input], cache).isCurrent(), false);
});
test("default metadata has a stable path in the OS temporary directory", context => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-codegen-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const output = path.join(directory, "output.ts");
const cacheFile = path.join(os.tmpdir(), "typescript-codegen", createHash("sha256").update(output).digest("hex") + ".json");
context.after(() => fs.rmSync(cacheFile, { force: true }));
const file = new GeneratedFile(output, []);
file.write("generated");
file.markCurrent();
assert.equal(fs.existsSync(cacheFile), true);
assert.equal(new GeneratedFile(output, []).isCurrent(), true);
fs.rmSync(cacheFile);
assert.equal(new GeneratedFile(output, []).isCurrent(), false);
file.markCurrent();
assert.equal(new GeneratedFile(output, []).isCurrent(), true);
});
test("metadata directories can disappear during generation", context => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-codegen-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const output = path.join(directory, "output.ts");
const cache = path.join(directory, "cache");
const file = new GeneratedFile(output, [], cache);
file.write("generated");
file.markCurrent();
fs.rmSync(cache, { recursive: true });
assert.equal(file.isCurrent(), false);
file.write("regenerated");
file.markCurrent();
assert.equal(new GeneratedFile(output, [], cache).isCurrent(), true);
});
test("generated files track each output independently", context => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-codegen-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const files = ["output.go", "locale.gz"].map(file => new GeneratedFile(path.join(directory, file), [], path.join(directory, "cache")));
for (const file of files) {
file.write("generated");
file.markCurrent();
}
assert.equal(files.every(file => file.isCurrent()), true);
fs.writeFileSync(files[1].fileName, "modified");
assert.equal(files[0].isCurrent(), true);
assert.equal(files[1].isCurrent(), false);
fs.writeFileSync(files[1].fileName, "generated");
assert.equal(files.every(file => file.isCurrent()), true);
fs.rmSync(files[1].fileName);
assert.equal(files[0].isCurrent(), true);
assert.equal(files[1].isCurrent(), false);
files[1].write("generated");
files[1].markCurrent();
files[1].invalidate();
assert.equal(files[0].isCurrent(), true);
assert.equal(files[1].isCurrent(), false);
});
test("generate:go runs Go generators directly and shares caches with Go fallback", async () => {
const root = path.resolve(import.meta.dirname, "../../..");
const generate = () => x("npx", ["hereby", "generate:go"], { throwOnError: true, nodeOptions: { cwd: root } });
const first = await generate();
assert.doesNotMatch(first.stdout, /\$ go generate|npm run --silent cache|\$ node .*generate-unicode-data/);
const files = fs.globSync(["tsc/internal/**/*generated.go", "packages/typescript/src/api/proto.generated.ts"], { cwd: root });
const timestamps = files.map(file => fs.statSync(path.join(root, file)).mtimeMs);
const current = await generate();
assert.doesNotMatch(current.stdout, /Generated codegen outputs|Generated Unicode tables/);
assert.match(current.stdout, /Unicode tables are up to date/);
assert.deepEqual(files.map(file => fs.statSync(path.join(root, file)).mtimeMs), timestamps);
const fallback = await x("go", ["-C", "./tsc", "generate", "./internal/diagnostics"], { throwOnError: true, nodeOptions: { cwd: root } });
assert.equal(fallback.stdout.match(/Codegen outputs are up to date/g)?.length, 2);
const nested = await x("npx", ["hereby", "generate:compileroptions"], { throwOnError: true, nodeOptions: { cwd: path.join(root, "tsc/internal/core") } });
assert.equal(nested.stdout.match(/Codegen outputs are up to date/g)?.length, 2);
});
test("generate includes standalone generators without Go traversal", async () => {
const root = path.resolve(import.meta.dirname, "../../..");
const { stdout } = await x("npx", ["hereby", "generate"], { throwOnError: true, nodeOptions: { cwd: root } });
const log = stripVTControlCharacters(stdout);
const eventIndex = (event: string) => {
const index = log.indexOf(event);
assert.ok(index >= 0, `Missing task event: ${event}`);
return index;
};
const compilerStart = eventIndex("Starting generate:compiler");
assert.ok(eventIndex("Finished generate:ast ") < compilerStart);
assert.ok(eventIndex("Finished generate:lsp ") < compilerStart);
assert.ok(eventIndex("Starting generate:sync") < compilerStart);
assert.ok(eventIndex("Starting generate:vendor") < compilerStart);
assert.ok(eventIndex("Finished generate:extension ") < eventIndex("Starting generate:extension-test"));
assert.doesNotMatch(stdout, /\$ go generate|npm run --silent cache/);
assert.match(stdout, /Unicode tables/);
assert.match(stdout, /Enums are up to date|All generated values match Go/);
assert.match(stdout, /Sync API is up to date|Formatting\.\.\./);
assert.match(stdout, /LSP bindings are up to date|Successfully generated .*lsp_generated\.go/);
assert.match(stdout, /generateLocBundle/);
assert.match(stdout, /generateLocTest/);
assert.deepEqual(
fs.readFileSync(path.join(root, "packages/typescript/vendor/vscode-jsonrpc/package.json")),
fs.readFileSync(path.join(root, "node_modules/vscode-jsonrpc/package.json")),
);
const lspOutput = path.join(root, "tsc/internal/lsp/lsproto/lsp_generated.go");
const timestamp = fs.statSync(lspOutput).mtimeMs;
const current = await x("npx", ["hereby", "generate:lsp"], { throwOnError: true, nodeOptions: { cwd: root } });
assert.match(current.stdout, /LSP bindings are up to date/);
assert.doesNotMatch(current.stdout, /Using vscode-languageclient/);
assert.equal(fs.statSync(lspOutput).mtimeMs, timestamp);
});
test("bundled generation skips unchanged library outputs", async () => {
const root = path.resolve(import.meta.dirname, "../../..");
const generate = () => x("go", ["-C", "./tsc", "generate", "./internal/bundled"], { throwOnError: true, nodeOptions: { cwd: root } });
await generate();
const files = ["libs_generated.go", "embed_generated.go"].map(file => path.join(root, "tsc/internal/bundled", file));
const timestamps = files.map(file => fs.statSync(file).mtimeMs);
assert.match((await generate()).stdout, /Codegen outputs are up to date\./);
assert.deepEqual(files.map(file => fs.statSync(file).mtimeMs), timestamps);
});
test("bundled generation tracks libraries, validates inputs, and supports force", async context => {
const root = path.resolve(import.meta.dirname, "../../..");
const directory = fs.mkdtempSync(path.join(root, "tsc/internal/_bundled-probe-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true, maxRetries: 10 }));
const notice = path.join(directory, "CopyrightNotice.txt");
const libs = path.join(directory, "libs");
fs.mkdirSync(libs);
fs.writeFileSync(notice, "Fixture notice\n\n");
const library = path.join(libs, "lib.first.d.ts");
const content = "Fixture notice\n\n\ndeclare const first: string;\n";
fs.writeFileSync(library, content);
const options = {
cwd: directory,
inputs: [path.join(root, "tsc/internal/bundled/generate.go"), "CopyrightNotice.txt", "libs/*"],
outputs: ["libs_generated.go", "embed_generated.go"],
commands: [["go", "run", path.join(root, "tsc/internal/bundled/generate.go")]],
};
const generate = (force = false) => cache({ ...options, force });
await generate();
const files = ["libs_generated.go", "embed_generated.go"].map(file => path.join(directory, file));
const originals = files.map(file => fs.readFileSync(file));
assert.equal(await generate(), true);
const added = path.join(libs, "lib.second.d.ts");
fs.writeFileSync(added, content);
assert.equal(await generate(), false);
for (const file of files) assert.match(fs.readFileSync(file, "utf8"), /lib\.second\.d\.ts/);
fs.rmSync(added);
assert.equal(await generate(), false);
for (const [index, file] of files.entries()) {
assert.deepEqual(fs.readFileSync(file), originals[index]);
fs.writeFileSync(file, "modified");
assert.equal(await generate(), false);
assert.deepEqual(fs.readFileSync(file), originals[index]);
fs.rmSync(file, { recursive: true, maxRetries: 10 });
assert.equal(await generate(), false);
assert.deepEqual(fs.readFileSync(file), originals[index]);
}
fs.writeFileSync(library, content.replace("string", "number"));
assert.equal(await generate(), false);
fs.writeFileSync(library, content.replaceAll("\n", "\r\n"));
await assert.rejects(generate());
fs.writeFileSync(library, content);
assert.equal(await generate(), false);
fs.mkdirSync(path.join(libs, "unexpected"));
await assert.rejects(generate());
fs.rmdirSync(path.join(libs, "unexpected"));
assert.equal(await generate(), false);
fs.writeFileSync(notice, "Changed fixture notice\n\n");
await assert.rejects(generate());
fs.writeFileSync(notice, "Fixture notice\n\n");
assert.equal(await generate(), false);
assert.equal(await generate(true), false);
assert.equal(await generate(), true);
});
test("stringer generation tracks declared inputs, options, output, and force", async context => {
const root = path.resolve(import.meta.dirname, "../../..");
const directory = fs.mkdtempSync(path.join(root, "tsc/internal/_stringer-probe-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const source = path.join(directory, "kind.go");
const extra = path.join(directory, "extra.go");
const output = path.join(directory, "kind_stringer_generated.go");
fs.writeFileSync(source, "package probe\n\ntype Kind int\n\nconst KindFirst Kind = 0\n");
const generate = (trimPrefix = false, force = false) =>
cache({
cwd: directory,
inputs: ["kind.go"],
outputs: ["kind_stringer_generated.go"],
env: { GOFILE: "kind.go" },
force,
commands: [
["go", "tool", "golang.org/x/tools/cmd/stringer", "-type=Kind", "-output=kind_stringer_generated.go", ...(trimPrefix ? ["-trimprefix=Kind"] : [])],
["dprint", "fmt", "kind_stringer_generated.go"],
],
});
await generate();
const timestamp = fs.statSync(output).mtimeMs;
assert.equal(await generate(), true);
assert.equal(fs.statSync(output).mtimeMs, timestamp);
fs.writeFileSync(extra, "package probe\n\nconst Unrelated = 1\n");
assert.equal(await generate(), true);
fs.appendFileSync(source, "\nconst KindSecond Kind = 1\n");
assert.equal(await generate(), false);
assert.match(fs.readFileSync(output, "utf8"), /KindSecond/);
fs.rmSync(extra);
assert.equal(await generate(), true);
fs.writeFileSync(source, "package probe\n\ntype Kind int\n\nconst KindFirst Kind = 0\n");
assert.equal(await generate(), false);
assert.doesNotMatch(fs.readFileSync(output, "utf8"), /KindSecond/);
assert.equal(await generate(true), false);
assert.match(fs.readFileSync(output, "utf8"), /_Kind_name = "First"/);
assert.equal(await generate(true), true);
fs.appendFileSync(output, "\n");
assert.equal(await generate(true), false);
fs.rmSync(output);
assert.equal(await generate(true), false);
assert.equal(await generate(true, true), false);
assert.equal(await generate(true), true);
fs.appendFileSync(source, "\nconst KindInvalid Kind = missing\n");
await assert.rejects(generate(true));
fs.writeFileSync(source, "package probe\n\ntype Kind int\n\nconst KindFirst Kind = 0\n");
assert.equal(await generate(true), false);
assert.equal(await generate(true), true);
});
test("moq generation tracks interface inputs, output, and force", async context => {
const root = path.resolve(import.meta.dirname, "../../..");
const directory = fs.mkdtempSync(path.join(root, "tsc/internal/_moq-probe-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const source = path.join(directory, "client.go");
const base = path.join(directory, "base.go");
fs.mkdirSync(path.join(directory, "mock"));
const output = path.join(directory, "mock/mock_generated.go");
fs.writeFileSync(base, "package probe\n\ntype Base interface { Read() string }\n");
fs.writeFileSync(source, "package probe\n\ntype Client interface { Base }\n");
const options = {
cwd: directory,
inputs: ["client.go", "base.go"],
outputs: ["mock/mock_generated.go"],
commands: [
["go", "tool", "github.com/matryer/moq", "-stub", "-fmt=goimports", "-pkg=mock", "-out=mock/mock_generated.go", ".", "Client"],
["dprint", "fmt", "mock/mock_generated.go"],
],
};
const generate = (force = false) => cache({ ...options, force });
await generate();
const timestamp = fs.statSync(output).mtimeMs;
assert.equal(await generate(), true);
assert.equal(fs.statSync(output).mtimeMs, timestamp);
fs.writeFileSync(base, "package probe\n\ntype Base interface { Read() string; Close() error }\n");
assert.equal(await generate(), false);
assert.match(fs.readFileSync(output, "utf8"), /CloseFunc/);
fs.rmSync(output);
assert.equal(await generate(), false);
assert.equal(await generate(true), false);
assert.equal(await generate(), true);
});
test("diagnostic generation tracks Go and locale outputs and supports force", async context => {
const root = path.resolve(import.meta.dirname, "../../..");
const directory = fs.mkdtempSync(path.join(root, "tsc/internal/_diagnostics-probe-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const output = path.join(directory, "diagnostics_generated.go");
const localized = path.join(directory, "loc_generated.go");
const options = {
cwd: path.join(root, "tsc/internal/diagnostics"),
inputs: ["generate.go", "diagnosticMessages.json", "extraDiagnosticMessages.json", "../{collections,json}/*.go", "../locale/lcl/*/diagnosticMessages/diagnosticMessages.generated.json.lcl"],
exclude: ["**/*_test.go"],
outputs: [output, localized, path.join(directory, "loc/*.json.gz")],
commands: [
["go", "run", "generate.go", "-diagnostics", output, "-loc", localized, "-locdir", path.join(directory, "loc")],
["dprint", "fmt", output, localized],
],
};
const generate = (force = false) => cache({ ...options, force });
await generate();
const locales = fs.globSync("loc/*.json.gz", { cwd: directory }).map(file => path.join(directory, file));
assert.ok(locales.length > 0);
const files = [output, localized, ...locales];
const timestamps = files.map(file => fs.statSync(file).mtimeMs);
assert.equal(await generate(), true);
assert.deepEqual(files.map(file => fs.statSync(file).mtimeMs), timestamps);
const archive = fs.readFileSync(locales[0]);
fs.rmSync(locales[0]);
assert.equal(await generate(), false);
assert.deepEqual(fs.readFileSync(locales[0]), archive);
fs.writeFileSync(locales[0], "modified");
assert.equal(await generate(), false);
assert.deepEqual(fs.readFileSync(locales[0]), archive);
const unexpected = path.join(directory, "loc/unexpected.json.gz");
fs.writeFileSync(unexpected, "unexpected");
assert.equal(await generate(), false);
assert.equal(fs.existsSync(unexpected), false);
fs.rmSync(path.join(directory, "loc"), { recursive: true });
assert.equal(await generate(), false);
assert.deepEqual(fs.readFileSync(locales[0]), archive);
fs.rmSync(localized);
assert.equal(await generate(), false);
assert.equal(await generate(true), false);
assert.equal(await generate(), true);
});
test("Unicode generation skips unchanged tables and formatting", async () => {
const root = path.resolve(import.meta.dirname, "../../..");
const generate = () => x("go", ["-C", "./tsc", "generate", "./internal/stringutil"], { throwOnError: true, nodeOptions: { cwd: root } });
await generate();
const files = ["js_case_generated.go", "identifier_parts_generated.go"].map(file => path.join(root, "tsc/internal/stringutil", file));
const timestamps = files.map(file => fs.statSync(file).mtimeMs);
const current = await generate();
assert.match(current.stdout, /Unicode tables are up to date\./);
assert.doesNotMatch(current.stdout, /Formatted/);
assert.deepEqual(files.map(file => fs.statSync(file).mtimeMs), timestamps);
});
test("Unicode generation repairs outputs and supports force", async context => {
const root = path.resolve(import.meta.dirname, "../../..");
const directory = fs.mkdtempSync(path.join(root, "tsc/internal/_unicode-probe-"));
context.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const command = [path.join(root, "tsc/internal/stringutil/_scripts/generate-unicode-data.mts"), "--outDir", directory];
const generate = (...args: string[]) => x(process.execPath, [...command, ...args], { throwOnError: true, nodeOptions: { cwd: root } });
await generate();
const files = ["js_case_generated.go", "identifier_parts_generated.go"].map(file => path.join(directory, file));
const originals = files.map(file => fs.readFileSync(file));
const timestamps = files.map(file => fs.statSync(file).mtimeMs);
assert.match((await generate()).stdout, /Unicode tables are up to date\./);
assert.deepEqual(files.map(file => fs.statSync(file).mtimeMs), timestamps);
for (const [index, file] of files.entries()) {
assert.deepEqual(originals[index], fs.readFileSync(path.join(root, "tsc/internal/stringutil", path.basename(file))));
fs.writeFileSync(file, "modified");
assert.match((await generate()).stdout, /Generated Unicode tables\./);
assert.deepEqual(fs.readFileSync(file), originals[index]);
fs.rmSync(file);
assert.match((await generate()).stdout, /Generated Unicode tables\./);
assert.deepEqual(fs.readFileSync(file), originals[index]);
}
const cacheFile = path.join(os.tmpdir(), "typescript-codegen", createHash("sha256").update(files[0]).digest("hex") + ".json");
context.after(() => fs.rmSync(cacheFile, { force: true }));
fs.rmSync(cacheFile);
assert.match((await generate()).stdout, /Generated Unicode tables\./);
assert.match((await generate("--force")).stdout, /Generated Unicode tables\./);
assert.match((await generate()).stdout, /Unicode tables are up to date\./);
const forced = await x(process.execPath, command, {
throwOnError: true,
nodeOptions: { cwd: root, env: { ...process.env, TSGO_HEREBY_FORCE: "1" } },
});
assert.match(forced.stdout, /Generated Unicode tables\./);
const overridden = await x(process.execPath, [...command, "--no-force"], {
throwOnError: true,
nodeOptions: { cwd: root, env: { ...process.env, TSGO_HEREBY_FORCE: "1" } },
});
assert.match(overridden.stdout, /Unicode tables are up to date\./);
});
test("enum generation skips unchanged outputs and Go verification", async () => {
const root = path.resolve(import.meta.dirname, "../../..");
const generate = (force = false) => x("npx", ["hereby", "generate:enums", ...(force ? ["--force"] : [])], { throwOnError: true, nodeOptions: { cwd: root } });
await generate();
const files = [
...fs.globSync("packages/typescript/src/enums/*.ts", { cwd: root }),
"tsc/internal/api/enum_values_generated.go",
].map(file => path.join(root, file));
const timestamps = files.map(file => fs.statSync(file).mtimeMs);
const { stdout } = await generate();
assert.match(stdout, /Enums are up to date\./);
assert.doesNotMatch(stdout, /Getting values from go/);
assert.deepEqual(files.map(file => fs.statSync(file).mtimeMs), timestamps);
const forced = await generate(true);
assert.match(forced.stdout, /All generated values match Go\./);
assert.match((await generate()).stdout, /Enums are up to date\./);
const verifier = path.join(root, "tsc/internal/api/enum_values_generated.go");
const cacheFile = path.join(os.tmpdir(), "typescript-codegen", createHash("sha256").update(verifier).digest("hex") + ".json");
fs.rmSync(cacheFile);
const regenerated = await generate();
assert.match(regenerated.stdout, /All generated values match Go\./);
assert.match((await generate()).stdout, /Enums are up to date\./);
});
test("AST generation forwards force to schema generators and the kind stringer", async () => {
const root = path.resolve(import.meta.dirname, "../../..");
const generate = (force = false) => x("npx", ["hereby", "generate:ast", ...(force ? ["--force"] : [])], { throwOnError: true, nodeOptions: { cwd: root } });
await generate();
const forced = await generate(true);
assert.match(forced.stdout, /Wrote .*encoder_generated\.go/);
assert.match(forced.stdout, /Wrote .*ast_generated\.go/);
assert.match(forced.stdout, /Generated .*ast\.generated\.ts/);
assert.match(forced.stdout, /Generated codegen outputs\./);
const output = path.join(root, "tsc/internal/ast/kind_stringer_generated.go");
const timestamp = fs.statSync(output).mtimeMs;
assert.doesNotMatch(forced.stdout, /\$ node .*tools\/scripts\/tsc\/generate\.ts/);
const current = await generate();
assert.doesNotMatch(current.stdout, /(?:Wrote|Generated) /);
assert.match(current.stdout, /Codegen outputs are up to date\./);
assert.equal(fs.statSync(output).mtimeMs, timestamp);
});
test("package generation forwards force to AST, encoder, and sync generators", async () => {
const root = path.resolve(import.meta.dirname, "../../..");
const generate = (force = false) => x("npm", ["run", "-w", "@typescript/typescript", "generate", ...(force ? ["--", "--force"] : [])], { throwOnError: true, nodeOptions: { cwd: root } });
await generate();
const forced = await generate(true);
assert.match(forced.stdout, /Generated .*ast\.generated\.ts/);
assert.match(forced.stdout, /Wrote .*encoder_generated\.go/);
assert.match(forced.stdout, /Formatting\.\.\./);
const current = await generate();
assert.doesNotMatch(current.stdout, /(?:Wrote|Generated) |Formatting\.\.\./);
assert.match(current.stdout, /Sync API is up to date\./);
});
test("API protocol generation caches formatted output and supports force", async context => {
const root = path.resolve(import.meta.dirname, "../../..");
const output = path.join(root, "packages/typescript/src/api/proto.generated.ts");
const generate = (force = false) => x("npx", ["hereby", "generate:api", ...(force ? ["--force"] : [])], { throwOnError: true, nodeOptions: { cwd: root } });
await generate();
const timestamp = fs.statSync(output).mtimeMs;
const current = await generate();
assert.match(current.stdout, /Codegen outputs are up to date\./);
assert.doesNotMatch(current.stdout, /\$ node .*cache\.mts/);
assert.equal(fs.statSync(output).mtimeMs, timestamp);
const unrelated = path.join(root, "tsc/internal/parser/codegen_cache_probe.go");
assert.equal(fs.existsSync(unrelated), false);
context.after(() => fs.rmSync(unrelated, { force: true }));
fs.writeFileSync(unrelated, "package parser\n");
assert.match((await generate()).stdout, /Codegen outputs are up to date\./);
fs.rmSync(unrelated);
const schema = path.join(root, "tsc/internal/api/requestfilesystem/codegen_cache_probe.go");
assert.equal(fs.existsSync(schema), false);
const original = fs.readFileSync(output, "utf8");
context.after(async () => {
if (fs.existsSync(schema)) {
fs.rmSync(schema);
await generate(true);
}
});
fs.writeFileSync(schema, 'package requestfilesystem\n\nconst KindCodegenCacheProbe Kind = "codegen-cache-probe"\n');
assert.match((await generate()).stdout, /Generated codegen outputs\./);
assert.match(fs.readFileSync(output, "utf8"), /"codegen-cache-probe"/);
fs.rmSync(schema);
assert.match((await generate()).stdout, /Generated codegen outputs\./);
assert.equal(fs.readFileSync(output, "utf8"), original);
assert.match((await generate(true)).stdout, /Generated codegen outputs\./);
assert.match((await generate()).stdout, /Codegen outputs are up to date\./);
const nested = await x("go", ["-C", "./tsc", "generate", "./internal/api"], {
throwOnError: true,
nodeOptions: { cwd: root, env: { ...process.env, TSGO_HEREBY_FORCE: "1" } },
});
assert.match(nested.stdout, /Generated codegen outputs\./);
assert.match((await generate()).stdout, /Codegen outputs are up to date\./);
});
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "nodenext",
"lib": ["esnext"],
"noEmit": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"allowImportingTsExtensions": true,
"types": ["node"]
}
}
+65
View File
@@ -0,0 +1,65 @@
import * as fs from "node:fs";
import path from "node:path";
import {
parseArgs,
type ParseArgsOptionsConfig,
} from "node:util";
import {
x,
xSync,
} from "tinyexec";
export const repoRoot = path.resolve(import.meta.dirname, "../../..");
export interface RunOptions {
captureOutput?: boolean;
cwd?: string;
env?: NodeJS.ProcessEnv;
signal?: AbortSignal;
}
export function formatCommandArg(arg: string) {
return arg && /^[\w@%+=:,./-]+$/.test(arg) ? arg : JSON.stringify(arg);
}
export function run(command: string, args: readonly string[] = [], options: RunOptions = {}) {
console.log("$ " + [command, ...args].map(formatCommandArg).join(" "));
return x(command, args, {
throwOnError: true,
...(options.signal ? { signal: options.signal } : {}),
nodeOptions: {
cwd: options.cwd,
env: options.env ? { ...process.env, ...options.env } : undefined,
stdio: options.captureOutput ? "pipe" : "inherit",
},
});
}
export function globInputs(patterns: string[], exclude: string[] = []): string[] {
return fs.globSync(patterns, { cwd: repoRoot, exclude }).map(file => path.join(repoRoot, file));
}
export function goInputs(): string[] {
return [import.meta.filename, ...globInputs(["go.work", "go.work.sum", "{tsc,tools}/go.{mod,sum}"])];
}
export function resolveForce(force?: boolean, environment = process.env.TSGO_HEREBY_FORCE): boolean {
return force ?? /^(1|true|yes|on)$/i.test(environment ?? "");
}
export function parseGeneratorArgs<const Options extends ParseArgsOptionsConfig>(options: Options, args = process.argv.slice(2)) {
const { values } = parseArgs({
args,
options: { ...options, force: { type: "boolean" as const } },
allowNegative: true,
});
return { values, force: resolveForce((values as { force?: boolean; }).force) };
}
export async function formatFiles(files: string[]): Promise<void> {
await x("dprint", ["fmt", ...files], { throwOnError: true, nodeOptions: { cwd: repoRoot, stdio: "inherit" } });
}
export function formatFilesSync(files: string[]): void {
xSync("dprint", ["fmt", ...files], { throwOnError: true, nodeOptions: { cwd: repoRoot, stdio: "inherit" } });
}
+24 -28
View File
@@ -10,10 +10,14 @@
* - packages/typescript/src/api/node/protocol.generated.ts
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { xSync } from "tinyexec";
import { GeneratedFile } from "../gen/generatedFile.mts";
import {
formatFilesSync,
parseGeneratorArgs,
repoRoot as ROOT,
} from "../gen/utils.mts";
import type {
KindType,
MemberInfo,
@@ -21,8 +25,6 @@ import type {
} from "./schema.ts";
import { api } from "./schema.ts";
const ROOT = path.resolve(import.meta.dirname!, "../../..");
// ═══════════════════════════════════════════════════════════════════════════
// Code writer
// ═══════════════════════════════════════════════════════════════════════════
@@ -2043,14 +2045,13 @@ function emitRemoteNodeClassClose(w: CodeWriter) {
// Main: generate and write all files
// ═══════════════════════════════════════════════════════════════════════════
function writeAndFormat(filePath: string, content: string, formatter: string) {
fs.writeFileSync(filePath, content);
function writeAndFormat(filePath: string, generate: () => string, force: boolean) {
const generated = new GeneratedFile(filePath, [import.meta.filename, path.join(ROOT, "tools/scripts/tsc/schema.ts"), path.join(ROOT, "tools/scripts/tsc/ast.json")]);
if (generated.isCurrent(force)) return;
generated.write(generate() + "\n");
try {
const [cmd, ...args] = formatter.split(" ");
xSync(cmd, [...args, filePath], {
throwOnError: true,
nodeOptions: { stdio: "inherit", cwd: ROOT },
});
formatFilesSync([filePath]);
generated.markCurrent();
}
catch {
console.warn(`Warning: formatter failed for ${filePath}`);
@@ -2058,47 +2059,42 @@ function writeAndFormat(filePath: string, content: string, formatter: string) {
console.log(`Wrote ${filePath}`);
}
export default function main() {
export default function main(force = false) {
console.log("Generating encoder/decoder code...");
const goEncoder = generateGoEncoder();
writeAndFormat(
path.join(ROOT, "tsc/internal/api/encoder/encoder_generated.go"),
goEncoder + "\n",
"dprint fmt",
generateGoEncoder,
force,
);
const goDecoder = generateGoDecoder();
writeAndFormat(
path.join(ROOT, "tsc/internal/api/encoder/decoder_generated.go"),
goDecoder + "\n",
"dprint fmt",
generateGoDecoder,
force,
);
const tsProtocol = generateTSProtocol();
writeAndFormat(
path.join(ROOT, "packages/typescript/src/api/node/protocol.generated.ts"),
tsProtocol + "\n",
"dprint fmt",
generateTSProtocol,
force,
);
const tsEncoder = generateTSEncoder();
writeAndFormat(
path.join(ROOT, "packages/typescript/src/api/node/encoder.generated.ts"),
tsEncoder + "\n",
"dprint fmt",
generateTSEncoder,
force,
);
const tsNode = generateTSNodeGenerated();
writeAndFormat(
path.join(ROOT, "packages/typescript/src/api/node/node.generated.ts"),
tsNode + "\n",
"dprint fmt",
generateTSNodeGenerated,
force,
);
console.log("Done!");
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main();
main(parseGeneratorArgs({}).force);
}
+17 -18
View File
@@ -14,10 +14,14 @@
* - Is*() type guard functions
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { xSync } from "tinyexec";
import { GeneratedFile } from "../gen/generatedFile.mts";
import {
formatFilesSync,
parseGeneratorArgs,
repoRoot as ROOT,
} from "../gen/utils.mts";
import type {
MemberInfo,
NodeType,
@@ -32,8 +36,6 @@ import {
// Load schema
// ────────────────────────────────────────────────────────────────────────────
const ROOT = path.resolve(import.meta.dirname!, "../../..");
// Members that participate in factory/visitor/clone (excludes noFactory)
function schemaMembers(node: NodeType): MemberInfo[] {
return node.members.filter(m => !m.noFactory);
@@ -984,8 +986,7 @@ function generateKind(): string {
w.write("");
w.write("package ast");
w.write("");
w.write("//go:generate go tool golang.org/x/tools/cmd/stringer -type=Kind -output=kind_stringer_generated.go");
w.write("//go:generate npx dprint fmt kind_stringer_generated.go");
w.write("//go:generate npx hereby generate:ast-stringer");
w.write("");
w.write("type Kind int16");
w.write("");
@@ -1082,27 +1083,25 @@ function generateKind(): string {
return w.toString();
}
function writeAndFormat(filePath: string, content: string) {
fs.writeFileSync(filePath, content);
xSync("dprint", ["fmt", filePath], {
throwOnError: true,
nodeOptions: { stdio: "inherit", cwd: ROOT },
});
function writeAndFormat(filePath: string, generateContent: () => string, force: boolean) {
const generated = new GeneratedFile(filePath, [import.meta.filename, path.join(ROOT, "tools/scripts/tsc/schema.ts"), path.join(ROOT, "tools/scripts/tsc/ast.json")]);
if (generated.isCurrent(force)) return;
generated.write(generateContent());
formatFilesSync([filePath]);
generated.markCurrent();
console.log(`Wrote ${filePath}`);
}
export default function main() {
export default function main(force = false) {
console.log("Generating Go AST code...");
const code = generate();
const outPath = path.join(ROOT, "tsc/internal/ast/ast_generated.go");
writeAndFormat(outPath, code + "\n");
writeAndFormat(outPath, () => generate() + "\n", force);
const kindCode = generateKind();
const kindOutPath = path.join(ROOT, "tsc/internal/ast/kind_generated.go");
writeAndFormat(kindOutPath, kindCode + "\n");
writeAndFormat(kindOutPath, () => generateKind() + "\n", force);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main();
main(parseGeneratorArgs({}).force);
}
+18 -16
View File
@@ -8,10 +8,14 @@
* Usage: node tools/scripts/tsc/generate-ts-ast.ts
*/
import * as fs from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { xSync } from "tinyexec";
import { GeneratedFile } from "../gen/generatedFile.mts";
import {
formatFilesSync,
parseGeneratorArgs,
repoRoot as ROOT,
} from "../gen/utils.mts";
import type {
MemberInfo,
NodeType,
@@ -26,8 +30,6 @@ import {
// Load schema
// ────────────────────────────────────────────────────────────────────────────
const ROOT = path.resolve(import.meta.dirname!, "../../..");
// TS members only (filter noTS/inherited issues)
function tsMembers(node: NodeType): MemberInfo[] {
return node.members.filter(member => !member.isKindParam() && !member.noTS);
@@ -1810,16 +1812,16 @@ function generateVisitor(): string {
// Main
// ────────────────────────────────────────────────────────────────────────────
function writeAndFormat(filePath: string, content: string) {
fs.writeFileSync(filePath, content);
xSync("dprint", ["fmt", filePath], {
throwOnError: true,
nodeOptions: { stdio: "inherit", cwd: ROOT },
});
function writeAndFormat(filePath: string, generate: () => string, force: boolean) {
const generated = new GeneratedFile(filePath, [import.meta.filename, path.join(ROOT, "tools/scripts/tsc/schema.ts"), path.join(ROOT, "tools/scripts/tsc/ast.json")]);
if (generated.isCurrent(force)) return;
generated.write(generate());
formatFilesSync([filePath]);
generated.markCurrent();
console.log(`Generated ${filePath}`);
}
export default function main() {
export default function main(force = false) {
console.log("Generating TS AST code...");
const factoryPath = path.join(ROOT, "packages/typescript/src/ast/factory.generated.ts");
@@ -1827,12 +1829,12 @@ export default function main() {
const astGenPath = path.join(ROOT, "packages/typescript/src/ast/ast.generated.ts");
const visitorPath = path.join(ROOT, "packages/typescript/src/ast/visitor.generated.ts");
writeAndFormat(astGenPath, generateAstGenerated());
writeAndFormat(factoryPath, generateFactory());
writeAndFormat(isGenPath, generateIsGenerated());
writeAndFormat(visitorPath, generateVisitor());
writeAndFormat(astGenPath, generateAstGenerated, force);
writeAndFormat(factoryPath, generateFactory, force);
writeAndFormat(isGenPath, generateIsGenerated, force);
writeAndFormat(visitorPath, generateVisitor, force);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main();
main(parseGeneratorArgs({}).force);
}
+6 -5
View File
@@ -1,14 +1,15 @@
import { fileURLToPath } from "node:url";
import { parseGeneratorArgs } from "../gen/utils.mts";
import generateEncoder from "./generate-encoder.ts";
import generateGoAST from "./generate-go-ast.ts";
import generateTSAST from "./generate-ts-ast.ts";
export default function generate() {
generateEncoder();
generateGoAST();
generateTSAST();
export default function generate(force = false) {
generateEncoder(force);
generateGoAST(force);
generateTSAST(force);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
generate();
generate(parseGeneratorArgs({}).force);
}
+1 -1
View File
@@ -2,6 +2,7 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { repoRoot as ROOT } from "../gen/utils.mts";
// ────────────────────────────────────────────────────────────────────────────
// Schema type definitions
@@ -1260,7 +1261,6 @@ export class SchemaAPI {
}
}
const ROOT = path.resolve(import.meta.dirname!, "../../..");
export const api = new SchemaAPI(JSON.parse(
fs.readFileSync(path.join(ROOT, "tools/scripts/tsc/ast.json"), "utf-8"),
) as Schema);
+4 -11
View File
@@ -1,11 +1,4 @@
{
"compilerOptions": {
"module": "nodenext",
"lib": ["esnext"],
"noEmit": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"allowImportingTsExtensions": true,
"types": ["node"]
}
}
{
"extends": "../gen/tsconfig.json",
"include": ["**/*.ts"]
}
+1 -2
View File
@@ -2,8 +2,7 @@
package ast
//go:generate go tool golang.org/x/tools/cmd/stringer -type=Kind -output=kind_stringer_generated.go
//go:generate npx dprint fmt kind_stringer_generated.go
//go:generate npx hereby generate:ast-stringer
type Kind int16
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/vfs"
)
//go:generate go run generate.go
//go:generate npx hereby generate:bundled
// Define the below here to consolidate documentation.
+1 -2
View File
@@ -11,8 +11,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/evaluator"
)
//go:generate go tool golang.org/x/tools/cmd/stringer -type=SignatureKind -output=stringer_generated.go
//go:generate npx dprint fmt stringer_generated.go
//go:generate npx hereby generate:checker
// ParseFlags
+1 -3
View File
@@ -9,9 +9,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/tspath"
)
//go:generate go tool golang.org/x/tools/cmd/stringer -type=ModuleKind -trimprefix=ModuleKind -output=modulekind_stringer_generated.go
//go:generate go tool golang.org/x/tools/cmd/stringer -type=ScriptTarget -trimprefix=ScriptTarget -output=scripttarget_stringer_generated.go
//go:generate npx dprint fmt modulekind_stringer_generated.go scripttarget_stringer_generated.go
//go:generate npx hereby generate:compileroptions
// CompilerOptions contains the compiler options exposed by the API.
type CompilerOptions struct {
+1 -2
View File
@@ -1,7 +1,6 @@
package core
//go:generate go tool golang.org/x/tools/cmd/stringer -type=LanguageVariant -output=languagevariant_stringer_generated.go
//go:generate npx dprint fmt languagevariant_stringer_generated.go
//go:generate npx hereby generate:languagevariant
type LanguageVariant int32
+1 -2
View File
@@ -1,7 +1,6 @@
package core
//go:generate go tool golang.org/x/tools/cmd/stringer -type=ScriptKind -output=scriptkind_stringer_generated.go
//go:generate npx dprint fmt scriptkind_stringer_generated.go
//go:generate npx hereby generate:scriptkind
type ScriptKind int32
+1 -2
View File
@@ -1,7 +1,6 @@
package core
//go:generate go tool golang.org/x/tools/cmd/stringer -type=Tristate -output=tristate_stringer_generated.go
//go:generate npx dprint fmt tristate_stringer_generated.go
//go:generate npx hereby generate:tristate
// Tristate
+1 -3
View File
@@ -13,9 +13,7 @@ import (
"golang.org/x/text/language"
)
//go:generate go run generate.go -diagnostics ./diagnostics_generated.go -loc ./loc_generated.go -locdir ./loc
//go:generate go tool golang.org/x/tools/cmd/stringer -type=Category -output=stringer_generated.go
//go:generate npx dprint fmt diagnostics_generated.go loc_generated.go stringer_generated.go
//go:generate npx hereby generate:diagnostics
type Category int32
+1 -2
View File
@@ -8,8 +8,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/tspath"
)
//go:generate go tool golang.org/x/tools/cmd/stringer -type=ExportSyntax -output=export_stringer_generated.go
//go:generate npx dprint fmt export_stringer_generated.go
//go:generate npx hereby generate:autoimport
// ModuleID uniquely identifies a module across multiple declarations.
// If the export is from an ambient module declaration, this is the module name.
@@ -12,28 +12,35 @@ const __dirname = path.dirname(__filename);
const metaModelPath = path.join(__dirname, "metaModel.json");
const metaModelSchemaPath = path.join(__dirname, "metaModelSchema.mts");
// Resolve the vscode-languageclient version from the root package-lock.json.
const lockfilePath = path.resolve(__dirname, "../../../../../package-lock.json");
const lockfile = JSON.parse(fs.readFileSync(lockfilePath, "utf-8"));
const clientVersion: string = lockfile.packages["node_modules/vscode-languageclient"].version;
export default async function fetchModel() {
const lockfilePath = path.resolve(__dirname, "../../../../../package-lock.json");
const lockfile = JSON.parse(fs.readFileSync(lockfilePath, "utf-8"));
const clientVersion: string = lockfile.packages["node_modules/vscode-languageclient"].version;
const ref = `release/client/${clientVersion}`;
console.log(`Using vscode-languageclient@${clientVersion}`);
const ref = `release/client/${clientVersion}`;
console.log(`Using vscode-languageclient@${clientVersion}`);
const metaModelURL = `https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/${ref}/protocol/metaModel.json`;
const metaModelSchemaURL = `https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/${ref}/tools/src/metaModel.ts`;
const metaModelURL = `https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/${ref}/protocol/metaModel.json`;
const metaModelSchemaURL = `https://raw.githubusercontent.com/microsoft/vscode-languageserver-node/${ref}/tools/src/metaModel.ts`;
const metaModelResponse = await fetch(metaModelURL);
const metaModel = await metaModelResponse.text();
fs.writeFileSync(metaModelPath, metaModel);
const metaModelResponse = await fetch(metaModelURL);
if (!metaModelResponse.ok) throw new Error(`Failed to fetch ${metaModelURL}: ${metaModelResponse.status}`);
const metaModel = await metaModelResponse.text();
JSON.parse(metaModel);
const metaModelSchemaResponse = await fetch(metaModelSchemaURL);
let metaModelSchema = await metaModelSchemaResponse.text();
const metaModelSchemaResponse = await fetch(metaModelSchemaURL);
if (!metaModelSchemaResponse.ok) throw new Error(`Failed to fetch ${metaModelSchemaURL}: ${metaModelSchemaResponse.status}`);
let metaModelSchema = await metaModelSchemaResponse.text();
// Patch the schema to add omitzeroValue property to Property type
metaModelSchema = metaModelSchema.replace(
/(\t \* Whether the property is deprecated or not\. If deprecated\n\t \* the property contains the deprecation message\.\n\t \*\/\n\tdeprecated\?: string;)\n}/m,
`$1\n\n\t/**\n\t * Whether this property uses omitzero without being a pointer.\n\t * Custom extension for special value types.\n\t */\n\tomitzeroValue?: boolean;\n}`,
);
metaModelSchema = metaModelSchema.replace(
/(\t \* Whether the property is deprecated or not\. If deprecated\n\t \* the property contains the deprecation message\.\n\t \*\/\n\tdeprecated\?: string;)\n}/m,
`$1\n\n\t/**\n\t * Whether this property uses omitzero without being a pointer.\n\t * Custom extension for special value types.\n\t */\n\tomitzeroValue?: boolean;\n}`,
);
fs.writeFileSync(metaModelSchemaPath, metaModelSchema);
fs.writeFileSync(metaModelPath, metaModel);
fs.writeFileSync(metaModelSchemaPath, metaModelSchema);
}
if (process.argv[1] === __filename) {
await fetchModel();
}
@@ -3665,7 +3665,7 @@ function getLocationUriProperty(structure: Structure) {
/**
* Main function
*/
async function main() {
export default async function generate() {
collectTypeDefinitions();
const generatedCode = generateCode();
fs.writeFileSync(out, generatedCode);
@@ -3678,7 +3678,6 @@ async function main() {
console.log(`Successfully generated ${out}`);
}
main().catch(e => {
console.error(e);
process.exit(1);
});
if (process.argv[1] === __filename) {
await generate();
}
+1 -2
View File
@@ -105,8 +105,7 @@ func ParseSyntheticProjectID(value string) (SyntheticProjectID, bool) {
return NewSyntheticProjectID(id), true
}
//go:generate go tool golang.org/x/tools/cmd/stringer -type=Kind -trimprefix=Kind -output=project_stringer_generated.go
//go:generate npx dprint fmt project_stringer_generated.go
//go:generate npx hereby generate:project
type Kind int
@@ -1,7 +1,12 @@
#!/usr/bin/env node
import * as fs from "fs";
import { fileURLToPath } from "node:url";
import * as path from "path";
import { GeneratedFile } from "../../../../tools/scripts/gen/generatedFile.mts";
import {
formatFiles,
parseGeneratorArgs,
} from "../../../../tools/scripts/gen/utils.mts";
// All Unicode data is sourced from a single version-pinned @unicode/unicode-*
// package so the generated tables are reproducible regardless of the Node.js
@@ -12,8 +17,6 @@ const UNICODE_VERSION = "15.1.0";
const PACKAGE = `@unicode/unicode-${UNICODE_VERSION}`;
const scriptDir = import.meta.dirname;
const CASE_OUTPUT_PATH = path.join(scriptDir, "..", "js_case_generated.go");
const IDENTIFIER_OUTPUT_PATH = path.join(scriptDir, "..", "identifier_parts_generated.go");
// A *unicode.RangeTable is split into 16-bit (BMP) and 32-bit (astral) ranges,
// each carrying a stride so arithmetic sequences (e.g. the alternating
@@ -228,20 +231,40 @@ ${renderRangeTable("unicodeESNextIdentifierPart", partTable)}
`;
}
async function main() {
export default async function generateUnicode(force = false, outDir = path.resolve(scriptDir, "..")) {
outDir = path.resolve(outDir);
const inputs = [
import.meta.filename,
fileURLToPath(import.meta.resolve(`${PACKAGE}/package.json`)),
];
const caseOutput = new GeneratedFile(path.join(outDir, "js_case_generated.go"), inputs);
const identifierOutput = new GeneratedFile(path.join(outDir, "identifier_parts_generated.go"), inputs);
const generatedFiles = [caseOutput, identifierOutput];
if (generatedFiles.every(file => file.isCurrent(force))) {
console.log("Unicode tables are up to date.");
return;
}
for (const file of generatedFiles) file.invalidate();
const simpleLowercase = await loadSimpleMapping("Simple_Case_Mapping/Lowercase");
const simpleUppercase = await loadSimpleMapping("Simple_Case_Mapping/Uppercase");
const entries = await buildSpecialCasing(simpleLowercase, simpleUppercase);
const casedTable = toRangeTable(await loadCodePoints("Binary_Property/Cased"));
const caseIgnorableTable = toRangeTable(await loadCodePoints("Binary_Property/Case_Ignorable"));
fs.writeFileSync(CASE_OUTPUT_PATH, renderCaseFile(entries, casedTable, caseIgnorableTable));
caseOutput.write(renderCaseFile(entries, casedTable, caseIgnorableTable));
const idStart = await loadCodePoints("Binary_Property/ID_Start");
const idContinue = await loadCodePoints("Binary_Property/ID_Continue");
// Other_ID_Start/Other_ID_Continue are already folded into ID_Start/ID_Continue.
const startTable = toRangeTable(idStart);
const partTable = toRangeTable([...idContinue, ...idStart]);
fs.writeFileSync(IDENTIFIER_OUTPUT_PATH, renderIdentifierFile(startTable, partTable));
identifierOutput.write(renderIdentifierFile(startTable, partTable));
await formatFiles(generatedFiles.map(file => file.fileName));
for (const file of generatedFiles) file.markCurrent();
console.log("Generated Unicode tables.");
}
await main();
if (process.argv[1] === import.meta.filename) {
const { values, force } = parseGeneratorArgs({ outDir: { type: "string" } });
await generateUnicode(force, values.outDir);
}
+1 -2
View File
@@ -1,4 +1,3 @@
package stringutil
//go:generate node ./_scripts/generate-unicode-data.mts
//go:generate npx dprint fmt js_case_generated.go identifier_parts_generated.go
//go:generate npx hereby generate:unicode
@@ -23,11 +23,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest"
)
//go:generate go tool github.com/matryer/moq -stub -fmt goimports -pkg projecttestutil -out clientmock_generated.go ../../project Client
//go:generate npx dprint fmt clientmock_generated.go
//go:generate go tool github.com/matryer/moq -stub -fmt goimports -pkg projecttestutil -out npmexecutormock_generated.go ../../project/ata NpmExecutor
//go:generate npx dprint fmt npmexecutormock_generated.go
//go:generate npx hereby generate:projecttestutil
const (
TestTypingsLocation = "/home/src/Library/Caches/typescript"
+1 -2
View File
@@ -5,8 +5,7 @@ import (
"time"
)
//go:generate go tool github.com/matryer/moq -fmt goimports -out vfsmock/mock_generated.go -pkg vfsmock . FS
//go:generate npx dprint fmt vfsmock/mock_generated.go
//go:generate npx hereby generate:vfs
// FS is a file system abstraction.
type FS interface {
+1 -2
View File
@@ -12,8 +12,7 @@ import (
"github.com/microsoft/TypeScript/tsc/internal/vfs"
)
//go:generate go tool golang.org/x/tools/cmd/stringer -type=Usage -trimprefix=Usage -output=stringer_generated.go
//go:generate npx dprint fmt stringer_generated.go
//go:generate npx hereby generate:vfsmatch
// This file implements the glob matching algorithm specified in MATCHING_ALGORITHM.md.