diff --git a/Herebyfile.mjs b/Herebyfile.mjs index fd5e48752b568..8e68f5d282a82 100644 --- a/Herebyfile.mjs +++ b/Herebyfile.mjs @@ -393,17 +393,39 @@ function entrypointBuildTask(options) { return { build, bundle, shim, main, watch }; } -const { main: tsc, watch: watchTsc } = entrypointBuildTask({ - name: "tsc", +const { main: tscReal, watch: watchTscReal } = entrypointBuildTask({ + name: "tscReal", description: "Builds the command-line compiler", buildDeps: [generateDiagnostics], project: "src/tsc", srcEntrypoint: "./src/tsc/tsc.ts", builtEntrypoint: "./built/local/tsc/tsc.js", + output: "./built/local/tscReal.js", + mainDeps: [generateLibs], +}); + +const { main: tscSnapshot, watch: watchTscSnapshot } = entrypointBuildTask({ + name: "tscSnapshot", + description: "Builds the command-line compiler", + buildDeps: [generateDiagnostics], + project: "src/tsc", + srcEntrypoint: "./src/tscSnapshot/tsc.ts", + builtEntrypoint: "./built/local/tscSnapshot/tsc.js", output: "./built/local/tsc.js", mainDeps: [generateLibs], }); -export { tsc, watchTsc }; + +export const tsc = task({ + name: "tsc", + description: "Builds the command-line compiler", + dependencies: [tscReal, tscSnapshot], +}); + +export const watchTsc = task({ + name: "watchTsc", + description: "Builds the command-line compiler", + dependencies: [watchTscReal, watchTscSnapshot], +}); const { main: services, build: buildServices, watch: watchServices } = entrypointBuildTask({ name: "services", diff --git a/scripts/produceLKG.mjs b/scripts/produceLKG.mjs index 47630c43db5f8..751693c7ad2b4 100644 --- a/scripts/produceLKG.mjs +++ b/scripts/produceLKG.mjs @@ -54,6 +54,7 @@ async function copyTypesMap() { async function copyScriptOutputs() { await copyFromBuiltLocal("cancellationToken.js"); await copyFromBuiltLocal("tsc.js"); + await copyFromBuiltLocal("tscReal.js"); await copyFromBuiltLocal("tsserver.js"); await copyFromBuiltLocal("tsserverlibrary.js"); await copyFromBuiltLocal("typescript.js"); diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 281622d004c77..ea760e613bd53 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -1469,8 +1469,8 @@ interface DirectoryWatcher extends FileWatcher { referenceCount: number; } -// TODO: GH#18217 this is used as if it's certainly defined in many places. -export let sys: System = (() => { +/** @internal */ +export function createSystem(): System { // NodeJS detects "\uFEFF" at the start of the string and *replaces* it with the actual // byte order mark from the specified encoding. Using any other byte order mark does // not actually work. @@ -2026,21 +2026,26 @@ export let sys: System = (() => { patchWriteFileEnsuringDirectory(sys); } return sys!; -})(); +} + +// TODO: GH#18217 this is used as if it's certainly defined in many places. +export let sys: System; /** @internal */ export function setSys(s: System) { sys = s; -} -if (sys && sys.getEnvironmentVariable) { - setCustomPollingValues(sys); - Debug.setAssertionLevel( - /^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) - ? AssertionLevel.Normal - : AssertionLevel.None, - ); -} -if (sys && sys.debugMode) { - Debug.isDebugging = true; + if (sys && sys.getEnvironmentVariable) { + setCustomPollingValues(sys); + Debug.setAssertionLevel( + /^development$/i.test(sys.getEnvironmentVariable("NODE_ENV")) + ? AssertionLevel.Normal + : AssertionLevel.None, + ); + } + if (sys && sys.debugMode) { + Debug.isDebugging = true; + } } + +setSys(createSystem()); diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index d7c3c42a01f61..9fa8b4a4568ea 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -9,16 +9,47 @@ ts.Debug.loggingHost = { }, }; -if (ts.Debug.isDebugging) { - ts.Debug.enableDebugInfo(); +interface V8 { + startupSnapshot?: { + isBuildingSnapshot(): boolean; + setDeserializeMainFunction(fn: () => void): void; + }; } -if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) { - ts.sys.tryEnableSourceMapsForHost(); +let v8: V8 | undefined; +try { + if (!process.versions.bun) { + v8 = require("v8"); + } } +catch { + // do nothing +} + +function main() { + if (ts.Debug.isDebugging) { + ts.Debug.enableDebugInfo(); + } + + if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) { + ts.sys.tryEnableSourceMapsForHost(); + } -if (ts.sys.setBlocking) { - ts.sys.setBlocking(); + if (ts.sys.setBlocking) { + ts.sys.setBlocking(); + } + + ts.executeCommandLine(ts.sys, ts.noop, ts.sys.args); } -ts.executeCommandLine(ts.sys, ts.noop, ts.sys.args); +if (v8?.startupSnapshot?.isBuildingSnapshot()) { + v8.startupSnapshot.setDeserializeMainFunction(() => { + // When we're executed as a snapshot, argv won't contain the js file anymore. + process.argv.splice(1, 0, __filename); + ts.setSys(ts.createSystem()); + main(); + }); +} +else { + main(); +} diff --git a/src/tsc/tsconfig.json b/src/tsc/tsconfig.json index 21e193ad5d70c..46f22c372f8ed 100644 --- a/src/tsc/tsconfig.json +++ b/src/tsc/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig-base", "compilerOptions": { + "types": ["node"] }, "references": [ { "path": "../compiler" }, diff --git a/src/tscSnapshot/tsc.ts b/src/tscSnapshot/tsc.ts new file mode 100644 index 0000000000000..b805594cfa2f9 --- /dev/null +++ b/src/tscSnapshot/tsc.ts @@ -0,0 +1,77 @@ +import fs = require("fs"); +import path = require("path"); +import cp = require("child_process"); + +let v8: typeof import("v8") | undefined; +try { + if (!process.versions.bun) { + v8 = require("v8"); + } +} +catch { + // do nothing +} + +const exe = path.join(__dirname, "tscReal.js"); + +if (!(v8 as any)?.startupSnapshot) { + require(exe); + throw new Error("unreachable"); +} + +const args = process.argv.slice(2); + +const doBuildSnapshot = process.env.TYPESCRIPT_BUILD_SNAPSHOT === "true"; + +function checksumFile(path: string) { + const crypto = require("crypto") as typeof import("crypto"); + // Benchmarking shows that sha1 is the fastest hash. + // It is theoretically insecure, but we're just using it to detect file mismatches. + // TODO(jakebailey): If sha1 is ever removed, this will fail; we should try catch + // and fall back to something from crypto.getHashes() if it does. + const hash = crypto.createHash("sha1"); + const file = fs.readFileSync(path); + hash.update(file); + return hash.digest("hex"); +} + +const exeHash = checksumFile(exe); +const blobName = `${exe}.${process.version}.${exeHash}.blob`; + +if (doBuildSnapshot) { + // Build and atomic rename. + const tmpName = `${blobName}.${process.pid}.tmp`; + cp.execFileSync( + process.execPath, + ["--snapshot-blob", tmpName, "--build-snapshot", exe], + { stdio: "ignore" }, + ); + try { + fs.renameSync(tmpName, blobName); + } + catch { + // If the rename fails, it's because another process beat us to it. + } + process.exit(0); +} + +if (!fs.existsSync(blobName)) { + cp.spawn( + process.execPath, + [__filename], + { + detached: true, + stdio: "ignore", + env: { ...process.env, TYPESCRIPT_BUILD_SNAPSHOT: "true" }, + }, + ).unref(); + require(exe); + throw new Error("unreachable"); +} + +try { + cp.execFileSync(process.execPath, ["--snapshot-blob", blobName, "--", ...args], { stdio: "inherit" }); +} +catch (e) { + process.exitCode = e.status; +} diff --git a/src/tscSnapshot/tsconfig.json b/src/tscSnapshot/tsconfig.json new file mode 100644 index 0000000000000..e39519532fd80 --- /dev/null +++ b/src/tscSnapshot/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig-base", + "compilerOptions": { + "types": ["node"] + }, + "references": [], + "include": ["**/*"] +}