TypeScript/Herebyfile.mjs

976 строки
37 KiB
JavaScript

// @ts-check
import { CancelToken } from "@esfx/canceltoken";
import assert from "assert";
import chalk from "chalk";
import chokidar from "chokidar";
import esbuild from "esbuild";
import { EventEmitter } from "events";
import fs from "fs";
import { glob } from "glob";
import { task } from "hereby";
import path from "path";
import { localizationDirectories } from "./scripts/build/localization.mjs";
import cmdLineOptions from "./scripts/build/options.mjs";
import {
buildProject,
cleanProject,
watchProject,
} from "./scripts/build/projects.mjs";
import {
localBaseline,
refBaseline,
runConsoleTests,
} from "./scripts/build/tests.mjs";
import {
Debouncer,
Deferred,
exec,
getDiffTool,
memoize,
needsUpdate,
readJson,
rimraf,
} from "./scripts/build/utils.mjs";
/** @typedef {ReturnType<typeof task>} Task */
void 0;
const copyrightFilename = "./scripts/CopyrightNotice.txt";
const getCopyrightHeader = memoize(async () => {
const contents = await fs.promises.readFile(copyrightFilename, "utf-8");
return contents.replace(/\r\n/g, "\n");
});
export const buildScripts = task({
name: "scripts",
description: "Builds files in the 'scripts' folder.",
run: () => buildProject("scripts"),
});
const libs = memoize(() => {
/** @type {{ libs: string[]; paths: Record<string, string | undefined>; }} */
const libraries = readJson("./src/lib/libs.json");
const libs = libraries.libs.map(lib => {
const relativeSources = ["header.d.ts", lib + ".d.ts"];
const relativeTarget = libraries.paths && libraries.paths[lib] || ("lib." + lib + ".d.ts");
const sources = relativeSources.map(s => path.posix.join("src/lib", s));
const target = `built/local/${relativeTarget}`;
return { target, sources };
});
return libs;
});
export const generateLibs = task({
name: "lib",
description: "Builds the library targets",
run: async () => {
await fs.promises.mkdir("./built/local", { recursive: true });
for (const lib of libs()) {
let output = await getCopyrightHeader();
for (const source of lib.sources) {
const contents = await fs.promises.readFile(source, "utf-8");
output += "\n" + contents.replace(/\r\n/g, "\n");
}
await fs.promises.writeFile(lib.target, output);
}
},
});
const diagnosticInformationMapTs = "src/compiler/diagnosticInformationMap.generated.ts";
const diagnosticMessagesJson = "src/compiler/diagnosticMessages.json";
const diagnosticMessagesGeneratedJson = "src/compiler/diagnosticMessages.generated.json";
export const generateDiagnostics = task({
name: "generate-diagnostics",
description: "Generates a diagnostic file in TypeScript based on an input JSON file",
run: async () => {
await exec(process.execPath, ["scripts/processDiagnosticMessages.mjs", diagnosticMessagesJson]);
},
});
const cleanDiagnostics = task({
name: "clean-diagnostics",
description: "Generates a diagnostic file in TypeScript based on an input JSON file",
hiddenFromTaskList: true,
run: async () => {
await rimraf(diagnosticInformationMapTs);
await rimraf(diagnosticMessagesGeneratedJson);
},
});
// Localize diagnostics
/**
* .lcg file is what localization team uses to know what messages to localize.
* The file is always generated in 'enu/diagnosticMessages.generated.json.lcg'
*/
const generatedLCGFile = "built/local/enu/diagnosticMessages.generated.json.lcg";
/**
* The localization target produces the two following transformations:
* 1. 'src\loc\lcl\<locale>\diagnosticMessages.generated.json.lcl' => 'built\local\<locale>\diagnosticMessages.generated.json'
* convert localized resources into a .json file the compiler can understand
* 2. 'src\compiler\diagnosticMessages.generated.json' => 'built\local\ENU\diagnosticMessages.generated.json.lcg'
* generate the lcg file (source of messages to localize) from the diagnosticMessages.generated.json
*/
const localizationTargets = localizationDirectories
.map(f => `built/local/${f}/diagnosticMessages.generated.json`)
.concat(generatedLCGFile);
const localize = task({
name: "localize",
dependencies: [generateDiagnostics],
run: async () => {
if (needsUpdate(diagnosticMessagesGeneratedJson, generatedLCGFile)) {
await exec(process.execPath, ["scripts/generateLocalizedDiagnosticMessages.mjs", "src/loc/lcl", "built/local", diagnosticMessagesGeneratedJson], { ignoreExitCode: true });
}
},
});
export const buildSrc = task({
name: "build-src",
description: "Builds the src project (all code)",
dependencies: [generateDiagnostics],
run: () => buildProject("src"),
});
export const watchSrc = task({
name: "watch-src",
description: "Watches the src project (all code)",
hiddenFromTaskList: true,
dependencies: [generateDiagnostics],
run: () => watchProject("src"),
});
export const cleanSrc = task({
name: "clean-src",
hiddenFromTaskList: true,
run: () => cleanProject("src"),
});
const dtsBundlerPath = "./scripts/dtsBundler.mjs";
/**
* @param {string} entrypoint
* @param {string} output
*/
async function runDtsBundler(entrypoint, output) {
await exec(process.execPath, [
dtsBundlerPath,
"--entrypoint",
entrypoint,
"--output",
output,
]);
}
/**
* @param {string} entrypoint
* @param {string} outfile
* @param {BundlerTaskOptions} [taskOptions]
*
* @typedef BundlerTaskOptions
* @property {boolean} [exportIsTsObject]
* @property {boolean} [treeShaking]
* @property {boolean} [usePublicAPI]
* @property {() => void} [onWatchRebuild]
*/
function createBundler(entrypoint, outfile, taskOptions = {}) {
const getOptions = memoize(async () => {
const copyright = await getCopyrightHeader();
const banner = taskOptions.exportIsTsObject ? "var ts = {}; ((module) => {" : "";
/** @type {esbuild.BuildOptions} */
const options = {
entryPoints: [entrypoint],
banner: { js: copyright + banner },
bundle: true,
outfile,
platform: "node",
target: ["es2020", "node14.17"],
format: "cjs",
sourcemap: "linked",
sourcesContent: false,
treeShaking: taskOptions.treeShaking,
packages: "external",
logLevel: "warning",
// legalComments: "none", // If we add copyright headers to the source files, uncomment.
};
if (taskOptions.usePublicAPI) {
options.external = ["./typescript.js"];
options.plugins = options.plugins || [];
options.plugins.push({
name: "remap-typescript-to-require",
setup(build) {
build.onLoad({ filter: /src[\\/]typescript[\\/]typescript\.ts$/ }, () => {
return { contents: `export * from "./typescript.js"` };
});
},
});
}
if (taskOptions.exportIsTsObject) {
// Monaco bundles us as ESM by wrapping our code with something that defines module.exports
// but then does not use it, instead using the `ts` variable. Ensure that if we think we're CJS
// that we still set `ts` to the module.exports object.
options.footer = { js: `})({ get exports() { return ts; }, set exports(v) { ts = v; if (typeof module !== "undefined" && module.exports) { module.exports = v; } } })` };
// esbuild converts calls to "require" to "__require"; this function
// calls the real require if it exists, or throws if it does not (rather than
// throwing an error like "require not defined"). But, since we want typescript
// to be consumable by other bundlers, we need to convert these calls back to
// require so our imports are visible again.
//
// To fix this, we redefine "require" to a name we're unlikely to use with the
// same length as "require", then replace it back to "require" after bundling,
// ensuring that source maps still work.
//
// See: https://github.com/evanw/esbuild/issues/1905
const require = "require";
const fakeName = "Q".repeat(require.length);
const fakeNameRegExp = new RegExp(fakeName, "g");
options.define = { [require]: fakeName };
// For historical reasons, TypeScript does not set __esModule. Hack esbuild's __toCommonJS to be a noop.
// We reference `__copyProps` to ensure the final bundle doesn't have any unreferenced code.
const toCommonJsRegExp = /var __toCommonJS .*/;
const toCommonJsRegExpReplacement = "var __toCommonJS = (mod) => (__copyProps, mod); // Modified helper to skip setting __esModule.";
options.plugins = options.plugins || [];
options.plugins.push(
{
name: "post-process",
setup: build => {
build.onEnd(async () => {
let contents = await fs.promises.readFile(outfile, "utf-8");
contents = contents.replace(fakeNameRegExp, require);
let matches = 0;
contents = contents.replace(toCommonJsRegExp, () => {
matches++;
return toCommonJsRegExpReplacement;
});
assert(matches === 1, "Expected exactly one match for __toCommonJS");
await fs.promises.writeFile(outfile, contents);
});
},
},
);
}
return options;
});
return {
build: async () => esbuild.build(await getOptions()),
watch: async () => {
/** @type {esbuild.BuildOptions} */
const options = { ...await getOptions(), logLevel: "info" };
if (taskOptions.onWatchRebuild) {
const onRebuild = taskOptions.onWatchRebuild;
options.plugins = (options.plugins?.slice(0) ?? []).concat([{
name: "watch",
setup: build => {
let firstBuild = true;
build.onEnd(() => {
if (firstBuild) {
firstBuild = false;
}
else {
onRebuild();
}
});
},
}]);
}
const ctx = await esbuild.context(options);
ctx.watch();
},
};
}
let printedWatchWarning = false;
/**
* @param {object} options
* @param {string} options.name
* @param {string} [options.description]
* @param {Task[]} [options.buildDeps]
* @param {string} options.project
* @param {string} options.srcEntrypoint
* @param {string} options.builtEntrypoint
* @param {string} options.output
* @param {boolean} [options.enableCompileCache]
* @param {Task[]} [options.mainDeps]
* @param {BundlerTaskOptions} [options.bundlerOptions]
*/
function entrypointBuildTask(options) {
const build = task({
name: `build-${options.name}`,
dependencies: options.buildDeps,
run: () => buildProject(options.project),
});
const mainDeps = options.mainDeps?.slice(0) ?? [];
let output = options.output;
if (options.enableCompileCache) {
const originalOutput = output;
output = path.join(path.dirname(output), "_" + path.basename(output));
const compileCacheShim = task({
name: `shim-compile-cache-${options.name}`,
run: async () => {
const outDir = path.dirname(originalOutput);
await fs.promises.mkdir(outDir, { recursive: true });
const moduleSpecifier = path.relative(outDir, output);
const lines = [
`// This file is a shim which defers loading the real module until the compile cache is enabled.`,
`try {`,
` const { enableCompileCache } = require("node:module");`,
` if (enableCompileCache) {`,
` enableCompileCache();`,
` }`,
`} catch {}`,
`module.exports = require("./${moduleSpecifier.replace(/[\\/]/g, "/")}");`,
];
await fs.promises.writeFile(originalOutput, lines.join("\n") + "\n");
},
});
mainDeps.push(compileCacheShim);
}
const bundler = createBundler(options.srcEntrypoint, output, options.bundlerOptions);
// If we ever need to bundle our own output, change this to depend on build
// and run esbuild on builtEntrypoint.
const bundle = task({
name: `bundle-${options.name}`,
dependencies: options.buildDeps,
run: () => bundler.build(),
});
/**
* Writes a CJS module that reexports another CJS file. E.g. given
* `options.builtEntrypoint = "./built/local/tsc/tsc.js"` and
* `options.output = "./built/local/tsc.js"`, this will create a file
* named "./built/local/tsc.js" containing:
*
* ```
* module.exports = require("./tsc/tsc.js")
* ```
*/
const shim = task({
name: `shim-${options.name}`,
run: async () => {
const outDir = path.dirname(output);
await fs.promises.mkdir(outDir, { recursive: true });
const moduleSpecifier = path.relative(outDir, options.builtEntrypoint);
await fs.promises.writeFile(output, `module.exports = require("./${moduleSpecifier.replace(/[\\/]/g, "/")}")`);
},
});
if (cmdLineOptions.bundle) {
mainDeps.push(bundle);
if (cmdLineOptions.typecheck) {
mainDeps.push(build);
}
}
else {
mainDeps.push(build, shim);
}
const main = task({
name: options.name,
description: options.description,
dependencies: mainDeps,
});
const watch = task({
name: `watch-${options.name}`,
hiddenFromTaskList: true, // This is best effort.
dependencies: (options.buildDeps ?? []).concat(options.mainDeps ?? []).concat(cmdLineOptions.bundle ? [] : [shim]),
run: () => {
// These watch functions return promises that resolve once watch mode has started,
// allowing them to operate as regular tasks, while creating unresolved promises
// in the background that keep the process running after all tasks have exited.
if (!printedWatchWarning) {
console.error(chalk.yellowBright("Warning: watch mode is incomplete and may not work as expected. Use at your own risk."));
printedWatchWarning = true;
}
if (!cmdLineOptions.bundle) {
return watchProject(options.project);
}
return bundler.watch();
},
});
return { build, bundle, shim, main, watch };
}
const { main: tsc, watch: watchTsc } = entrypointBuildTask({
name: "tsc",
description: "Builds the command-line compiler",
buildDeps: [generateDiagnostics],
project: "src/tsc",
srcEntrypoint: "./src/tsc/tsc.ts",
builtEntrypoint: "./built/local/tsc/tsc.js",
output: "./built/local/tsc.js",
mainDeps: [generateLibs],
enableCompileCache: true,
});
export { tsc, watchTsc };
const { main: services, build: buildServices, watch: watchServices } = entrypointBuildTask({
name: "services",
description: "Builds the typescript.js library",
buildDeps: [generateDiagnostics],
project: "src/typescript",
srcEntrypoint: "./src/typescript/typescript.ts",
builtEntrypoint: "./built/local/typescript/typescript.js",
output: "./built/local/typescript.js",
mainDeps: [generateLibs],
bundlerOptions: { exportIsTsObject: true },
});
export { services, watchServices };
export const dtsServices = task({
name: "dts-services",
description: "Bundles typescript.d.ts",
dependencies: [buildServices],
run: async () => {
if (needsUpdate(["./built/local/typescript/tsconfig.tsbuildinfo", dtsBundlerPath], ["./built/local/typescript.d.ts", "./built/local/typescript.internal.d.ts"])) {
await runDtsBundler("./built/local/typescript/typescript.d.ts", "./built/local/typescript.d.ts");
}
},
});
const { main: tsserver, watch: watchTsserver } = entrypointBuildTask({
name: "tsserver",
description: "Builds the language server",
buildDeps: [generateDiagnostics],
project: "src/tsserver",
srcEntrypoint: "./src/tsserver/server.ts",
builtEntrypoint: "./built/local/tsserver/server.js",
output: "./built/local/tsserver.js",
mainDeps: [generateLibs, services],
bundlerOptions: { usePublicAPI: true },
enableCompileCache: true,
});
export { tsserver, watchTsserver };
export const min = task({
name: "min",
description: "Builds only tsc and tsserver",
dependencies: [tsc, tsserver],
});
export const watchMin = task({
name: "watch-min",
description: "Watches only tsc and tsserver",
hiddenFromTaskList: true,
dependencies: [watchTsc, watchTsserver],
});
// This is technically not enough to make tsserverlibrary loadable in the
// browser, but it's unlikely that anyone has actually been doing that.
const lsslJs = `
if (typeof module !== "undefined" && module.exports) {
module.exports = require("./typescript.js");
}
else {
throw new Error("tsserverlibrary requires CommonJS; use typescript.js instead");
}
`;
const lsslDts = `
import ts = require("./typescript.js");
export = ts;
`;
const lsslDtsInternal = `
import ts = require("./typescript.internal.js");
export = ts;
`;
/**
* @param {string} contents
*/
async function fileContentsWithCopyright(contents) {
return await getCopyrightHeader() + contents.trim().replace(/\r\n/g, "\n") + "\n";
}
const lssl = task({
name: "lssl",
description: "Builds language service server library",
dependencies: [services],
run: async () => {
await fs.promises.writeFile("./built/local/tsserverlibrary.js", await fileContentsWithCopyright(lsslJs));
},
});
export const dtsLssl = task({
name: "dts-lssl",
description: "Bundles tsserverlibrary.d.ts",
dependencies: [dtsServices],
run: async () => {
await fs.promises.writeFile("./built/local/tsserverlibrary.d.ts", await fileContentsWithCopyright(lsslDts));
await fs.promises.writeFile("./built/local/tsserverlibrary.internal.d.ts", await fileContentsWithCopyright(lsslDtsInternal));
},
});
export const dts = task({
name: "dts",
dependencies: [dtsServices, dtsLssl],
});
const testRunner = "./built/local/run.js";
const watchTestsEmitter = new EventEmitter();
const { main: tests, watch: watchTests } = entrypointBuildTask({
name: "tests",
description: "Builds the test infrastructure",
buildDeps: [generateDiagnostics],
project: "src/testRunner",
srcEntrypoint: "./src/testRunner/_namespaces/Harness.ts",
builtEntrypoint: "./built/local/testRunner/runner.js",
output: testRunner,
mainDeps: [generateLibs],
bundlerOptions: {
// Ensure we never drop any dead code, which might be helpful while debugging.
treeShaking: false,
onWatchRebuild() {
watchTestsEmitter.emit("rebuild");
},
},
});
export { tests, watchTests };
export const runEslintRulesTests = task({
name: "run-eslint-rules-tests",
description: "Runs the eslint rule tests",
run: () => runConsoleTests("scripts/eslint/tests", "mocha-fivemat-progress-reporter", /*runInParallel*/ false),
});
export const lint = task({
name: "lint",
description: "Runs eslint on the compiler and scripts sources.",
run: async () => {
const folder = ".";
const formatter = cmdLineOptions.ci ? "stylish" : "autolinkable-stylish";
const args = [
"node_modules/eslint/bin/eslint",
"--cache",
"--cache-location",
`${folder}/.eslintcache`,
"--format",
formatter,
"--report-unused-disable-directives",
"--max-warnings",
"0",
];
if (cmdLineOptions.fix) {
args.push("--fix");
}
args.push(folder);
console.log(`Linting: ${args.join(" ")}`);
return exec(process.execPath, args);
},
});
export const format = task({
name: "format",
description: "Formats the codebase.",
run: () => exec(process.execPath, ["node_modules/dprint/bin.js", "fmt"]),
});
export const checkFormat = task({
name: "check-format",
description: "Checks that the codebase is formatted.",
run: () => exec(process.execPath, ["node_modules/dprint/bin.js", "check"], { ignoreStdout: true }),
});
export const knip = task({
name: "knip",
description: "Runs knip.",
dependencies: [generateDiagnostics],
run: () => exec(process.execPath, ["node_modules/knip/bin/knip.js", "--tags=+internal,-knipignore", "--exclude=duplicates,enumMembers", ...(cmdLineOptions.fix ? ["--fix"] : [])]),
});
const { main: typingsInstaller, watch: watchTypingsInstaller } = entrypointBuildTask({
name: "typings-installer",
buildDeps: [generateDiagnostics],
project: "src/typingsInstaller",
srcEntrypoint: "./src/typingsInstaller/nodeTypingsInstaller.ts",
builtEntrypoint: "./built/local/typingsInstaller/nodeTypingsInstaller.js",
output: "./built/local/typingsInstaller.js",
mainDeps: [services],
bundlerOptions: { usePublicAPI: true },
enableCompileCache: true,
});
const { main: watchGuard, watch: watchWatchGuard } = entrypointBuildTask({
name: "watch-guard",
project: "src/watchGuard",
srcEntrypoint: "./src/watchGuard/watchGuard.ts",
builtEntrypoint: "./built/local/watchGuard/watchGuard.js",
output: "./built/local/watchGuard.js",
});
export const generateTypesMap = task({
name: "generate-types-map",
run: async () => {
await fs.promises.mkdir("./built/local", { recursive: true });
const source = "src/server/typesMap.json";
const target = "built/local/typesMap.json";
const contents = await fs.promises.readFile(source, "utf-8");
JSON.parse(contents); // Validates that the JSON parses.
await fs.promises.writeFile(target, contents.replace(/\r\n/g, "\n"));
},
});
// Drop a copy of diagnosticMessages.generated.json into the built/local folder. This allows
// it to be synced to the Azure DevOps repo, so that it can get picked up by the build
// pipeline that generates the localization artifacts that are then fed into the translation process.
const builtLocalDiagnosticMessagesGeneratedJson = "built/local/diagnosticMessages.generated.json";
const copyBuiltLocalDiagnosticMessages = task({
name: "copy-built-local-diagnostic-messages",
dependencies: [generateDiagnostics],
run: async () => {
const contents = await fs.promises.readFile(diagnosticMessagesGeneratedJson, "utf-8");
JSON.parse(contents); // Validates that the JSON parses.
await fs.promises.writeFile(builtLocalDiagnosticMessagesGeneratedJson, contents);
},
});
export const otherOutputs = task({
name: "other-outputs",
description: "Builds miscelaneous scripts and documents distributed with the LKG",
dependencies: [typingsInstaller, watchGuard, generateTypesMap, copyBuiltLocalDiagnosticMessages],
});
export const watchOtherOutputs = task({
name: "watch-other-outputs",
description: "Builds miscelaneous scripts and documents distributed with the LKG",
hiddenFromTaskList: true,
dependencies: [watchTypingsInstaller, watchWatchGuard, generateTypesMap, copyBuiltLocalDiagnosticMessages],
});
export const local = task({
name: "local",
description: "Builds the full compiler and services",
dependencies: [localize, tsc, tsserver, services, lssl, otherOutputs, dts],
});
export default local;
export const watchLocal = task({
name: "watch-local",
description: "Watches the full compiler and services",
hiddenFromTaskList: true,
dependencies: [localize, watchTsc, watchTsserver, watchServices, lssl, watchOtherOutputs, dts, watchSrc],
});
const runtestsDeps = [tests, generateLibs].concat(cmdLineOptions.typecheck ? [dts] : []);
export const runTests = task({
name: "runtests",
description: "Runs the tests using the built run.js file.",
dependencies: runtestsDeps,
run: () => runConsoleTests(testRunner, "mocha-fivemat-progress-reporter", /*runInParallel*/ false),
});
// task("runtests").flags = {
// "-t --tests=<regex>": "Pattern for tests to run.",
// " --failed": "Runs tests listed in '.failed-tests'.",
// " --coverage": "Generate test coverage using c8",
// "-r --reporter=<reporter>": "The mocha reporter to use.",
// "-i --break": "Runs tests in inspector mode (NodeJS 8 and later)",
// " --keepFailed": "Keep tests in .failed-tests even if they pass",
// " --light": "Run tests in light mode (fewer verifications, but tests run faster)",
// " --dirty": "Run tests without first cleaning test output directories",
// " --stackTraceLimit=<limit>": "Sets the maximum number of stack frames to display. Use 'full' to show all frames.",
// " --no-color": "Disables color",
// " --timeout=<ms>": "Overrides the default test timeout.",
// " --built": "Compile using the built version of the compiler.",
// " --shards": "Total number of shards running tests (default: 1)",
// " --shardId": "1-based ID of this shard (default: 1)",
// };
export const runTestsAndWatch = task({
name: "runtests-watch",
dependencies: [watchTests],
run: async () => {
if (!cmdLineOptions.tests && !cmdLineOptions.failed) {
console.log(chalk.redBright(`You must specifiy either --tests/-t or --failed to use 'runtests-watch'.`));
return;
}
let watching = true;
let running = true;
let lastTestChangeTimeMs = Date.now();
let testsChangedDeferred = /** @type {Deferred<void>} */ (new Deferred());
let testsChangedCancelSource = CancelToken.source();
const testsChangedDebouncer = new Debouncer(1_000, endRunTests);
const testCaseWatcher = chokidar.watch([
"tests/cases/**/*.*",
"tests/lib/**/*.*",
"tests/projects/**/*.*",
], {
ignorePermissionErrors: true,
alwaysStat: true,
});
process.on("SIGINT", endWatchMode);
process.on("beforeExit", endWatchMode);
watchTestsEmitter.on("rebuild", onRebuild);
testCaseWatcher.on("all", onChange);
while (watching) {
const promise = testsChangedDeferred.promise;
const token = testsChangedCancelSource.token;
if (!token.signaled) {
running = true;
try {
await runConsoleTests(testRunner, "mocha-fivemat-progress-reporter", /*runInParallel*/ false, { token, watching: true });
}
catch {
// ignore
}
running = false;
}
if (watching) {
console.log(chalk.yellowBright(`[watch] test run complete, waiting for changes...`));
await promise;
}
}
function onRebuild() {
beginRunTests(testRunner);
}
/**
* @param {'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'} 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 tests were
// run.
if (stats && stats.mtimeMs <= lastTestChangeTimeMs) {
return;
}
break;
}
beginRunTests(path);
}
/**
* @param {string} path
*/
function beginRunTests(path) {
if (testsChangedDebouncer.empty) {
console.log(chalk.yellowBright(`[watch] tests changed due to '${path}', restarting...`));
if (running) {
console.log(chalk.yellowBright("[watch] aborting in-progress test run..."));
}
testsChangedCancelSource.cancel();
testsChangedCancelSource = CancelToken.source();
}
testsChangedDebouncer.enqueue();
}
function endRunTests() {
lastTestChangeTimeMs = Date.now();
testsChangedDeferred.resolve();
testsChangedDeferred = /** @type {Deferred<void>} */ (new Deferred());
}
function endWatchMode() {
if (watching) {
watching = false;
console.log(chalk.yellowBright("[watch] exiting watch mode..."));
testsChangedCancelSource.cancel();
testCaseWatcher.close();
watchTestsEmitter.off("rebuild", onRebuild);
}
}
},
});
const doRunTestsParallel = task({
name: "do-runtests-parallel",
description: "Runs all the tests in parallel using the built run.js file.",
dependencies: runtestsDeps,
run: () => runConsoleTests(testRunner, "min", /*runInParallel*/ cmdLineOptions.workers > 1),
});
export const runTestsParallel = task({
name: "runtests-parallel",
description: "Runs all the tests in parallel using the built run.js file, linting in parallel if --lint=true.",
dependencies: [doRunTestsParallel].concat(cmdLineOptions.lint ? [lint] : []),
});
// task("runtests-parallel").flags = {
// " --coverage": "Generate test coverage using c8",
// " --light": "Run tests in light mode (fewer verifications, but tests run faster).",
// " --keepFailed": "Keep tests in .failed-tests even if they pass.",
// " --dirty": "Run tests without first cleaning test output directories.",
// " --stackTraceLimit=<limit>": "Sets the maximum number of stack frames to display. Use 'full' to show all frames.",
// " --workers=<number>": "The number of parallel workers to use.",
// " --timeout=<ms>": "Overrides the default test timeout.",
// " --built": "Compile using the built version of the compiler.",
// " --shards": "Total number of shards running tests (default: 1)",
// " --shardId": "1-based ID of this shard (default: 1)",
// };
export const testBrowserIntegration = task({
name: "test-browser-integration",
description: "Runs scripts/browserIntegrationTest.mjs which tests that typescript.js loads in a browser",
dependencies: [services],
run: () => exec(process.execPath, ["scripts/browserIntegrationTest.mjs"]),
});
export const diff = task({
name: "diff",
description: "Diffs the compiler baselines using the diff tool specified by the 'DIFF' environment variable",
run: () => exec(getDiffTool(), [refBaseline, localBaseline], { ignoreExitCode: true, waitForExit: false }),
});
/**
* @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 glob(`${localBaseline}/**`, { nodir: true, ignore: `${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 glob(`${localBaseline}/**/*.delete`, { nodir: true });
for (const p of toDelete) {
const out = localPathToRefPath(p).replace(/\.delete$/, "");
await rimraf(out);
}
};
}
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),
});
// TODO(rbuckton): Determine if we still need this task. Depending on a relative
// path here seems like a bad idea.
export const updateSublime = task({
name: "update-sublime",
description: "Updates the sublime plugin's tsserver",
dependencies: [tsserver],
run: async () => {
for (const file of ["built/local/tsserver.js", "built/local/tsserver.js.map"]) {
await fs.promises.copyFile(file, path.resolve("../TypeScript-Sublime-Plugin/tsserver/", path.basename(file)));
}
},
});
export const produceLKG = task({
name: "LKG",
description: "Makes a new LKG out of the built js files",
dependencies: [local],
run: async () => {
if (!cmdLineOptions.bundle) {
throw new Error("LKG cannot be created when --bundle=false");
}
const expectedFiles = [
"built/local/tsc.js",
"built/local/_tsc.js",
"built/local/tsserver.js",
"built/local/_tsserver.js",
"built/local/tsserverlibrary.js",
"built/local/tsserverlibrary.d.ts",
"built/local/typescript.js",
"built/local/typescript.d.ts",
"built/local/typingsInstaller.js",
"built/local/_typingsInstaller.js",
"built/local/watchGuard.js",
].concat(libs().map(lib => lib.target));
const missingFiles = expectedFiles
.concat(localizationTargets)
.filter(f => !fs.existsSync(f));
if (missingFiles.length > 0) {
throw new Error("Cannot replace the LKG unless all built targets are present in directory 'built/local/'. The following files are missing:\n" + missingFiles.join("\n"));
}
await exec(process.execPath, ["scripts/produceLKG.mjs"]);
},
});
export const lkg = task({
name: "lkg",
hiddenFromTaskList: true,
dependencies: [produceLKG],
});
export const cleanBuilt = task({
name: "clean-built",
hiddenFromTaskList: true,
run: () => fs.promises.rm("built", { recursive: true, force: true }),
});
export const clean = task({
name: "clean",
description: "Cleans build outputs",
dependencies: [cleanBuilt, cleanDiagnostics],
});
export const configureNightly = task({
name: "configure-nightly",
description: "Runs scripts/configurePrerelease.mjs to prepare a build for nightly publishing",
run: () => exec(process.execPath, ["scripts/configurePrerelease.mjs", "dev", "package.json", "src/compiler/corePublic.ts"]),
});
export const configureInsiders = task({
name: "configure-insiders",
description: "Runs scripts/configurePrerelease.mjs to prepare a build for insiders publishing",
run: () => exec(process.execPath, ["scripts/configurePrerelease.mjs", "insiders", "package.json", "src/compiler/corePublic.ts"]),
});
export const configureExperimental = task({
name: "configure-experimental",
description: "Runs scripts/configurePrerelease.mjs to prepare a build for experimental publishing",
run: () => exec(process.execPath, ["scripts/configurePrerelease.mjs", "experimental", "package.json", "src/compiler/corePublic.ts"]),
});
export const help = task({
name: "help",
description: "Prints the top-level tasks.",
hiddenFromTaskList: true,
run: () => exec("hereby", ["--tasks"], { hidePrompt: true }),
});