// @ts-check import AdmZip from "adm-zip"; import chokidar from "chokidar"; import { task } from "hereby"; import assert from "node:assert"; import crypto from "node:crypto"; import fs from "node:fs"; import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; import url from "node:url"; import { parseArgs, styleText, } from "node:util"; import * as tar from "tar"; import { xSync } from "tinyexec"; import { enableFileFingerprintCache, run, } from "./tools/scripts/gen/utils.mts"; enableFileFingerprintCache(); if (process.platform === "win32") { process.chdir(fs.realpathSync.native(process.cwd())); } const __filename = url.fileURLToPath(new URL(import.meta.url)); const __dirname = path.dirname(__filename); 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 {import("./tools/scripts/gen/utils.mts").RunOptions} RunOptions */ /** * @param {string} command * @param {readonly string[]} [args] * @param {Omit} [options] */ function runOutput(command, args, options) { return run(command, args, { ...options, captureOutput: true }); } /** * @param {string} name * @param {boolean} defaultValue * @returns {boolean} */ function parseEnvBoolean(name, defaultValue = false) { name = "TSGO_HEREBY_" + name.toUpperCase(); const value = process.env[name]; if (!value) { return defaultValue; } switch (value.toUpperCase()) { case "1": case "TRUE": case "YES": case "ON": return true; case "0": case "FALSE": case "NO": case "OFF": return false; } throw new Error(`Invalid value for ${name}: ${value}`); } const { values: rawOptions } = parseArgs({ args: process.argv.slice(2), options: { tests: { type: "string", short: "t" }, fix: { type: "boolean" }, force: { type: "boolean", default: parseEnvBoolean("FORCE") }, api: { type: "boolean" }, all: { type: "boolean" }, debug: { type: "boolean" }, dirty: { type: "boolean" }, release: { type: "boolean" }, setPrerelease: { type: "string" }, forRelease: { type: "boolean" }, respectGoEnv: { type: "boolean" }, vscodeTypescriptRelease: { type: "boolean" }, race: { type: "boolean", default: parseEnvBoolean("RACE") }, noembed: { type: "boolean", default: parseEnvBoolean("NOEMBED") }, concurrentTestPrograms: { type: "boolean", default: parseEnvBoolean("CONCURRENT_TEST_PROGRAMS") }, coverage: { type: "boolean", default: parseEnvBoolean("COVERAGE") }, }, strict: false, allowPositionals: true, allowNegative: true, }); // We can't use parseArgs' strict mode as it errors on hereby's --tasks flag. /** * @typedef {{ [K in keyof typeof rawOptions as {} extends Record ? never : K]: typeof rawOptions[K] }} Options */ const options = /** @type {Options} */ (rawOptions); // Native release branches can edit these constants to publish a fixed stable version. // Main publishes prerelease builds of the TypeScript package. const nativePreviewReleaseProfile = /** @type {"native-preview" | "typescript"} */ ("typescript"); const nativePreviewReleaseVersion = /** @type {string | undefined} */ (undefined); const releaseVscodeTypescript = !!options.vscodeTypescriptRelease; const produceNativePreviewVsix = releaseVscodeTypescript; const produceTypeScriptNightlyVsix = !nativePreviewReleaseVersion && !releaseVscodeTypescript; const usePublishedPlatformPackagesForVsix = releaseVscodeTypescript; const produceAnyVsix = produceNativePreviewVsix || produceTypeScriptNightlyVsix; const publishAsTypescript = nativePreviewReleaseProfile === "typescript"; if (releaseVscodeTypescript && options.setPrerelease) { throw new Error("vscode-typescript releases use the extension's package.json version and do not accept setPrerelease"); } if (!releaseVscodeTypescript && options.forRelease && !options.setPrerelease && (!nativePreviewReleaseVersion || produceAnyVsix)) { throw new Error("forRelease requires setPrerelease unless nativePreviewReleaseVersion is hardcoded and VSIX production is disabled"); } if (options.respectGoEnv && options.forRelease) { throw new Error("respectGoEnv cannot be combined with forRelease"); } if (options.respectGoEnv && options.setPrerelease) { throw new Error("respectGoEnv requires the version declared in the source"); } if (releaseVscodeTypescript && !publishAsTypescript) { throw new Error("vscode-typescript releases require nativePreviewReleaseProfile to be 'typescript'"); } const defaultGoBuildTags = [ ...(options.noembed ? ["noembed"] : []), ]; /** * @param {...string} extra * @returns {string[]} */ function goBuildTags(...extra) { const tags = new Set(defaultGoBuildTags.concat(extra)); return tags.size ? [`-tags=${[...tags].join(",")}`] : []; } const goBuildFlags = [ ...(options.race ? ["-race"] : []), // https://github.com/go-delve/delve/blob/62cd2d423c6a85991e49d6a70cc5cb3e97d6ceef/Documentation/usage/dlv_exec.md?plain=1#L12 ...(options.debug ? ["-gcflags=all=-N -l"] : []), ]; const goBuildEnv = { ...(options.race ? {} : { CGO_ENABLED: "0" }), }; /** * @template T * @param {() => T} fn * @returns {() => T} */ function memoize(fn) { /** @type {T} */ let value; return () => { if (fn !== undefined) { value = fn(); fn = /** @type {any} */ (undefined); } return value; }; } /** * @param {string} pattern * @param {string[]} [exclude] */ async function globFiles(pattern, exclude) { const files = []; const absolute = path.isAbsolute(pattern); for await (const entry of fs.promises.glob(pattern, { exclude, withFileTypes: true })) { if (entry.isFile()) { const file = path.join(entry.parentPath, entry.name); files.push(absolute ? file : path.relative(process.cwd(), file)); } } return files; } /** * @param {(() => Promise)[]} tasks * @param {number} concurrency */ async function runWithConcurrencyLimit(tasks, concurrency) { const queue = tasks.values(); /** @type {unknown[]} */ const errors = []; const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, async () => { for (const task of queue) { try { await task(); } catch (error) { errors.push(error); } } }); await Promise.all(workers); if (errors.length === 1) { throw errors[0]; } if (errors.length > 1) { throw new AggregateError(errors, `${errors.length} concurrent tasks failed`); } } const builtLocal = "./built/local"; const libsDir = "./tsc/internal/bundled/libs"; const libsRegexp = /(?:^|[\\/])internal[\\/]bundled[\\/]libs[\\/]/; /** * @param {string} out */ async function generateLibs(out) { await fs.promises.mkdir(out, { recursive: true }); const libs = await fs.promises.readdir(libsDir); await Promise.all(libs.map(async lib => { fs.promises.copyFile(path.join(libsDir, lib), path.join(out, lib)); })); } export const lib = task({ name: "lib", description: "Copies the libs to built/local.", run: () => generateLibs(builtLocal), }); /** * Gets the release build flags for stripping debug info. * @param {string} [versionOverride] Optional version to embed in the binary. * @returns {string[]} */ function getReleaseBuildFlags(versionOverride) { let ldflags = "-ldflags=-s -w"; if (versionOverride) { ldflags += ` -X github.com/microsoft/TypeScript/tsc/internal/core.version=${versionOverride}`; } return ["-trimpath", ldflags]; } /** * @param {object} [opts] * @param {string} [opts.out] * @param {AbortSignal} [opts.abortSignal] * @param {Record} [opts.env] * @param {string[]} [opts.extraFlags] */ function buildTsc(opts) { opts ||= {}; const out = opts.out ?? path.resolve("./built/local/tsc" + (process.platform === "win32" ? ".exe" : "")); const env = { ...(options.respectGoEnv ? {} : goBuildEnv), ...opts.env }; return run("go", ["build", ...goBuildFlags, ...(opts.extraFlags ?? []), ...goBuildTags("noembed"), "-o", out, "./cmd/tsc"], { signal: opts.abortSignal, env, cwd: "./tsc", }); } export const tscBuild = task({ name: "tsc:build", description: "Builds the tsc binary.", run: async () => { await buildTsc({ extraFlags: options.release ? getReleaseBuildFlags() : [] }); }, }); export const tsgo = task({ name: "tsgo", dependencies: [lib, tscBuild], }); export const local = task({ name: "local", dependencies: [tsgo], }); export const build = task({ name: "build", dependencies: [local], }); export const buildWatch = task({ name: "build:watch", description: "Builds the tsc binary and watches for changes.", run: async () => { await watchDebounced("build:watch", async (paths, abortSignal) => { let libsChanged = false; let goChanged = false; if (paths) { for (const p of paths) { if (libsRegexp.test(p)) { libsChanged = true; } else if (p.endsWith(".go")) { goChanged = true; } if (libsChanged && goChanged) { break; } } } else { libsChanged = true; goChanged = true; } if (libsChanged) { console.log("Generating libs..."); await generateLibs(builtLocal); } if (goChanged) { console.log("Building tsgo..."); await buildTsc({ abortSignal }); } }, { paths: ["tsc/cmd", "tsc/internal"], ignored: path => /[\\/]testdata[\\/]/.test(path), }); }, }); export const cleanBuilt = task({ name: "clean:built", hiddenFromTaskList: true, run: () => rimraf("built"), }); /** @type {(() => Promise)[]} */ const goGenerateActions = []; async function runGenerateGo() { for (const generate of goGenerateActions) { await generate(); } // These generators load Go packages, so all generated Go sources must be current. await runGenerateEnums(); await runGenerateAPI(); } 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)} 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"), ]); async function runGenerateOptionDefinitions() { const { default: generate } = await import("./tools/scripts/tsc/generate-options.ts"); await generate(!!options.force); } async function runGenerateCompilerOptions() { await runGenerateOptionDefinitions(); await runGoGenerator("generate:compileroptions", stringerGenerator("tsc/internal/core/options_generated.go", "ModuleKind", "modulekind_stringer_generated.go", "ModuleKind")); await runGoGenerator("generate:compileroptions", stringerGenerator("tsc/internal/core/options_generated.go", "ScriptTarget", "scripttarget_stringer_generated.go", "ScriptTarget")); } goGenerateActions.push(runGenerateCompilerOptions); export const generateCompilerOptions = task({ name: "generate:compileroptions", description: "Generates compiler options and their dependent API files. Pass --force to regenerate unchanged files.", run: async () => { await runGenerateCompilerOptions(); await runGenerateEnums(); await runGenerateAPI(); }, }); 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", "../../../tools/LocProject.json", "../{collections,json}/*.go", "loc/*.generated.json"], exclude: ["**/*_test.go"], outputs: ["diagnostics_generated.go", "diagnosticMessages.generated.json", "loc_generated.go", "loc/*.json.gz"], commands: [ [ "go", "run", "generate.go", "-diagnostics", "diagnostics_generated.go", "-loc", "loc_generated.go", "-locdir", "loc", "-locproject", "../../../tools/LocProject.json", "-locsource", "diagnosticMessages.generated.json", ], ["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() { const { default: cache } = await import("./tools/scripts/gen/cache.mts"); await cache({ cwd: __dirname, inputs: [__filename, "tools/scripts/gen/extensionLocalization.mts", "packages/vscode-typescript/package.json", "packages/vscode-typescript/src/**/*"], outputs: ["packages/vscode-typescript/l10n/bundle.l10n.json"], commands: [["npm", "run", "-w", "native-preview", "generateLocBundle"]], envInputs: [], force: !!options.force, }); } export const generateExtension = task({ name: "generate:extension", description: "Generates files in the extension. Pass --force to regenerate unchanged files.", run: runGenerateExtension, }); async function runGenerateExtensionTest() { const { default: cache } = await import("./tools/scripts/gen/cache.mts"); await cache({ cwd: __dirname, inputs: [__filename, "tools/scripts/gen/extensionLocalization.mts", "packages/vscode-typescript/package.json", "packages/vscode-typescript/l10n/bundle.l10n.json", "packages/vscode-typescript/package.nls.json"], outputs: ["packages/vscode-typescript/l10n/bundle.l10n.qps-ploc.json", "packages/vscode-typescript/package.nls.qps-ploc.json"], commands: [["npm", "run", "-w", "native-preview", "generateLocTest"]], envInputs: [], force: !!options.force, }); } export const generateExtensionTest = task({ name: "generate:extension-test", description: "Generates pseudo-localized extension resources. Pass --force to regenerate unchanged files.", dependencies: [generateExtension], run: runGenerateExtensionTest, }); async function runGenerateLSP() { const { GeneratedFile } = await import("./tools/scripts/gen/generatedFile.mts"); const directory = path.join(__dirname, "tools/scripts/lsp"); 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("./tools/scripts/lsp/fetchModel.mts"); await fetchModel(); for (const file of modelFiles) file.markCurrent(); } const output = new GeneratedFile(path.join(__dirname, "tsc/internal/lsp/lsproto/lsp_generated.go"), [ __filename, path.join(directory, "generate.mts"), ...modelFiles.map(file => file.fileName), ]); if (!output.isCurrent(!!options.force)) { output.invalidate(); const { default: generate } = await import("./tools/scripts/lsp/generate.mts"); await generate(); output.markCurrent(); } else { console.log("LSP bindings are up to date."); } const typeScriptOutput = new GeneratedFile(path.join(__dirname, "packages/typescript/src/vscode/protocol.generated.ts"), [ __filename, path.join(directory, "generate.mts"), path.join(directory, "generateTypeScript.mts"), path.join(directory, "typeScript.mts"), path.join(__dirname, "packages/vscode-typescript/src/lspMiddleware.ts"), ...modelFiles.map(file => file.fileName), ]); if (!typeScriptOutput.isCurrent(!!options.force)) { typeScriptOutput.invalidate(); const { default: generate } = await import("./tools/scripts/lsp/generateTypeScript.mts"); await generate(); typeScriptOutput.markCurrent(); } } export const generateLSP = task({ name: "generate:lsp", description: "Generates Go LSP bindings and extension API types from the pinned protocol model. Pass --force to regenerate unchanged files.", run: runGenerateLSP, }); async function runGenerateEnums() { const { default: generate } = await import("./tools/scripts/tsc/generate-enums.ts"); await generate(!!options.force); } export const generateEnums = task({ name: "generate:enums", description: "Generates TypeScript enums from metadata and 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, 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 runGeneratePreferences() { await runGoGenerator("generate:preferences", { file: "tsc/internal/ls/lsutil/userpreferences.go", cwd: __dirname, inputs: ["tools/userPreferences.schema.json", "tools/gen-preferences/*.go"], exclude: ["**/*_test.go"], envInputs: [], outputs: [ "tsc/internal/ls/lsutil/userpreferences_generated.go", "packages/typescript/src/api/userPreferences.generated.ts", ], commands: [ ["go", "-C", "./tools", "run", "./gen-preferences", "./userPreferences.schema.json", "../tsc/internal/ls/lsutil/userpreferences_generated.go", "../packages/typescript/src/api/userPreferences.generated.ts"], ["dprint", "fmt", "tsc/internal/ls/lsutil/userpreferences_generated.go", "packages/typescript/src/api/userPreferences.generated.ts"], ], }); } export const generatePreferences = goGenerateTask("generate:preferences", runGeneratePreferences); async function runGenerateAPI() { await runGeneratePreferences(); 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"], 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. Pass --force to regenerate unchanged files.", run: runGenerateAPI, }); const vendorJsonrpcDir = "packages/typescript/vendor/vscode-jsonrpc"; const vendorJsonrpcSrc = "node_modules/vscode-jsonrpc"; // Files copied verbatim from the installed vscode-jsonrpc package into the // vendored copy. Only the runtime files needed by the `#vscode-jsonrpc/node` // import (lib + typings + package.json) plus license/readme are vendored. const vendorJsonrpcFiles = ["package.json", "README.md", "License.txt", "lib", "typings"]; async function runGenerateVendor() { const { GeneratedFile } = await import("./tools/scripts/gen/generatedFile.mts"); const src = path.join(__dirname, vendorJsonrpcSrc); const dest = path.join(__dirname, vendorJsonrpcDir); if (!fs.existsSync(src)) { throw new Error(`${vendorJsonrpcSrc} is not installed; run \`npm ci\` first.`); } const manifest = new GeneratedFile(path.join(dest, "package.json"), [__filename, path.join(src, "package.json")]); if (manifest.isCurrent(!!options.force)) { console.log("Vendored vscode-jsonrpc files are up to date."); return; } manifest.invalidate(); await rimraf(dest); await fs.promises.mkdir(dest, { recursive: true }); for (const file of vendorJsonrpcFiles) { await cpRecursive(path.join(src, file), path.join(dest, file)); } manifest.markCurrent(); } export const generateVendor = task({ name: "generate:vendor", description: "Updates the vendored copy of vscode-jsonrpc from node_modules. Pass --force to regenerate unchanged files.", run: runGenerateVendor, }); const generateCompiler = task({ name: "generate:compiler", hiddenFromTaskList: true, dependencies: [generateAST, generateLSP], run: runGenerateGo, }); 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(() => { if (options.coverage) { fs.mkdirSync(coverageDir, { recursive: true }); } }); /** * @param {string} taskName */ function goTestFlags(taskName) { ensureCoverageDirExists(); return [ ...goBuildFlags, ...goBuildTags(), ...(options.tests ? [`-run=${options.tests}`] : []), ...(options.coverage ? [`-coverprofile=${path.join(coverageDir, "coverage." + taskName + ".out")}`, "-coverpkg=./..."] : []), ]; } function getGODEBUG() { const key = "tracebackancestors"; const setting = `${key}=10`; const existing = process.env.GODEBUG ?? ""; if (!existing) return setting; if (existing.includes(`${key}=`)) return existing; return `${existing},${setting}`; } const goTestEnv = { GODEBUG: getGODEBUG(), ...(options.concurrentTestPrograms ? { TS_TEST_PROGRAM_SINGLE_THREADED: "false" } : {}), // Go test caching takes a long time on Windows. // https://github.com/golang/go/issues/72992 ...(process.platform === "win32" ? { GOFLAGS: "-count=1" } : {}), }; const baselineTrackingEnabled = false && ![ options.tests, options.noembed, options.concurrentTestPrograms, options.race, options.dirty, ].some(Boolean); const goTestSumFlags = [ "--format-hide-empty-pkg", "--hide-summary", "skipped", ]; /** * Collects all baseline files that were used during the test run. * @param {string} trackingDir * @returns {Promise>} */ async function collectUsedBaselines(trackingDir) { /** @type {Set} */ const usedBaselines = new Set(); if (!fs.existsSync(trackingDir)) { return usedBaselines; } const trackingFiles = await fs.promises.readdir(trackingDir); for (const file of trackingFiles) { const content = await fs.promises.readFile(path.join(trackingDir, file), "utf-8"); for (const line of content.split("\n")) { const trimmed = line.trim(); if (trimmed) { usedBaselines.add(trimmed); } } } return usedBaselines; } /** * Checks for unused baseline files and reports them. * @param {string} trackingDir * @returns {Promise} List of unused baseline file paths. */ async function checkUnusedBaselines(trackingDir) { const usedBaselines = await collectUsedBaselines(trackingDir); if (usedBaselines.size === 0) { // No baselines recorded - either no tests ran or tracking wasn't set up properly return []; } const allBaselines = await globFiles(`${refBaseline}/**`); const unusedBaselines = allBaselines .map(p => path.relative(refBaseline, p)) .filter(p => !usedBaselines.has(p)); return unusedBaselines; } /** * @param {string} taskName */ function gotestsumArgs(taskName) { return [ ...goTestSumFlags, "--", ...goTestFlags(taskName), ]; } /** * @param {string} taskName */ function goTest(taskName) { return ["go", "test"].concat(goTestFlags(taskName)); } async function runTests() { if (!options.dirty) { await rimraf(localBaseline); await fs.promises.mkdir(localBaseline, { recursive: true }); } // Create a tmp directory for baseline tracking if enabled /** @type {string | undefined} */ let trackingDir; /** @type {(() => Promise) | undefined} */ let cleanupTracking; if (baselineTrackingEnabled) { const tempTrackingDir = fs.mkdtempSync(path.join(os.tmpdir(), "tsgo-baseline-tracking-")); trackingDir = tempTrackingDir; cleanupTracking = () => rimraf(tempTrackingDir); } try { const testEnv = { ...goTestEnv, ...(trackingDir ? { TSGO_BASELINE_TRACKING_DIR: trackingDir } : {}), }; await gotestsumTool.run([...gotestsumArgs("tests"), "./...", ...(isCI ? ["--timeout=45m"] : [])], { env: testEnv, cwd: "./tsc", }); // Check for unused baselines after tests complete if (trackingDir) { const unusedBaselines = await checkUnusedBaselines(trackingDir); if (unusedBaselines.length > 0) { console.error(styleText("red", `\nFound ${unusedBaselines.length} unused baseline file(s):`)); for (const baseline of unusedBaselines.slice(0, 20)) { console.error(styleText("red", ` ${baseline}`)); } if (unusedBaselines.length > 20) { console.error(styleText("red", ` ... and ${unusedBaselines.length - 20} more`)); } // Create .delete files for each unused baseline so baseline-accept can remove them for (const baseline of unusedBaselines) { const deleteFilePath = path.join(localBaseline, baseline + ".delete"); await fs.promises.mkdir(path.dirname(deleteFilePath), { recursive: true }); await fs.promises.writeFile(deleteFilePath, ""); } console.error(styleText("red", `\nRun 'hereby baseline-accept' to delete them.`)); throw new Error(`Found ${unusedBaselines.length} unused baseline file(s). Run 'hereby baseline-accept' to delete them.`); } } } finally { if (cleanupTracking) { await cleanupTracking(); } } } async function runTestExtension() { await run("npm", ["test", "-w", "native-preview"]); } export const testTsc = task({ name: "test:tsc", description: "Runs all tests in the tsc module.", run: runTests, }); export const testExtension = task({ name: "test:extension", description: "Runs the VS Code extension tests.", run: runTestExtension, }); export const test = task({ name: "test", description: "Alias for test:tsc.", dependencies: [testTsc], }); async function runTestBenchmarks() { // Run the benchmarks once to ensure they compile and run without errors. const command = goTest("benchmarks"); await run(command[0], [...command.slice(1), "-run=-", "-bench=.", "-benchtime=1x", "./..."], { env: goTestEnv, cwd: "./tsc" }); } export const testBenchmarks = task({ name: "test:benchmarks", description: "Runs Go benchmarks once; excluded from validate.", run: runTestBenchmarks, }); async function runTestTools() { await gotestsumTool.run([...gotestsumArgs("tools"), "./..."], { env: goTestEnv, cwd: path.join(__dirname, "tools") }); } async function runTestAPI() { // Running the package script doesn't work on Windows; some path escaping isn't done correctly and the test runner runs no tests. await run("node", ["--conditions", "@typescript/source", "--test", "./test/**/*.test.ts"], { cwd: "./packages/typescript" }); } async function runTestAPIBenchmarks() { for (const variant of ["async", "sync", "generators"]) { await run("node", ["--conditions", "@typescript/source", `./test/${variant}/api.bench.ts`, "--singleIteration"], { cwd: "./packages/typescript" }); } } export const testTools = task({ name: "test:tools", description: "Runs all tests in the tools module.", run: runTestTools, }); export const testCodegen = task({ name: "test:codegen", description: "Runs incremental codegen tests.", run: async () => { await run("go", ["-C", "tsc", "mod", "download"]); await run("node", ["--test", "--test-concurrency=1", "./tools/scripts/gen/*.test.mts", "./tools/scripts/tsc/*.test.ts"]); }, }); export const buildAPI = task({ name: "build:api", description: "Builds @typescript/typescript JS API.", run: async () => { await run("npm", ["run", "-w", "@typescript/typescript", "build"]); }, }); async function runCheckAPIDeclarations() { await run("node", ["./node_modules/typescript/bin/tsc", "-p", "packages/typescript/tsconfig.dist.json"]); } export const checkAPIDeclarations = task({ name: "check:api-declarations", description: "Type-checks the emitted @typescript/typescript declaration files.", dependencies: [buildAPI], run: runCheckAPIDeclarations, }); async function runBuildAPITests(generateSources = true) { if (generateSources) await runGenerateSync(); await run("npm", ["run", "-w", "@typescript/typescript", "build:test"]); } export const buildAPITests = task({ name: "build:api:test", description: "Builds the @typescript/typescript JS API tests.", dependencies: [generateEnums, generateAPI], run: runBuildAPITests, }); export const testAPI = task({ name: "test:api", description: "Runs the @typescript/typescript JS API tests.", dependencies: [tsgo, buildAPITests], run: runTestAPI, }); export const testAPIBenchmarks = task({ name: "test:benchmarks:api", description: "Runs async, sync, and generator API benchmarks once; excluded from validate.", dependencies: [tsgo, buildAPITests], run: runTestAPIBenchmarks, }); export const testAll = task({ name: "test:all", 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. await runTests(); await runTestExtension(); await runTestBenchmarks(); await runTestTools(); await runTestAPI(); await runTestAPIBenchmarks(); }, }); /** * @param {{ * toolPath: string; * globs: string[]; * build: (toolPath: string) => Promise; * exclude?: string[]; * }} spec */ function createCachedTool({ toolPath, globs, build, exclude }) { toolPath = path.resolve(toolPath); const hashPath = toolPath + ".hash"; const files = fs.globSync(globs, { exclude }).filter(file => fs.statSync(file).isFile()).map(file => path.resolve(file)); files.sort(); const ensure = memoize(async () => { const hash = crypto.createHash("sha256"); for (const file of files) { hash.update(file); hash.update(fs.readFileSync(file)); } const digest = hash.digest("hex") + "\n"; if ( fs.existsSync(toolPath) && fs.existsSync(hashPath) && fs.readFileSync(hashPath, "utf8") === digest ) { return; } await build(toolPath); fs.writeFileSync(hashPath, digest); }); return { ensure, /** @param {string[]} args @param {RunOptions} [options] */ run: async (args, options) => { await ensure(); return run(toolPath, args, options); }, }; } const gotestsumTool = createCachedTool({ toolPath: `./tools/gotestsum${process.platform === "win32" ? ".exe" : ""}`, globs: ["./tools/go.mod", "./tools/go.sum"], build: async () => { await run("go", ["install", "gotest.tools/gotestsum"], { cwd: "./tools", env: { GOBIN: path.resolve("./tools") }, }); }, }); const golangciLintPackage = memoize(() => { const golangciLintYml = fs.readFileSync(".custom-gcl.yml", "utf8"); const pattern = /^version:\s*(v\d+\.\d+\.\d+).*$/m; const match = pattern.exec(golangciLintYml); if (!match) { throw new Error("Expected version in .custom-gcl.yml"); } const version = match[1]; const major = version.split(".")[0]; const versionSuffix = ["v0", "v1"].includes(major) ? "" : "/" + major; return `github.com/golangci/golangci-lint${versionSuffix}/cmd/golangci-lint@${version}`; }); const customLinterTool = createCachedTool({ toolPath: `./tools/custom-gcl${process.platform === "win32" ? ".exe" : ""}`, globs: [ "./tools/go.mod", "./tools/customlint/**/*", "./.custom-gcl.yml", ], exclude: ["**/testdata/**"], build: async toolPath => { await run("go", ["run", golangciLintPackage(), "custom"]); await run(toolPath, ["cache", "clean"]); }, }); export const lint = task({ name: "lint", description: "Runs golangci-lint.", run: runLint, }); async function runLint() { const lintArgs = ["run"]; if (defaultGoBuildTags.length) { lintArgs.push("--build-tags", defaultGoBuildTags.join(",")); } if (options.fix) { lintArgs.push("--fix"); } await customLinterTool.run([...lintArgs, "--config", "../.golangci.yml"], { cwd: "./tsc" }); console.log("Linting tools"); await customLinterTool.run([...lintArgs, "--config", "../.golangci.yml"], { cwd: "./tools" }); } export const installTools = task({ name: "install-tools", description: "Installs optional tools for developing within the repo.", run: async () => { await Promise.all([ gotestsumTool.ensure(), customLinterTool.ensure(), ]); }, }); export const format = task({ name: "format", description: "Formats the repo.", run: runFormat, }); async function runFormat() { await run("dprint", ["fmt"]); } export const validate = task({ name: "validate", 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} action */ const runValidation = async (name, action) => { try { await action(); } catch (error) { failures.push({ name, error }); console.error(styleText("red", `${name} failed; continuing validation.`)); } }; await runValidation("test:tsc", runTests); await runValidation("test:extension", runTestExtension); await runValidation("check:api-declarations", runCheckAPIDeclarations); if (options.api || options.all) { if (!options.all) await runGenerateEnums(); await runBuildAPITests(!options.all); await runValidation("test:api", runTestAPI); } if (options.all) { await runValidation("test:tools", runTestTools); await runValidation("test:options", async () => { await run("node", ["--test", "./tools/scripts/tsc/options.test.ts"]); }); await runValidation("test:smoke", runSmokeTest); // in CI this is run with `--race` } await runValidation("lint", runLint); await runValidation("format", runFormat); if (failures.length) { throw new AggregateError( failures.map(failure => failure.error), `Validation failed: ${failures.map(failure => failure.name).join(", ")}`, ); } }, }); async function runSmokeTest() { await run("./built/local/tsc", ["-p", "./tsc/testdata/fixtures/compiler", "--noEmit", "--singleThreaded"]); await run("./built/local/tsc", ["-p", "./tsc/testdata/fixtures/compiler", "--noEmit"]); } export const smokeTest = task({ name: "test:smoke", description: "Runs the smoke tests.", dependencies: [build], run: runSmokeTest, }); export const checkFormat = task({ name: "check:format", description: "Checks that the repo is formatted.", run: async () => { await run("dprint", ["check"]); }, }); export const checkHerebyfile = task({ name: "check:herebyfile", description: "Type-checks Herebyfile.mjs.", run: () => run("node", [ "./node_modules/typescript/bin/tsc", "--noEmit", "--allowJs", "--allowImportingTsExtensions", "--checkJs", "--target", "es2022", "--lib", "es2024,esnext.array,esnext.collection,esnext.iterator", "--module", "nodenext", "--moduleResolution", "nodenext", "--types", "node", "--strict", "--esModuleInterop", "--skipLibCheck", "Herebyfile.mjs", ]), }); export const checkVsceVersion = task({ name: "check:vsce-version", description: "Checks that Azure release jobs use the repository's pinned vsce version.", run: () => { for (const validVersion of ["0.0.0", "1.2.3", "10.20.30"]) { assert(stableThreeComponentVersionPattern.test(validVersion), `${validVersion} should be a valid stable version.`); } for (const invalidVersion of ["01.0.0", "1.02.0", "1.2.03", "1.2", "1.2.3-beta"]) { assert(!stableThreeComponentVersionPattern.test(invalidVersion), `${invalidVersion} should not be a valid stable version.`); } const packageJson = JSON.parse(fs.readFileSync("./packages/vscode-typescript/package.json", "utf8")); const packageLock = JSON.parse(fs.readFileSync("./package-lock.json", "utf8")); const version = packageJson.devDependencies?.["@vscode/vsce"]; if (typeof version !== "string" || !stableThreeComponentVersionPattern.test(version)) { throw new Error(`packages/vscode-typescript must pin @vscode/vsce to an exact version, got ${JSON.stringify(version)}.`); } const workspaceVersion = packageLock.packages?.["packages/vscode-typescript"]?.devDependencies?.["@vscode/vsce"]; const installedVersion = packageLock.packages?.["node_modules/@vscode/vsce"]?.version; if (workspaceVersion !== version || installedVersion !== version) { throw new Error( `@vscode/vsce version mismatch: package.json=${version}, package-lock workspace=${workspaceVersion}, package-lock package=${installedVersion}.`, ); } const setupVsce = fs.readFileSync("./tools/pipelines/steps/setup-vsce.yml", "utf8"); const activeSetupLines = setupVsce.split(/\r?\n/).filter(line => !line.trimStart().startsWith("#")); const installCommand = `- bash: npm install --no-save @vscode/vsce@${version}`; if (activeSetupLines.filter(line => line.trim() === installCommand).length !== 1) { throw new Error(`tools/pipelines/steps/setup-vsce.yml must install exactly @vscode/vsce@${version}.`); } }, }); const scriptTsconfigs = [ "./tools/scripts/gen/tsconfig.json", "./tools/scripts/tsc/tsconfig.json", "./tools/scripts/lsp/tsconfig.json", ]; export const checkScripts = task({ name: "check:scripts", description: "Type-checks TypeScript scripts.", run: async () => { for (const tsconfig of scriptTsconfigs) { console.log(`Type-checking ${tsconfig}`); await run("tsc", ["-p", tsconfig]); } }, }); /** * @param {string} localBaseline Path to the local copy of the baselines * @param {string} refBaseline Path to the reference copy of the baselines */ function baselineAcceptTask(localBaseline, refBaseline) { /** * @param {string} p */ function localPathToRefPath(p) { const relative = path.relative(localBaseline, p); return path.join(refBaseline, relative); } return async () => { const toCopy = await globFiles(`${localBaseline}/**`, [`${localBaseline}/**/*.delete`]); for (const p of toCopy) { const out = localPathToRefPath(p); await fs.promises.mkdir(path.dirname(out), { recursive: true }); await fs.promises.copyFile(p, out); } const toDelete = await globFiles(`${localBaseline}/**/*.delete`); for (const p of toDelete) { const out = localPathToRefPath(p).replace(/\.delete$/, ""); await rimraf(out); await rimraf(p); // also delete the .delete file so that it no longer shows up in a diff tool. } }; } const localBaseline = "tsc/testdata/baselines/local/"; const refBaseline = "tsc/testdata/baselines/reference/"; export const baselineAccept = task({ name: "baseline-accept", description: "Makes the most recent test results the new baseline, overwriting the old baseline.", run: baselineAcceptTask(localBaseline, refBaseline), }); function getDiffTool() { const program = process.env.DIFF; if (!program) { console.warn("Add the 'DIFF' environment variable to the path of the program you want to use."); process.exit(1); } return program; } export const diff = task({ name: "diff", description: "Diffs baselines using the diff tool specified by the 'DIFF' environment variable", run: () => run(getDiffTool(), [refBaseline, localBaseline]), }); /** * @param {fs.PathLike} p */ function rimraf(p) { // The rimraf package uses maxRetries=10 on Windows, but Node's fs.rm does not have that special case. return fs.promises.rm(p, { recursive: true, force: true, maxRetries: process.platform === "win32" ? 10 : 0 }); } /** @typedef {{ * name: string; * paths: string | string[]; * ignored?: (path: string) => boolean; * run: (paths: Set, abortSignal: AbortSignal) => void | Promise; * }} WatchTask */ void 0; /** * @param {string} name * @param {(paths: Set | undefined, abortSignal: AbortSignal) => void | Promise} run * @param {object} options * @param {string | string[]} options.paths * @param {(path: string) => boolean} [options.ignored] * @param {string} [options.name] */ async function watchDebounced(name, run, options) { let watching = true; let running = true; let lastChangeTimeMs = Date.now(); let changedDeferred = /** @type {Deferred} */ (new Deferred()); let abortController = new AbortController(); const debouncer = new Debouncer(1_000, endRun); const watcher = chokidar.watch(options.paths, { ignored: options.ignored, ignorePermissionErrors: true, alwaysStat: true, }); // The paths that have changed since the last run. /** @type {Set | undefined} */ let paths; process.on("SIGINT", endWatchMode); process.on("beforeExit", endWatchMode); watcher.on("all", onChange); while (watching) { const promise = changedDeferred.promise; const token = abortController.signal; if (!token.aborted) { running = true; try { const thePaths = paths; paths = new Set(); await run(thePaths, token); } catch { // ignore } running = false; } if (watching) { console.log(styleText("yellowBright", `[${name}] run complete, waiting for changes...`)); await promise; } } console.log("end"); /** * @param {'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir' | 'all' | 'ready' | 'raw' | 'error'} eventName * @param {string} path * @param {fs.Stats | undefined} stats */ function onChange(eventName, path, stats) { switch (eventName) { case "change": case "unlink": case "unlinkDir": break; case "add": case "addDir": // skip files that are detected as 'add' but haven't actually changed since the last time we ran. if (stats && stats.mtimeMs <= lastChangeTimeMs) { return; } break; } beginRun(path); } /** * @param {string} path */ function beginRun(path) { if (debouncer.empty) { console.log(styleText("yellowBright", `[${name}] changed due to '${path}', restarting...`)); if (running) { console.log(styleText("yellowBright", `[${name}] aborting in-progress run...`)); } abortController.abort(); abortController = new AbortController(); } debouncer.enqueue(); paths ??= new Set(); paths.add(path); } function endRun() { lastChangeTimeMs = Date.now(); changedDeferred.resolve(); changedDeferred = /** @type {Deferred} */ (new Deferred()); } function endWatchMode() { if (watching) { watching = false; console.log(styleText("yellowBright", `[${name}] exiting watch mode...`)); abortController.abort(); watcher.close(); } } } /** * @template T */ export class Deferred { constructor() { /** @type {Promise} */ this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } } export class Debouncer { /** * @param {number} timeout * @param {() => Promise | void} action */ constructor(timeout, action) { this._timeout = timeout; this._action = action; /** @type {ReturnType | undefined} */ this._timer = undefined; /** @type {Deferred | undefined} */ this._deferred = undefined; } get empty() { return !this._deferred; } enqueue() { if (this._timer) { clearTimeout(this._timer); this._timer = undefined; } if (!this._deferred) { this._deferred = new Deferred(); } this._timer = setTimeout(() => this.run(), 100); return this._deferred.promise; } run() { if (this._timer) { clearTimeout(this._timer); this._timer = undefined; } const deferred = this._deferred; assert(deferred); this._deferred = undefined; try { deferred.resolve(this._action()); } catch (e) { deferred.reject(e); } } } const getVersion = memoize(() => { if (nativePreviewReleaseVersion) { return nativePreviewReleaseVersion; } const f = fs.readFileSync("./tsc/internal/core/version.go", "utf8"); const match = f.match(/var version\s*=\s*"(\d+\.\d+\.\d+)(-[^"]+)?"/); if (!match) { throw new Error("Failed to extract version from version.go"); } let version = match[1]; if (options.setPrerelease) { version += `-${options.setPrerelease}`; } else if (match[2]) { version += match[2]; } return version; }); function getPublishTag() { if (publishAsTypescript) { const version = getVersion(); if (!version) { throw new Error("Publishing as 'typescript' requires a version before selecting an npm tag."); } const match = version.match(/-(dev|beta|rc)(?:[.-]|$)/); if (match?.[1]) return match[1] === "dev" ? "next" : match[1]; if (version === nativePreviewReleaseVersion && stableThreeComponentVersionPattern.test(version)) return "latest"; throw new Error(`Refusing to publish 'typescript' with the latest tag from non-release version ${version}.`); } return "latest"; } const extensionDir = path.resolve("./packages/vscode-typescript"); const nightlyExtensionDir = path.resolve("./packages/vscode-typescript-nightly"); const builtNpm = path.resolve("./built/npm"); const builtVsix = path.resolve("./built/vsix"); const typeScriptReleaseInfoPath = path.resolve("./built/typescript-release-info.json"); const builtPublishedPlatformPackages = path.resolve("./built/published-platform-packages"); const builtSignTmp = path.resolve("./built/sign-tmp"); const publishedTypeScriptAliasPackageName = "@typescript/bundled-typescript"; const releasePackageEnv = { COREPACK_ENABLE_STRICT: "0" }; const getReleasePackageRegistry = memoize(async () => { const { stdout } = await runOutput("npm", ["config", "get", "registry"], { env: releasePackageEnv }); return stdout.trim(); }); const getVscodeTypeScriptExtensionPackageJson = memoize(() => JSON.parse(fs.readFileSync(path.join(extensionDir, "package.json"), "utf8"))); function getVscodeTypeScriptExtensionVersion() { const version = getVscodeTypeScriptExtensionPackageJson().version; if (typeof version !== "string" || !stableThreeComponentVersionPattern.test(version)) { throw new Error(`packages/vscode-typescript/package.json must contain a stable three-component version, got ${JSON.stringify(version)}.`); } return version; } const getSignTempDir = memoize(async () => { const dir = path.resolve(builtSignTmp); await rimraf(dir); await fs.promises.mkdir(dir, { recursive: true }); return dir; }); const cleanSignTempDirectory = task({ name: "clean:sign-tmp", hiddenFromTaskList: true, run: runCleanSignTempDirectory, }); function runCleanSignTempDirectory() { return rimraf(builtSignTmp); } let signCount = 0; /** * @param {string} value */ function assertMsbuildXmlValue(value) { assert(value.length > 0 && !/[^\w./\\: -]/.test(value), `Unsupported MSBuild XML value: ${JSON.stringify(value)}`); return value; } /** * @typedef {{ * SignFileRecordList: { * SignFileList: { SrcPath: string; DstPath: string | null }[]; * Certs: Cert; * MacAppName: string | undefined * }[] * }} DDSignFileList * * @param {DDSignFileList} filelist */ async function sign(filelist, unchangedOutputOkay = false) { console.log("filelist:", JSON.stringify(filelist, undefined, 4)); if (!process.env.MICROBUILD_PLUGIN_DIRECTORY) { console.log(styleText("yellow", "Faking signing because MICROBUILD_PLUGIN_DIRECTORY is not set.")); // Fake signing for testing. for (const record of filelist.SignFileRecordList) { for (const file of record.SignFileList) { const src = file.SrcPath; const dst = file.DstPath ?? src; if (!fs.existsSync(src)) { throw new Error(`Source file does not exist: ${src}`); } const dstDir = path.dirname(dst); if (!fs.existsSync(dstDir)) { throw new Error(`Destination directory does not exist: ${dstDir}`); } if (dst.endsWith(".sig")) { console.log(`Faking signature for ${src} -> ${dst}`); // No great way to fake a signature. await fs.promises.writeFile(dst, "fake signature"); } else { if (src === dst) { console.log(`Faking signing ${src}`); } else { console.log(`Faking signing ${src} -> ${dst}`); } const contents = await fs.promises.readFile(src); await fs.promises.writeFile(dst, contents); } } } return; } const signingWorkaround = true; let signingFilelist = filelist; /** @type {{ source: string; target: string }[]} */ const signingWorkaroundFiles = []; if (signingWorkaround) { // DstPath is currently broken in the signing tool. // Copy all of the files to a new tempdir and then leave DstPath unset // so that it's overwritten, then move the file to the destination. console.log("Working around DstPath bug"); /** @type {DDSignFileList} */ const newFileList = { SignFileRecordList: filelist.SignFileRecordList.map(list => { return { Certs: list.Certs, SignFileList: list.SignFileList.map(file => { const dstPath = file.DstPath; if (dstPath === null) { return file; } const src = file.SrcPath; // File extensions must be preserved; use a prefix. const dstPathTemp = `${path.dirname(src)}/signing-temp-${path.basename(src)}`; console.log(`Copying: ${src} -> ${dstPathTemp}`); fs.cpSync(src, dstPathTemp); signingWorkaroundFiles.push({ source: dstPathTemp, target: dstPath }); return { SrcPath: dstPathTemp, DstPath: null, }; }), MacAppName: list.MacAppName, }; }), }; signingFilelist = newFileList; console.log("new filelist:", JSON.stringify(signingFilelist, undefined, 4)); } /** @type {Map} */ const srcHashes = new Map(); for (const record of filelist.SignFileRecordList) { for (const file of record.SignFileList) { const src = file.SrcPath; const dst = file.DstPath ?? src; if (!fs.existsSync(src)) { throw new Error(`Source file does not exist: ${src}`); } const hash = crypto.createHash("sha256").update(fs.readFileSync(src)).digest("hex"); srcHashes.set(src, hash); console.log(`Will sign ${src} -> ${dst}`); console.log(` sha256: ${hash}`); } } const tmp = await getSignTempDir(); const propsPath = path.resolve(tmp, `signing-items-${signCount++}.props`); const signingItems = signingFilelist.SignFileRecordList.flatMap(record => record.SignFileList.map(file => ({ path: file.SrcPath, cert: record.Certs, macAppName: record.MacAppName }))); const items = signingItems.map(({ path: filePath, cert, macAppName }) => ` ${assertMsbuildXmlValue(cert)} None${ macAppName ? ` ${assertMsbuildXmlValue(macAppName)}` : "" } ` ).join("\n"); await fs.promises.writeFile( propsPath, ` ${items} `, ); try { await run("dotnet", [ "build", path.resolve("tools/signing/Sign.csproj"), "--target:AfterBuild", "--verbosity:normal", "-p:SignType=real", `-p:SignFilesDir=${path.resolve("built")}`, `-p:FilesToSignPropsFile=${propsPath}`, `-p:MicroBuildOverridePluginDirectory=${process.env.MICROBUILD_PLUGIN_DIRECTORY}`, ]); } finally { await fs.promises.unlink(propsPath); } if (signingWorkaround) { // Now, copy the files back. for (const { source, target } of signingWorkaroundFiles) { console.log(`Moving signed file: ${source} -> ${target}`); await fs.promises.rename(source, target); } } /** @type {string[]} */ let failures = []; for (const record of filelist.SignFileRecordList) { for (const file of record.SignFileList) { const src = file.SrcPath; const dst = file.DstPath ?? src; if (!fs.existsSync(dst)) { failures.push(`Signed file does not exist: ${dst}`); const newSrcHash = crypto.createHash("sha256").update(fs.readFileSync(src)).digest("hex"); const oldSrcHash = srcHashes.get(src); assert(oldSrcHash); if (oldSrcHash !== newSrcHash) { failures.push(` Source file changed during signing: ${src}\n before: ${oldSrcHash}\n after: ${newSrcHash}`); } continue; } const srcHash = srcHashes.get(src); assert(srcHash); const dstHash = crypto.createHash("sha256").update(fs.readFileSync(dst)).digest("hex"); if (srcHash === dstHash) { const message = `Signed file is identical to source file (not signed?): ${src} -> ${dst}\n sha256: ${dstHash}`; if (unchangedOutputOkay) { console.log(message); } else { failures.push(message); continue; } } if (src === dst) { console.log(`Signed ${src}`); } else { console.log(`Signed ${src} -> ${dst}`); } console.log(` sha256: ${dstHash}`); } } if (failures.length) { throw new Error("Some files failed to sign:\n" + failures.map(f => " - " + f).join("\n")); } } /** * @param {string} src * @param {string} dest * @param {(p: string) => boolean} [filter] */ function cpRecursive(src, dest, filter) { return fs.promises.cp(src, dest, { recursive: true, filter: filter ? src => filter(src.replace(/\\/g, "/")) : undefined, }); } /** * @param {string} src * @param {string} dest */ function cpWithoutNodeModulesOrTsconfig(src, dest) { return cpRecursive(src, dest, p => !p.endsWith("/node_modules") && !p.endsWith("/tsconfig.json")); } const mainNativePreviewPackage = { npmPackageName: publishAsTypescript ? "typescript" : "@typescript/typescript", npmDir: path.join(builtNpm, publishAsTypescript ? "typescript" : "native-preview"), npmTarball: path.join(builtNpm, publishAsTypescript ? "typescript.tgz" : "native-preview.tgz"), }; const typescriptMacEntitlements = [ "com.apple.security.cs.allow-dyld-environment-variables", "com.apple.security.cs.disable-library-validation", ]; function createTypeScriptMacEntitlementsPlist() { const entries = typescriptMacEntitlements.map(entitlement => ` ${entitlement}\n `).join("\n"); return ` ${entries} `; } /** * @typedef {"win32" | "linux" | "darwin" | "aix" | "android" | "freebsd" | "netbsd" | "openbsd" | "sunos"} OS * @typedef {"x64" | "arm" | "arm64" | "ia32" | "ppc64" | "loong64" | "mips64el" | "riscv64" | "s390x"} Arch * @typedef {"Microsoft400" | "LinuxSign" | "MacDeveloperHarden" | "8020" | "VSCodePublisher"} Cert * @typedef {`${OS | "alpine"}-${Exclude | "armhf"}`} VSCodeTarget * @typedef {{ name: string; sourceDir: string }} VsixExtensionPackage * @typedef {{ nodeOs: string; vscodeTarget: string; sourceDir: string; extensionDir: string; vsixPath: string; vsixManifestPath: string; vsixSignaturePath: string }} VsixExtension * @typedef {{ GOOS: string; GOARCH: string }} GoDistTarget * @typedef {{ os: OS; arch: Arch; cert?: Cert; vsix?: boolean; alpine?: boolean }} Platform */ void 0; /** @type {VsixExtensionPackage[]} */ const vsixExtensionPackages = [ ...(produceNativePreviewVsix ? [{ name: "native-preview", sourceDir: extensionDir }] : []), ...(produceTypeScriptNightlyVsix ? [{ name: "vscode-typescript-nightly", sourceDir: nightlyExtensionDir }] : []), ]; /** * npm package platforms supported by the native release. * The native-preview package publishes only the entries with vsix: true; * the typescript package publishes the full list. * BSD targets that are not in Node's supported-platforms table are best-effort * and limited to mainstream 64-bit x64/arm64 architectures. * alpine is set only for the subset that also produces Alpine VSIXes. * cert defaults to LinuxSign. * @type {Platform[]} */ const platforms = [ { os: "win32", arch: "x64", vsix: true, cert: "Microsoft400" }, { os: "win32", arch: "arm64", vsix: true, cert: "Microsoft400" }, { os: "linux", arch: "x64", vsix: true, alpine: true }, { os: "linux", arch: "arm", vsix: true }, { os: "linux", arch: "arm64", vsix: true, alpine: true }, { os: "darwin", arch: "x64", vsix: true, cert: "MacDeveloperHarden" }, { os: "darwin", arch: "arm64", vsix: true, cert: "MacDeveloperHarden" }, { os: "aix", arch: "ppc64" }, { os: "android", arch: "arm64" }, { os: "freebsd", arch: "arm64" }, { os: "freebsd", arch: "x64" }, { os: "linux", arch: "loong64" }, { os: "linux", arch: "mips64el" }, { os: "linux", arch: "ppc64" }, { os: "linux", arch: "riscv64" }, { os: "linux", arch: "s390x" }, { os: "netbsd", arch: "arm64" }, { os: "netbsd", arch: "x64" }, { os: "openbsd", arch: "arm64" }, { os: "openbsd", arch: "x64" }, { os: "sunos", arch: "x64" }, // Wasm? ]; const ignoredGoTargets = new Map([ ["android/386", "Android is not a Node runtime target TypeScript supports"], ["android/amd64", "Android is not a Node runtime target TypeScript supports"], ["android/arm", "Android is not a Node runtime target TypeScript supports"], ["freebsd/386", "FreeBSD is experimental in Node and limited here to mainstream 64-bit x64/arm64"], ["freebsd/arm", "FreeBSD is experimental in Node and limited here to mainstream 64-bit x64/arm64"], ["linux/386", "ia32 means 32-bit x86, which TypeScript does not support for native packages"], ["linux/ppc64", "Node supports Linux ppc64le; npm's ppc64 CPU name cannot select big-endian ppc64 separately"], ["netbsd/386", "NetBSD is not in Node's supported-platforms table and is limited here to mainstream 64-bit x64/arm64"], ["netbsd/arm", "NetBSD is not in Node's supported-platforms table and is limited here to mainstream 64-bit x64/arm64"], ["openbsd/386", "OpenBSD is not in Node's supported-platforms table and is limited here to mainstream 64-bit x64/arm64"], ["openbsd/arm", "OpenBSD is not in Node's supported-platforms table and is limited here to mainstream 64-bit x64/arm64"], ["openbsd/ppc64", "OpenBSD is not in Node's supported-platforms table and is limited here to mainstream 64-bit x64/arm64"], ["openbsd/riscv64", "OpenBSD is not in Node's supported-platforms table and is limited here to mainstream 64-bit x64/arm64"], ["solaris/amd64", "Node documents SmartOS/sunos rather than Oracle Solaris; sunos-x64 publishes illumos/amd64 for that runtime family"], ["windows/386", "ia32 means 32-bit x86, which TypeScript does not support for native packages"], ]); /** * @param {string} os * @returns {"windows" | "illumos" | "darwin" | "linux" | "aix" | "android" | "freebsd" | "netbsd" | "openbsd"} */ function nodeToGOOS(os) { switch (os) { case "win32": return "windows"; case "sunos": return "illumos"; case "darwin": case "linux": case "aix": case "android": case "freebsd": case "netbsd": case "openbsd": return os; default: throw new Error(`Unsupported OS: ${os}`); } } /** * @param {string} arch * @param {string} os * @returns {"amd64" | "386" | "mips64le" | "ppc64" | "ppc64le" | "arm" | "arm64" | "loong64" | "riscv64" | "s390x"} */ function nodeToGOARCH(arch, os) { switch (arch) { case "x64": return "amd64"; case "ia32": return "386"; case "mips64el": return "mips64le"; case "ppc64": return os === "aix" ? "ppc64" : "ppc64le"; case "arm": case "arm64": case "loong64": case "riscv64": case "s390x": return arch; default: throw new Error(`Unsupported ARCH: ${arch}`); } } const getPlatforms = memoize(() => { const publishTag = releaseVscodeTypescript ? undefined : getPublishTag(); let supportedPlatforms = !releaseVscodeTypescript && publishAsTypescript && publishTag !== "next" ? platforms : platforms.filter(({ vsix }) => vsix); if (!options.forRelease) { supportedPlatforms = supportedPlatforms.filter(({ os, arch }) => os === process.platform && arch === process.arch); assert.equal(supportedPlatforms.length, 1, "No supported platforms found"); } return supportedPlatforms.map(({ os, arch, cert = "LinuxSign", vsix, alpine }) => { const packageBaseName = publishAsTypescript ? "typescript" : "native-preview"; const npmDirName = `${packageBaseName}-${os}-${arch}`; const npmDir = path.join(builtNpm, npmDirName); const npmTarball = `${npmDir}.tgz`; const npmPackageName = `@typescript/${npmDirName}`; /** @type {VsixExtension[]} */ let extensions = []; if (produceAnyVsix && vsix) { /** @type {string[]} */ const vscodeTargets = [`${os}-${arch === "arm" ? "armhf" : arch}`]; if (alpine) { vscodeTargets.push(`alpine-${arch === "arm" ? "armhf" : arch}`); } extensions = vscodeTargets.flatMap(vscodeTarget => vsixExtensionPackages.map(({ name: packageName, sourceDir }) => { const extensionDir = path.join(builtVsix, `${packageName}-${vscodeTarget}`); const vsixPath = extensionDir + ".vsix"; const vsixManifestPath = extensionDir + ".manifest"; const vsixSignaturePath = extensionDir + ".signature.p7s"; return { nodeOs: os, vscodeTarget, sourceDir, extensionDir, vsixPath, vsixManifestPath, vsixSignaturePath, }; }) ); } return { nodeOs: os, nodeArch: arch, goos: nodeToGOOS(os), goarch: nodeToGOARCH(arch, os), npmPackageName, npmDirName, npmDir, npmTarball, extensions, cert, }; }); }); export const checkPlatforms = task({ name: "typescript:check-platforms", hiddenFromTaskList: true, run: runCheckPlatforms, }); /** * @param {GoDistTarget} target */ function goDistTargetToPlatform(target) { const goTarget = `${target.GOOS}/${target.GOARCH}`; if (ignoredGoTargets.has(goTarget)) { return undefined; } /** @type {OS | undefined} */ let nodeOs; switch (target.GOOS) { case "windows": nodeOs = "win32"; break; case "illumos": nodeOs = "sunos"; break; case "aix": case "android": case "darwin": case "freebsd": case "linux": case "netbsd": case "openbsd": nodeOs = target.GOOS; break; default: return undefined; } /** @type {Arch | undefined} */ let nodeArch; switch (target.GOARCH) { case "386": nodeArch = "ia32"; break; case "amd64": nodeArch = "x64"; break; case "mips64le": nodeArch = "mips64el"; break; case "ppc64": nodeArch = "ppc64"; break; case "ppc64le": nodeArch = "ppc64"; break; case "arm": case "arm64": case "loong64": case "riscv64": case "s390x": nodeArch = target.GOARCH; break; default: return undefined; } return `${nodeOs}-${nodeArch}`; } async function runCheckPlatforms() { const { stdout } = await runOutput("go", ["tool", "dist", "list", "-json"]); /** @type {GoDistTarget[]} */ const goTargets = JSON.parse(stdout); const goTargetSet = new Set(goTargets.map(({ GOOS, GOARCH }) => `${GOOS}/${GOARCH}`)); /** @type {[os: OS, arch: Arch][]} */ const packagePlatforms = platforms.map(({ os, arch }) => /** @type {[OS, Arch]} */ ([os, arch])); const actual = new Set(packagePlatforms.map(([os, arch]) => `${os}-${arch}`)); const expected = new Set(goTargets.map(goDistTargetToPlatform).filter(platform => platform !== undefined)); const errors = []; for (const [os, arch] of packagePlatforms) { const goTarget = `${nodeToGOOS(os)}/${nodeToGOARCH(arch, os)}`; if (!goTargetSet.has(goTarget)) { errors.push(`Configured package platform ${os}-${arch} maps to unsupported Go target ${goTarget}.`); } } const missing = [...expected].filter(platform => !actual.has(platform)); if (missing.length) { errors.push(`Missing package platform(s) for the current Go toolchain: ${missing.join(", ")}.`); } const extra = [...actual].filter(platform => !expected.has(platform)); if (extra.length) { errors.push(`Unexpected package platform(s), or missing exclusion policy: ${extra.join(", ")}.`); } if (errors.length) { throw new Error(`native-preview platform list is out of sync with 'go tool dist list':\n${errors.map(e => ` - ${e}`).join("\n")}`); } } /** * Recursively strips `@typescript/source` export conditions from a package.json object. * Processes `exports` and `imports` fields, skipping past subpath keys (starting with "." * or "#") and recursing into condition objects. After removal, simplifies objects that have * only a single `default` key down to their bare value. * @param {Record} packageJson */ function stripSourceConditions(packageJson) { for (const field of ["exports", "imports"]) { if (packageJson[field] != null && typeof packageJson[field] === "object") { packageJson[field] = stripConditionsFromValue(packageJson[field]); } } } /** * @param {any} value * @returns {any} */ function stripConditionsFromValue(value) { if (value == null || typeof value !== "object") { return value; } delete value["@typescript/source"]; for (const key of Object.keys(value)) { value[key] = stripConditionsFromValue(value[key]); } // Simplify: if only "default" remains, collapse to its value. const keys = Object.keys(value); if (keys.length === 1 && keys[0] === "default") { return value["default"]; } return value; } export const buildNativePreviewPackages = task({ name: "typescript:build", description: "Builds TypeScript npm packages for the current platform. Pass --respectGoEnv to preserve caller-provided Go build settings.", run: runBuildNativePreviewPackages, }); export const writeTypeScriptReleaseInfo = task({ name: "typescript:release-info", hiddenFromTaskList: true, run: async () => { await fs.promises.mkdir(path.dirname(typeScriptReleaseInfoPath), { recursive: true }); await fs.promises.writeFile( typeScriptReleaseInfoPath, JSON.stringify({ version: getVersion(), npmTag: getPublishTag() }, undefined, 4) + "\n", ); }, }); async function runBuildNativePreviewPackages() { if (usePublishedPlatformPackagesForVsix) { checkPublishedPlatformPackagesForVsix(); await rimraf(builtNpm); console.log("Skipping npm package builds; VSIX packaging will use published platform packages."); return; } await rimraf(builtNpm); const platforms = getPlatforms(); const inputDir = "./packages/typescript"; const inputPackageJson = JSON.parse(fs.readFileSync(path.join(inputDir, "package.json"), "utf8")); inputPackageJson.version = getVersion(); delete inputPackageJson.private; inputPackageJson.files = [...new Set([...(inputPackageJson.files ?? []), "NOTICE.txt"])]; if (publishAsTypescript) { inputPackageJson.bin = { tsc: "./bin/tsc", }; inputPackageJson.description = "TypeScript is a language for application scale JavaScript development"; inputPackageJson.homepage = "https://www.typescriptlang.org/"; inputPackageJson.keywords = [ "TypeScript", "Microsoft", "compiler", "language", "javascript", ]; inputPackageJson.bugs = { url: "https://github.com/microsoft/TypeScript/issues", }; inputPackageJson.repository = { type: "git", url: "https://github.com/microsoft/TypeScript.git", }; delete inputPackageJson.scripts; delete inputPackageJson.devDependencies; } stripSourceConditions(inputPackageJson); if (fs.existsSync(".git")) { const { stdout: gitHead } = await runOutput("git", ["rev-parse", "HEAD"]); inputPackageJson.gitHead = gitHead.trim(); } inputPackageJson.publishConfig = { access: "public", tag: getPublishTag(), }; const mainPackage = { ...inputPackageJson, name: mainNativePreviewPackage.npmPackageName, optionalDependencies: Object.fromEntries(platforms.map(p => [p.npmPackageName, getVersion()])), }; const mainPackageDir = mainNativePreviewPackage.npmDir; await fs.promises.mkdir(mainPackageDir, { recursive: true }); // Copy package contents excluding node_modules and dist (dist is copied separately after build). // The package.json "files" field controls what npm pack actually includes. await cpRecursive(inputDir, mainPackageDir, p => !p.endsWith("/node_modules") && !p.includes("/dist")); if (publishAsTypescript) { await fs.promises.writeFile(path.join(mainPackageDir, "bin", "tsc"), '#!/usr/bin/env node\nimport "../lib/tsc.js";\n'); await fs.promises.chmod(path.join(mainPackageDir, "bin", "tsc"), 0o755); await fs.promises.copyFile(path.join(inputDir, "typescript-package-readme.md"), path.join(mainPackageDir, "README.md")); } await fs.promises.writeFile(path.join(mainPackageDir, "package.json"), JSON.stringify(mainPackage, undefined, 4)); await fs.promises.copyFile("LICENSE.txt", path.join(mainPackageDir, "LICENSE")); await fs.promises.copyFile("NOTICE.txt", path.join(mainPackageDir, "NOTICE.txt")); // Build JS API and copy dist into the package. await run("npm", ["run", "-w", "@typescript/typescript", "build"]); await runCheckAPIDeclarations(); await cpRecursive(path.join(inputDir, "dist"), path.join(mainPackageDir, "dist")); // Validate that .d.ts files contain no external imports (all imports must start with "." or "#"). const dtsFiles = await globFiles(`${mainPackageDir}/dist/**/*.d.ts`); const importErrors = []; for (const dtsFile of dtsFiles) { const content = await fs.promises.readFile(dtsFile, "utf-8"); const relPath = path.relative(mainPackageDir, dtsFile); for (const [i, line] of content.split("\n").entries()) { // Match: import ... from "specifier" / export ... from "specifier" const fromMatch = line.match(/(?:import|export)\s.*?\sfrom\s+["']([^"']+)["']/); if (fromMatch && !fromMatch[1].startsWith(".") && !fromMatch[1].startsWith("#")) { importErrors.push(`${relPath}:${i + 1}: external import declaration "${fromMatch[1]}"`); } // Match: import("specifier") for (const m of line.matchAll(/import\(["']([^"']+)["']\)/g)) { if (!m[1].startsWith(".") && !m[1].startsWith("#")) { importErrors.push(`${relPath}:${i + 1}: external dynamic import "${m[1]}"`); } } } } if (importErrors.length) { throw new Error(`Found external imports in .d.ts files:\n${importErrors.map(e => " " + e).join("\n")}`); } const extraFlags = options.respectGoEnv ? [] : getReleaseBuildFlags(options.setPrerelease || nativePreviewReleaseVersion ? getVersion() : undefined); const platformBuilders = platforms.map(({ npmDir, npmPackageName, nodeOs, nodeArch, goos, goarch }) => async () => { const packageJson = { ...inputPackageJson, bin: undefined, files: ["lib", "NOTICE.txt"], imports: undefined, dependencies: undefined, name: npmPackageName, os: [nodeOs], cpu: [nodeArch], exports: { "./package.json": "./package.json", }, }; const out = path.join(npmDir, "lib"); await fs.promises.mkdir(out, { recursive: true }); await fs.promises.writeFile(path.join(npmDir, "package.json"), JSON.stringify(packageJson, undefined, 4)); await fs.promises.copyFile("LICENSE.txt", path.join(npmDir, "LICENSE")); await fs.promises.copyFile("NOTICE.txt", path.join(npmDir, "NOTICE.txt")); const readme = [ `# \`${npmPackageName}\``, "", `This package provides ${nodeOs}-${nodeArch} support for [${mainNativePreviewPackage.npmPackageName}](https://www.npmjs.com/package/${mainNativePreviewPackage.npmPackageName}).`, ]; await fs.promises.writeFile(path.join(npmDir, "README.md"), readme.join("\n") + "\n"); await generateLibs(out); const exeName = nativePreviewExeName(nodeOs); await buildTsc({ out: publishAsTypescript ? path.join(out, exeName) : out, env: { GOOS: goos, GOARCH: goarch, ...(options.respectGoEnv ? {} : { GOARM: "6", CGO_ENABLED: "0" }), }, extraFlags, }); }); if (isCI) { for (const build of platformBuilders) { await build(); // Build machines have too little space. // Clear the Go build cache between platforms. await run("go", ["clean", "-cache"]); } } else { await runWithConcurrencyLimit(platformBuilders, os.availableParallelism()); } } /** * @param {ReturnType} platforms */ async function testNativePreviewPackage(platforms) { const hostPlatform = platforms.find(({ nodeOs, nodeArch }) => nodeOs === process.platform && nodeArch === process.arch); assert(hostPlatform, `No package was built for the host platform ${process.platform}-${process.arch}`); const testRoot = path.resolve("built/package-test"); const nodeModules = path.join(testRoot, "node_modules"); const mainPackageDir = path.join(nodeModules, ...mainNativePreviewPackage.npmPackageName.split("/")); const platformPackageDir = path.join(nodeModules, ...hostPlatform.npmPackageName.split("/")); const sourceFile = path.join(testRoot, "index.ts"); await rimraf(testRoot); try { await cpRecursive(mainNativePreviewPackage.npmDir, mainPackageDir); await cpRecursive(hostPlatform.npmDir, platformPackageDir); await fs.promises.writeFile(sourceFile, 'export const value: string = "value";\n'); const require = createRequire(sourceFile); const { stdout } = await runOutput("npm", ["pack", "--dry-run", "--json", mainPackageDir]); /** @type {{ files: { path: string }[] }[]} */ const packed = JSON.parse(stdout); for (const name of ["tsconfig", "jsconfig"]) { const schemaPath = `schemas/${name}.schema.json`; assert(packed[0].files.some(file => file.path === schemaPath), `Package is missing ${schemaPath}`); assert.deepEqual( await fs.promises.readFile(path.join(mainPackageDir, schemaPath)), await fs.promises.readFile(path.join("packages/typescript", schemaPath)), ); assert.equal(require.resolve(`${mainNativePreviewPackage.npmPackageName}/${schemaPath}`), path.join(mainPackageDir, schemaPath)); } const binName = publishAsTypescript ? "tsc" : "tsgo"; const binPath = path.join(mainPackageDir, "bin", binName); const { stdout: versionOutput } = await runOutput(process.execPath, [binPath, "--version"]); assert.equal(versionOutput.trim(), `Version ${getVersion()}`); const { stdout: listFilesOutput } = await runOutput(process.execPath, [binPath, "--noEmit", "--listFiles", sourceFile]); assert(!listFilesOutput.includes("bundled:///"), "Packaged compiler listed an embedded library path"); const expectedLib = path.resolve(platformPackageDir, "lib", "lib.es5.d.ts"); const listedFiles = listFilesOutput .split(/\r?\n/) .filter(Boolean) .map(file => path.resolve(file)); assert(listedFiles.includes(expectedLib), `Expected packaged compiler to list ${expectedLib}`); } finally { await rimraf(testRoot); } } export const testNativePreviewPackageTask = task({ name: "typescript:test-package", description: "Tests the TypeScript npm package for the current platform.", dependencies: options.forRelease ? undefined : [buildNativePreviewPackages], run: () => testNativePreviewPackage(getPlatforms()), }); export const signNativePreviewPackages = task({ name: "typescript:sign", hiddenFromTaskList: true, run: runSignNativePreviewPackages, }); /** * @param {string} nodeOs */ function nativePreviewExeName(nodeOs) { const baseName = publishAsTypescript ? "tsc" : "tsgo"; return nodeOs === "win32" ? `${baseName}.exe` : baseName; } async function runSignNativePreviewPackages() { if (!options.forRelease) { throw new Error("This task should not be run in non-release builds."); } if (usePublishedPlatformPackagesForVsix) { checkPublishedPlatformPackagesForVsix(); console.log("Skipping npm package signing; VSIX packaging will use published platform packages."); return; } const platforms = getPlatforms(); /** @type {Map} */ const filelistByCert = new Map(); for (const { npmDir, nodeOs, cert, npmDirName } of platforms) { let certFilelist = filelistByCert.get(cert); if (!certFilelist) { filelistByCert.set(cert, certFilelist = []); } certFilelist.push({ tmpName: npmDirName, path: path.join(npmDir, "lib", nativePreviewExeName(nodeOs)), }); } const tmp = await getSignTempDir(); const typescriptMacEntitlementsPath = path.join(tmp, "typescript-macos-entitlements.plist"); await fs.promises.writeFile(typescriptMacEntitlementsPath, createTypeScriptMacEntitlementsPlist()); /** @type {DDSignFileList} */ const filelist = { SignFileRecordList: [], }; /** @type {{ path: string; unsignedZipPath: string; signedZipPath: string; notarizedZipPath: string; }[]} */ const macZips = []; // First, sign the files. for (const [cert, filelistPaths] of filelistByCert) { switch (cert) { case "Microsoft400": filelist.SignFileRecordList.push({ SignFileList: filelistPaths.map(p => ({ SrcPath: p.path, DstPath: null })), Certs: cert, MacAppName: undefined, }); break; case "LinuxSign": filelist.SignFileRecordList.push({ SignFileList: filelistPaths.map(p => ({ SrcPath: p.path, DstPath: p.path + ".sig" })), Certs: cert, MacAppName: undefined, }); break; case "MacDeveloperHarden": // Mac signing requires putting files into zips and then signing those, // along with a notarization step. for (const p of filelistPaths) { // ESRP preserves entitlements from an existing ad-hoc signature. await runOutput("go", ["-C", "./tools", "run", "./cmd/machotool", "sign", typescriptMacEntitlementsPath, p.path]); const unsignedZipPath = path.join(tmp, `${p.tmpName}.unsigned.zip`); const signedZipPath = path.join(tmp, `${p.tmpName}.signed.zip`); const notarizedZipPath = path.join(tmp, `${p.tmpName}.notarized.zip`); const zip = new AdmZip(); zip.addLocalFile(p.path); zip.writeZip(unsignedZipPath); macZips.push({ path: p.path, unsignedZipPath, signedZipPath, notarizedZipPath, }); } filelist.SignFileRecordList.push({ SignFileList: macZips.map(p => ({ SrcPath: p.unsignedZipPath, DstPath: p.signedZipPath })), Certs: cert, MacAppName: undefined, // MacAppName is only for notarization }); break; default: throw new Error(`Unknown cert: ${cert}`); } } await sign(filelist); // All of the files have been signed in place / had signatures added. if (macZips.length) { // Now, notarize the Mac files. /** @type {DDSignFileList} */ const notarizeFilelist = { SignFileRecordList: [ { SignFileList: macZips.map(p => ({ SrcPath: p.signedZipPath, DstPath: p.notarizedZipPath })), Certs: "8020", // "MacNotarize" (friendly name not supported by the tooling) MacAppName: "MicrosoftTypeScript", }, ], }; // Notarizing does not change the file, it just sends it to Apple, so ignore the case // where the input files are the same as the output files. await sign(notarizeFilelist, /*unchangedOutputOkay*/ true); // Finally, unzip the notarized files and move them back to their original locations. for (const p of macZips) { const zip = new AdmZip(p.notarizedZipPath); zip.extractEntryTo(path.basename(p.path), path.dirname(p.path), false, true); } // chmod +x the unzipped files. for (const p of macZips) { await fs.promises.chmod(p.path, 0o755); await runOutput("go", ["-C", "./tools", "run", "./cmd/machotool", "verify", typescriptMacEntitlementsPath, p.path]); } } } export const packNativePreviewPackages = task({ name: "typescript:pack", hiddenFromTaskList: true, dependencies: options.forRelease ? undefined : [buildNativePreviewPackages, cleanSignTempDirectory], run: runPackNativePreviewPackages, }); async function runPackNativePreviewPackages() { if (usePublishedPlatformPackagesForVsix) { checkPublishedPlatformPackagesForVsix(); await rimraf(builtNpm); console.log("Skipping npm package packing; VSIX packaging will use published platform packages."); return; } const platforms = getPlatforms(); await Promise.all([mainNativePreviewPackage, ...platforms].map(async ({ npmDir, npmTarball }) => { const { stdout } = await runOutput("npm", ["pack", "--json", npmDir]); const filename = JSON.parse(stdout)[0].filename.replace("@", "").replace("/", "-"); await fs.promises.rename(filename, npmTarball); })); // npm packages need to be published in dependency order: platform packages // first, then the main package that references them as optionalDependencies. const publishManifest = { stages: [ platforms.map(p => ({ filename: path.basename(p.npmTarball), })), [ { filename: path.basename(mainNativePreviewPackage.npmTarball), }, ], ], }; const publishManifestPath = path.join(builtNpm, "publish-manifest.json"); await fs.promises.writeFile(publishManifestPath, JSON.stringify(publishManifest, undefined, 4) + "\n"); } export const packVsixExtensions = task({ name: "vscode-typescript:pack", hiddenFromTaskList: true, dependencies: options.forRelease || usePublishedPlatformPackagesForVsix ? undefined : [packNativePreviewPackages], run: runPackVsixExtensions, }); /** @type {Map>} */ const publishedPlatformPackageDirs = new Map(); const getPublishedTypeScriptPackageDir = memoize(() => { const candidates = [ path.join(extensionDir, "node_modules", publishedTypeScriptAliasPackageName), path.join(__dirname, "node_modules", publishedTypeScriptAliasPackageName), ]; for (const candidate of candidates) { if (fs.existsSync(path.join(candidate, "package.json"))) { return candidate; } } throw new Error(`Could not find ${publishedTypeScriptAliasPackageName}; run npm install first.`); }); const getPublishedTypeScriptPackageJson = memoize(() => { const packageJson = JSON.parse(fs.readFileSync(path.join(getPublishedTypeScriptPackageDir(), "package.json"), "utf8")); if (packageJson.name !== "typescript") { throw new Error(`${publishedTypeScriptAliasPackageName} should alias the typescript package, but found ${packageJson.name}.`); } if (!packageJson.version || typeof packageJson.version !== "string") { throw new Error(`${publishedTypeScriptAliasPackageName} package.json did not contain a version.`); } if (!packageJson.optionalDependencies || typeof packageJson.optionalDependencies !== "object") { throw new Error(`${publishedTypeScriptAliasPackageName} package.json did not contain platform optionalDependencies.`); } return packageJson; }); function getPublishedTypeScriptVersion() { const version = getPublishedTypeScriptPackageJson().version; if (releaseVscodeTypescript && !stableThreeComponentVersionPattern.test(version)) { throw new Error(`vscode-typescript releases require a stable three-component TypeScript version, got ${version}.`); } return version; } function checkPublishedPlatformPackagesForVsix() { if (!options.forRelease) { throw new Error("usePublishedPlatformPackagesForVsix requires forRelease"); } getPublishedTypeScriptVersion(); } const getPackageLock = memoize(() => JSON.parse(fs.readFileSync(path.join(__dirname, "package-lock.json"), "utf8"))); /** * @param {string} npmPackageName */ async function getPublishedPlatformPackageDir(npmPackageName) { let promise = publishedPlatformPackageDirs.get(npmPackageName); if (!promise) { promise = getPublishedPlatformPackageDirWorker(npmPackageName); publishedPlatformPackageDirs.set(npmPackageName, promise); } return promise; } /** * @param {string} npmPackageName */ async function getPublishedPlatformPackageDirWorker(npmPackageName) { const dest = path.join(builtPublishedPlatformPackages, "node_modules", ...npmPackageName.split("/")); const lib = path.join(dest, "lib"); if (fs.existsSync(lib)) { return dest; } await fs.promises.mkdir(dest, { recursive: true }); const tarballDestination = path.join(builtPublishedPlatformPackages, "tarballs"); await fs.promises.mkdir(tarballDestination, { recursive: true }); const version = getPublishedTypeScriptPackageJson().optionalDependencies[npmPackageName]; if (!version || typeof version !== "string") { throw new Error(`${publishedTypeScriptAliasPackageName} does not depend on ${npmPackageName}.`); } const lockEntry = getPackageLock().packages[`node_modules/${npmPackageName}`]; if (!lockEntry) { throw new Error(`package-lock.json does not contain ${npmPackageName}; run npm install.`); } if (lockEntry.version !== version) { throw new Error(`package-lock.json has ${npmPackageName}@${lockEntry.version}, but ${publishedTypeScriptAliasPackageName} depends on ${version}.`); } if (!lockEntry.resolved || typeof lockEntry.resolved !== "string") { throw new Error(`package-lock.json entry for ${npmPackageName}@${version} does not contain a tarball URL.`); } if (!lockEntry.integrity || typeof lockEntry.integrity !== "string") { throw new Error(`package-lock.json entry for ${npmPackageName}@${version} does not contain an integrity hash.`); } const resolved = new URL(lockEntry.resolved); if (resolved.origin === "https://registry.npmjs.org") { const registry = new URL(await getReleasePackageRegistry()); resolved.protocol = registry.protocol; resolved.host = registry.host; resolved.pathname = path.posix.join(registry.pathname, resolved.pathname); } console.log(`Fetching locked ${npmPackageName}@${version} tarball from ${resolved}.`); const response = await fetch(resolved); if (!response.ok) { throw new Error(`Failed to fetch ${npmPackageName}@${version}: ${response.status} ${response.statusText}.`); } const tarball = Buffer.from(await response.arrayBuffer()); const integrityMatch = /^sha512-(.+)$/.exec(lockEntry.integrity); if (!integrityMatch) { throw new Error(`Unsupported integrity hash for ${npmPackageName}@${version}: ${lockEntry.integrity}.`); } const expectedIntegrity = Buffer.from(integrityMatch[1], "base64"); const actualIntegrity = crypto.createHash("sha512").update(tarball).digest(); if (expectedIntegrity.length !== actualIntegrity.length || !crypto.timingSafeEqual(expectedIntegrity, actualIntegrity)) { throw new Error(`Integrity check failed for ${npmPackageName}@${version}.`); } const tarballPath = path.join(tarballDestination, path.basename(resolved.pathname)); await fs.promises.writeFile(tarballPath, tarball); await tar.x({ file: tarballPath, cwd: dest, strip: 1 }); if (!fs.existsSync(lib)) { throw new Error(`Published platform package ${npmPackageName}@${version} did not contain a lib directory.`); } return dest; } async function runPackVsixExtensions() { await rimraf(builtVsix); await fs.promises.mkdir(builtVsix, { recursive: true }); if (usePublishedPlatformPackagesForVsix) { checkPublishedPlatformPackagesForVsix(); publishedPlatformPackageDirs.clear(); await rimraf(builtPublishedPlatformPackages); } const platforms = getPlatforms(); const extensions = platforms.flatMap(({ npmTarball, npmPackageName, extensions }) => extensions.map(e => ({ npmTarball, npmPackageName, ...e }))); if (!extensions.length) { console.log("No VSIX targets configured; skipping extension packaging."); return; } // We don't use vscode:prepublish, as that would run the build for each package below. await run("npm", ["run", "-w", "@typescript/typescript", "build"]); await run("npm", ["run", "bundle:release"], { cwd: extensionDir, env: releasePackageEnv }); let version = "0.0.0"; if (options.forRelease) { if (releaseVscodeTypescript) { version = getVscodeTypeScriptExtensionVersion(); } else { // No real semver prerelease versioning. // https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prerelease-extensions assert(options.setPrerelease, "forRelease is true but setPrerelease is not set"); const prerelease = options.setPrerelease; assert(typeof prerelease === "string", "setPrerelease is not a string"); // parse `dev..`. const match = prerelease.match(/dev\.(\d+)\.(\d+)/); if (!match) { throw new Error(`Prerelease version should be in the form of dev.., but got ${prerelease}`); } // Set version to `0..`. version = `0.${match[1]}.${match[2]}`; } } console.log("Version:", version); await Promise.all(extensions.map(async ({ npmTarball, npmPackageName, nodeOs, vscodeTarget, sourceDir, extensionDir: thisExtensionDir, vsixPath, vsixManifestPath, vsixSignaturePath }) => { const nodeModules = path.join(thisExtensionDir, "node_modules"); const embeddedPlatformPackageDir = path.join(nodeModules, ...npmPackageName.split("/")); const embeddedTypeScriptDir = path.join(nodeModules, "typescript"); await cpWithoutNodeModulesOrTsconfig(sourceDir, thisExtensionDir); if (usePublishedPlatformPackagesForVsix) { await cpRecursive(await getPublishedPlatformPackageDir(npmPackageName), embeddedPlatformPackageDir); await cpRecursive(getPublishedTypeScriptPackageDir(), embeddedTypeScriptDir, p => !p.endsWith("/node_modules")); } else { await fs.promises.mkdir(embeddedPlatformPackageDir, { recursive: true }); await fs.promises.mkdir(embeddedTypeScriptDir, { recursive: true }); await tar.x({ file: npmTarball, cwd: embeddedPlatformPackageDir, strip: 1 }); await tar.x({ file: mainNativePreviewPackage.npmTarball, cwd: embeddedTypeScriptDir, strip: 1 }); } await fs.promises.chmod(path.join(embeddedPlatformPackageDir, "lib", nativePreviewExeName(nodeOs)), 0o755); const embeddedTypeScriptPackageJson = JSON.parse(await fs.promises.readFile(path.join(embeddedTypeScriptDir, "package.json"), "utf8")); const packageJsonPath = path.join(thisExtensionDir, "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); packageJson.version = version; packageJson.bundledTypeScriptVersion = usePublishedPlatformPackagesForVsix ? getPublishedTypeScriptVersion() : getVersion(); packageJson.dependencies = { typescript: embeddedTypeScriptPackageJson.name === "typescript" ? embeddedTypeScriptPackageJson.version : `npm:${embeddedTypeScriptPackageJson.name}@${embeddedTypeScriptPackageJson.version}`, }; fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, undefined, 4)); await fs.promises.copyFile("NOTICE.txt", path.join(thisExtensionDir, "NOTICE.txt")); await run("vsce", ["package", version, "--no-update-package-json", "--no-yarn", "--out", vsixPath, "--target", vscodeTarget], { cwd: thisExtensionDir, env: releasePackageEnv, }); await testVsixPackage(vsixPath, thisExtensionDir, npmPackageName, nodeOs, vscodeTarget); if (options.forRelease) { await run("vsce", ["generate-manifest", "--packagePath", vsixPath, "--out", vsixManifestPath], { cwd: thisExtensionDir, env: releasePackageEnv, }); await fs.promises.cp(vsixManifestPath, vsixSignaturePath); } })); } /** * @param {string} vsixPath * @param {string} extensionPath * @param {string} platformPackageName * @param {string} nodeOs * @param {string} vscodeTarget */ async function testVsixPackage(vsixPath, extensionPath, platformPackageName, nodeOs, vscodeTarget) { const zip = new AdmZip(vsixPath); for (const packageName of ["typescript", platformPackageName]) { const packagePath = path.join(extensionPath, "node_modules", ...packageName.split("/")); const files = await fs.promises.readdir(packagePath, { recursive: true, withFileTypes: true }); for (const file of files) { if (!file.isFile()) continue; const filePath = path.join(file.parentPath, file.name); const archivePath = `extension/node_modules/${packageName}/${path.relative(packagePath, filePath).replace(/\\/g, "/")}`; const entry = zip.getEntry(archivePath); assert(entry, `VSIX is missing ${archivePath}`); assert.deepEqual(entry.getData(), await fs.promises.readFile(filePath), `VSIX changed ${archivePath}`); } } const hostTarget = `${process.platform}-${process.arch === "arm" ? "armhf" : process.arch}`; if (vscodeTarget !== hostTarget) return; const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "typescript-vsix-test-")); try { zip.extractAllTo(directory); const platformPath = path.join(directory, "extension", "node_modules", ...platformPackageName.split("/")); await fs.promises.chmod(path.join(platformPath, "lib", nativePreviewExeName(nodeOs)), 0o755); await fs.promises.writeFile(path.join(directory, "tsconfig.json"), '{"files": []}\n'); await run(process.execPath, [ "--input-type=module", "--eval", ` import assert from "node:assert/strict"; import { createRequire } from "node:module"; import { pathToFileURL } from "node:url"; const require = createRequire(process.cwd() + "/extension/node_modules/typescript/package.json"); const manifest = require("./package.json"); for (const mode of ["async", "sync"]) { const exportPath = manifest.exports?.["./" + mode] ? mode : "unstable/" + mode; const { API } = await import(pathToFileURL(require.resolve(manifest.name + "/" + exportPath)).href); const api = new API(); try { assert.ok(await api.parseConfigFile(process.cwd() + "/tsconfig.json")); } finally { await api.close(); } } `, ], { cwd: directory }); } finally { await fs.promises.rm(directory, { recursive: true, force: true }); } } export const signVsixExtensions = task({ name: "vscode-typescript:sign", hiddenFromTaskList: true, run: runSignVsixExtensions, }); async function runSignVsixExtensions() { if (!options.forRelease) { throw new Error("This task should not be run in non-release builds."); } const platforms = getPlatforms(); const extensions = platforms.flatMap(({ npmDir, extensions }) => extensions.map(e => ({ npmDir, ...e }))); if (!extensions.length) { console.log("No VSIX targets configured; skipping extension signing."); return; } await sign({ SignFileRecordList: [ { SignFileList: extensions.map(({ vsixSignaturePath }) => ({ SrcPath: vsixSignaturePath, DstPath: null })), Certs: "VSCodePublisher", MacAppName: undefined, }, ], }); if (!process.env.MICROBUILD_PLUGIN_DIRECTORY) { console.log("Skipping VSIX signature verification because signing was faked."); return; } for (const { vsixPath, vsixManifestPath, vsixSignaturePath } of extensions) { await run("vsce", [ "verify-signature", "--packagePath", vsixPath, "--manifestPath", vsixManifestPath, "--signaturePath", vsixSignaturePath, ]); } } async function runWriteVscodeTypeScriptReleaseManifest() { const platforms = getPlatforms(); const extensions = platforms.flatMap(({ extensions }) => extensions); /** @type {Record} */ const artifacts = {}; for (const extension of extensions) { for (const artifactPath of [extension.vsixPath, extension.vsixManifestPath, extension.vsixSignaturePath]) { const filename = path.basename(artifactPath); artifacts[filename] = { sha256: crypto.createHash("sha256").update(await fs.promises.readFile(artifactPath)).digest("hex"), }; } } const packageJson = getVscodeTypeScriptExtensionPackageJson(); const manifest = { extension: `${packageJson.publisher}.${packageJson.name}`, extensionVersion: getVscodeTypeScriptExtensionVersion(), bundledTypeScriptVersion: getPublishedTypeScriptVersion(), signType: process.env.VSCODE_TYPESCRIPT_SIGN_TYPE, sourceRef: process.env.BUILD_SOURCEBRANCH || process.env.GITHUB_REF || undefined, sourceCommit: process.env.BUILD_SOURCEVERSION || process.env.GITHUB_SHA || undefined, targets: extensions.map(({ vscodeTarget }) => vscodeTarget), artifacts, }; await fs.promises.writeFile(path.join(builtVsix, "release-manifest.json"), JSON.stringify(manifest, undefined, 4) + "\n"); } export const vscodeTypescriptRelease = task({ name: "vscode-typescript:release", hiddenFromTaskList: true, run: async () => { if (!options.forRelease || !releaseVscodeTypescript) { throw new Error("vscode-typescript:release requires --forRelease and --vscodeTypescriptRelease"); } await runPackVsixExtensions(); await runSignVsixExtensions(); await runWriteVscodeTypeScriptReleaseManifest(); await runCleanSignTempDirectory(); }, }); export const nativePreviewRelease = task({ name: "typescript:release", hiddenFromTaskList: true, run: async () => { if (releaseVscodeTypescript) { throw new Error("typescript:release cannot be used with --vscodeTypescriptRelease; use vscode-typescript:release"); } if (!options.forRelease || !options.setPrerelease && (!nativePreviewReleaseVersion || produceAnyVsix)) { throw new Error("typescript:release requires --forRelease and --setPrerelease flags, unless nativePreviewReleaseVersion is hardcoded and VSIX production is disabled. Example: npx hereby typescript:release --forRelease --setPrerelease=dev.1.0"); } await runBuildNativePreviewPackages(); await runSignNativePreviewPackages(); await runPackNativePreviewPackages(); await runPackVsixExtensions(); await runSignVsixExtensions(); await runCleanSignTempDirectory(); }, }); export const nativePreview = task({ name: "native-preview", hiddenFromTaskList: true, dependencies: options.forRelease ? undefined : [packNativePreviewPackages, packVsixExtensions], run: options.forRelease ? async () => { throw new Error("This task should not be run in release builds."); } : undefined, }); export const allChecks = task({ name: "all-checks", description: "Runs all checks for the Go code (fourslash, lint, tests, etc.)", run: async () => { await runTests(); await runFormat(); await runLint(); await runTests(); }, }); export const tidy = task({ name: "tidy", description: "Tidies both Go modules and synchronizes the workspace.", run: async () => { await run("go", ["mod", "tidy"], { cwd: "./tsc" }); await run("go", ["mod", "tidy"], { cwd: "./tools" }); await run("go", ["work", "sync"]); }, });