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