• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// @ts-check
2import assert from "assert";
3import path from "path";
4import fs from "fs";
5import del from "del";
6import { task } from "hereby";
7import _glob from "glob";
8import util from "util";
9import chalk from "chalk";
10import { exec, readJson, getDiffTool, getDirSize, memoize, needsUpdate } from "./scripts/build/utils.mjs";
11import { runConsoleTests, refBaseline, localBaseline, refRwcBaseline, localRwcBaseline } from "./scripts/build/tests.mjs";
12import { buildProject as realBuildProject, cleanProject, watchProject } from "./scripts/build/projects.mjs";
13import { localizationDirectories } from "./scripts/build/localization.mjs";
14import cmdLineOptions from "./scripts/build/options.mjs";
15import esbuild from "esbuild";
16import { fileURLToPath } from "url";
17
18const glob = util.promisify(_glob);
19
20/** @typedef {ReturnType<typeof task>} Task */
21void 0;
22
23const copyrightFilename = "CopyrightNotice.txt";
24const getCopyrightHeader = memoize(async () => {
25    const contents = await fs.promises.readFile(copyrightFilename, "utf-8");
26    return contents.replace(/\r\n/g, "\n");
27});
28
29
30// TODO(jakebailey): This is really gross. If the build is cancelled (i.e. Ctrl+C), the modification will persist.
31// Waiting on: https://github.com/microsoft/TypeScript/issues/51164
32let currentlyBuilding = 0;
33let oldTsconfigBase;
34
35/** @type {typeof realBuildProject} */
36const buildProjectWithEmit = async (...args) => {
37    const tsconfigBasePath = "./src/tsconfig-base.json";
38
39    // Not using fs.promises here, to ensure we are synchronous until running the real build.
40
41    if (currentlyBuilding === 0) {
42        oldTsconfigBase = fs.readFileSync(tsconfigBasePath, "utf-8");
43        fs.writeFileSync(tsconfigBasePath, oldTsconfigBase.replace(`"emitDeclarationOnly": true,`, `"emitDeclarationOnly": false, // DO NOT COMMIT`));
44    }
45
46    currentlyBuilding++;
47
48    await realBuildProject(...args);
49
50    currentlyBuilding--;
51
52    if (currentlyBuilding === 0) {
53        fs.writeFileSync(tsconfigBasePath, oldTsconfigBase);
54    }
55};
56
57
58const buildProject = cmdLineOptions.bundle ? realBuildProject : buildProjectWithEmit;
59
60
61export const buildScripts = task({
62    name: "scripts",
63    description: "Builds files in the 'scripts' folder.",
64    run: () => buildProject("scripts")
65});
66
67const libs = memoize(() => {
68    /** @type {{ libs: string[]; paths: Record<string, string | undefined>; }} */
69    const libraries = readJson("./src/lib/libs.json");
70    const libs = libraries.libs.map(lib => {
71        const relativeSources = ["header.d.ts", lib + ".d.ts"];
72        const relativeTarget = libraries.paths && libraries.paths[lib] || ("lib." + lib + ".d.ts");
73        const sources = relativeSources.map(s => path.posix.join("src/lib", s));
74        const target = `built/local/${relativeTarget}`;
75        return { target, sources };
76    });
77    return libs;
78});
79
80
81export const generateLibs = task({
82    name: "lib",
83    description: "Builds the library targets",
84    run: async () => {
85        await fs.promises.mkdir("./built/local", { recursive: true });
86        for (const lib of libs()) {
87            let output = await getCopyrightHeader();
88
89            for (const source of lib.sources) {
90                const contents = await fs.promises.readFile(source, "utf-8");
91                // TODO(jakebailey): "\n\n" is for compatibility with our current tests; our test baselines
92                // are sensitive to the positions of things in the lib files. Eventually remove this,
93                // or remove lib.d.ts line numbers from our baselines.
94                output += "\n\n" + contents.replace(/\r\n/g, "\n");
95            }
96
97            await fs.promises.writeFile(lib.target, output);
98        }
99    },
100});
101
102
103const diagnosticInformationMapTs = "src/compiler/diagnosticInformationMap.generated.ts";
104const diagnosticMessagesJson = "src/compiler/diagnosticMessages.json";
105const diagnosticMessagesGeneratedJson = "src/compiler/diagnosticMessages.generated.json";
106
107export const generateDiagnostics = task({
108    name: "generate-diagnostics",
109    description: "Generates a diagnostic file in TypeScript based on an input JSON file",
110    run: async () => {
111        await exec(process.execPath, ["scripts/processDiagnosticMessages.mjs", diagnosticMessagesJson]);
112    }
113});
114
115const cleanDiagnostics = task({
116    name: "clean-diagnostics",
117    description: "Generates a diagnostic file in TypeScript based on an input JSON file",
118    hiddenFromTaskList: true,
119    run: () => del([diagnosticInformationMapTs, diagnosticMessagesGeneratedJson]),
120});
121
122
123// Localize diagnostics
124/**
125 * .lcg file is what localization team uses to know what messages to localize.
126 * The file is always generated in 'enu/diagnosticMessages.generated.json.lcg'
127 */
128const generatedLCGFile = "built/local/enu/diagnosticMessages.generated.json.lcg";
129
130/**
131 * The localization target produces the two following transformations:
132 *    1. 'src\loc\lcl\<locale>\diagnosticMessages.generated.json.lcl' => 'built\local\<locale>\diagnosticMessages.generated.json'
133 *       convert localized resources into a .json file the compiler can understand
134 *    2. 'src\compiler\diagnosticMessages.generated.json' => 'built\local\ENU\diagnosticMessages.generated.json.lcg'
135 *       generate the lcg file (source of messages to localize) from the diagnosticMessages.generated.json
136 */
137const localizationTargets = localizationDirectories
138    .map(f => `built/local/${f}/diagnosticMessages.generated.json`)
139    .concat(generatedLCGFile);
140
141const localize = task({
142    name: "localize",
143    dependencies: [generateDiagnostics],
144    run: async () => {
145        if (needsUpdate(diagnosticMessagesGeneratedJson, generatedLCGFile)) {
146            return exec(process.execPath, ["scripts/generateLocalizedDiagnosticMessages.mjs", "src/loc/lcl", "built/local", diagnosticMessagesGeneratedJson], { ignoreExitCode: true });
147        }
148    }
149});
150
151export const buildSrc = task({
152    name: "build-src",
153    description: "Builds the src project (all code)",
154    dependencies: [generateDiagnostics],
155    run: () => buildProject("src"),
156});
157
158export const watchSrc = task({
159    name: "watch-src",
160    description: "Watches the src project (all code)",
161    hiddenFromTaskList: true,
162    dependencies: [generateDiagnostics],
163    run: () => watchProject("src"),
164});
165
166export const cleanSrc = task({
167    name: "clean-src",
168    hiddenFromTaskList: true,
169    run: () => cleanProject("src"),
170});
171
172/**
173 * @param {string} entrypoint
174 * @param {string} output
175 */
176async function runDtsBundler(entrypoint, output) {
177    await exec(process.execPath, [
178        "./scripts/dtsBundler.mjs",
179        "--entrypoint",
180        entrypoint,
181        "--output",
182        output,
183    ]);
184}
185
186
187/**
188 * @param {string} entrypoint
189 * @param {string} outfile
190 * @param {BundlerTaskOptions} [taskOptions]
191 *
192 * @typedef BundlerTaskOptions
193 * @property {string[]} [external]
194 * @property {boolean} [exportIsTsObject]
195 * @property {boolean} [treeShaking]
196 */
197function createBundler(entrypoint, outfile, taskOptions = {}) {
198    const getOptions = memoize(async () => {
199        const copyright = await getCopyrightHeader();
200        const banner = taskOptions.exportIsTsObject ? "var ts = {}; ((module) => {" : "";
201        /** @type {esbuild.BuildOptions} */
202        const options = {
203            entryPoints: [entrypoint],
204            banner: { js: copyright + banner },
205            bundle: true,
206            outfile,
207            platform: "node",
208            target: "es2018",
209            format: "cjs",
210            sourcemap: "linked",
211            sourcesContent: false,
212            treeShaking: taskOptions.treeShaking,
213            external: [
214                ...(taskOptions.external ?? []),
215                "source-map-support",
216            ],
217            logLevel: "warning",
218            // legalComments: "none", // If we add copyright headers to the source files, uncomment.
219            plugins: [
220                {
221                    name: "no-node-modules",
222                    setup: (build) => {
223                        build.onLoad({ filter: /[\\/]node_modules[\\/]/ }, () => {
224                            // Ideally, we'd use "--external:./node_modules/*" here, but that doesn't work; we
225                            // will instead end up with paths to node_modules rather than the package names.
226                            // Instead, we'll return a load error when we see that we're trying to bundle from
227                            // node_modules, then explicitly declare which external dependencies we rely on, which
228                            // ensures that the correct module specifier is kept in the output (the non-wildcard
229                            // form works properly). It also helps us keep tabs on what external dependencies we
230                            // may be importing, which is handy.
231                            //
232                            // See: https://github.com/evanw/esbuild/issues/1958
233                            return {
234                                errors: [{ text: 'Attempted to bundle from node_modules; ensure "external" is set correctly.' }]
235                            };
236                        });
237                    }
238                },
239                {
240                    name: "fix-require",
241                    setup: (build) => {
242                        build.onEnd(async () => {
243                            // esbuild converts calls to "require" to "__require"; this function
244                            // calls the real require if it exists, or throws if it does not (rather than
245                            // throwing an error like "require not defined"). But, since we want typescript
246                            // to be consumable by other bundlers, we need to convert these calls back to
247                            // require so our imports are visible again.
248                            //
249                            // The leading spaces are to keep the offsets the same within the files to keep
250                            // source maps working (though this only really matters for the line the require is on).
251                            //
252                            // See: https://github.com/evanw/esbuild/issues/1905
253                            let contents = await fs.promises.readFile(outfile, "utf-8");
254                            contents = contents.replace(/__require\(/g, "  require(");
255                            await fs.promises.writeFile(outfile, contents);
256                        });
257                    },
258                }
259            ]
260        };
261
262        if (taskOptions.exportIsTsObject) {
263            // Monaco bundles us as ESM by wrapping our code with something that defines module.exports
264            // but then does not use it, instead using the `ts` variable. Ensure that if we think we're CJS
265            // that we still set `ts` to the module.exports object.
266            options.footer = { js: `})(typeof module !== "undefined" && module.exports ? module : { exports: ts });\nif (typeof module !== "undefined" && module.exports) { ts = module.exports; }` };
267
268
269            // esbuild converts calls to "require" to "__require"; this function
270            // calls the real require if it exists, or throws if it does not (rather than
271            // throwing an error like "require not defined"). But, since we want typescript
272            // to be consumable by other bundlers, we need to convert these calls back to
273            // require so our imports are visible again.
274            //
275            // To fix this, we redefine "require" to a name we're unlikely to use with the
276            // same length as "require", then replace it back to "require" after bundling,
277            // ensuring that source maps still work.
278            //
279            // See: https://github.com/evanw/esbuild/issues/1905
280            const require = "require";
281            const fakeName = "Q".repeat(require.length);
282            const fakeNameRegExp = new RegExp(fakeName, "g");
283            options.define = { [require]: fakeName };
284
285            // For historical reasons, TypeScript does not set __esModule. Hack esbuild's __toCommonJS to be a noop.
286            // We reference `__copyProps` to ensure the final bundle doesn't have any unreferenced code.
287            const toCommonJsRegExp = /var __toCommonJS .*/;
288            const toCommonJsRegExpReplacement = "var __toCommonJS = (mod) => (__copyProps, mod); // Modified helper to skip setting __esModule.";
289            options.plugins = [
290                {
291                    name: "post-process",
292                    setup: build => {
293                        build.onEnd(async () => {
294                            let contents = await fs.promises.readFile(outfile, "utf-8");
295                            contents = contents.replace(fakeNameRegExp, require);
296                            let matches = 0;
297                            contents = contents.replace(toCommonJsRegExp, () => {
298                                matches++;
299                                return toCommonJsRegExpReplacement;
300                            });
301                            assert(matches === 1, "Expected exactly one match for __toCommonJS");
302                            await fs.promises.writeFile(outfile, contents);
303                        });
304                    },
305                },
306            ];
307        }
308
309        return options;
310    });
311
312    return {
313        build: async () => esbuild.build(await getOptions()),
314        watch: async () => esbuild.build({ ...await getOptions(), watch: true, logLevel: "info" }),
315    };
316}
317
318let printedWatchWarning = false;
319
320/**
321 * @param {object} options
322 * @param {string} options.name
323 * @param {string} [options.description]
324 * @param {Task[]} [options.buildDeps]
325 * @param {string} options.project
326 * @param {string} options.srcEntrypoint
327 * @param {string} options.builtEntrypoint
328 * @param {string} options.output
329 * @param {Task[]} [options.mainDeps]
330 * @param {BundlerTaskOptions} [options.bundlerOptions]
331 */
332function entrypointBuildTask(options) {
333    const build = task({
334        name: `build-${options.name}`,
335        dependencies: options.buildDeps,
336        run: () => buildProject(options.project),
337    });
338
339    const bundler = createBundler(options.srcEntrypoint, options.output, options.bundlerOptions);
340
341    // If we ever need to bundle our own output, change this to depend on build
342    // and run esbuild on builtEntrypoint.
343    const bundle = task({
344        name: `bundle-${options.name}`,
345        dependencies: options.buildDeps,
346        run: () => bundler.build(),
347    });
348
349    /**
350     * Writes a CJS module that reexports another CJS file. E.g. given
351     * `options.builtEntrypoint = "./built/local/tsc/tsc.js"` and
352     * `options.output = "./built/local/tsc.js"`, this will create a file
353     * named "./built/local/tsc.js" containing:
354     *
355     * ```
356     * module.exports = require("./tsc/tsc.js")
357     * ```
358     */
359    const shim = task({
360        name: `shim-${options.name}`,
361        run: async () => {
362            const outDir = path.dirname(options.output);
363            await fs.promises.mkdir(outDir, { recursive: true });
364            const moduleSpecifier = path.relative(outDir, options.builtEntrypoint);
365            await fs.promises.writeFile(options.output, `module.exports = require("./${moduleSpecifier}")`);
366        },
367    });
368
369    const main = task({
370        name: options.name,
371        description: options.description,
372        dependencies: (options.mainDeps ?? []).concat(cmdLineOptions.bundle ? [bundle] : [build, shim]),
373    });
374
375    const watch = task({
376        name: `watch-${options.name}`,
377        hiddenFromTaskList: true, // This is best effort.
378        dependencies: (options.buildDeps ?? []).concat(options.mainDeps ?? []).concat(cmdLineOptions.bundle ? [] : [shim]),
379        run: () => {
380            // These watch functions return promises that resolve once watch mode has started,
381            // allowing them to operate as regular tasks, while creating unresolved promises
382            // in the background that keep the process running after all tasks have exited.
383            if (!printedWatchWarning) {
384                console.error(chalk.yellowBright("Warning: watch mode is incomplete and may not work as expected. Use at your own risk."));
385                printedWatchWarning = true;
386            }
387
388            if (!cmdLineOptions.bundle) {
389                return watchProject(options.project);
390            }
391            return bundler.watch();
392        }
393    });
394
395    return { build, bundle, shim, main, watch };
396}
397
398const { main: arkTsLinter } = entrypointBuildTask({
399    name: "linter",
400    buildDeps: [generateDiagnostics],
401    project: "src/linter",
402    srcEntrypoint: "./src/linter/linter.ts",
403    builtEntrypoint: "./built/local/linter/linter.js",
404    output: "./built/local/linter.js",
405    bundlerOptions: {
406        // Ensure we never drop any dead code, which might be helpful while debugging.
407        treeShaking: false,
408        // These are directly imported via import statements and should not be bundled.
409        external: [
410            "json5",
411        ],
412    },
413});
414
415const { main: tsc, watch: watchTsc } = entrypointBuildTask({
416    name: "tsc",
417    description: "Builds the command-line compiler",
418    buildDeps: [generateDiagnostics],
419    project: "src/tsc",
420    srcEntrypoint: "./src/tsc/tsc.ts",
421    builtEntrypoint: "./built/local/tsc/tsc.js",
422    output: "./built/local/tsc.js",
423    mainDeps: [generateLibs, arkTsLinter],
424    bundlerOptions: {
425        // Ensure we never drop any dead code, which might be helpful while debugging.
426        treeShaking: false,
427        // These are directly imported via import statements and should not be bundled.
428        external: [
429            "json5",
430        ],
431    },
432});
433export { tsc, watchTsc };
434
435
436const { main: services, build: buildServices, watch: watchServices } = entrypointBuildTask({
437    name: "services",
438    description: "Builds the typescript.js library",
439    buildDeps: [generateDiagnostics],
440    project: "src/typescript",
441    srcEntrypoint: "./src/typescript/typescript.ts",
442    builtEntrypoint: "./built/local/typescript/typescript.js",
443    output: "./built/local/typescript.js",
444    mainDeps: [generateLibs, arkTsLinter],
445    bundlerOptions: {
446        exportIsTsObject: true,
447        // Ensure we never drop any dead code, which might be helpful while debugging.
448        treeShaking: false,
449        // These are directly imported via import statements and should not be bundled.
450        external: [
451            "json5",
452        ],
453     },
454});
455export { services, watchServices };
456
457export const dtsServices = task({
458    name: "dts-services",
459    description: "Bundles typescript.d.ts",
460    dependencies: [buildServices],
461    run: async () => {
462        if (needsUpdate("./built/local/typescript/tsconfig.tsbuildinfo", ["./built/local/typescript.d.ts", "./built/local/typescript.internal.d.ts"])) {
463            runDtsBundler("./built/local/typescript/typescript.d.ts", "./built/local/typescript.d.ts");
464        }
465    },
466});
467
468
469const { main: tsserver, watch: watchTsserver } = entrypointBuildTask({
470    name: "tsserver",
471    description: "Builds the language server",
472    buildDeps: [generateDiagnostics],
473    project: "src/tsserver",
474    srcEntrypoint: "./src/tsserver/server.ts",
475    builtEntrypoint: "./built/local/tsserver/server.js",
476    output: "./built/local/tsserver.js",
477    mainDeps: [generateLibs, arkTsLinter],
478    // Even though this seems like an exectuable, so could be the default CJS,
479    // this is used in the browser too. Do the same thing that we do for our
480    // libraries and generate an IIFE with name `ts`, as to not pollute the global
481    // scope.
482    bundlerOptions: {
483        exportIsTsObject: true,
484        external: [
485            "json5",
486        ],
487    },
488});
489export { tsserver, watchTsserver };
490
491
492export const min = task({
493    name: "min",
494    description: "Builds only tsc and tsserver",
495    dependencies: [tsc, tsserver],
496});
497
498export const watchMin = task({
499    name: "watch-min",
500    description: "Watches only tsc and tsserver",
501    hiddenFromTaskList: true,
502    dependencies: [watchTsc, watchTsserver],
503});
504
505
506
507const { main: lssl, build: buildLssl, watch: watchLssl } = entrypointBuildTask({
508    name: "lssl",
509    description: "Builds language service server library",
510    buildDeps: [generateDiagnostics],
511    project: "src/tsserverlibrary",
512    srcEntrypoint: "./src/tsserverlibrary/tsserverlibrary.ts",
513    builtEntrypoint: "./built/local/tsserverlibrary/tsserverlibrary.js",
514    output: "./built/local/tsserverlibrary.js",
515    mainDeps: [generateLibs, arkTsLinter],
516    bundlerOptions: {
517        exportIsTsObject: true,
518        external: [
519            "json5",
520        ],
521    },
522});
523export { lssl, watchLssl };
524
525export const dtsLssl = task({
526    name: "dts-lssl",
527    description: "Bundles tsserverlibrary.d.ts",
528    dependencies: [buildLssl],
529    run: async () => {
530        if (needsUpdate("./built/local/tsserverlibrary/tsconfig.tsbuildinfo", ["./built/local/tsserverlibrary.d.ts", "./built/local/tsserverlibrary.internal.d.ts"])) {
531            await runDtsBundler("./built/local/tsserverlibrary/tsserverlibrary.d.ts", "./built/local/tsserverlibrary.d.ts");
532        }
533    }
534});
535
536export const dts = task({
537    name: "dts",
538    dependencies: [dtsServices, dtsLssl],
539});
540
541
542const testRunner = "./built/local/run.js";
543const __dirname = path.dirname(fileURLToPath(import.meta.url));
544const ohTestCasesGeneration = task({
545    name: "ohTestCasesGeneration",
546    description: "Generate oh test cases",
547    run: async () => {
548        if (!fs.existsSync(path.join(__dirname, "tests/cases/fourslash/oh/")) || !fs.existsSync(path.join(__dirname, "tests/cases/compiler-oh/"))) {
549            await exec(process.execPath, ["scripts/ohTestCasesGenerationScript.js", diagnosticMessagesJson]);
550        }
551    }
552});
553
554const { main: tests, watch: watchTests } = entrypointBuildTask({
555    name: "tests",
556    description: "Builds the test infrastructure",
557    buildDeps: [generateDiagnostics],
558    project: "src/testRunner",
559    srcEntrypoint: "./src/testRunner/_namespaces/Harness.ts",
560    builtEntrypoint: "./built/local/testRunner/runner.js",
561    output: testRunner,
562    mainDeps: [generateLibs, arkTsLinter, ohTestCasesGeneration],
563    bundlerOptions: {
564        // Ensure we never drop any dead code, which might be helpful while debugging.
565        treeShaking: false,
566        // These are directly imported via import statements and should not be bundled.
567        external: [
568            "chai",
569            "del",
570            "diff",
571            "mocha",
572            "ms",
573            "json5",
574        ],
575    },
576});
577export { tests, watchTests };
578
579
580export const runEslintRulesTests = task({
581    name: "run-eslint-rules-tests",
582    description: "Runs the eslint rule tests",
583    run: () => runConsoleTests("scripts/eslint/tests", "mocha-fivemat-progress-reporter", /*runInParallel*/ false),
584});
585
586export const lint = task({
587    name: "lint",
588    description: "Runs eslint on the compiler and scripts sources.",
589    run: async () => {
590        const folder = ".";
591        const formatter = cmdLineOptions.ci ? "stylish" : "autolinkable-stylish";
592        const args = [
593            "node_modules/eslint/bin/eslint",
594            "--cache",
595            "--cache-location", `${folder}/.eslintcache`,
596            "--format", formatter,
597        ];
598
599        if (cmdLineOptions.fix) {
600            args.push("--fix");
601        }
602
603        args.push(folder);
604
605        console.log(`Linting: ${args.join(" ")}`);
606        return exec(process.execPath, args);
607    }
608});
609
610const { main: cancellationToken, watch: watchCancellationToken } = entrypointBuildTask({
611    name: "cancellation-token",
612    project: "src/cancellationToken",
613    srcEntrypoint: "./src/cancellationToken/cancellationToken.ts",
614    builtEntrypoint: "./built/local/cancellationToken/cancellationToken.js",
615    output: "./built/local/cancellationToken.js",
616});
617
618const { main: typingsInstaller, watch: watchTypingsInstaller } = entrypointBuildTask({
619    name: "typings-installer",
620    buildDeps: [generateDiagnostics],
621    project: "src/typingsInstaller",
622    srcEntrypoint: "./src/typingsInstaller/nodeTypingsInstaller.ts",
623    builtEntrypoint: "./built/local/typingsInstaller/nodeTypingsInstaller.js",
624    output: "./built/local/typingsInstaller.js",
625    bundlerOptions: {
626        external: [
627            "json5",
628        ],
629    },
630});
631
632const { main: watchGuard, watch: watchWatchGuard } = entrypointBuildTask({
633    name: "watch-guard",
634    project: "src/watchGuard",
635    srcEntrypoint: "./src/watchGuard/watchGuard.ts",
636    builtEntrypoint: "./built/local/watchGuard/watchGuard.js",
637    output: "./built/local/watchGuard.js",
638});
639
640export const generateTypesMap = task({
641    name: "generate-types-map",
642    run: async () => {
643        const source = "src/server/typesMap.json";
644        const target = "built/local/typesMap.json";
645        const contents = await fs.promises.readFile(source, "utf-8");
646        JSON.parse(contents); // Validates that the JSON parses.
647        await fs.promises.writeFile(target, contents);
648    }
649});
650
651
652// Drop a copy of diagnosticMessages.generated.json into the built/local folder. This allows
653// it to be synced to the Azure DevOps repo, so that it can get picked up by the build
654// pipeline that generates the localization artifacts that are then fed into the translation process.
655const builtLocalDiagnosticMessagesGeneratedJson = "built/local/diagnosticMessages.generated.json";
656const copyBuiltLocalDiagnosticMessages = task({
657    name: "copy-built-local-diagnostic-messages",
658    dependencies: [generateDiagnostics],
659    run: async () => {
660        const contents = await fs.promises.readFile(diagnosticMessagesGeneratedJson, "utf-8");
661        JSON.parse(contents); // Validates that the JSON parses.
662        await fs.promises.writeFile(builtLocalDiagnosticMessagesGeneratedJson, contents);
663    }
664});
665
666
667export const otherOutputs = task({
668    name: "other-outputs",
669    description: "Builds miscelaneous scripts and documents distributed with the LKG",
670    dependencies: [cancellationToken, typingsInstaller, watchGuard, generateTypesMap, copyBuiltLocalDiagnosticMessages],
671});
672
673export const watchOtherOutputs = task({
674    name: "watch-other-outputs",
675    description: "Builds miscelaneous scripts and documents distributed with the LKG",
676    hiddenFromTaskList: true,
677    dependencies: [watchCancellationToken, watchTypingsInstaller, watchWatchGuard, generateTypesMap, copyBuiltLocalDiagnosticMessages],
678});
679
680export const local = task({
681    name: "local",
682    description: "Builds the full compiler and services",
683    dependencies: [localize, tsc, tsserver, services, lssl, otherOutputs, dts],
684});
685export default local;
686
687export const watchLocal = task({
688    name: "watch-local",
689    description: "Watches the full compiler and services",
690    hiddenFromTaskList: true,
691    dependencies: [localize, watchTsc, watchTsserver, watchServices, watchLssl, watchOtherOutputs, dts],
692});
693
694
695export const runTests = task({
696    name: "runtests",
697    description: "Runs the tests using the built run.js file.",
698    dependencies: [tests, generateLibs, dts],
699    run: () => runConsoleTests(testRunner, "mocha-fivemat-progress-reporter", /*runInParallel*/ false),
700});
701// task("runtests").flags = {
702//     "-t --tests=<regex>": "Pattern for tests to run.",
703//     "   --failed": "Runs tests listed in '.failed-tests'.",
704//     "-r --reporter=<reporter>": "The mocha reporter to use.",
705//     "-i --break": "Runs tests in inspector mode (NodeJS 8 and later)",
706//     "   --keepFailed": "Keep tests in .failed-tests even if they pass",
707//     "   --light": "Run tests in light mode (fewer verifications, but tests run faster)",
708//     "   --dirty": "Run tests without first cleaning test output directories",
709//     "   --stackTraceLimit=<limit>": "Sets the maximum number of stack frames to display. Use 'full' to show all frames.",
710//     "   --no-color": "Disables color",
711//     "   --timeout=<ms>": "Overrides the default test timeout.",
712//     "   --built": "Compile using the built version of the compiler.",
713//     "   --shards": "Total number of shards running tests (default: 1)",
714//     "   --shardId": "1-based ID of this shard (default: 1)",
715// };
716
717export const runTestsParallel = task({
718    name: "runtests-parallel",
719    description: "Runs all the tests in parallel using the built run.js file.",
720    dependencies: [tests, generateLibs, dts],
721    run: () => runConsoleTests(testRunner, "min", /*runInParallel*/ cmdLineOptions.workers > 1),
722});
723// task("runtests-parallel").flags = {
724//     "   --light": "Run tests in light mode (fewer verifications, but tests run faster).",
725//     "   --keepFailed": "Keep tests in .failed-tests even if they pass.",
726//     "   --dirty": "Run tests without first cleaning test output directories.",
727//     "   --stackTraceLimit=<limit>": "Sets the maximum number of stack frames to display. Use 'full' to show all frames.",
728//     "   --workers=<number>": "The number of parallel workers to use.",
729//     "   --timeout=<ms>": "Overrides the default test timeout.",
730//     "   --built": "Compile using the built version of the compiler.",
731//     "   --shards": "Total number of shards running tests (default: 1)",
732//     "   --shardId": "1-based ID of this shard (default: 1)",
733// };
734
735export const testBrowserIntegration = task({
736    name: "test-browser-integration",
737    description: "Runs scripts/browserIntegrationTest.mjs which tests that typescript.js loads in a browser",
738    dependencies: [services],
739    run: () => exec(process.execPath, ["scripts/browserIntegrationTest.mjs"]),
740});
741
742export const diff = task({
743    name: "diff",
744    description: "Diffs the compiler baselines using the diff tool specified by the 'DIFF' environment variable",
745    run: () => exec(getDiffTool(), [refBaseline, localBaseline], { ignoreExitCode: true, waitForExit: false }),
746});
747
748export const diffRwc = task({
749    name: "diff-rwc",
750    description: "Diffs the RWC baselines using the diff tool specified by the 'DIFF' environment variable",
751    run: () => exec(getDiffTool(), [refRwcBaseline, localRwcBaseline], { ignoreExitCode: true, waitForExit: false }),
752});
753
754/**
755 * @param {string} localBaseline Path to the local copy of the baselines
756 * @param {string} refBaseline Path to the reference copy of the baselines
757 */
758function baselineAcceptTask(localBaseline, refBaseline) {
759    /**
760     * @param {string} p
761     */
762    function localPathToRefPath(p) {
763        const relative = path.relative(localBaseline, p);
764        return path.join(refBaseline, relative);
765    }
766
767    return async () => {
768        const toCopy = await glob(`${localBaseline}/**`, { nodir: true, ignore: `${localBaseline}/**/*.delete` });
769        for (const p of toCopy) {
770            const out = localPathToRefPath(p);
771            await fs.promises.mkdir(path.dirname(out), { recursive: true });
772            await fs.promises.copyFile(p, out);
773        }
774        const toDelete = await glob(`${localBaseline}/**/*.delete`, { nodir: true });
775        for (const p of toDelete) {
776            const out = localPathToRefPath(p);
777            await fs.promises.rm(out);
778        }
779    };
780}
781
782export const baselineAccept = task({
783    name: "baseline-accept",
784    description: "Makes the most recent test results the new baseline, overwriting the old baseline",
785    run: baselineAcceptTask(localBaseline, refBaseline),
786});
787
788export const baselineAcceptRwc = task({
789    name: "baseline-accept-rwc",
790    description: "Makes the most recent rwc test results the new baseline, overwriting the old baseline",
791    run: baselineAcceptTask(localRwcBaseline, refRwcBaseline),
792});
793
794// TODO(rbuckton): Determine if we still need this task. Depending on a relative
795//                 path here seems like a bad idea.
796export const updateSublime = task({
797    name: "update-sublime",
798    description: "Updates the sublime plugin's tsserver",
799    dependencies: [tsserver],
800    run: async () => {
801        for (const file of ["built/local/tsserver.js", "built/local/tsserver.js.map"]) {
802            await fs.promises.copyFile(file, path.resolve("../TypeScript-Sublime-Plugin/tsserver/", path.basename(file)));
803        }
804    }
805});
806
807// TODO(rbuckton): Should the path to DefinitelyTyped be configurable via an environment variable?
808export const importDefinitelyTypedTests = task({
809    name: "importDefinitelyTypedTests",
810    description: "Runs the importDefinitelyTypedTests script to copy DT's tests to the TS-internal RWC tests",
811    run: () => exec(process.execPath, ["scripts/importDefinitelyTypedTests.mjs", "./", "../DefinitelyTyped"]),
812});
813
814
815export const produceLKG = task({
816    name: "LKG",
817    description: "Makes a new LKG out of the built js files",
818    dependencies: [localize, tsc, tsserver, services, lssl, otherOutputs, dts],
819    run: async () => {
820        if (!cmdLineOptions.bundle) {
821            throw new Error("LKG cannot be created when --bundle=false");
822        }
823
824        const expectedFiles = [
825            "built/local/cancellationToken.js",
826            "built/local/tsc.js",
827            "built/local/tsserver.js",
828            "built/local/tsserverlibrary.js",
829            "built/local/tsserverlibrary.d.ts",
830            "built/local/typescript.js",
831            "built/local/typescript.d.ts",
832            "built/local/typingsInstaller.js",
833            "built/local/watchGuard.js",
834        ].concat(libs().map(lib => lib.target));
835        const missingFiles = expectedFiles
836            .concat(localizationTargets)
837            .filter(f => !fs.existsSync(f));
838        if (missingFiles.length > 0) {
839            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"));
840        }
841        const sizeBefore = getDirSize("lib");
842        await exec(process.execPath, ["scripts/produceLKG.mjs"]);
843        const sizeAfter = getDirSize("lib");
844        if (sizeAfter > (sizeBefore * 1.10)) {
845            throw new Error("The lib folder increased by 10% or more. This likely indicates a bug.");
846        }
847    }
848});
849
850export const lkg = task({
851    name: "lkg",
852    hiddenFromTaskList: true,
853    dependencies: [produceLKG],
854});
855
856export const generateSpec = task({
857    name: "generate-spec",
858    description: "Generates a Markdown version of the Language Specification",
859    hiddenFromTaskList: true,
860    run: () => exec("cscript", ["//nologo", "scripts/word2md.mjs", path.resolve("doc/TypeScript Language Specification - ARCHIVED.docx"), path.resolve("doc/spec-ARCHIVED.md")]),
861});
862
863export const cleanBuilt = task({
864    name: "clean-built",
865    hiddenFromTaskList: true,
866    run: () => del("built"),
867});
868
869export const clean = task({
870    name: "clean",
871    description: "Cleans build outputs",
872    dependencies: [cleanBuilt, cleanDiagnostics],
873});
874
875export const configureNightly = task({
876    name: "configure-nightly",
877    description: "Runs scripts/configurePrerelease.mjs to prepare a build for nightly publishing",
878    run: () => exec(process.execPath, ["scripts/configurePrerelease.mjs", "dev", "package.json", "src/compiler/corePublic.ts"]),
879});
880
881export const configureInsiders = task({
882    name: "configure-insiders",
883    description: "Runs scripts/configurePrerelease.mjs to prepare a build for insiders publishing",
884    run: () => exec(process.execPath, ["scripts/configurePrerelease.mjs", "insiders", "package.json", "src/compiler/corePublic.ts"]),
885});
886
887export const configureExperimental = task({
888    name: "configure-experimental",
889    description: "Runs scripts/configurePrerelease.mjs to prepare a build for experimental publishing",
890    run: () => exec(process.execPath, ["scripts/configurePrerelease.mjs", "experimental", "package.json", "src/compiler/corePublic.ts"]),
891});
892
893export const help = task({
894    name: "help",
895    description: "Prints the top-level tasks.",
896    hiddenFromTaskList: true,
897    run: () => exec("hereby", ["--tasks"], { hidePrompt: true }),
898});
899