| author | |
| committer | |
| log | 9375c779707fb95c06670db6da87441720b2d2e2 |
| tree | 3864ba37529e2e094f3118fc041a580c365a88ec |
| parent | 6f00d3881a197dd33c2cb43c6a11a9e759e56089 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 227 insertions(+), 9 deletions(-)
apps/server/src/cloud/pinnedRuntime.test.ts+48| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { assert, it } from "@effect/vitest"; | 3 | import { assert, it } from "@effect/vitest"; |
| 3 | import * as Deferred from "effect/Deferred"; | 4 | import * as Deferred from "effect/Deferred"; |
| ... | @@ -94,6 +95,19 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { | ... | @@ -94,6 +95,19 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { |
| 94 | assert.deepEqual(commands, ["tar"]); | 95 | assert.deepEqual(commands, ["tar"]); |
| 95 | assert.equal(yield* fs.readFileString(paths.sentinelPath), `${version}\n`); | 96 | assert.equal(yield* fs.readFileString(paths.sentinelPath), `${version}\n`); |
| 96 | assert.isFalse(yield* fs.exists(path.join(paths.versionDir, "t3-runtime-archive"))); | 97 | assert.isFalse(yield* fs.exists(path.join(paths.versionDir, "t3-runtime-archive"))); |
| 98 | if ((yield* HostProcessPlatform) !== "win32") { | ||
| 99 | // The old launcher must still be able to start this archive after the | ||
| 100 | // first npm-to-executable update, including from the final directory. | ||
| 101 | yield* fs.writeFileString(paths.entryPath, '#!/bin/sh\nprintf "%s\\n" "$@"\n'); | ||
| 102 | yield* fs.chmod(paths.entryPath, 0o755); | ||
| 103 | const runner = yield* ProcessRunner.make(); | ||
| 104 | const legacyStart = yield* runner.run({ | ||
| 105 | command: process.execPath, | ||
| 106 | args: [path.join(paths.versionDir, "node_modules/t3/dist/bin.mjs"), "serve"], | ||
| 107 | }); | ||
| 108 | assert.equal(Number(legacyStart.code), 0, legacyStart.stderr); | ||
| 109 | assert.equal(legacyStart.stdout.trim(), "serve"); | ||
| 110 | } | ||
| 97 | }), | 111 | }), |
| 98 | ); | 112 | ); |
| 99 | 113 | ||
| ... | @@ -209,6 +223,40 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { | ... | @@ -209,6 +223,40 @@ it.layer(NodeServices.layer)("ensurePinnedRuntimeInstalled", (it) => { |
| 209 | }), | 223 | }), |
| 210 | ); | 224 | ); |
| 211 | 225 | ||
| 226 | it.effect("backfills a cached archive without downloading or replacing it", () => | ||
| 227 | Effect.gen(function* () { | ||
| 228 | const fs = yield* FileSystem.FileSystem; | ||
| 229 | const path = yield* Path.Path; | ||
| 230 | const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pinned-legacy-cache-" }); | ||
| 231 | const cached = pinnedRuntimePaths(path, baseDir, version, "linux"); | ||
| 232 | const legacyEntry = path.join(cached.versionDir, "node_modules/t3/dist/bin.mjs"); | ||
| 233 | yield* fs.makeDirectory(cached.versionDir, { recursive: true }); | ||
| 234 | yield* fs.writeFileString(cached.entryPath, "cached executable\n"); | ||
| 235 | yield* fs.writeFileString(cached.sentinelPath, `${version}\n`); | ||
| 236 | const requests: string[] = []; | ||
| 237 | const commands: string[] = []; | ||
| 238 | yield* ensurePinnedRuntimeInstalled({ | ||
| 239 | baseDir, | ||
| 240 | version, | ||
| 241 | fs, | ||
| 242 | path, | ||
| 243 | platform: "linux", | ||
| 244 | arch: "x64", | ||
| 245 | httpClient: releaseHttpClient(yield* validChecksums, requests), | ||
| 246 | runner: extractingRunner(fs, path, commands), | ||
| 247 | validate: () => | ||
| 248 | fs.exists(legacyEntry).pipe( | ||
| 249 | Effect.flatMap((exists) => (exists ? Effect.void : Effect.die("missing legacy entry"))), | ||
| 250 | Effect.orDie, | ||
| 251 | ), | ||
| 252 | }); | ||
| 253 | assert.deepEqual(requests, []); | ||
| 254 | assert.deepEqual(commands, []); | ||
| 255 | assert.equal(yield* fs.readFileString(cached.entryPath), "cached executable\n"); | ||
| 256 | assert.equal(yield* fs.readFileString(cached.sentinelPath), `${version}\n`); | ||
| 257 | }), | ||
| 258 | ); | ||
| 259 | |||
| 212 | it.effect("preserves a completed runtime when validation fails", () => | 260 | it.effect("preserves a completed runtime when validation fails", () => |
| 213 | Effect.gen(function* () { | 261 | Effect.gen(function* () { |
| 214 | const fs = yield* FileSystem.FileSystem; | 262 | const fs = yield* FileSystem.FileSystem; |
apps/server/src/cloud/pinnedRuntime.ts+26-1| ... | @@ -8,6 +8,7 @@ import * as Option from "effect/Option"; | ... | @@ -8,6 +8,7 @@ import * as Option from "effect/Option"; |
| 8 | import * as Semaphore from "effect/Semaphore"; | 8 | import * as Semaphore from "effect/Semaphore"; |
| 9 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; | 9 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; |
| 10 | 10 | ||
| 11 | import { legacyCliLauncherScript } from "@t3tools/shared/legacyCliLauncher"; | ||
| 11 | import { | 12 | import { |
| 12 | CLI_RELEASE_CHECKSUMS_FILE, | 13 | CLI_RELEASE_CHECKSUMS_FILE, |
| 13 | cliArchiveFileName, | 14 | cliArchiveFileName, |
| ... | @@ -229,6 +230,24 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( | ... | @@ -229,6 +230,24 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( |
| 229 | input: PinnedRuntimeInstallInput, | 230 | input: PinnedRuntimeInstallInput, |
| 230 | ) { | 231 | ) { |
| 231 | const { fs } = input; | 232 | const { fs } = input; |
| 233 | // Old service launchers still use the npm entry point, including when an | ||
| 234 | // archive was cached before this compatibility wrapper existed. | ||
| 235 | const ensureLegacyEntry = Effect.fn("cloud.pinned_runtime.ensure_legacy_entry")( | ||
| 236 | function* (versionDir: string) { | ||
| 237 | const legacyDir = input.path.join(versionDir, "node_modules", "t3", "dist"); | ||
| 238 | const entryPath = input.path.join(legacyDir, "bin.mjs"); | ||
| 239 | if (yield* fs.exists(entryPath)) return; | ||
| 240 | yield* fs.makeDirectory(legacyDir, { recursive: true }); | ||
| 241 | yield* fs.writeFileString(entryPath, legacyCliLauncherScript("archive")); | ||
| 242 | }, | ||
| 243 | Effect.mapError( | ||
| 244 | (cause) => | ||
| 245 | new PinnedRuntimeInstallError({ | ||
| 246 | step: "writing the legacy service entry point", | ||
| 247 | cause, | ||
| 248 | }), | ||
| 249 | ), | ||
| 250 | ); | ||
| 232 | const paths = pinnedRuntimePaths(input.path, input.baseDir, input.version, input.platform); | 251 | const paths = pinnedRuntimePaths(input.path, input.baseDir, input.version, input.platform); |
| 233 | const [versionDirExists, entryExists, sentinel] = yield* Effect.all([ | 252 | const [versionDirExists, entryExists, sentinel] = yield* Effect.all([ |
| 234 | fs.exists(paths.versionDir), | 253 | fs.exists(paths.versionDir), |
| ... | @@ -242,6 +261,7 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( | ... | @@ -242,6 +261,7 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( |
| 242 | const alreadyPinned = | 261 | const alreadyPinned = |
| 243 | entryExists && Option.isSome(sentinel) && sentinel.value.trim() === input.version; | 262 | entryExists && Option.isSome(sentinel) && sentinel.value.trim() === input.version; |
| 244 | if (alreadyPinned) { | 263 | if (alreadyPinned) { |
| 264 | yield* ensureLegacyEntry(paths.versionDir); | ||
| 245 | yield* input.validate(paths); | 265 | yield* input.validate(paths); |
| 246 | return paths; | 266 | return paths; |
| 247 | } | 267 | } |
| ... | @@ -290,6 +310,8 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( | ... | @@ -290,6 +310,8 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( |
| 290 | return yield* Effect.gen(function* () { | 310 | return yield* Effect.gen(function* () { |
| 291 | yield* installFromArchive(input, stagingDir); | 311 | yield* installFromArchive(input, stagingDir); |
| 292 | 312 | ||
| 313 | yield* ensureLegacyEntry(stagingDir); | ||
| 314 | |||
| 293 | yield* input.validate(stagingPaths); | 315 | yield* input.validate(stagingPaths); |
| 294 | yield* fs | 316 | yield* fs |
| 295 | .writeFileString(stagingPaths.sentinelPath, `${input.version}\n`) | 317 | .writeFileString(stagingPaths.sentinelPath, `${input.version}\n`) |
| ... | @@ -328,7 +350,10 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( | ... | @@ -328,7 +350,10 @@ const installPinnedRuntime = Effect.fn("cloud.pinned_runtime.ensure_installed")( |
| 328 | ), | 350 | ), |
| 329 | ), | 351 | ), |
| 330 | ); | 352 | ); |
| 331 | if (!published) yield* input.validate(paths); | 353 | if (!published) { |
| 354 | yield* ensureLegacyEntry(paths.versionDir); | ||
| 355 | yield* input.validate(paths); | ||
| 356 | } | ||
| 332 | return paths; | 357 | return paths; |
| 333 | }).pipe( | 358 | }).pipe( |
| 334 | Effect.ensuring(fs.remove(stagingDir, { recursive: true, force: true }).pipe(Effect.ignore)), | 359 | Effect.ensuring(fs.remove(stagingDir, { recursive: true, force: true }).pipe(Effect.ignore)), |
packages/shared/package.json+4| ... | @@ -3,6 +3,10 @@ | ... | @@ -3,6 +3,10 @@ |
| 3 | "private": true, | 3 | "private": true, |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "exports": { | 5 | "exports": { |
| 6 | "./legacyCliLauncher": { | ||
| 7 | "types": "./src/legacyCliLauncher.ts", | ||
| 8 | "import": "./src/legacyCliLauncher.ts" | ||
| 9 | }, | ||
| 6 | "./delimitedPreview": { | 10 | "./delimitedPreview": { |
| 7 | "types": "./src/delimitedPreview.ts", | 11 | "types": "./src/delimitedPreview.ts", |
| 8 | "import": "./src/delimitedPreview.ts" | 12 | "import": "./src/delimitedPreview.ts" |
packages/shared/src/legacyCliLauncher.test.ts created+62| ... | @@ -0,0 +1,62 @@ | ||
| 1 | // @effect-diagnostics nodeBuiltinImport:off - Exercises real Node IPC and process signals. | ||
| 2 | import * as NodeChildProcess from "node:child_process"; | ||
| 3 | import * as NodeEvents from "node:events"; | ||
| 4 | import * as NodeFSP from "node:fs/promises"; | ||
| 5 | import * as NodeOS from "node:os"; | ||
| 6 | import * as NodePath from "node:path"; | ||
| 7 | import { expect, it } from "vite-plus/test"; | ||
| 8 | |||
| 9 | import { legacyCliLauncherScript } from "./legacyCliLauncher.ts"; | ||
| 10 | |||
| 11 | // oxlint-disable-next-line t3code/no-global-process-runtime -- This test launches a real host executable. | ||
| 12 | const hostPlatform = NodeOS.platform(); | ||
| 13 | // oxlint-disable-next-line t3code/no-global-process-runtime -- Match the real executable used by the subprocess. | ||
| 14 | const hostArch = NodeOS.arch(); | ||
| 15 | |||
| 16 | // The fixture executable uses a POSIX shebang. The wrapper itself also runs on Windows. | ||
| 17 | it.skipIf(hostPlatform === "win32").each(["npm", "archive"] as const)( | ||
| 18 | "keeps %s service IPC, arguments, and termination connected", | ||
| 19 | async (distribution) => { | ||
| 20 | const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-legacy-launcher-")); | ||
| 21 | const entry = NodePath.join(root, "node_modules/t3/dist/bin.mjs"); | ||
| 22 | const executable = | ||
| 23 | distribution === "archive" | ||
| 24 | ? NodePath.join(root, "t3") | ||
| 25 | : NodePath.join(root, `node_modules/@t3code/t3-${hostPlatform}-${hostArch}/t3`); | ||
| 26 | await NodeFSP.mkdir(NodePath.dirname(entry), { recursive: true }); | ||
| 27 | await NodeFSP.mkdir(NodePath.dirname(executable), { recursive: true }); | ||
| 28 | await NodeFSP.writeFile( | ||
| 29 | NodePath.join(NodePath.dirname(executable), "package.json"), | ||
| 30 | '{"type":"commonjs"}', | ||
| 31 | ); | ||
| 32 | await NodeFSP.writeFile(entry, legacyCliLauncherScript(distribution)); | ||
| 33 | await NodeFSP.writeFile( | ||
| 34 | executable, | ||
| 35 | `#!${process.execPath} | ||
| 36 | process.on("SIGTERM", () => process.exit(23)); | ||
| 37 | process.on("message", message => process.send({ reply: message })); | ||
| 38 | process.send({ args: process.argv.slice(2) }); | ||
| 39 | `, | ||
| 40 | ); | ||
| 41 | await NodeFSP.chmod(executable, 0o755); | ||
| 42 | const child = NodeChildProcess.fork(entry, ["serve", "a path with spaces"], { silent: true }); | ||
| 43 | try { | ||
| 44 | expect((await NodeEvents.EventEmitter.once(child, "message"))[0]).toEqual({ | ||
| 45 | args: ["serve", "a path with spaces"], | ||
| 46 | }); | ||
| 47 | const reply = NodeEvents.EventEmitter.once(child, "message"); | ||
| 48 | child.send({ type: "trial-accepted" }); | ||
| 49 | expect((await reply)[0]).toEqual({ reply: { type: "trial-accepted" } }); | ||
| 50 | const exit = NodeEvents.EventEmitter.once(child, "exit"); | ||
| 51 | child.kill("SIGTERM"); | ||
| 52 | expect(await exit).toEqual([23, null]); | ||
| 53 | } finally { | ||
| 54 | if (child.exitCode === null && child.signalCode === null) { | ||
| 55 | const exit = NodeEvents.EventEmitter.once(child, "exit"); | ||
| 56 | child.kill("SIGTERM"); | ||
| 57 | await exit; | ||
| 58 | } | ||
| 59 | await NodeFSP.rm(root, { recursive: true, force: true }); | ||
| 60 | } | ||
| 61 | }, | ||
| 62 | ); | ||
packages/shared/src/legacyCliLauncher.ts created+36| ... | @@ -0,0 +1,36 @@ | ||
| 1 | /** Node entry point for service launchers installed before executable releases. */ | ||
| 2 | export function legacyCliLauncherScript(distribution: "npm" | "archive"): string { | ||
| 3 | const executable = | ||
| 4 | distribution === "npm" | ||
| 5 | ? 'join(dirname(require.resolve("@t3code/t3-" + process.platform + "-" + process.arch + "/package.json")), executableName)' | ||
| 6 | : 'resolve(dirname(fileURLToPath(import.meta.url)), "../../..", executableName)'; | ||
| 7 | return `import { spawn } from "node:child_process"; | ||
| 8 | import { constants } from "node:os"; | ||
| 9 | import { dirname, join, resolve } from "node:path"; | ||
| 10 | import { fileURLToPath } from "node:url"; | ||
| 11 | import { createRequire } from "node:module"; | ||
| 12 | const require = createRequire(import.meta.url); | ||
| 13 | const executableName = process.platform === "win32" ? "t3.exe" : "t3"; | ||
| 14 | const executable = ${executable}; | ||
| 15 | const ipc = process.send !== undefined; | ||
| 16 | const child = spawn(executable, process.argv.slice(2), { | ||
| 17 | stdio: ipc ? ["inherit", "inherit", "inherit", "ipc"] : "inherit", | ||
| 18 | }); | ||
| 19 | const fail = (error) => { | ||
| 20 | if (!error) return; | ||
| 21 | process.stderr.write("t3: " + error.message + "\\n"); | ||
| 22 | child.kill("SIGTERM"); | ||
| 23 | process.exitCode = 1; | ||
| 24 | }; | ||
| 25 | if (ipc) { | ||
| 26 | process.on("message", (message) => { if (child.connected) child.send(message, fail); }); | ||
| 27 | child.on("message", (message) => { if (process.connected) process.send(message, fail); }); | ||
| 28 | process.on("disconnect", () => { if (child.connected) child.disconnect(); }); | ||
| 29 | } | ||
| 30 | for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) { | ||
| 31 | process.on(signal, () => child.kill(signal)); | ||
| 32 | } | ||
| 33 | child.on("error", (error) => { fail(error); process.exit(1); }); | ||
| 34 | child.on("exit", (code, signal) => process.exit(code ?? 128 + (constants.signals[signal] || 1))); | ||
| 35 | `; | ||
| 36 | } | ||
scripts/build-npm-platform-packages.test.ts+45-7| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { assert, it } from "@effect/vitest"; | 3 | import { assert, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| ... | @@ -151,7 +152,7 @@ it.layer(NodeServices.layer)("build-npm-platform-packages", (it) => { | ... | @@ -151,7 +152,7 @@ it.layer(NodeServices.layer)("build-npm-platform-packages", (it) => { |
| 151 | assert.equal(launcherManifest.name, "t3"); | 152 | assert.equal(launcherManifest.name, "t3"); |
| 152 | assert.equal(launcherManifest.version, VERSION); | 153 | assert.equal(launcherManifest.version, VERSION); |
| 153 | assert.deepStrictEqual(launcherManifest.bin, { t3: "./bin/t3.js" }); | 154 | assert.deepStrictEqual(launcherManifest.bin, { t3: "./bin/t3.js" }); |
| 154 | assert.deepStrictEqual(launcherManifest.files, ["bin"]); | 155 | assert.deepStrictEqual(launcherManifest.files, ["bin", "dist"]); |
| 155 | assert.deepStrictEqual(launcherManifest.optionalDependencies, { | 156 | assert.deepStrictEqual(launcherManifest.optionalDependencies, { |
| 156 | "@t3code/t3-darwin-arm64": VERSION, | 157 | "@t3code/t3-darwin-arm64": VERSION, |
| 157 | "@t3code/t3-linux-x64": VERSION, | 158 | "@t3code/t3-linux-x64": VERSION, |
| ... | @@ -182,13 +183,50 @@ it.layer(NodeServices.layer)("build-npm-platform-packages", (it) => { | ... | @@ -182,13 +183,50 @@ it.layer(NodeServices.layer)("build-npm-platform-packages", (it) => { |
| 182 | 183 | ||
| 183 | // NODE_PATH stands in for node_modules: require.resolve finds the | 184 | // NODE_PATH stands in for node_modules: require.resolve finds the |
| 184 | // platform package there exactly as it would after `npm install`. | 185 | // platform package there exactly as it would after `npm install`. |
| 186 | const hostPlatform = yield* HostProcessPlatform; | ||
| 187 | const hostArch = yield* HostProcessArchitecture; | ||
| 185 | const env = { ...process.env, NODE_PATH: fixture.outputDir } as Record<string, string>; | 188 | const env = { ...process.env, NODE_PATH: fixture.outputDir } as Record<string, string>; |
| 186 | const passthrough = yield* run(process.execPath, ["bin/t3.js", "serve", "--port", "1234"], { | 189 | if (KEYS.some((key) => key === `${hostPlatform}-${hostArch}`)) { |
| 187 | cwd: launcherDir, | 190 | const passthrough = yield* run(process.execPath, ["bin/t3.js", "serve", "--port", "1234"], { |
| 188 | env, | 191 | cwd: launcherDir, |
| 189 | }); | 192 | env, |
| 190 | assert.equal(passthrough.stdout.trim(), "stub linux-x64 serve --port 1234"); | 193 | }); |
| 191 | assert.equal(passthrough.exitCode, 7); | 194 | assert.equal( |
| 195 | passthrough.stdout.trim(), | ||
| 196 | `stub ${hostPlatform}-${hostArch} serve --port 1234`, | ||
| 197 | ); | ||
| 198 | assert.equal(passthrough.exitCode, 7); | ||
| 199 | |||
| 200 | // Run the entry point used by already-installed service updaters from | ||
| 201 | // the published tarball, including their preflight arguments. | ||
| 202 | const installedLauncher = path.join(fixture.root, "installed-launcher"); | ||
| 203 | yield* fs.makeDirectory(installedLauncher); | ||
| 204 | const unpack = yield* run( | ||
| 205 | "tar", | ||
| 206 | ["-xf", path.join(fixture.outputDir, "t3.tgz"), "-C", installedLauncher], | ||
| 207 | { | ||
| 208 | cwd: fixture.root, | ||
| 209 | }, | ||
| 210 | ); | ||
| 211 | assert.equal(unpack.exitCode, 0, unpack.stderr); | ||
| 212 | const legacy = yield* run( | ||
| 213 | process.execPath, | ||
| 214 | [ | ||
| 215 | "dist/bin.mjs", | ||
| 216 | "__service-preflight", | ||
| 217 | "--database-path", | ||
| 218 | "a database.sqlite", | ||
| 219 | "--launcher-protocol", | ||
| 220 | "2", | ||
| 221 | ], | ||
| 222 | { cwd: path.join(installedLauncher, "package"), env }, | ||
| 223 | ); | ||
| 224 | assert.equal( | ||
| 225 | legacy.stdout.trim(), | ||
| 226 | `stub ${hostPlatform}-${hostArch} __service-preflight --database-path a database.sqlite --launcher-protocol 2`, | ||
| 227 | ); | ||
| 228 | assert.equal(legacy.exitCode, 7); | ||
| 229 | } | ||
| 192 | 230 | ||
| 193 | const unsupported = yield* run(process.execPath, ["bin/t3.js", "--version"], { | 231 | const unsupported = yield* run(process.execPath, ["bin/t3.js", "--version"], { |
| 194 | cwd: launcherDir, | 232 | cwd: launcherDir, |
scripts/build-npm-platform-packages.ts+6-1| ... | @@ -19,6 +19,7 @@ | ... | @@ -19,6 +19,7 @@ |
| 19 | * bundleDependencies needs an arborist tree these flattened installs are | 19 | * bundleDependencies needs an arborist tree these flattened installs are |
| 20 | * not), whereas `npm publish <tarball>` uploads the bytes as given. | 20 | * not), whereas `npm publish <tarball>` uploads the bytes as given. |
| 21 | */ | 21 | */ |
| 22 | import { legacyCliLauncherScript } from "@t3tools/shared/legacyCliLauncher"; | ||
| 22 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | 23 | import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; |
| 23 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 24 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 24 | import * as Effect from "effect/Effect"; | 25 | import * as Effect from "effect/Effect"; |
| ... | @@ -137,7 +138,7 @@ export function npmLauncherPackageManifest( | ... | @@ -137,7 +138,7 @@ export function npmLauncherPackageManifest( |
| 137 | license: serverPackageJson.license, | 138 | license: serverPackageJson.license, |
| 138 | repository: serverPackageJson.repository, | 139 | repository: serverPackageJson.repository, |
| 139 | bin: { t3: "./bin/t3.js" }, | 140 | bin: { t3: "./bin/t3.js" }, |
| 140 | files: ["bin"], | 141 | files: ["bin", "dist"], |
| 141 | optionalDependencies: Object.fromEntries( | 142 | optionalDependencies: Object.fromEntries( |
| 142 | platformKeys.map((key) => [npmPlatformPackageName(key), version]), | 143 | platformKeys.map((key) => [npmPlatformPackageName(key), version]), |
| 143 | ), | 144 | ), |
| ... | @@ -342,6 +343,10 @@ const stageLauncherPackage = Effect.fn("stageLauncherPackage")(function* (input: | ... | @@ -342,6 +343,10 @@ const stageLauncherPackage = Effect.fn("stageLauncherPackage")(function* (input: |
| 342 | const launcherScript = path.join(stageDir, "bin/t3.js"); | 343 | const launcherScript = path.join(stageDir, "bin/t3.js"); |
| 343 | yield* fs.writeFileString(launcherScript, NPM_LAUNCHER_SCRIPT); | 344 | yield* fs.writeFileString(launcherScript, NPM_LAUNCHER_SCRIPT); |
| 344 | yield* fs.chmod(launcherScript, 0o755); | 345 | yield* fs.chmod(launcherScript, 0o755); |
| 346 | // Older service updaters and launchers run this exact path with Node. | ||
| 347 | // Keep it in the package so they can preflight and start the new executable. | ||
| 348 | yield* fs.makeDirectory(path.join(stageDir, "dist")); | ||
| 349 | yield* fs.writeFileString(path.join(stageDir, "dist/bin.mjs"), legacyCliLauncherScript("npm")); | ||
| 345 | const readme = yield* path.fromFileUrl(new URL("../apps/server/README.md", import.meta.url)); | 350 | const readme = yield* path.fromFileUrl(new URL("../apps/server/README.md", import.meta.url)); |
| 346 | if (yield* fs.exists(readme)) { | 351 | if (yield* fs.exists(readme)) { |
| 347 | yield* fs.copyFile(readme, path.join(stageDir, "README.md")); | 352 | yield* fs.copyFile(readme, path.join(stageDir, "README.md")); |