Files
TypeScript/Herebyfile.mjs

3151 lines
116 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 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 {
x,
xSync,
} from "tinyexec";
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 {{
* captureOutput?: boolean;
* cwd?: string;
* env?: NodeJS.ProcessEnv;
* signal?: AbortSignal;
* }} RunOptions
*/
/**
* @param {string} arg
*/
function formatCommandArg(arg) {
return arg && /^[\w@%+=:,./-]+$/.test(arg) ? arg : JSON.stringify(arg);
}
/**
* @param {string} command
* @param {readonly string[]} [args]
* @param {RunOptions} [options]
*/
function run(command, args = [], options = {}) {
console.log("$ " + [command, ...args].map(formatCommandArg).join(" "));
return x(command, args, {
throwOnError: true,
...(options.signal ? { signal: options.signal } : {}),
nodeOptions: {
cwd: options.cwd,
env: options.env ? { ...process.env, ...options.env } : undefined,
stdio: options.captureOutput ? "pipe" : "inherit",
},
});
}
/**
* @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" },
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"),
});
async function runGenerate() {
return await run("go", ["generate", "-v", "./..."], { cwd: "./tsc" });
}
export const generate = task({
name: "generate",
description: "Runs go generate on the project.",
run: runGenerate,
});
async function runGenerateExtension() {
return await run("npm", ["run", "-w", "native-preview", "generateLocBundle"]);
}
export const generateExtension = task({
name: "generate:extension",
description: "Generates files in the extension",
run: runGenerateExtension,
});
// ── Enum generation from Go source ──────────────────────────────
/**
* @typedef {{
* name: string;
* goPrefix: string;
* goFile: string;
* outDir: string;
* stringEnum?: boolean;
* excludeMembers?: readonly string[];
* valueReplacements?: Record<string, string>;
* }} EnumDef
*/
/** @type {EnumDef[]} */
const enumDefs = [
{ name: "SymbolFlags", goPrefix: "SymbolFlags", goFile: "tsc/internal/ast/symbolflags.go", outDir: "packages/typescript/src/enums" },
{ name: "CheckFlags", goPrefix: "CheckFlags", goFile: "tsc/internal/ast/checkflags.go", outDir: "packages/typescript/src/enums" },
{ name: "TypeFlags", goPrefix: "TypeFlags", goFile: "tsc/internal/checker/types.go", outDir: "packages/typescript/src/enums" },
{ name: "ObjectFlags", goPrefix: "ObjectFlags", goFile: "tsc/internal/checker/types.go", outDir: "packages/typescript/src/enums" },
{ name: "SignatureFlags", goPrefix: "SignatureFlags", goFile: "tsc/internal/checker/types.go", outDir: "packages/typescript/src/enums" },
{ name: "SignatureKind", goPrefix: "SignatureKind", goFile: "tsc/internal/checker/types.go", outDir: "packages/typescript/src/enums" },
{ name: "IndexKind", goPrefix: "IndexKind", goFile: "tsc/internal/checker/types.go", outDir: "packages/typescript/src/enums" },
{ name: "ElementFlags", goPrefix: "ElementFlags", goFile: "tsc/internal/checker/types.go", outDir: "packages/typescript/src/enums" },
{ name: "TypePredicateKind", goPrefix: "TypePredicateKind", goFile: "tsc/internal/checker/types.go", outDir: "packages/typescript/src/enums" },
{ name: "TypeFormatFlags", goPrefix: "TypeFormatFlags", goFile: "tsc/internal/checker/types.go", outDir: "packages/typescript/src/enums" },
{ name: "DiagnosticCategory", goPrefix: "Category", goFile: "tsc/internal/diagnostics/diagnostics.go", outDir: "packages/typescript/src/enums" },
{ name: "SyntaxKind", goPrefix: "Kind", goFile: "tsc/internal/ast/kind_generated.go", outDir: "packages/typescript/src/enums" },
{ name: "NodeFlags", goPrefix: "NodeFlags", goFile: "tsc/internal/ast/nodeflags.go", outDir: "packages/typescript/src/enums" },
{ name: "OuterExpressionKinds", goPrefix: "OEK", goFile: "tsc/internal/ast/utilities.go", outDir: "packages/typescript/src/enums" },
{ name: "ModifierFlags", goPrefix: "ModifierFlags", goFile: "tsc/internal/ast/modifierflags.go", outDir: "packages/typescript/src/enums" },
{ name: "ModuleKind", goPrefix: "ModuleKind", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" },
{ name: "ModuleResolutionKind", goPrefix: "ModuleResolutionKind", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" },
{ name: "ModuleDetectionKind", goPrefix: "ModuleDetectionKind", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" },
{ name: "NewLineKind", goPrefix: "NewLineKind", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" },
{ name: "JsxEmit", goPrefix: "JsxEmit", goFile: "tsc/internal/core/compileroptions.go", outDir: "packages/typescript/src/enums" },
{ name: "ScriptKind", goPrefix: "ScriptKind", goFile: "tsc/internal/core/scriptkind.go", outDir: "packages/typescript/src/enums" },
{ name: "TokenFlags", goPrefix: "TokenFlags", goFile: "tsc/internal/ast/tokenflags.go", outDir: "packages/typescript/src/enums" },
{ name: "DiagnosticDirectivePolicy", goPrefix: "MappedDiagnosticDirectivePolicy", goFile: "tsc/internal/ast/ast.go", outDir: "packages/typescript/src/enums" },
{ name: "SpanMapKind", goPrefix: "Kind", goFile: "tsc/internal/spanmap/spanmap.go", outDir: "packages/typescript/src/enums" },
{ name: "SpanMapFidelity", goPrefix: "Fidelity", goFile: "tsc/internal/spanmap/spanmap.go", outDir: "packages/typescript/src/enums" },
{ name: "SpanMapFeature", goPrefix: "Feature", goFile: "tsc/internal/spanmap/spanmap.go", outDir: "packages/typescript/src/enums" },
{ name: "NodeBuilderFlags", goPrefix: "Flags", goFile: "tsc/internal/nodebuilder/types.go", outDir: "packages/typescript/src/enums" },
{ name: "CompletionItemKind", goPrefix: "CompletionItemKind", goFile: "tsc/internal/lsp/lsproto/lsp_generated.go", outDir: "packages/typescript/src/enums" },
{ name: "EmitOnly", goPrefix: "Emit", goFile: "tsc/internal/compiler/emitter.go", outDir: "packages/typescript/src/enums", excludeMembers: ["OnlyBuilderSignature"] },
// String enum: Go stores internal names with a "\xFE" sentinel prefix, but the escaped
// form sent over the wire uses "__" (see EscapeSymbolName), so map the sentinel accordingly.
{ name: "InternalSymbolName", goPrefix: "InternalSymbolName", goFile: "tsc/internal/ast/symbol.go", outDir: "packages/typescript/src/enums", stringEnum: true, valueReplacements: { InternalSymbolNamePrefix: "__" } },
];
/**
* @param {string} block
* @param {EnumDef} def
* @returns {EnumMember[]}
*/
function parseGoConstBlock(block, def) {
const prefix = def.goPrefix;
const members = [];
let iotaCounter = 0;
let iotaExpression;
const lines = block.split("\n");
let i = 0;
while (i < lines.length) {
const rawLine = lines[i];
const line = rawLine.replace(/\/\/.*$/, "").trim();
if (!line) {
i++;
continue;
}
// Match: PrefixName Type = value or PrefixName = value
const fullMatch = line.match(new RegExp(`^(${prefix}\\w+)\\s+(?:\\S+\\s*)?=\\s*(.+)$`));
// Match bare iota continuation: just PrefixName
const bareMatch = !fullMatch && iotaExpression !== undefined
? line.match(new RegExp(`^(${prefix}\\w+)$`))
: null;
if (!fullMatch && !bareMatch) {
i++;
continue;
}
const goName = fullMatch ? fullMatch[1] : /** @type {RegExpMatchArray} */ (bareMatch)[1];
let goValue = fullMatch ? fullMatch[2].trim() : "";
const memberName = goName.slice(prefix.length);
// Accumulate continuation lines ending with |
i++;
while (i < lines.length && goValue.endsWith("|")) {
const nextRaw = lines[i];
const nextLine = nextRaw.replace(/\/\/.*$/, "").trim();
if (!nextLine) {
i++;
continue;
}
goValue += " " + nextLine;
i++;
}
let tsValue;
if (def.stringEnum) {
tsValue = parseGoStringValue(goValue, def.valueReplacements ?? {});
}
else {
let numericValue = goValue;
if (goValue.includes("iota")) {
iotaExpression = goValue;
numericValue = goValue.replace(/\biota\b/g, String(iotaCounter));
}
else if (iotaExpression !== undefined && goValue === "") {
numericValue = iotaExpression.replace(/\biota\b/g, String(iotaCounter));
}
tsValue = translateGoNumericExpression(numericValue, prefix);
}
members.push({ name: memberName, value: tsValue });
iotaCounter++;
}
return members;
}
const goBinaryPrecedence = new Map([
["||", 1],
["&&", 2],
["==", 3],
["!=", 3],
["<", 3],
["<=", 3],
[">", 3],
[">=", 3],
["+", 4],
["-", 4],
["|", 4],
["^", 4],
["*", 5],
["/", 5],
["%", 5],
["<<", 5],
[">>", 5],
["&", 5],
["&^", 5],
]);
const tsBinaryPrecedence = new Map([
["||", 4],
["&&", 5],
["|", 6],
["^", 7],
["&", 8],
["==", 9],
["!=", 9],
["<", 10],
["<=", 10],
[">", 10],
[">=", 10],
["<<", 11],
[">>", 11],
["+", 12],
["-", 12],
["*", 13],
["/", 13],
["%", 13],
]);
/**
* @typedef {{ kind: "token"; text: string }
* | { kind: "unary"; operator: string; operand: GoExpression }
* | { kind: "binary"; operator: string; left: GoExpression; right: GoExpression }
* | { kind: "parenthesized"; expression: GoExpression }} GoExpression
*/
/**
* Parse with Go's precedence and print with TypeScript's precedence, adding parentheses where
* copying the expression verbatim would change its meaning.
* @param {string} expression
* @param {string} prefix
*/
function translateGoNumericExpression(expression, prefix) {
const tokens = expression.match(/<<|>>|&\^|\|\||&&|==|!=|<=|>=|[()+\-*/%&|^<>]|(?:0[xX][\dA-Fa-f_]+|0[bB][01_]+|0[oO][0-7_]+|\d[\d_]*)|[A-Za-z_]\w*/g) ?? [];
const withoutWhitespace = expression.replace(/\s/g, "");
if (tokens.join("") !== withoutWhitespace) {
throw new Error(`Cannot parse numeric enum value: ${expression}`);
}
let tokenIndex = 0;
/** @returns {GoExpression} */
function parseUnary() {
const token = tokens[tokenIndex];
if (token === "+" || token === "-" || token === "^") {
tokenIndex++;
return { kind: "unary", operator: token === "^" ? "~" : token, operand: parseUnary() };
}
if (token === "(") {
tokenIndex++;
const inner = parseBinary(1);
if (tokens[tokenIndex] !== ")") {
throw new Error(`Unmatched parenthesis in numeric enum value: ${expression}`);
}
tokenIndex++;
return { kind: "parenthesized", expression: inner };
}
if (token === undefined || goBinaryPrecedence.has(token) || token === ")") {
throw new Error(`Expected operand in numeric enum value: ${expression}`);
}
tokenIndex++;
return { kind: "token", text: token.replace(new RegExp(`^${prefix}`), "") };
}
/**
* @param {number} minimumPrecedence
* @returns {GoExpression}
*/
function parseBinary(minimumPrecedence) {
let left = parseUnary();
while (true) {
const operator = tokens[tokenIndex];
const precedence = goBinaryPrecedence.get(operator);
if (precedence === undefined || precedence < minimumPrecedence) break;
tokenIndex++;
const right = parseBinary(precedence + 1);
left = { kind: "binary", operator, left, right };
}
return left;
}
const parsed = parseBinary(1);
if (tokenIndex !== tokens.length) {
throw new Error(`Unexpected token '${tokens[tokenIndex]}' in numeric enum value: ${expression}`);
}
/**
* @param {GoExpression} node
* @param {number} minimumPrecedence
* @returns {string}
*/
function print(node, minimumPrecedence) {
if (node.kind === "token") return node.text;
if (node.kind === "parenthesized") return `(${print(node.expression, 0)})`;
if (node.kind === "unary") {
const text = `${node.operator}${print(node.operand, 14)}`;
return 14 < minimumPrecedence ? `(${text})` : text;
}
const operator = node.operator === "&^" ? "&" : node.operator;
const precedence = tsBinaryPrecedence.get(operator);
assert(precedence !== undefined);
const right = node.operator === "&^"
? `~${print(node.right, 14)}`
: print(node.right, precedence + 1);
const text = `${print(node.left, precedence)} ${operator} ${right}`;
return precedence < minimumPrecedence ? `(${text})` : text;
}
return print(parsed, 0);
}
/**
* Resolve a Go string-constant expression (e.g. `Prefix + "call"` or `"export="`)
* into a quoted, JS-escaped TypeScript string literal. `replacements` maps bare
* Go identifiers (such as a sentinel-prefix constant) to their literal value.
* @param {string} goValue
* @param {Record<string, string>} replacements
* @returns {string}
*/
function parseGoStringValue(goValue, replacements) {
let result = "";
for (const part of goValue.split("+").map(p => p.trim())) {
if (Object.prototype.hasOwnProperty.call(replacements, part)) {
result += replacements[part];
continue;
}
const stringMatch = part.match(/^"((?:[^"\\]|\\.)*)"$/);
if (stringMatch === null) {
throw new Error(`Cannot parse string enum value: ${goValue}`);
}
// Interpret Go escape sequences via JSON, then re-stringify below.
result += JSON.parse(`"${stringMatch[1]}"`);
}
return JSON.stringify(result);
}
/**
* @typedef {{
* name: string;
* value: string;
* }} EnumMember
*/
/**
* @param {EnumDef} def
* @returns {EnumMember[]}
*/
function parseGoEnum(def) {
const source = fs.readFileSync(def.goFile, "utf-8");
const constBlockRegex = /const\s*\(([\s\S]*?)\n\)/g;
for (const match of source.matchAll(constBlockRegex)) {
const members = parseGoConstBlock(match[1], def).filter(member => !def.excludeMembers?.includes(member.name));
if (members.length > 0) return topoSortMembers(members);
}
throw new Error(`No members found for enum ${def.name} in ${def.goFile}`);
}
/**
* Topologically sort enum members so composite members appear after
* all members they reference (Go allows forward references, TS does not).
* @param {EnumMember[]} members
* @returns {EnumMember[]}
*/
function topoSortMembers(members) {
const nameSet = new Set(members.map(m => m.name));
/** @type {Map<string, Set<string>>} */
const deps = new Map();
for (const m of members) {
/** @type {Set<string>} */
const refs = new Set();
// Find all identifier references in the value that are other member names
for (const [ref] of m.value.matchAll(/\b([A-Za-z_]\w*)\b/g)) {
if (ref !== m.name && nameSet.has(ref)) refs.add(ref);
}
deps.set(m.name, refs);
}
const sorted = /** @type {EnumMember[]} */ ([]);
const visited = new Set();
const visiting = new Set();
/** @param {string} name */
function visit(name) {
if (visited.has(name)) return;
if (visiting.has(name)) return; // cycle — keep original order
visiting.add(name);
for (const dep of deps.get(name) ?? []) {
visit(dep);
}
visiting.delete(name);
visited.add(name);
sorted.push(/** @type {EnumMember} */ (members.find(m => m.name === name)));
}
for (const m of members) {
visit(m.name);
}
return sorted;
}
/**
* @param {EnumDef} def
* @param {EnumMember[]} members
* @returns {string}
*/
function renderEnumTS(def, members) {
const header = `// Code generated by Herebyfile.mjs generate:enums from ${def.goFile}. DO NOT EDIT.\n\n`;
const lines = members.map(m => ` ${m.name} = ${m.value},`);
return `${header}export enum ${def.name} {\n${lines.join("\n")}\n}\n`;
}
const enumValuesGeneratedGoPath = "tsc/internal/api/enum_values_generated.go";
/**
* @typedef {{
* def: EnumDef
* code: string,
* fileNames: string[]
* members: EnumMember[]
* }} GeneratedEnum
*/
/**
* Ask the Go compiler what it actually thinks each numeric member's value is, so that
* generated TS values can be checked against Go's own arithmetic rather than trusting that
* copying operator-by-operator text from Go into JS preserves precedence/semantics.
*
* Writes tsc/internal/api/enum_values_generated.go, a standalone program that imports every
* package referenced by enumDefs and references each member by its original Go identifier
* (not by re-deriving it from the parsed TS text), so Go itself — not this script — computes
* the ground-truth value, then prints them as JSON. `internal/api` is used as the host package
* because it already imports (nearly) every package enums are sourced from.
*
* @param {GeneratedEnum[]} generatedEnums
* @returns {Promise<Record<string, Record<string, number>>>} enum def name -> (memberName -> Go value)
*/
async function computeGoGroundTruth(generatedEnums) {
/** @type {Map<string, {importPath: string, pkgName: string}>} */
const packagesByDir = new Map();
/**
* @param {EnumDef} def
* @returns {string}
*/
function getPackageName(def) {
const dir = path.dirname(def.goFile);
const importPath = `github.com/microsoft/TypeScript/tsc/${dir.replace(/^tsc[\\/]/, "")}`.replace(/\\/g, "/");
let info = packagesByDir.get(dir);
if (info === undefined) {
info = { importPath, pkgName: path.basename(dir) };
packagesByDir.set(dir, info);
}
return info.pkgName;
}
/** @type {string[]} */
const entries = [];
for (const { def, members } of generatedEnums) {
if (def.stringEnum) continue;
const pkgName = getPackageName(def);
/** @type {string[]} */
const memberEntries = members.map(m => {
return `\t\t\t${JSON.stringify(m.name)}: toInt32(${pkgName}.${def.goPrefix}${m.name}),`;
});
entries.push(
`\t\t${JSON.stringify(def.name)}: {\n${memberEntries.join("\n")}\n\t\t},`,
);
}
const importLines = [...packagesByDir.values()]
.sort((a, b) => a.importPath.localeCompare(b.importPath))
.map(({ pkgName, importPath }) => `\t${pkgName} "${importPath}"`);
const goSource = `//go:build ignore
// Code generated by Herebyfile.mjs generate:enums. DO NOT EDIT.
// Running this program prints the real Go-evaluated value of every generated enum member as
// JSON, so generate:enums can validate that the TypeScript it emits agrees with Go's own
// arithmetic (catching, e.g., operator-precedence mistakes introduced by copying Go expression
// text into TypeScript verbatim).
package main
import (
\t"encoding/json"
\t"os"
${importLines.join("\n")}
)
func main() {
\tvalues := map[string]map[string]int32{
${entries.join("\n")}
\t}
\tif err := json.NewEncoder(os.Stdout).Encode(values); err != nil {
\t\tpanic(err)
\t}
}
// A generic function call (unlike a constant conversion) forces Go to evaluate the conversion at
// runtime, truncating uint32-backed flags with a leading bitwise-not the same way JS's 32-bit
// bitwise operators would, instead of rejecting "constant overflows int32" at compile time.
func toInt32[T ~int8 | ~int16 | ~int32 | ~int | ~uint8 | ~uint16 | ~uint32](v T) int32 {
\treturn int32(v)
}
`;
fs.writeFileSync(enumValuesGeneratedGoPath, goSource);
await run("dprint", ["fmt", enumValuesGeneratedGoPath]);
const { stdout } = await runOutput("go", ["run", enumValuesGeneratedGoPath]);
/** @type {Record<string, Record<string, number>>} */
const parsed = JSON.parse(stdout);
return parsed;
}
/**
* Evaluate the generated IIFE in a sandbox and return each member's actual runtime value, so
* validation checks what TS really computes rather than re-deriving it from the source text.
* @param {string} enumSource
* @param {string} enumName
* @returns {Promise<Record<string, number | string>>}
*/
async function evaluateEnumMembers(enumSource, enumName) {
const enumModule = await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(enumSource)}`);
/** @type {Record<string, number | string>} */
const enumObj = enumModule[enumName];
return enumObj;
}
async function runGenerateEnums() {
const ts = /** @type {typeof import("typescript")} */ (await import("typescript"));
/**
* @param {string} enumSource
* @param {string} enumName
* @returns {string}
*/
function transpile(enumSource, enumName) {
return ts.transpileModule(enumSource, {
compilerOptions: {
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
},
}).outputText;
}
/**
* @param {string} enumSource
* @param {string} enumName
* @returns {string}
*/
function convertEnumToTs(enumSource, enumName) {
return enumSource.replace(
`export var ${enumName};`,
`export var ${enumName}: any;`,
);
}
console.log("Generating enums from Go source...");
/** @type {Array<GeneratedEnum>} */
const generatedEnums = [];
for (const def of enumDefs) {
const members = parseGoEnum(def);
const camelName = def.name.charAt(0).toLowerCase() + def.name.slice(1);
fs.mkdirSync(def.outDir, { recursive: true });
// Generate .enum.ts (TypeScript enum — used for types)
const enumTS = renderEnumTS(def, members);
const enumPath = path.join(def.outDir, `${camelName}.enum.ts`);
fs.writeFileSync(enumPath, enumTS);
// Generate .ts (IIFE — used at runtime)
const enumJsCode = transpile(enumTS, def.name);
const iifeSource = convertEnumToTs(enumJsCode, def.name);
const iifePath = path.join(def.outDir, `${camelName}.ts`);
fs.writeFileSync(iifePath, iifeSource);
generatedEnums.push({
code: enumJsCode,
def,
members,
fileNames: [enumPath, iifePath],
});
console.log(` ${def.name}: ${members.length} members → ${camelName}.enum.ts, ${camelName}.ts`);
}
console.log("Getting values from go");
const goValuesByEnum = await computeGoGroundTruth(generatedEnums);
/** @type {string[]} */
const mismatches = [];
for (const { def, members, code } of generatedEnums) {
if (def.stringEnum) continue;
const goValues = goValuesByEnum[def.name];
assert(goValues, `Enum ${def.name} was not outputted from GO`);
const tsValues = await evaluateEnumMembers(code, def.name);
for (const m of members) {
const goValue = goValues[m.name];
const tsValue = tsValues[m.name];
if (tsValue !== goValue) {
mismatches.push(
`${def.name}.${m.name}: Go says ${goValue}, but generated TS (\`${m.value}\`) evaluates to ${tsValue}`,
);
}
}
}
if (mismatches.length > 0) {
throw new Error(
`Generated enum values disagree with Go (likely an operator precedence or transcription bug in generate:enums):\n` +
mismatches.map(m => ` - ${m}`).join("\n"),
);
}
console.log("All generated values match Go.");
await run("dprint", ["fmt", ...generatedEnums.flatMap(e => e.fileNames)]);
console.log("Done.");
}
export const generateEnums = task({
name: "generate:enums",
description: "Generates TypeScript enum files from Go source.",
run: runGenerateEnums,
});
export const generateAST = task({
name: "generate:ast",
description: "Generates AST and encoder files from ast.json.",
run: () => run("node", ["./tools/scripts/tsc/generate.ts"]),
});
async function runGenerateAPI() {
await run("go", ["-C", "./tools", "run", "./gen-proto", "../tsc/internal/api/proto.go", "../packages/typescript/src/api/proto.generated.ts"]);
await run("npx", ["dprint", "fmt", "packages/typescript/src/api/proto.generated.ts"]);
}
export const generateAPI = task({
name: "generate:api",
description: "Generates API files from internal/api/proto.go and internal/api/session.go.",
run: runGenerateAPI,
});
// ── Vendored npm dependencies ───────────────────────────────────
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 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.`);
}
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));
}
}
export const generateVendor = task({
name: "generate:vendor",
description: "Updates the vendored copy of vscode-jsonrpc from node_modules.",
run: runGenerateVendor,
});
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 all benchmarks.",
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" });
}
export const testTools = task({
name: "test:tools",
description: "Runs all tests in the tools module.",
run: runTestTools,
});
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 runBuildAPITests() {
await run("npm", ["run", "-w", "@typescript/typescript", "generate:sync"]);
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 testAll = task({
name: "test:all",
description: "Runs ALL tests in the repo, including benchmarks, tools, and the API tests.",
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();
},
});
/**
* @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: "Builds, tests, lints, and formats the repo. Pass --api to include API tests, or --all to include all ancilliary repository tests.",
dependencies: [build],
run: async () => {
/** @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 runGenerate();
await runValidation("test:tsc", runTests);
await runValidation("test:extension", runTestExtension);
if (options.api || options.all) {
await runGenerateEnums(); // prereqs for test:api not included in `validate` deps
await runGenerateAPI();
await runBuildAPITests();
await runValidation("test:api", runTestAPI);
}
if (options.all) {
await runGenerateExtension();
await runGenerateVendor();
await runValidation("test:benchmarks", runTestBenchmarks);
await runValidation("test:tools", runTestTools);
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",
"--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/tsc/tsconfig.json",
"./tsc/internal/lsp/lsproto/_generate/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;
/**
* @typedef {{
* SignFileRecordList: {
* SignFileList: { SrcPath: string; DstPath: string | null }[];
* Certs: Cert;
* MacAppName: string | undefined
* }[]
* }} DDSignFileList
*
* @param {DDSignFileList} filelist
*/
async function sign(filelist, unchangedOutputOkay = false) {
let data = JSON.stringify(filelist, undefined, 4);
console.log("filelist:", data);
if (!process.env.MBSIGN_APPFOLDER) {
console.log(styleText("yellow", "Faking signing because MBSIGN_APPFOLDER 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;
/** @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,
};
}),
};
data = JSON.stringify(newFileList, undefined, 4);
console.log("new filelist:", data);
}
/** @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 filelistPath = path.resolve(tmp, `signing-filelist-${signCount++}.json`);
await fs.promises.writeFile(filelistPath, data);
try {
const dll = path.join(process.env.MBSIGN_APPFOLDER, "DDSignFiles.dll");
const filelistFlag = `/filelist:${filelistPath}`;
await run("dotnet", [dll, "--", filelistFlag]);
}
finally {
await fs.promises.unlink(filelistPath);
}
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 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 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 : [buildNativePreviewPackages, cleanSignTempDirectory],
run: runPackVsixExtensions,
});
/** @type {Map<string, Promise<string>>} */
const publishedPlatformPackageLibDirs = new Map();
const getPublishedTypeScriptPackageJson = memoize(() => {
const candidates = [
path.join(extensionDir, "node_modules", publishedTypeScriptAliasPackageName, "package.json"),
path.join(__dirname, "node_modules", publishedTypeScriptAliasPackageName, "package.json"),
];
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
const packageJson = JSON.parse(fs.readFileSync(candidate, "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;
}
}
throw new Error(`Could not find ${publishedTypeScriptAliasPackageName}; run npm install first.`);
});
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 getPublishedPlatformPackageLibDir(npmPackageName) {
let promise = publishedPlatformPackageLibDirs.get(npmPackageName);
if (!promise) {
promise = getPublishedPlatformPackageLibDirWorker(npmPackageName);
publishedPlatformPackageLibDirs.set(npmPackageName, promise);
}
return promise;
}
/**
* @param {string} npmPackageName
*/
async function getPublishedPlatformPackageLibDirWorker(npmPackageName) {
const dest = path.join(builtPublishedPlatformPackages, "node_modules", ...npmPackageName.split("/"));
const lib = path.join(dest, "lib");
if (fs.existsSync(lib)) {
return lib;
}
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 lib;
}
async function runPackVsixExtensions() {
await rimraf(builtVsix);
await fs.promises.mkdir(builtVsix, { recursive: true });
if (usePublishedPlatformPackagesForVsix) {
checkPublishedPlatformPackagesForVsix();
publishedPlatformPackageLibDirs.clear();
await rimraf(builtPublishedPlatformPackages);
}
const platforms = getPlatforms();
const extensions = platforms.flatMap(({ npmDir, npmPackageName, extensions }) => extensions.map(e => ({ npmDir, 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", "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 ({ npmDir, npmPackageName, nodeOs, vscodeTarget, sourceDir, extensionDir: thisExtensionDir, vsixPath, vsixManifestPath, vsixSignaturePath }) => {
const npmLibDir = usePublishedPlatformPackagesForVsix
? await getPublishedPlatformPackageLibDir(npmPackageName)
: path.join(npmDir, "lib");
const extensionLibDir = path.join(thisExtensionDir, "lib");
await fs.promises.mkdir(extensionLibDir, { recursive: true });
await cpWithoutNodeModulesOrTsconfig(sourceDir, thisExtensionDir);
await cpWithoutNodeModulesOrTsconfig(npmLibDir, extensionLibDir);
await fs.promises.chmod(path.join(extensionLibDir, nativePreviewExeName(nodeOs)), 0o755);
const packageJsonPath = path.join(thisExtensionDir, "package.json");
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
packageJson.version = version;
packageJson.bundledTypeScriptVersion = usePublishedPlatformPackagesForVsix ? getPublishedTypeScriptVersion() : getVersion();
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-dependencies", "--out", vsixPath, "--target", vscodeTarget], {
cwd: thisExtensionDir,
env: releasePackageEnv,
});
if (options.forRelease) {
await run("vsce", ["generate-manifest", "--packagePath", vsixPath, "--out", vsixManifestPath], {
cwd: thisExtensionDir,
env: releasePackageEnv,
});
await fs.promises.cp(vsixManifestPath, vsixSignaturePath);
}
}));
}
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,
},
],
});
}
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"]);
},
});