mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-10-29 00:45:56 +08:00
3107 lines
117 KiB
JavaScript
3107 lines
117 KiB
JavaScript
// @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<RunOptions, "captureOutput">} [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<K, 1> ? 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<void>)[]} 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<string, string | undefined>} [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<void>)[]} */
|
|
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<void>)} generators
|
|
*/
|
|
function goGenerateTask(name, generators) {
|
|
const run = typeof generators === "function" ? generators : async () => {
|
|
for (const generator of generators) await runGoGenerator(name, generator);
|
|
};
|
|
goGenerateActions.push(run);
|
|
return task({
|
|
name,
|
|
description: `Generates ${name.slice("generate:".length)} files. Pass --force to regenerate unchanged files.`,
|
|
run,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {string} file
|
|
* @param {string} type
|
|
* @param {string} output
|
|
* @param {string} [trimPrefix]
|
|
* @returns {GoGenerator}
|
|
*/
|
|
function stringerGenerator(file, type, output, trimPrefix) {
|
|
return {
|
|
file,
|
|
inputs: [],
|
|
outputs: [output],
|
|
commands: [
|
|
["go", "tool", "golang.org/x/tools/cmd/stringer", `-type=${type}`, ...(trimPrefix ? [`-trimprefix=${trimPrefix}`] : []), `-output=${output}`],
|
|
["dprint", "fmt", output],
|
|
],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {string} file
|
|
* @param {string} type
|
|
* @param {string} output
|
|
* @param {{ source?: string; packageName: string; inputs?: string[]; stub?: boolean }} options
|
|
* @returns {GoGenerator}
|
|
*/
|
|
function moqGenerator(file, type, output, { source = ".", packageName, inputs = [], stub = false }) {
|
|
return {
|
|
file,
|
|
inputs,
|
|
outputs: [output],
|
|
commands: [
|
|
["go", "tool", "github.com/matryer/moq", ...(stub ? ["-stub"] : []), "-fmt", "goimports", "-pkg", packageName, "-out", output, source, type],
|
|
["dprint", "fmt", output],
|
|
],
|
|
};
|
|
}
|
|
|
|
async function runGenerateASTStringer() {
|
|
await runGoGenerator("generate:ast-stringer", stringerGenerator("tsc/internal/ast/kind_generated.go", "Kind", "kind_stringer_generated.go"));
|
|
}
|
|
|
|
export const generateASTStringer = goGenerateTask("generate:ast-stringer", runGenerateASTStringer);
|
|
|
|
export const generateBundled = goGenerateTask("generate:bundled", [{
|
|
file: "tsc/internal/bundled/bundled.go",
|
|
inputs: ["generate.go", "CopyrightNotice.txt", "libs/*"],
|
|
outputs: ["libs_generated.go", "embed_generated.go"],
|
|
commands: [["go", "run", "generate.go"]],
|
|
}]);
|
|
|
|
export const generateChecker = goGenerateTask("generate:checker", [
|
|
stringerGenerator("tsc/internal/checker/types.go", "SignatureKind", "stringer_generated.go"),
|
|
]);
|
|
|
|
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<Set<string>>}
|
|
*/
|
|
async function collectUsedBaselines(trackingDir) {
|
|
/** @type {Set<string>} */
|
|
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<string[]>} 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<void>) | 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<void>;
|
|
* 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<void>} 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<string>, abortSignal: AbortSignal) => void | Promise<unknown>;
|
|
* }} WatchTask */
|
|
void 0;
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {(paths: Set<string> | undefined, abortSignal: AbortSignal) => void | Promise<unknown>} 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<void>} */ (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<string> | 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<void>} */ (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<T>} */
|
|
this.promise = new Promise((resolve, reject) => {
|
|
this.resolve = resolve;
|
|
this.reject = reject;
|
|
});
|
|
}
|
|
}
|
|
|
|
export class Debouncer {
|
|
/**
|
|
* @param {number} timeout
|
|
* @param {() => Promise<any> | void} action
|
|
*/
|
|
constructor(timeout, action) {
|
|
this._timeout = timeout;
|
|
this._action = action;
|
|
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
this._timer = undefined;
|
|
/** @type {Deferred<any> | 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<string, string>} */
|
|
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 }) =>
|
|
` <FilesToSign Include="${assertMsbuildXmlValue(filePath)}">
|
|
<Authenticode>${assertMsbuildXmlValue(cert)}</Authenticode>
|
|
<StrongName>None</StrongName>${
|
|
macAppName ? `
|
|
<MacAppName>${assertMsbuildXmlValue(macAppName)}</MacAppName>` : ""
|
|
}
|
|
</FilesToSign>`
|
|
).join("\n");
|
|
await fs.promises.writeFile(
|
|
propsPath,
|
|
`<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
|
<ItemGroup>
|
|
${items}
|
|
</ItemGroup>
|
|
</Project>
|
|
`,
|
|
);
|
|
|
|
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 => ` <key>${entitlement}</key>\n <true/>`).join("\n");
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
${entries}
|
|
</dict>
|
|
</plist>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* @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<Arch, "arm"> | "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<string, any>} 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<typeof getPlatforms>} 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<Cert, { tmpName: string; path: string }[]>} */
|
|
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<string, Promise<string>>} */
|
|
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.<number>.<number>`.
|
|
const match = prerelease.match(/dev\.(\d+)\.(\d+)/);
|
|
if (!match) {
|
|
throw new Error(`Prerelease version should be in the form of dev.<number>.<number>, but got ${prerelease}`);
|
|
}
|
|
// Set version to `0.<number>.<number>`.
|
|
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<string, { sha256: string }>} */
|
|
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"]);
|
|
},
|
|
});
|