From b9633deac016a22a11e2f43a8c50cde7767d71f5 Mon Sep 17 00:00:00 2001 From: Dan Muller Date: Sat, 23 Jan 2021 22:33:42 +0000 Subject: [PATCH] feat(typescript): create a better ts_project worker --- examples/react_webpack/BUILD.bazel | 3 + examples/react_webpack/package.json | 3 +- examples/react_webpack/tsconfig.json | 7 +- examples/react_webpack/yarn.lock | 29 +++- packages/typescript/BUILD.bazel | 2 +- packages/typescript/internal/ts_project.bzl | 45 ++--- .../typescript/internal/worker/BUILD.bazel | 27 +-- packages/typescript/internal/worker/index.js | 95 ++++++++++ .../internal/worker/worker_adapter.js | 99 ----------- .../internal/worker/worker_adapter.ts | 164 ++++++++++++++++++ packages/typescript/replacements.bzl | 5 +- .../test/ts_project/worker/BUILD.bazel | 1 - .../typescript/test/ts_project/worker/big.ts | 2 +- 13 files changed, 333 insertions(+), 149 deletions(-) create mode 100644 packages/typescript/internal/worker/index.js delete mode 100644 packages/typescript/internal/worker/worker_adapter.js create mode 100644 packages/typescript/internal/worker/worker_adapter.ts diff --git a/examples/react_webpack/BUILD.bazel b/examples/react_webpack/BUILD.bazel index 25fcecc6d7..ac291d38ce 100644 --- a/examples/react_webpack/BUILD.bazel +++ b/examples/react_webpack/BUILD.bazel @@ -16,6 +16,9 @@ sass( ts_project( # Experimental: Start a tsc daemon to watch for changes to make recompiles faster. supports_workers = True, + # Show an example of how to run the daemon with a custom tsc. + typescript_package = "@npm//ttypescript", + typescript_require_path = "ttypescript", deps = [ "@npm//@types", "@npm//csstype", diff --git a/examples/react_webpack/package.json b/examples/react_webpack/package.json index 1baa594b8a..66a74fb42b 100644 --- a/examples/react_webpack/package.json +++ b/examples/react_webpack/package.json @@ -16,7 +16,8 @@ "typescript": "^3.6.3", "webpack": "^4.41.0", "webpack-cli": "^3.3.9", - "webpack-dev-server": "^3.8.2" + "webpack-dev-server": "^3.8.2", + "ttypescript": "^1.5.12" }, "scripts": { "build": "bazel build //...", diff --git a/examples/react_webpack/tsconfig.json b/examples/react_webpack/tsconfig.json index 7ea5cf1299..6fb171bace 100644 --- a/examples/react_webpack/tsconfig.json +++ b/examples/react_webpack/tsconfig.json @@ -4,6 +4,11 @@ "lib": [ "ES2015", "DOM" + ], + "plugins": [ + { + "transform": "transformer-module" + }, ] }, // When using ts_project in worker mode, we run outside the Bazel sandbox (unless using --worker_sandboxing). @@ -12,4 +17,4 @@ "*.tsx", "*.ts" ] -} +} \ No newline at end of file diff --git a/examples/react_webpack/yarn.lock b/examples/react_webpack/yarn.lock index d776d65f1d..71ded5d83f 100644 --- a/examples/react_webpack/yarn.lock +++ b/examples/react_webpack/yarn.lock @@ -1807,7 +1807,7 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.1: +has@^1.0.1, has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -2109,6 +2109,13 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-core-module@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" + integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -3032,6 +3039,11 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -3462,6 +3474,14 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= +resolve@>=1.9.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -4080,6 +4100,13 @@ tty-browserify@0.0.0: resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= +ttypescript@^1.5.12: + version "1.5.12" + resolved "https://registry.yarnpkg.com/ttypescript/-/ttypescript-1.5.12.tgz#27a8356d7d4e719d0075a8feb4df14b52384f044" + integrity sha512-1ojRyJvpnmgN9kIHmUnQPlEV1gq+VVsxVYjk/NfvMlHSmYxjK5hEvOOU2MQASrbekTUiUM7pR/nXeCc8bzvMOQ== + dependencies: + resolve ">=1.9.0" + type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" diff --git a/packages/typescript/BUILD.bazel b/packages/typescript/BUILD.bazel index bb851b493e..64c07e41c7 100644 --- a/packages/typescript/BUILD.bazel +++ b/packages/typescript/BUILD.bazel @@ -96,7 +96,7 @@ pkg_npm( ":npm_version_check", "//packages/typescript/internal:BUILD", "//packages/typescript/internal:ts_project_options_validator.js", - "//packages/typescript/internal/worker", + "//packages/typescript/internal/worker:filegroup", ] + select({ # FIXME: fix stardoc on Windows; //packages/typescript:index.md generation fails with: # ERROR: D:/b/62unjjin/external/npm_bazel_typescript/BUILD.bazel:36:1: Couldn't build file diff --git a/packages/typescript/internal/ts_project.bzl b/packages/typescript/internal/ts_project.bzl index ee49db42da..1eb2e14bc0 100644 --- a/packages/typescript/internal/ts_project.bzl +++ b/packages/typescript/internal/ts_project.bzl @@ -14,14 +14,7 @@ _DEFAULT_TSC = ( "//typescript/bin:tsc" ) -_DEFAULT_TSC_BIN = ( - # BEGIN-INTERNAL - "@npm" + - # END-INTERNAL - "//:node_modules/typescript/bin/tsc" -) - -_DEFAULT_TYPESCRIPT_MODULE = ( +_DEFAULT_TYPESCRIPT_PACKAGE = ( # BEGIN-INTERNAL "@npm" + # END-INTERNAL @@ -49,7 +42,7 @@ _ATTRS = { # that compiler might allow more sources than tsc does. "srcs": attr.label_list(allow_files = True, mandatory = True), "supports_workers": attr.bool(default = False), - "tsc": attr.label(default = Label(_DEFAULT_TSC), executable = True, cfg = "target"), + "tsc": attr.label(default = Label(_DEFAULT_TSC), executable = True, cfg = "host"), "tsconfig": attr.label(mandatory = True, allow_single_file = [".json"]), } @@ -209,6 +202,7 @@ def _ts_project_impl(ctx): inputs = inputs, arguments = [arguments], outputs = outputs, + mnemonic = "TsProject", executable = "tsc", execution_requirements = execution_requirements, progress_message = "%s %s [tsc -p %s]" % ( @@ -337,8 +331,8 @@ def ts_project_macro( emit_declaration_only = False, ts_build_info_file = None, tsc = None, - worker_tsc_bin = _DEFAULT_TSC_BIN, - worker_typescript_module = _DEFAULT_TYPESCRIPT_MODULE, + typescript_package = _DEFAULT_TYPESCRIPT_PACKAGE, + typescript_require_path = "typescript", validate = True, supports_workers = False, declaration_dir = None, @@ -506,14 +500,13 @@ def ts_project_macro( For example, `tsc = "@my_deps//typescript/bin:tsc"` Or you can pass a custom compiler binary instead. - worker_tsc_bin: Label of the TypeScript compiler binary to run when running in worker mode. + typescript_package: Label of the package containing all data deps of tsc. - For example, `tsc = "@my_deps//node_modules/typescript/bin/tsc"` - Or you can pass a custom compiler binary instead. + For example, `typescript_package = "@my_deps//typescript"` - worker_typescript_module: Label of the package containing all data deps of worker_tsc_bin. + typescript_require_path: Module name which resolves to typescript_package when required - For example, `tsc = "@my_deps//typescript"` + For example, `typescript_require_path = "typescript"` validate: boolean; whether to check that the tsconfig settings match the attributes. @@ -642,25 +635,18 @@ def ts_project_macro( # but that's our own code, so we don't. "@npm//protobufjs", # END-INTERNAL - Label("//packages/typescript/internal/worker:worker"), - Label(worker_tsc_bin), - Label(worker_typescript_module), + Label(typescript_package), + Label("//packages/typescript/internal/worker:filegroup"), tsconfig, ], entry_point = Label("//packages/typescript/internal/worker:worker_adapter"), templated_args = [ - "$(execpath {})".format(Label(worker_tsc_bin)), - "--project", - "$(execpath {})".format(tsconfig), - # FIXME: should take out_dir into account - "--outDir", - "$(RULEDIR)", - # FIXME: what about other settings like declaration_dir, root_dir, etc + "--typescript_require_path", + typescript_require_path, ], ) tsc = ":" + tsc_worker - typings_out_dir = declaration_dir if declaration_dir else out_dir tsbuildinfo_path = ts_build_info_file if ts_build_info_file else name + ".tsbuildinfo" js_outs = [] @@ -701,9 +687,6 @@ Check the srcs attribute to see that some .ts files are present (or .js files wi buildinfo_out = tsbuildinfo_path if composite or incremental else None, tsc = tsc, link_workspace_root = link_workspace_root, - supports_workers = select({ - "@bazel_tools//src/conditions:host_windows": False, - "//conditions:default": supports_workers, - }), + supports_workers = supports_workers, **kwargs ) diff --git a/packages/typescript/internal/worker/BUILD.bazel b/packages/typescript/internal/worker/BUILD.bazel index f6593042a2..ece8edbef1 100644 --- a/packages/typescript/internal/worker/BUILD.bazel +++ b/packages/typescript/internal/worker/BUILD.bazel @@ -1,8 +1,18 @@ # BEGIN-INTERNAL - -load("//internal/common:copy_to_bin.bzl", "copy_to_bin") +load("//packages/typescript:checked_in_ts_project.bzl", "checked_in_ts_project") load("//third_party/github.com/bazelbuild/bazel-skylib:rules/copy_file.bzl", "copy_file") +# To update index.js run: +# bazel run //packages/typescript/internal/worker:worker_adapter_check_compiled.update + +checked_in_ts_project( + name = "worker_adapter", + src = "worker_adapter.ts", + checked_in_js = "index.js", + visibility = ["//visibility:public"], + deps = ["@npm//@types/node"], +) + # Copy the proto file to a matching third_party/... nested directory # so the runtime require() statements still work _worker_proto_dir = "third_party/github.com/bazelbuild/bazel/src/main/protobuf" @@ -22,14 +32,6 @@ copy_file( visibility = ["//visibility:public"], ) -copy_to_bin( - name = "worker_adapter", - srcs = [ - "worker_adapter.js", - ], - visibility = ["//visibility:public"], -) - filegroup( name = "package_contents", srcs = [ @@ -41,12 +43,13 @@ filegroup( # END-INTERNAL exports_files([ - "worker_adapter.js", + "index.js", ]) filegroup( - name = "worker", + name = "filegroup", srcs = [ + "index.js", "third_party/github.com/bazelbuild/bazel/src/main/protobuf/worker_protocol.proto", "worker.js", "worker_adapter.js", diff --git a/packages/typescript/internal/worker/index.js b/packages/typescript/internal/worker/index.js new file mode 100644 index 0000000000..c139082dce --- /dev/null +++ b/packages/typescript/internal/worker/index.js @@ -0,0 +1,95 @@ +/* THIS FILE GENERATED FROM .ts; see BUILD.bazel */ /* clang-format off */"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const ts = require("typescript"); +const MNEMONIC = "TsProject"; +const worker = require("./worker"); +let createWatchCompilerHost; +const formatHost = { + getCanonicalFileName: (path) => path, + getCurrentDirectory: ts.sys.getCurrentDirectory, + getNewLine: () => ts.sys.newLine, +}; +const reportDiagnostic = (diagnostic) => { + worker.log(ts.formatDiagnostic(diagnostic, formatHost)); +}; +const reportWatchStatusChanged = (diagnostic) => { + worker.debug(ts.formatDiagnostic(diagnostic, formatHost)); +}; +function createWatchProgram(options, tsconfigPath, setTimeout) { + const host = createWatchCompilerHost(tsconfigPath, options, Object.assign(Object.assign({}, ts.sys), { setTimeout }), ts.createEmitAndSemanticDiagnosticsBuilderProgram, reportDiagnostic, reportWatchStatusChanged); + return ts.createWatchProgram(host); +} +let workerRequestTimestamp; +let cachedWatchedProgram; +let consolidateChangesCallback; +let cachedWatchProgramArgs; +function getWatchProgram(args) { + const newWatchArgs = args.join(" "); + if (cachedWatchedProgram && + cachedWatchProgramArgs && + cachedWatchProgramArgs !== newWatchArgs) { + cachedWatchedProgram.close(); + cachedWatchedProgram = undefined; + cachedWatchProgramArgs = undefined; + } + if (!cachedWatchedProgram) { + const parsedArgs = ts.parseCommandLine(args); + const tsconfigPath = args[args.indexOf("--project") + 1]; + cachedWatchProgramArgs = newWatchArgs; + cachedWatchedProgram = createWatchProgram(parsedArgs.options, tsconfigPath, (callback) => { + consolidateChangesCallback = callback; + }); + } + return cachedWatchedProgram; +} +function emitOnce(args) { + const watchProgram = getWatchProgram(args); + if (consolidateChangesCallback) { + consolidateChangesCallback(); + } + return new Promise((res) => { + var _a; + workerRequestTimestamp = Date.now(); + const result = (_a = watchProgram) === null || _a === void 0 ? void 0 : _a.getProgram().emit(undefined, undefined, { + isCancellationRequested: function (timestamp) { + return timestamp !== workerRequestTimestamp; + }.bind(null, workerRequestTimestamp), + throwIfCancellationRequested: function (timestamp) { + if (timestamp !== workerRequestTimestamp) { + throw new ts.OperationCanceledException(); + } + }.bind(null, workerRequestTimestamp), + }); + res(result && result.diagnostics.length === 0); + }); +} +function main() { + const typescriptRequirePath = process.argv[process.argv.indexOf('--typescript_require_path') + 1]; + try { + const customTypescriptModule = require(typescriptRequirePath); + createWatchCompilerHost = customTypescriptModule.createWatchCompilerHost; + } + catch (e) { + worker.log(`typescript_require_path '${typescriptRequirePath}' could not be resolved`); + throw e; + } + if (process.argv.includes("--persistent_worker")) { + worker.log(`Running ${MNEMONIC} as a Bazel worker`); + worker.runWorkerLoop(emitOnce); + } + else { + worker.log(`Running ${MNEMONIC} as a standalone process`); + worker.log(`Started a new process to perform this action. Your build might be misconfigured, try + --strategy=${MNEMONIC}=worker`); + let argsFilePath = process.argv.pop(); + if (argsFilePath.startsWith('@')) { + argsFilePath = argsFilePath.slice(1); + } + const args = fs.readFileSync(argsFilePath).toString().split('\n'); + emitOnce(args).finally(() => { var _a; return (_a = cachedWatchedProgram) === null || _a === void 0 ? void 0 : _a.close(); }); + } +} +if (require.main === module) { + main(); +} diff --git a/packages/typescript/internal/worker/worker_adapter.js b/packages/typescript/internal/worker/worker_adapter.js deleted file mode 100644 index 38240228b7..0000000000 --- a/packages/typescript/internal/worker/worker_adapter.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @fileoverview wrapper program around the TypeScript compiler, tsc - * - * It intercepts the Bazel Persistent Worker protocol, using it to remote-control tsc running as a - * child process. In between builds, the tsc process is stopped (akin to ctrl-z in a shell) and then - * resumed (akin to `fg`) when the inputs have changed. - * - * See https://medium.com/@mmorearty/how-to-create-a-persistent-worker-for-bazel-7738bba2cabb - * for more background (note, that is documenting a different implementation) - */ -const child_process = require('child_process'); -const MNEMONIC = 'TsProject'; -const worker = require('./worker'); - -const workerArg = process.argv.indexOf('--persistent_worker') -if (workerArg > 0) { - process.argv.splice(workerArg, 1, '--watch') - - if (process.platform !== 'linux' && process.platform !== 'darwin') { - throw new Error(`Worker mode is only supported on linux and darwin, not ${process.platform}. - See https://github.com/bazelbuild/rules_nodejs/issues/2277`); - } -} - -const [tscBin, ...tscArgs] = process.argv.slice(2); - -const child = child_process.spawn( - tscBin, - tscArgs, - {stdio: 'pipe'}, -); -function awaitOneBuild() { - child.kill('SIGCONT') - - let buffer = []; - return new Promise((res) => { - function awaitBuild(s) { - buffer.push(s); - - if (s.includes('Watching for file changes.')) { - child.kill('SIGSTOP') - - const success = s.includes('Found 0 errors.'); - res(success); - - child.stdout.removeListener('data', awaitBuild); - - if (!success) { - console.error( - `\nError output from tsc worker:\n\n ${ - buffer.slice(1).map(s => s.toString()).join('').replace(/\n/g, '\n ')}`, - ) - } - - buffer = []; - } - }; - child.stdout.on('data', awaitBuild); - }); -} - -async function main() { - // Bazel will pass a special argument to the program when it's running us as a worker - if (workerArg > 0) { - worker.log(`Running ${MNEMONIC} as a Bazel worker`); - - worker.runWorkerLoop(awaitOneBuild); - } else { - // Running standalone so stdout is available as usual - console.log(`Running ${MNEMONIC} as a standalone process`); - console.error( - `Started a new process to perform this action. Your build might be misconfigured, try - --strategy=${MNEMONIC}=worker`); - - const stdoutbuffer = []; - child.stdout.on('data', data => stdoutbuffer.push(data)); - - const stderrbuffer = []; - child.stderr.on('data', data => stderrbuffer.push(data)); - - child.on('exit', code => { - if (code !== 0) { - console.error( - `\nstdout from tsc:\n\n ${ - stdoutbuffer.map(s => s.toString()).join('').replace(/\n/g, '\n ')}`, - ) - console.error( - `\nstderr from tsc:\n\n ${ - stderrbuffer.map(s => s.toString()).join('').replace(/\n/g, '\n ')}`, - ) - } - process.exit(code) - }); - } -} - -if (require.main === module) { - main(); -} diff --git a/packages/typescript/internal/worker/worker_adapter.ts b/packages/typescript/internal/worker/worker_adapter.ts new file mode 100644 index 0000000000..335110f7b1 --- /dev/null +++ b/packages/typescript/internal/worker/worker_adapter.ts @@ -0,0 +1,164 @@ +/** + * @fileoverview wrapper program around the TypeScript compiler, tsc + * + * It intercepts the Bazel Persistent Worker protocol, using it to + * remote-control tsc running in watch mode. It tells the compiler process to + * consolidate file changes only when it receives a request from the worker + * protocol. + * + * See https://medium.com/@mmorearty/how-to-create-a-persistent-worker-for-bazel-7738bba2cabb + * for more background on the worker protocol. + */ +import * as fs from 'fs'; +import * as ts from 'typescript'; + +const MNEMONIC = "TsProject"; +const worker = require("./worker"); + +let createWatchCompilerHost: typeof ts.createWatchCompilerHost; + +const formatHost: ts.FormatDiagnosticsHost = { + getCanonicalFileName: (path) => path, + getCurrentDirectory: ts.sys.getCurrentDirectory, + getNewLine: () => ts.sys.newLine, +}; + +/** + * Prints a diagnostic result for every compiler error or warning. + */ +const reportDiagnostic: ts.DiagnosticReporter = (diagnostic) => { + worker.log(ts.formatDiagnostic(diagnostic, formatHost)); +}; + +/** + * Prints a diagnostic every time the watch status changes. + * This is mainly for messages like "Starting compilation" or "Compilation completed". + */ +const reportWatchStatusChanged: ts.WatchStatusReporter = (diagnostic) => { + worker.debug(ts.formatDiagnostic(diagnostic, formatHost)); +}; + +function createWatchProgram( + options: ts.CompilerOptions, + tsconfigPath: string, + setTimeout: ts.System["setTimeout"] +) { + const host = createWatchCompilerHost( + tsconfigPath, options, {...ts.sys, setTimeout}, + ts.createEmitAndSemanticDiagnosticsBuilderProgram, reportDiagnostic, + reportWatchStatusChanged); + + // `createWatchProgram` creates an initial program, watches files, and updates + // the program over time. + return ts.createWatchProgram(host); +} + +/** + * Timestamp of the last worker request. + */ +let workerRequestTimestamp: number | undefined; +/** + * The typescript compiler in watch mode. + */ +let cachedWatchedProgram: + | ts.WatchOfConfigFile + | undefined; +/** + * Callback provided by ts.System which should be called at the point at which + * file system changes should be consolidated into a new emission from the + * watcher. + */ +let consolidateChangesCallback: ((...args: any[]) => void) | undefined; +let cachedWatchProgramArgs: string | undefined; + +function getWatchProgram( + args: string[] +): ts.WatchOfConfigFile { + const newWatchArgs = args.join(" "); + + // Check to see if the watch program needs to be updated or if we can re-use the old one. + if ( + cachedWatchedProgram && + cachedWatchProgramArgs && + cachedWatchProgramArgs !== newWatchArgs + ) { + cachedWatchedProgram.close(); + cachedWatchedProgram = undefined; + cachedWatchProgramArgs = undefined; + } + + // If we have not yet created a watch + if (!cachedWatchedProgram) { + const parsedArgs = ts.parseCommandLine(args); + const tsconfigPath = args[args.indexOf("--project") + 1]; + + cachedWatchProgramArgs = newWatchArgs; + cachedWatchedProgram = createWatchProgram( + parsedArgs.options, + tsconfigPath, + (callback) => { + consolidateChangesCallback = callback; + } + ); + } + + return cachedWatchedProgram; +} + +function emitOnce(args: string[]) { + const watchProgram = getWatchProgram(args); + + if (consolidateChangesCallback) { + consolidateChangesCallback(); + } + + return new Promise((res) => { + workerRequestTimestamp = Date.now(); + const result = watchProgram?.getProgram().emit(undefined, undefined, { + isCancellationRequested: function(timestamp: number) { + return timestamp !== workerRequestTimestamp; + }.bind(null, workerRequestTimestamp), + throwIfCancellationRequested: function(timestamp: number) { + if (timestamp !== workerRequestTimestamp) { + throw new ts.OperationCanceledException(); + } + }.bind(null, workerRequestTimestamp), + }); + + res(result && result.diagnostics.length === 0); + }); +} + +function main() { + const typescriptRequirePath = process.argv[process.argv.indexOf('--typescript_require_path') + 1]; + try { + const customTypescriptModule = require(typescriptRequirePath); + createWatchCompilerHost = customTypescriptModule.createWatchCompilerHost; + } catch (e) { + worker.log(`typescript_require_path '${typescriptRequirePath}' could not be resolved`) + throw e; + } + if (process.argv.includes("--persistent_worker")) { + worker.log(`Running ${MNEMONIC} as a Bazel worker`); + worker.runWorkerLoop(emitOnce); + } else { + worker.log(`Running ${MNEMONIC} as a standalone process`); + worker.log( + `Started a new process to perform this action. Your build might be misconfigured, try + --strategy=${MNEMONIC}=worker` + ); + + let argsFilePath = process.argv.pop()!; + if (argsFilePath.startsWith('@')) { + argsFilePath = argsFilePath.slice(1) + } + const args = fs.readFileSync(argsFilePath).toString().split('\n'); + emitOnce(args).finally(() => + cachedWatchedProgram?.close() + ); + } +} + +if (require.main === module) { + main(); +} diff --git a/packages/typescript/replacements.bzl b/packages/typescript/replacements.bzl index 5665ffe7a7..aa4d1899d2 100644 --- a/packages/typescript/replacements.bzl +++ b/packages/typescript/replacements.bzl @@ -24,7 +24,10 @@ TYPESCRIPT_REPLACEMENTS = dict( # @build_bazel_rules_typescript//:npm_bazel_typescript_package # use this alternate fencing "(#|\\/\\/)\\s+BEGIN-DEV-ONLY[\\w\\W]+?(#|\\/\\/)\\s+END-DEV-ONLY": "", - "//packages/typescript/internal/worker:worker_adapter": "//@bazel/typescript/internal/worker:worker_adapter.js", + # Replace the worker filegroup with the entire @bazel/typescript node_module and its transitive node_modules + "//packages/typescript/internal/worker:filegroup": "//@bazel/typescript", + # Change the worker entry point from the checked_in_ts_project target to the checked in .js + "//packages/typescript/internal/worker:worker_adapter": "//@bazel/typescript/internal/worker:index.js", # This file gets vendored into our repo "@build_bazel_rules_typescript//internal:common": "//@bazel/typescript/internal:common", # Replace the local compiler label with one that comes from npm diff --git a/packages/typescript/test/ts_project/worker/BUILD.bazel b/packages/typescript/test/ts_project/worker/BUILD.bazel index 73924e9036..1abe48d0de 100644 --- a/packages/typescript/test/ts_project/worker/BUILD.bazel +++ b/packages/typescript/test/ts_project/worker/BUILD.bazel @@ -2,7 +2,6 @@ load("//packages/typescript:index.bzl", "ts_project") ts_project( supports_workers = True, - tags = ["fix-windows"], tsconfig = { "compilerOptions": { "declaration": True, diff --git a/packages/typescript/test/ts_project/worker/big.ts b/packages/typescript/test/ts_project/worker/big.ts index 124564061e..6d90a44168 100644 --- a/packages/typescript/test/ts_project/worker/big.ts +++ b/packages/typescript/test/ts_project/worker/big.ts @@ -1,3 +1,3 @@ // TODO: make it big enough to slow down tsc -export const a: number = 2; +export const a: number = 2350;