| author | |
| committer | |
| log | 06de59b3d67e5be427ce4c7c8d41e91265709aec |
| tree | 368aa98631522c7fd97c8c1b0cf7dd4bc66a7cfd |
| parent | 683aa87096899aa191e150429415ecc78966f538 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>20 files changed, 255 insertions(+), 407 deletions(-)
.gitignore+1| ... | @@ -5,6 +5,7 @@ node_modules | ... | @@ -5,6 +5,7 @@ node_modules |
| 5 | *.log | 5 | *.log |
| 6 | *.tsbuildinfo | 6 | *.tsbuildinfo |
| 7 | apps/*/dist | 7 | apps/*/dist |
| 8 | apps/*/dist-exe | ||
| 8 | infra/*/dist | 9 | infra/*/dist |
| 9 | .astro | 10 | .astro |
| 10 | packages/*/dist | 11 | packages/*/dist |
apps/server/package.json+1-3| ... | @@ -17,16 +17,15 @@ | ... | @@ -17,16 +17,15 @@ |
| 17 | "scripts": { | 17 | "scripts": { |
| 18 | "dev": "node --watch src/bin.ts", | 18 | "dev": "node --watch src/bin.ts", |
| 19 | "build:bundle": "vp pack && vp pack src/service-launcher.ts --out-dir dist --no-clean", | 19 | "build:bundle": "vp pack && vp pack src/service-launcher.ts --out-dir dist --no-clean", |
| 20 | "build:exe": "node scripts/cli.ts build-exe", | ||
| 20 | "start": "node dist/bin.mjs", | 21 | "start": "node dist/bin.mjs", |
| 21 | "typecheck": "tsc --noEmit", | 22 | "typecheck": "tsc --noEmit", |
| 22 | "test": "vp test run" | 23 | "test": "vp test run" |
| 23 | }, | 24 | }, |
| 24 | "dependencies": { | 25 | "dependencies": { |
| 25 | "@anthropic-ai/claude-agent-sdk": "^0.3.260", | 26 | "@anthropic-ai/claude-agent-sdk": "^0.3.260", |
| 26 | "@effect/platform-bun": "catalog:", | ||
| 27 | "@effect/platform-node": "catalog:", | 27 | "@effect/platform-node": "catalog:", |
| 28 | "@effect/platform-node-shared": "catalog:", | 28 | "@effect/platform-node-shared": "catalog:", |
| 29 | "@effect/sql-sqlite-bun": "catalog:", | ||
| 30 | "@ff-labs/fff-node": "0.9.4", | 29 | "@ff-labs/fff-node": "0.9.4", |
| 31 | "@opencode-ai/sdk": "^1.3.15", | 30 | "@opencode-ai/sdk": "^1.3.15", |
| 32 | "effect": "catalog:", | 31 | "effect": "catalog:", |
| ... | @@ -44,7 +43,6 @@ | ... | @@ -44,7 +43,6 @@ |
| 44 | "@t3tools/ssh": "workspace:*", | 43 | "@t3tools/ssh": "workspace:*", |
| 45 | "@t3tools/tailscale": "workspace:*", | 44 | "@t3tools/tailscale": "workspace:*", |
| 46 | "@t3tools/web": "workspace:*", | 45 | "@t3tools/web": "workspace:*", |
| 47 | "@types/bun": "1.3.14", | ||
| 48 | "@types/node": "catalog:", | 46 | "@types/node": "catalog:", |
| 49 | "@types/yauzl": "^3.4.0", | 47 | "@types/yauzl": "^3.4.0", |
| 50 | "effect-acp": "workspace:*", | 48 | "effect-acp": "workspace:*", |
apps/server/scripts/cli.ts+49-1| ... | @@ -15,6 +15,7 @@ import { | ... | @@ -15,6 +15,7 @@ import { |
| 15 | resolveWebAssetBrandForPackageVersion, | 15 | resolveWebAssetBrandForPackageVersion, |
| 16 | resolveWebIconOverrides, | 16 | resolveWebIconOverrides, |
| 17 | } from "../../../scripts/lib/brand-assets.ts"; | 17 | } from "../../../scripts/lib/brand-assets.ts"; |
| 18 | import { findEsmImportsOfExternalPackages } from "../../../scripts/lib/cli-external-packages.ts"; | ||
| 18 | import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts"; | 19 | import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts"; |
| 19 | import { fromJsonStringPretty } from "@t3tools/shared/schemaJson"; | 20 | import { fromJsonStringPretty } from "@t3tools/shared/schemaJson"; |
| 20 | import { fromYaml } from "@t3tools/shared/schemaYaml"; | 21 | import { fromYaml } from "@t3tools/shared/schemaYaml"; |
| ... | @@ -25,6 +26,7 @@ import { | ... | @@ -25,6 +26,7 @@ import { |
| 25 | ServerCliCommandExitError, | 26 | ServerCliCommandExitError, |
| 26 | ServerCliDevelopmentIconSourceMissingError, | 27 | ServerCliDevelopmentIconSourceMissingError, |
| 27 | ServerCliDevelopmentIconTargetMissingError, | 28 | ServerCliDevelopmentIconTargetMissingError, |
| 29 | ServerCliExecutableImportError, | ||
| 28 | ServerCliPublishIconSourceMissingError, | 30 | ServerCliPublishIconSourceMissingError, |
| 29 | ServerCliPublishIconTargetMissingError, | 31 | ServerCliPublishIconTargetMissingError, |
| 30 | } from "./cliErrors.ts"; | 32 | } from "./cliErrors.ts"; |
| ... | @@ -175,6 +177,52 @@ const buildCmd = Command.make( | ... | @@ -175,6 +177,52 @@ const buildCmd = Command.make( |
| 175 | }), | 177 | }), |
| 176 | ).pipe(Command.withDescription("Build the server package (tsdown + bundle web client).")); | 178 | ).pipe(Command.withDescription("Build the server package (tsdown + bundle web client).")); |
| 177 | 179 | ||
| 180 | // --------------------------------------------------------------------------- | ||
| 181 | // build-exe subcommand | ||
| 182 | // --------------------------------------------------------------------------- | ||
| 183 | |||
| 184 | const buildExeCmd = Command.make( | ||
| 185 | "build-exe", | ||
| 186 | { | ||
| 187 | verbose: Flag.boolean("verbose").pipe(Flag.withDefault(false)), | ||
| 188 | }, | ||
| 189 | (config) => | ||
| 190 | Effect.gen(function* () { | ||
| 191 | const path = yield* Path.Path; | ||
| 192 | const fs = yield* FileSystem.FileSystem; | ||
| 193 | const repoRoot = yield* RepoRoot; | ||
| 194 | const serverDir = path.join(repoRoot, "apps/server"); | ||
| 195 | |||
| 196 | yield* Effect.log("[cli] Building single-executable..."); | ||
| 197 | const spawnCommand = yield* resolveSpawnCommand("vp", ["pack"]); | ||
| 198 | yield* runCommand( | ||
| 199 | ChildProcess.make(spawnCommand.command, spawnCommand.args, { | ||
| 200 | cwd: serverDir, | ||
| 201 | env: { ...process.env, T3CODE_PACK_EXE: "1" }, | ||
| 202 | stdout: config.verbose ? "inherit" : "ignore", | ||
| 203 | stderr: "inherit", | ||
| 204 | shell: spawnCommand.shell, | ||
| 205 | }), | ||
| 206 | ); | ||
| 207 | |||
| 208 | // The executable can only `import` built-ins. A file-backed import | ||
| 209 | // passes the bundler and `node dist/bin.mjs`, then throws inside the | ||
| 210 | // binary, so read the emitted module graph rather than trusting config. | ||
| 211 | const bundlePath = path.join(serverDir, "dist-exe/bin.mjs"); | ||
| 212 | const specifiers = findEsmImportsOfExternalPackages(yield* fs.readFileString(bundlePath)); | ||
| 213 | if (specifiers.length > 0) { | ||
| 214 | return yield* new ServerCliExecutableImportError({ bundlePath, specifiers }); | ||
| 215 | } | ||
| 216 | yield* Effect.log( | ||
| 217 | "[cli] Built dist-exe/t3 (expects client/, resource-monitor/, and the runtime-external node_modules beside it; scripts/build-cli-archive.ts assembles that tree)", | ||
| 218 | ); | ||
| 219 | }), | ||
| 220 | ).pipe( | ||
| 221 | Command.withDescription( | ||
| 222 | "Build the server as a Node single-executable (needs a Node 25.7+ host for --build-sea). The binary still resolves native packages from a node_modules tree beside it.", | ||
| 223 | ), | ||
| 224 | ); | ||
| 225 | |||
| 178 | // --------------------------------------------------------------------------- | 226 | // --------------------------------------------------------------------------- |
| 179 | // publish subcommand | 227 | // publish subcommand |
| 180 | // --------------------------------------------------------------------------- | 228 | // --------------------------------------------------------------------------- |
| ... | @@ -309,7 +357,7 @@ const publishCmd = Command.make( | ... | @@ -309,7 +357,7 @@ const publishCmd = Command.make( |
| 309 | 357 | ||
| 310 | const cli = Command.make("cli").pipe( | 358 | const cli = Command.make("cli").pipe( |
| 311 | Command.withDescription("T3 server build & publish CLI."), | 359 | Command.withDescription("T3 server build & publish CLI."), |
| 312 | Command.withSubcommands([buildCmd, publishCmd]), | 360 | Command.withSubcommands([buildCmd, buildExeCmd, publishCmd]), |
| 313 | ); | 361 | ); |
| 314 | 362 | ||
| 315 | Command.run(cli, { version: "0.0.0" }).pipe( | 363 | Command.run(cli, { version: "0.0.0" }).pipe( |
apps/server/scripts/cliErrors.ts+12| ... | @@ -68,3 +68,15 @@ export class ServerCliBuildAssetMissingError extends Schema.TaggedError<ServerCl | ... | @@ -68,3 +68,15 @@ export class ServerCliBuildAssetMissingError extends Schema.TaggedError<ServerCl |
| 68 | return `Missing build asset: ${this.assetPath}. Run the build subcommand first.`; | 68 | return `Missing build asset: ${this.assetPath}. Run the build subcommand first.`; |
| 69 | } | 69 | } |
| 70 | } | 70 | } |
| 71 | |||
| 72 | export class ServerCliExecutableImportError extends Schema.TaggedError<ServerCliExecutableImportError>()( | ||
| 73 | "ServerCliExecutableImportError", | ||
| 74 | { | ||
| 75 | bundlePath: Schema.String, | ||
| 76 | specifiers: Schema.Array(Schema.String), | ||
| 77 | }, | ||
| 78 | ) { | ||
| 79 | override get message(): string { | ||
| 80 | return `${this.bundlePath} imports file-backed packages that a single-executable cannot resolve: ${this.specifiers.join(", ")}. Load them through createRequire instead.`; | ||
| 81 | } | ||
| 82 | } |
apps/server/src/persistence/Errors.test.ts-19| ... | @@ -3,7 +3,6 @@ import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; | ... | @@ -3,7 +3,6 @@ import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Schema from "effect/Schema"; | 4 | import * as Schema from "effect/Schema"; |
| 5 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 5 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 6 | import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError"; | ||
| 7 | 6 | ||
| 8 | import { PersistenceDecodeError, PersistenceSqlError, toPersistenceSqlError } from "./Errors.ts"; | 7 | import { PersistenceDecodeError, PersistenceSqlError, toPersistenceSqlError } from "./Errors.ts"; |
| 9 | 8 | ||
| ... | @@ -55,24 +54,6 @@ it("reads the condition through a wrapping driver error", () => { | ... | @@ -55,24 +54,6 @@ it("reads the condition through a wrapping driver error", () => { |
| 55 | assert.equal(error.detail, "SQLITE(5) database is locked"); | 54 | assert.equal(error.detail, "SQLITE(5) database is locked"); |
| 56 | }); | 55 | }); |
| 57 | 56 | ||
| 58 | it.each([{ errno: 1555, code: "SQLITE_CONSTRAINT_PRIMARYKEY" }, { errno: 1 }])( | ||
| 59 | "names Bun SQLite condition $errno through the SQL error wrapper", | ||
| 60 | (condition) => { | ||
| 61 | const driver = Object.assign(new Error("bun-sql-private-sentinel"), { | ||
| 62 | name: "SQLiteError", | ||
| 63 | ...condition, | ||
| 64 | }); | ||
| 65 | const cause = new SqlError({ reason: classifySqliteError(driver) }); | ||
| 66 | const error = toPersistenceSqlError("AuthSessionRepository.create:query")(cause); | ||
| 67 | |||
| 68 | assert.equal( | ||
| 69 | error.message, | ||
| 70 | `SQL error in AuthSessionRepository.create:query: SQLITE(${condition.errno})`, | ||
| 71 | ); | ||
| 72 | assert.equal(error.cause, cause); | ||
| 73 | }, | ||
| 74 | ); | ||
| 75 | |||
| 76 | it.each([ | 57 | it.each([ |
| 77 | new Error("unhelpful"), | 58 | new Error("unhelpful"), |
| 78 | Object.assign(new Error("file not found"), { errno: -2, code: "ENOENT" }), | 59 | Object.assign(new Error("file not found"), { errno: -2, code: "ENOENT" }), |
apps/server/src/persistence/Errors.ts+1-10| ... | @@ -75,7 +75,7 @@ const isPersistenceDecodeError = Schema.is(PersistenceDecodeError); | ... | @@ -75,7 +75,7 @@ const isPersistenceDecodeError = Schema.is(PersistenceDecodeError); |
| 75 | 75 | ||
| 76 | /** | 76 | /** |
| 77 | * Read a SQLite condition through SQL error wrappers. | 77 | * Read a SQLite condition through SQL error wrappers. |
| 78 | * Use Node's fixed description or Bun's numeric code, never the driver message. | 78 | * Use node:sqlite's fixed description, never the driver message. |
| 79 | */ | 79 | */ |
| 80 | function sqliteCondition(cause: unknown): string | undefined { | 80 | function sqliteCondition(cause: unknown): string | undefined { |
| 81 | let value = cause; | 81 | let value = cause; |
| ... | @@ -88,15 +88,6 @@ function sqliteCondition(cause: unknown): string | undefined { | ... | @@ -88,15 +88,6 @@ function sqliteCondition(cause: unknown): string | undefined { |
| 88 | ) { | 88 | ) { |
| 89 | return `SQLITE(${value.errcode}) ${value.errstr}`; | 89 | return `SQLITE(${value.errcode}) ${value.errstr}`; |
| 90 | } | 90 | } |
| 91 | if ( | ||
| 92 | "name" in value && | ||
| 93 | value.name === "SQLiteError" && | ||
| 94 | "errno" in value && | ||
| 95 | typeof value.errno === "number" && | ||
| 96 | Number.isInteger(value.errno) | ||
| 97 | ) { | ||
| 98 | return `SQLITE(${value.errno})`; | ||
| 99 | } | ||
| 100 | value = "cause" in value ? value.cause : undefined; | 91 | value = "cause" in value ? value.cause : undefined; |
| 101 | } | 92 | } |
| 102 | return undefined; | 93 | return undefined; |
apps/server/src/persistence/Layers/Sqlite.ts+3-25| ... | @@ -3,33 +3,11 @@ import * as Layer from "effect/Layer"; | ... | @@ -3,33 +3,11 @@ import * as Layer from "effect/Layer"; |
| 3 | import * as FileSystem from "effect/FileSystem"; | 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Path from "effect/Path"; | 4 | import * as Path from "effect/Path"; |
| 5 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 5 | import * as SqlClient from "effect/unstable/sql/SqlClient"; |
| 6 | import type { SqlError } from "effect/unstable/sql/SqlError"; | 6 | import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient"; |
| 7 | 7 | ||
| 8 | import { runMigrations } from "../Migrations.ts"; | 8 | import { runMigrations } from "../Migrations.ts"; |
| 9 | import { ServerConfig } from "../../config.ts"; | 9 | import { ServerConfig } from "../../config.ts"; |
| 10 | 10 | ||
| 11 | type RuntimeSqliteLayerConfig = { | ||
| 12 | readonly filename: string; | ||
| 13 | readonly spanAttributes?: Record<string, unknown>; | ||
| 14 | }; | ||
| 15 | |||
| 16 | type Loader = { | ||
| 17 | layer: (config: RuntimeSqliteLayerConfig) => Layer.Layer<SqlClient.SqlClient, SqlError>; | ||
| 18 | }; | ||
| 19 | const defaultSqliteClientLoaders = { | ||
| 20 | bun: () => import("@effect/sql-sqlite-bun/SqliteClient"), | ||
| 21 | node: () => import("@t3tools/shared/nodeSqliteClient"), | ||
| 22 | } satisfies Record<string, () => Promise<Loader>>; | ||
| 23 | |||
| 24 | const makeRuntimeSqliteLayer = Effect.fn("makeRuntimeSqliteLayer")(function* ( | ||
| 25 | config: RuntimeSqliteLayerConfig, | ||
| 26 | ) { | ||
| 27 | const runtime = process.versions.bun !== undefined ? "bun" : "node"; | ||
| 28 | const loader = defaultSqliteClientLoaders[runtime]; | ||
| 29 | const clientModule = yield* Effect.promise<Loader>(loader); | ||
| 30 | return clientModule.layer(config); | ||
| 31 | }, Layer.unwrap); | ||
| 32 | |||
| 33 | const setup = Layer.effectDiscard( | 11 | const setup = Layer.effectDiscard( |
| 34 | Effect.gen(function* () { | 12 | Effect.gen(function* () { |
| 35 | const sql = yield* SqlClient.SqlClient; | 13 | const sql = yield* SqlClient.SqlClient; |
| ... | @@ -50,7 +28,7 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")( | ... | @@ -50,7 +28,7 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")( |
| 50 | 28 | ||
| 51 | return Layer.provideMerge( | 29 | return Layer.provideMerge( |
| 52 | setup, | 30 | setup, |
| 53 | makeRuntimeSqliteLayer({ | 31 | NodeSqliteClient.layer({ |
| 54 | filename: dbPath, | 32 | filename: dbPath, |
| 55 | spanAttributes: { | 33 | spanAttributes: { |
| 56 | "db.name": path.basename(dbPath), | 34 | "db.name": path.basename(dbPath), |
| ... | @@ -62,7 +40,7 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")( | ... | @@ -62,7 +40,7 @@ export const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")( |
| 62 | 40 | ||
| 63 | export const SqlitePersistenceMemory = Layer.provideMerge( | 41 | export const SqlitePersistenceMemory = Layer.provideMerge( |
| 64 | setup, | 42 | setup, |
| 65 | makeRuntimeSqliteLayer({ filename: ":memory:" }), | 43 | NodeSqliteClient.layer({ filename: ":memory:" }), |
| 66 | ); | 44 | ); |
| 67 | 45 | ||
| 68 | export const layerConfig = Layer.unwrap( | 46 | export const layerConfig = Layer.unwrap( |
apps/server/src/server.ts+20-63| ... | @@ -1,3 +1,8 @@ | ... | @@ -1,3 +1,8 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off | ||
| 2 | import * as NodeHttp from "node:http"; | ||
| 3 | |||
| 4 | import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; | ||
| 5 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 1 | import { | 6 | import { |
| 2 | EnvironmentHttpApi, | 7 | EnvironmentHttpApi, |
| 3 | ProviderDriverKind, | 8 | ProviderDriverKind, |
| ... | @@ -29,6 +34,7 @@ import { guardHttpResponseWriteErrors } from "./httpResponseErrorGuard.ts"; | ... | @@ -29,6 +34,7 @@ import { guardHttpResponseWriteErrors } from "./httpResponseErrorGuard.ts"; |
| 29 | import { fixPath } from "./os-jank.ts"; | 34 | import { fixPath } from "./os-jank.ts"; |
| 30 | import { websocketRpcRouteLayer } from "./ws.ts"; | 35 | import { websocketRpcRouteLayer } from "./ws.ts"; |
| 31 | import * as ExternalLauncher from "./process/externalLauncher.ts"; | 36 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
| 37 | import * as NodePtyAdapter from "./terminal/NodePtyAdapter.ts"; | ||
| 32 | import { pullRequestHttpApiLayer } from "./pullRequest/http.ts"; | 38 | import { pullRequestHttpApiLayer } from "./pullRequest/http.ts"; |
| 33 | import * as PullRequestProviderRegistry from "./pullRequest/PullRequestProviderRegistry.ts"; | 39 | import * as PullRequestProviderRegistry from "./pullRequest/PullRequestProviderRegistry.ts"; |
| 34 | import * as PullRequestService from "./pullRequest/PullRequestService.ts"; | 40 | import * as PullRequestService from "./pullRequest/PullRequestService.ts"; |
| ... | @@ -163,17 +169,7 @@ const ApplicationObservabilityLive = ObservabilityLive.pipe( | ... | @@ -163,17 +169,7 @@ const ApplicationObservabilityLive = ObservabilityLive.pipe( |
| 163 | Layer.provideMerge(ResourceAttributionLayerLive), | 169 | Layer.provideMerge(ResourceAttributionLayerLive), |
| 164 | ); | 170 | ); |
| 165 | 171 | ||
| 166 | const PtyAdapterLive = Layer.unwrap( | 172 | const PtyAdapterLive = NodePtyAdapter.layer; |
| 167 | Effect.gen(function* () { | ||
| 168 | if (typeof Bun !== "undefined") { | ||
| 169 | const BunPtyAdapter = yield* Effect.promise(() => import("./terminal/BunPtyAdapter.ts")); | ||
| 170 | return BunPtyAdapter.layer; | ||
| 171 | } else { | ||
| 172 | const NodePtyAdapter = yield* Effect.promise(() => import("./terminal/NodePtyAdapter.ts")); | ||
| 173 | return NodePtyAdapter.layer; | ||
| 174 | } | ||
| 175 | }), | ||
| 176 | ); | ||
| 177 | 173 | ||
| 178 | const ServerSettingsLayerLive = ServerSettings.layer.pipe( | 174 | const ServerSettingsLayerLive = ServerSettings.layer.pipe( |
| 179 | Layer.provide(ServerSecretStore.layer), | 175 | Layer.provide(ServerSecretStore.layer), |
| ... | @@ -226,61 +222,22 @@ const RelayClientLive = Layer.unwrap( | ... | @@ -226,61 +222,22 @@ const RelayClientLive = Layer.unwrap( |
| 226 | const HttpServerLive = Layer.unwrap( | 222 | const HttpServerLive = Layer.unwrap( |
| 227 | Effect.gen(function* () { | 223 | Effect.gen(function* () { |
| 228 | const config = yield* ServerConfig.ServerConfig; | 224 | const config = yield* ServerConfig.ServerConfig; |
| 229 | if (typeof Bun !== "undefined") { | 225 | return NodeHttpServer.layer(() => guardHttpResponseWriteErrors(NodeHttp.createServer()), { |
| 230 | const BunHttpServer = yield* Effect.promise( | 226 | host: config.host ?? "127.0.0.1", |
| 231 | () => import("@effect/platform-bun/BunHttpServer"), | 227 | port: config.port, |
| 232 | ); | 228 | gracefulShutdownTimeout: HTTP_PREEMPTIVE_SHUTDOWN_GRACE_MS, |
| 233 | return BunHttpServer.layer({ | 229 | // Negotiate permessage-deflate with clients that offer it; clients |
| 234 | port: config.port, | 230 | // that don't still get uncompressed frames on their connection. |
| 235 | hostname: config.host ?? "127.0.0.1", | 231 | // Context takeover stays enabled (ws default) so the compression |
| 236 | gracefulShutdownTimeout: HTTP_PREEMPTIVE_SHUTDOWN_GRACE_MS, | 232 | // window is shared across frames — that also makes small frames cheap |
| 237 | websocket: { | 233 | // to compress, so no size threshold is set (ws only honors |
| 238 | // Negotiate permessage-deflate with clients that offer it; clients | 234 | // `threshold` when context takeover is disabled). |
| 239 | // that don't still get uncompressed frames on their connection. A | 235 | websocket: { perMessageDeflate: true }, |
| 240 | // dedicated compressor keeps a per-connection sliding window | 236 | }); |
| 241 | // (context takeover) so the compression dictionary is shared across | ||
| 242 | // server-to-client frames. Decompression uses the shared | ||
| 243 | // decompressor: uWebSockets' dedicated decompressor path can abort | ||
| 244 | // connections (close 1006) on valid DEFLATE input — see | ||
| 245 | // https://github.com/uNetworking/uWebSockets.js/issues/633. | ||
| 246 | perMessageDeflate: { | ||
| 247 | compress: "dedicated", | ||
| 248 | decompress: "shared", | ||
| 249 | }, | ||
| 250 | }, | ||
| 251 | }); | ||
| 252 | } else { | ||
| 253 | const [NodeHttpServer, NodeHttp] = yield* Effect.all([ | ||
| 254 | Effect.promise(() => import("@effect/platform-node/NodeHttpServer")), | ||
| 255 | Effect.promise(() => import("node:http")), | ||
| 256 | ]); | ||
| 257 | return NodeHttpServer.layer(() => guardHttpResponseWriteErrors(NodeHttp.createServer()), { | ||
| 258 | host: config.host ?? "127.0.0.1", | ||
| 259 | port: config.port, | ||
| 260 | gracefulShutdownTimeout: HTTP_PREEMPTIVE_SHUTDOWN_GRACE_MS, | ||
| 261 | // Negotiate permessage-deflate with clients that offer it; clients | ||
| 262 | // that don't still get uncompressed frames on their connection. | ||
| 263 | // Context takeover stays enabled (ws default) so the compression | ||
| 264 | // window is shared across frames — that also makes small frames cheap | ||
| 265 | // to compress, so no size threshold is set (ws only honors | ||
| 266 | // `threshold` when context takeover is disabled). | ||
| 267 | websocket: { perMessageDeflate: true }, | ||
| 268 | }); | ||
| 269 | } | ||
| 270 | }), | 237 | }), |
| 271 | ); | 238 | ); |
| 272 | 239 | ||
| 273 | const PlatformServicesLive = Layer.unwrap( | 240 | const PlatformServicesLive = NodeServices.layer; |
| 274 | Effect.gen(function* () { | ||
| 275 | if (typeof Bun !== "undefined") { | ||
| 276 | const { layer } = yield* Effect.promise(() => import("@effect/platform-bun/BunServices")); | ||
| 277 | return layer; | ||
| 278 | } else { | ||
| 279 | const { layer } = yield* Effect.promise(() => import("@effect/platform-node/NodeServices")); | ||
| 280 | return layer; | ||
| 281 | } | ||
| 282 | }), | ||
| 283 | ); | ||
| 284 | 241 | ||
| 285 | const ReactorLayerLive = Layer.empty.pipe( | 242 | const ReactorLayerLive = Layer.empty.pipe( |
| 286 | Layer.provideMerge(OrchestrationReactorLive), | 243 | Layer.provideMerge(OrchestrationReactorLive), |
apps/server/src/terminal/BunPtyAdapter.test.ts deleted-44| ... | @@ -1,44 +0,0 @@ | ||
| 1 | import { assert, expect, it } from "@effect/vitest"; | ||
| 2 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 3 | import * as Cause from "effect/Cause"; | ||
| 4 | import * as Effect from "effect/Effect"; | ||
| 5 | import * as Exit from "effect/Exit"; | ||
| 6 | |||
| 7 | import * as BunPtyAdapter from "./BunPtyAdapter.ts"; | ||
| 8 | |||
| 9 | it("describes unavailable Bun PTY operations structurally", () => { | ||
| 10 | const error = new BunPtyAdapter.BunPtyOperationUnavailableError({ | ||
| 11 | operation: "resize", | ||
| 12 | pid: 42, | ||
| 13 | }); | ||
| 14 | |||
| 15 | expect(error).toMatchObject({ | ||
| 16 | _tag: "BunPtyOperationUnavailableError", | ||
| 17 | operation: "resize", | ||
| 18 | pid: 42, | ||
| 19 | }); | ||
| 20 | expect(error.message).toBe("Bun PTY resize is unavailable for process 42."); | ||
| 21 | }); | ||
| 22 | |||
| 23 | it.effect("reports unsupported platforms with a structured startup defect", () => | ||
| 24 | Effect.gen(function* () { | ||
| 25 | const exit = yield* BunPtyAdapter.make().pipe( | ||
| 26 | Effect.provideService(HostProcessPlatform, "win32"), | ||
| 27 | Effect.exit, | ||
| 28 | ); | ||
| 29 | |||
| 30 | expect(Exit.isFailure(exit)).toBe(true); | ||
| 31 | if (Exit.isFailure(exit)) { | ||
| 32 | expect(Cause.hasDies(exit.cause)).toBe(true); | ||
| 33 | const error = Cause.squash(exit.cause); | ||
| 34 | assert.instanceOf(error, BunPtyAdapter.BunPtyUnsupportedPlatformError); | ||
| 35 | expect(error).toMatchObject({ | ||
| 36 | _tag: "BunPtyUnsupportedPlatformError", | ||
| 37 | platform: "win32", | ||
| 38 | }); | ||
| 39 | expect(error.message).toBe( | ||
| 40 | "Bun PTY terminal support is unavailable on win32. Please use Node.js (e.g. by running `npx t3`) instead.", | ||
| 41 | ); | ||
| 42 | } | ||
| 43 | }), | ||
| 44 | ); | ||
apps/server/src/terminal/BunPtyAdapter.ts deleted-155| ... | @@ -1,155 +0,0 @@ | ||
| 1 | /// <reference types="bun" /> | ||
| 2 | |||
| 3 | import * as Effect from "effect/Effect"; | ||
| 4 | import * as Layer from "effect/Layer"; | ||
| 5 | import * as Schema from "effect/Schema"; | ||
| 6 | import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | ||
| 7 | |||
| 8 | import * as PtyAdapter from "./PtyAdapter.ts"; | ||
| 9 | |||
| 10 | export class BunPtyUnsupportedPlatformError extends Schema.TaggedError<BunPtyUnsupportedPlatformError>()( | ||
| 11 | "BunPtyUnsupportedPlatformError", | ||
| 12 | { | ||
| 13 | platform: Schema.Literal("win32"), | ||
| 14 | }, | ||
| 15 | ) { | ||
| 16 | override get message(): string { | ||
| 17 | return `Bun PTY terminal support is unavailable on ${this.platform}. Please use Node.js (e.g. by running \`npx t3\`) instead.`; | ||
| 18 | } | ||
| 19 | } | ||
| 20 | |||
| 21 | export class BunPtyOperationUnavailableError extends Schema.TaggedError<BunPtyOperationUnavailableError>()( | ||
| 22 | "BunPtyOperationUnavailableError", | ||
| 23 | { | ||
| 24 | operation: Schema.Literals(["write", "resize"]), | ||
| 25 | pid: Schema.Number, | ||
| 26 | }, | ||
| 27 | ) { | ||
| 28 | override get message(): string { | ||
| 29 | return `Bun PTY ${this.operation} is unavailable for process ${this.pid}.`; | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | class BunPtyProcess implements PtyAdapter.PtyProcess { | ||
| 34 | private readonly dataListeners = new Set<(data: string) => void>(); | ||
| 35 | private readonly exitListeners = new Set<(event: PtyAdapter.PtyExitEvent) => void>(); | ||
| 36 | private readonly decoder = new TextDecoder(); | ||
| 37 | private readonly process: Bun.Subprocess; | ||
| 38 | private didExit = false; | ||
| 39 | |||
| 40 | constructor(process: Bun.Subprocess) { | ||
| 41 | this.process = process; | ||
| 42 | void this.process.exited | ||
| 43 | .then((exitCode) => { | ||
| 44 | this.emitExit({ | ||
| 45 | exitCode: Number.isInteger(exitCode) ? exitCode : 0, | ||
| 46 | signal: typeof this.process.signalCode === "number" ? this.process.signalCode : null, | ||
| 47 | }); | ||
| 48 | }) | ||
| 49 | .catch(() => { | ||
| 50 | this.emitExit({ exitCode: 1, signal: null }); | ||
| 51 | }); | ||
| 52 | } | ||
| 53 | |||
| 54 | get pid(): number { | ||
| 55 | return this.process.pid; | ||
| 56 | } | ||
| 57 | |||
| 58 | write(data: string): void { | ||
| 59 | if (!this.process.terminal) { | ||
| 60 | throw new BunPtyOperationUnavailableError({ operation: "write", pid: this.pid }); | ||
| 61 | } | ||
| 62 | this.process.terminal.write(data); | ||
| 63 | } | ||
| 64 | |||
| 65 | resize(cols: number, rows: number): void { | ||
| 66 | if (!this.process.terminal?.resize) { | ||
| 67 | throw new BunPtyOperationUnavailableError({ operation: "resize", pid: this.pid }); | ||
| 68 | } | ||
| 69 | this.process.terminal.resize(cols, rows); | ||
| 70 | } | ||
| 71 | |||
| 72 | kill(signal?: string): void { | ||
| 73 | if (!signal) { | ||
| 74 | this.process.kill(); | ||
| 75 | return; | ||
| 76 | } | ||
| 77 | this.process.kill(signal as NodeJS.Signals); | ||
| 78 | } | ||
| 79 | |||
| 80 | onData(callback: (data: string) => void): () => void { | ||
| 81 | this.dataListeners.add(callback); | ||
| 82 | return () => { | ||
| 83 | this.dataListeners.delete(callback); | ||
| 84 | }; | ||
| 85 | } | ||
| 86 | |||
| 87 | onExit(callback: (event: PtyAdapter.PtyExitEvent) => void): () => void { | ||
| 88 | this.exitListeners.add(callback); | ||
| 89 | return () => { | ||
| 90 | this.exitListeners.delete(callback); | ||
| 91 | }; | ||
| 92 | } | ||
| 93 | |||
| 94 | emitData(data: Uint8Array): void { | ||
| 95 | if (this.didExit) return; | ||
| 96 | const text = this.decoder.decode(data, { stream: true }); | ||
| 97 | if (text.length === 0) return; | ||
| 98 | for (const listener of this.dataListeners) { | ||
| 99 | listener(text); | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | private emitExit(event: PtyAdapter.PtyExitEvent): void { | ||
| 104 | if (this.didExit) return; | ||
| 105 | this.didExit = true; | ||
| 106 | |||
| 107 | const remainder = this.decoder.decode(); | ||
| 108 | if (remainder.length > 0) { | ||
| 109 | for (const listener of this.dataListeners) { | ||
| 110 | listener(remainder); | ||
| 111 | } | ||
| 112 | } | ||
| 113 | |||
| 114 | for (const listener of this.exitListeners) { | ||
| 115 | listener(event); | ||
| 116 | } | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | export const make = Effect.fn("BunPtyAdapter.make")(function* () { | ||
| 121 | const platform = yield* HostProcessPlatform; | ||
| 122 | if (platform === "win32") { | ||
| 123 | return yield* Effect.die(new BunPtyUnsupportedPlatformError({ platform })); | ||
| 124 | } | ||
| 125 | return PtyAdapter.PtyAdapter.of({ | ||
| 126 | spawn: (input) => | ||
| 127 | Effect.try({ | ||
| 128 | try: () => { | ||
| 129 | let processHandle: BunPtyProcess | null = null; | ||
| 130 | const command = [input.shell, ...(input.args ?? [])]; | ||
| 131 | const subprocess = Bun.spawn(command, { | ||
| 132 | cwd: input.cwd, | ||
| 133 | env: input.env, | ||
| 134 | terminal: { | ||
| 135 | cols: input.cols, | ||
| 136 | rows: input.rows, | ||
| 137 | data: (_terminal, data) => { | ||
| 138 | processHandle?.emitData(data); | ||
| 139 | }, | ||
| 140 | }, | ||
| 141 | }); | ||
| 142 | processHandle = new BunPtyProcess(subprocess); | ||
| 143 | return processHandle; | ||
| 144 | }, | ||
| 145 | catch: (cause) => | ||
| 146 | new PtyAdapter.PtySpawnError({ | ||
| 147 | adapter: "bun", | ||
| 148 | shell: input.shell, | ||
| 149 | cause, | ||
| 150 | }), | ||
| 151 | }), | ||
| 152 | }); | ||
| 153 | }); | ||
| 154 | |||
| 155 | export const layer = Layer.effect(PtyAdapter.PtyAdapter, make()); | ||
apps/server/src/terminal/NodePtyAdapter.test.ts+6-2| ... | @@ -19,7 +19,7 @@ const spawn = vi.fn(() => ({ | ... | @@ -19,7 +19,7 @@ const spawn = vi.fn(() => ({ |
| 19 | onExit: vi.fn(() => ({ dispose: vi.fn() })), | 19 | onExit: vi.fn(() => ({ dispose: vi.fn() })), |
| 20 | })); | 20 | })); |
| 21 | 21 | ||
| 22 | vi.mock("node-pty", () => ({ spawn })); | 22 | const fakeNodePty = { spawn } as unknown as typeof import("node-pty"); |
| 23 | 23 | ||
| 24 | const makeTestLayer = (platform: NodeJS.Platform = "win32") => | 24 | const makeTestLayer = (platform: NodeJS.Platform = "win32") => |
| 25 | NodePtyAdapter.layer.pipe( | 25 | NodePtyAdapter.layer.pipe( |
| ... | @@ -28,6 +28,7 @@ const makeTestLayer = (platform: NodeJS.Platform = "win32") => | ... | @@ -28,6 +28,7 @@ const makeTestLayer = (platform: NodeJS.Platform = "win32") => |
| 28 | NodeServices.layer, | 28 | NodeServices.layer, |
| 29 | Layer.succeed(HostProcessPlatform, platform), | 29 | Layer.succeed(HostProcessPlatform, platform), |
| 30 | Layer.succeed(HostProcessArchitecture, "x64"), | 30 | Layer.succeed(HostProcessArchitecture, "x64"), |
| 31 | Layer.succeed(NodePtyAdapter.NodePtyModuleLoaderRef, () => Promise.resolve(fakeNodePty)), | ||
| 31 | ), | 32 | ), |
| 32 | ), | 33 | ), |
| 33 | ); | 34 | ); |
| ... | @@ -125,7 +126,10 @@ it.effect("preserves a caller-provided TERM in the spawn env on win32", () => | ... | @@ -125,7 +126,10 @@ it.effect("preserves a caller-provided TERM in the spawn env on win32", () => |
| 125 | it.effect("reports native module load failures as structured startup defects", () => | 126 | it.effect("reports native module load failures as structured startup defects", () => |
| 126 | Effect.gen(function* () { | 127 | Effect.gen(function* () { |
| 127 | const cause = new Error("native binding could not be loaded"); | 128 | const cause = new Error("native binding could not be loaded"); |
| 128 | const exit = yield* NodePtyAdapter.make(() => Promise.reject(cause)).pipe(Effect.exit); | 129 | const exit = yield* NodePtyAdapter.make().pipe( |
| 130 | Effect.provideService(NodePtyAdapter.NodePtyModuleLoaderRef, () => Promise.reject(cause)), | ||
| 131 | Effect.exit, | ||
| 132 | ); | ||
| 129 | 133 | ||
| 130 | assert.isTrue(Exit.isFailure(exit)); | 134 | assert.isTrue(Exit.isFailure(exit)); |
| 131 | if (Exit.isFailure(exit)) { | 135 | if (Exit.isFailure(exit)) { |
apps/server/src/terminal/NodePtyAdapter.ts+18-4| ... | @@ -1,5 +1,6 @@ | ... | @@ -1,5 +1,6 @@ |
| 1 | import * as NodeModule from "node:module"; | 1 | import * as NodeModule from "node:module"; |
| 2 | 2 | ||
| 3 | import * as Context from "effect/Context"; | ||
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 5 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; | 6 | import * as Layer from "effect/Layer"; |
| ... | @@ -24,10 +25,24 @@ export class NodePtyModuleLoadError extends Schema.TaggedError<NodePtyModuleLoad | ... | @@ -24,10 +25,24 @@ export class NodePtyModuleLoadError extends Schema.TaggedError<NodePtyModuleLoad |
| 24 | 25 | ||
| 25 | type NodePtyModuleLoader = () => Promise<typeof import("node-pty")>; | 26 | type NodePtyModuleLoader = () => Promise<typeof import("node-pty")>; |
| 26 | 27 | ||
| 28 | // node-pty stays external to the CLI bundle because it dlopens a native | ||
| 29 | // addon. Inside a Node single-executable, `import()` cannot load files from | ||
| 30 | // disk (only built-ins resolve), while `require` always reads the real | ||
| 31 | // filesystem, so both the module and its spawn-helper resolve through it. | ||
| 32 | const requireForNodePty = NodeModule.createRequire(import.meta.url); | ||
| 33 | |||
| 34 | const loadNodePty: NodePtyModuleLoader = () => | ||
| 35 | Promise.resolve().then(() => requireForNodePty("node-pty") as typeof import("node-pty")); | ||
| 36 | |||
| 37 | /** Injectable so tests can substitute a fake module; `require` bypasses module mocks. */ | ||
| 38 | export const NodePtyModuleLoaderRef = Context.Reference<NodePtyModuleLoader>( | ||
| 39 | "server/terminal/NodePtyModuleLoader", | ||
| 40 | { defaultValue: () => loadNodePty }, | ||
| 41 | ); | ||
| 42 | |||
| 27 | let didEnsureSpawnHelperExecutable = false; | 43 | let didEnsureSpawnHelperExecutable = false; |
| 28 | 44 | ||
| 29 | const resolveNodePtySpawnHelperPath = Effect.gen(function* () { | 45 | const resolveNodePtySpawnHelperPath = Effect.gen(function* () { |
| 30 | const requireForNodePty = NodeModule.createRequire(import.meta.url); | ||
| 31 | const path = yield* Path.Path; | 46 | const path = yield* Path.Path; |
| 32 | const fs = yield* FileSystem.FileSystem; | 47 | const fs = yield* FileSystem.FileSystem; |
| 33 | const platform = yield* HostProcessPlatform; | 48 | const platform = yield* HostProcessPlatform; |
| ... | @@ -113,9 +128,8 @@ class NodePtyProcess implements PtyAdapter.PtyProcess { | ... | @@ -113,9 +128,8 @@ class NodePtyProcess implements PtyAdapter.PtyProcess { |
| 113 | } | 128 | } |
| 114 | } | 129 | } |
| 115 | 130 | ||
| 116 | export const make = Effect.fn("NodePtyAdapter.make")(function* ( | 131 | export const make = Effect.fn("NodePtyAdapter.make")(function* () { |
| 117 | loadNodePtyModule: NodePtyModuleLoader = () => import("node-pty"), | 132 | const loadNodePtyModule = yield* NodePtyModuleLoaderRef; |
| 118 | ) { | ||
| 119 | const fs = yield* FileSystem.FileSystem; | 133 | const fs = yield* FileSystem.FileSystem; |
| 120 | const path = yield* Path.Path; | 134 | const path = yield* Path.Path; |
| 121 | const platform = yield* HostProcessPlatform; | 135 | const platform = yield* HostProcessPlatform; |
apps/server/src/workspace/WorkspaceSearchIndex.ts+20-11| ... | @@ -1,13 +1,15 @@ | ... | @@ -1,13 +1,15 @@ |
| 1 | import { | 1 | import * as NodeModule from "node:module"; |
| 2 | type DirItem, | 2 | |
| 3 | type DirSearchResult, | 3 | import type { |
| 4 | type FileItem, | 4 | DirItem, |
| 5 | FileFinder, | 5 | DirSearchResult, |
| 6 | type GrepCursor, | 6 | FileItem, |
| 7 | type MixedItem, | 7 | FileFinder as FileFinderType, |
| 8 | type MixedSearchResult, | 8 | GrepCursor, |
| 9 | type Result, | 9 | MixedItem, |
| 10 | type SearchResult, | 10 | MixedSearchResult, |
| 11 | Result, | ||
| 12 | SearchResult, | ||
| 11 | } from "@ff-labs/fff-node"; | 13 | } from "@ff-labs/fff-node"; |
| 12 | import * as Context from "effect/Context"; | 14 | import * as Context from "effect/Context"; |
| 13 | import * as Effect from "effect/Effect"; | 15 | import * as Effect from "effect/Effect"; |
| ... | @@ -25,6 +27,13 @@ import type { | ... | @@ -25,6 +27,13 @@ import type { |
| 25 | } from "@t3tools/contracts"; | 27 | } from "@t3tools/contracts"; |
| 26 | import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; | 28 | import { isWorkspaceImagePreviewPath } from "@t3tools/shared/filePreview"; |
| 27 | 29 | ||
| 30 | // fff-node stays external to the CLI bundle because it dlopens a native | ||
| 31 | // library. A static `import` of an external package is a hard error inside a | ||
| 32 | // Node single-executable (only built-ins resolve there), so load it through | ||
| 33 | // `require`, which reads from the real filesystem in every runtime. | ||
| 34 | const requireForFff = NodeModule.createRequire(import.meta.url); | ||
| 35 | const { FileFinder } = requireForFff("@ff-labs/fff-node") as typeof import("@ff-labs/fff-node"); | ||
| 36 | |||
| 28 | const WORKSPACE_INDEX_MAX_ENTRIES = 25_000; | 37 | const WORKSPACE_INDEX_MAX_ENTRIES = 25_000; |
| 29 | const WORKSPACE_INDEX_PAGE_SIZE = WORKSPACE_INDEX_MAX_ENTRIES + 2; | 38 | const WORKSPACE_INDEX_PAGE_SIZE = WORKSPACE_INDEX_MAX_ENTRIES + 2; |
| 30 | const WORKSPACE_INDEX_SCAN_TIMEOUT = "15 seconds"; | 39 | const WORKSPACE_INDEX_SCAN_TIMEOUT = "15 seconds"; |
| ... | @@ -330,7 +339,7 @@ const createFinder = Effect.fn("WorkspaceSearchIndex.createFinder")(function* ( | ... | @@ -330,7 +339,7 @@ const createFinder = Effect.fn("WorkspaceSearchIndex.createFinder")(function* ( |
| 330 | 339 | ||
| 331 | const waitForIndexReady = Effect.fn("WorkspaceSearchIndex.waitForIndexReady")(function* <E>( | 340 | const waitForIndexReady = Effect.fn("WorkspaceSearchIndex.waitForIndexReady")(function* <E>( |
| 332 | cwd: string, | 341 | cwd: string, |
| 333 | finder: FileFinder, | 342 | finder: FileFinderType, |
| 334 | onFailure: (input: { readonly reason: string; readonly cause?: unknown }) => E, | 343 | onFailure: (input: { readonly reason: string; readonly cause?: unknown }) => E, |
| 335 | ): Effect.fn.Return<void, E | WorkspaceSearchIndexScanTimedOut> { | 344 | ): Effect.fn.Return<void, E | WorkspaceSearchIndexScanTimedOut> { |
| 336 | const result = yield* Effect.tryPromise({ | 345 | const result = yield* Effect.tryPromise({ |
apps/server/vite.config.ts+24-3| ... | @@ -23,6 +23,12 @@ export { shouldBundleCliDependency }; | ... | @@ -23,6 +23,12 @@ export { shouldBundleCliDependency }; |
| 23 | const repoEnv = loadRepoEnv(); | 23 | const repoEnv = loadRepoEnv(); |
| 24 | const cliBuildChannel = packageJson.version.includes("-nightly.") ? "nightly" : "latest"; | 24 | const cliBuildChannel = packageJson.version.includes("-nightly.") ? "nightly" : "latest"; |
| 25 | 25 | ||
| 26 | // `build:exe` wraps the same bundle in a Node single-executable. tsdown's exe | ||
| 27 | // step refuses multi-chunk output and counts the sourcemap as a chunk, and the | ||
| 28 | // executable needs a host Node that supports `--build-sea` (25.7+), so this is | ||
| 29 | // a separate mode rather than a second entry in the default build. | ||
| 30 | const packExecutable = process.env.T3CODE_PACK_EXE === "1"; | ||
| 31 | |||
| 26 | export default mergeConfig( | 32 | export default mergeConfig( |
| 27 | baseConfig, | 33 | baseConfig, |
| 28 | defineConfig({ | 34 | defineConfig({ |
| ... | @@ -36,10 +42,25 @@ export default mergeConfig( | ... | @@ -36,10 +42,25 @@ export default mergeConfig( |
| 36 | }, | 42 | }, |
| 37 | }, | 43 | }, |
| 38 | pack: { | 44 | pack: { |
| 39 | entry: ["src/bin.ts", "src/claudeHistoryWorker.ts"], | 45 | // The executable embeds one entry; the history worker becomes a hidden |
| 40 | outDir: "dist", | 46 | // subcommand there instead of a sibling script. |
| 41 | sourcemap: true, | 47 | entry: packExecutable ? ["src/bin.ts"] : ["src/bin.ts", "src/claudeHistoryWorker.ts"], |
| 48 | outDir: packExecutable ? "dist-exe" : "dist", | ||
| 49 | sourcemap: !packExecutable, | ||
| 42 | clean: true, | 50 | clean: true, |
| 51 | ...(packExecutable | ||
| 52 | ? { | ||
| 53 | exe: { | ||
| 54 | fileName: "t3", | ||
| 55 | outDir: "dist-exe", | ||
| 56 | // Node's SEA docs: `import()` does not work when useCodeCache is | ||
| 57 | // true, and the server reaches several modules that way. The | ||
| 58 | // cache is also platform-bound, so leaving it off keeps the | ||
| 59 | // build correct on any host. | ||
| 60 | seaConfig: { useCodeCache: false }, | ||
| 61 | }, | ||
| 62 | } | ||
| 63 | : {}), | ||
| 43 | deps: { | 64 | deps: { |
| 44 | // Both halves are required. `alwaysBundle` forces the JS dependencies in | 65 | // Both halves are required. `alwaysBundle` forces the JS dependencies in |
| 45 | // (declared deps are external by default, which is what this change is | 66 | // (declared deps are external by default, which is what this change is |
packages/shared/src/nodeSqliteClient.ts-12| ... | @@ -7,7 +7,6 @@ | ... | @@ -7,7 +7,6 @@ |
| 7 | import * as NodeSqlite from "node:sqlite"; | 7 | import * as NodeSqlite from "node:sqlite"; |
| 8 | 8 | ||
| 9 | import * as Cache from "effect/Cache"; | 9 | import * as Cache from "effect/Cache"; |
| 10 | import * as Config from "effect/Config"; | ||
| 11 | import * as Duration from "effect/Duration"; | 10 | import * as Duration from "effect/Duration"; |
| 12 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 13 | import * as Fiber from "effect/Fiber"; | 12 | import * as Fiber from "effect/Fiber"; |
| ... | @@ -26,10 +25,6 @@ import * as Statement from "effect/unstable/sql/Statement"; | ... | @@ -26,10 +25,6 @@ import * as Statement from "effect/unstable/sql/Statement"; |
| 26 | 25 | ||
| 27 | const ATTR_DB_SYSTEM_NAME = "db.system.name"; | 26 | const ATTR_DB_SYSTEM_NAME = "db.system.name"; |
| 28 | 27 | ||
| 29 | export const TypeId: TypeId = "~local/sqlite-node/SqliteClient"; | ||
| 30 | |||
| 31 | export type TypeId = "~local/sqlite-node/SqliteClient"; | ||
| 32 | |||
| 33 | export interface SqliteClientConfig { | 28 | export interface SqliteClientConfig { |
| 34 | readonly filename: string; | 29 | readonly filename: string; |
| 35 | readonly readonly?: boolean | undefined; | 30 | readonly readonly?: boolean | undefined; |
| ... | @@ -315,13 +310,6 @@ const makeMemory = ( | ... | @@ -315,13 +310,6 @@ const makeMemory = ( |
| 315 | }, | 310 | }, |
| 316 | ); | 311 | ); |
| 317 | 312 | ||
| 318 | export const layerConfig = ( | ||
| 319 | config: Config.Wrap<SqliteClientConfig>, | ||
| 320 | ): Layer.Layer<Client.SqlClient, Config.ConfigError | SqlError> => | ||
| 321 | Layer.effect(Client.SqlClient, Config.unwrap(config).pipe(Effect.flatMap(make))).pipe( | ||
| 322 | Layer.provide(Reactivity.layer), | ||
| 323 | ); | ||
| 324 | |||
| 325 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient, SqlError> => | 313 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient, SqlError> => |
| 326 | Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer)); | 314 | Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer)); |
| 327 | 315 |
patches/@ff-labs__fff-node@0.9.4.patch+14| ... | @@ -37,3 +37,17 @@ index ee181aef5007e4bf34a49479c089ca30f73a320b..327e2c55c83cc4c50d396a3109190ef1 | ... | @@ -37,3 +37,17 @@ index ee181aef5007e4bf34a49479c089ca30f73a320b..327e2c55c83cc4c50d396a3109190ef1 |
| 37 | } | 37 | } |
| 38 | } | 38 | } |
| 39 | catch { | 39 | catch { |
| 40 | diff --git a/package.json b/package.json | ||
| 41 | --- a/package.json | ||
| 42 | +++ b/package.json | ||
| 43 | @@ -8,8 +8,9 @@ | ||
| 44 | "types": "dist/src/index.d.ts", | ||
| 45 | "exports": { | ||
| 46 | ".": { | ||
| 47 | + "types": "./dist/src/index.d.ts", | ||
| 48 | "import": "./dist/src/index.js", | ||
| 49 | - "types": "./dist/src/index.d.ts" | ||
| 50 | + "require": "./dist/src/index.js" | ||
| 51 | } | ||
| 52 | }, | ||
| 53 | "files": [ |
pnpm-lock.yaml+9-24| ... | @@ -59,11 +59,9 @@ overrides: | ... | @@ -59,11 +59,9 @@ overrides: |
| 59 | '@clerk/react': 6.14.7 | 59 | '@clerk/react': 6.14.7 |
| 60 | '@clerk/shared': 4.30.1 | 60 | '@clerk/shared': 4.30.1 |
| 61 | '@effect/atom-react': 4.0.0-rc.112 | 61 | '@effect/atom-react': 4.0.0-rc.112 |
| 62 | '@effect/platform-bun': 4.0.0-rc.112 | ||
| 63 | '@effect/platform-node': 4.0.0-rc.112 | 62 | '@effect/platform-node': 4.0.0-rc.112 |
| 64 | '@effect/platform-node-shared': 4.0.0-rc.112 | 63 | '@effect/platform-node-shared': 4.0.0-rc.112 |
| 65 | '@effect/sql-pg': 4.0.0-rc.112 | 64 | '@effect/sql-pg': 4.0.0-rc.112 |
| 66 | '@effect/sql-sqlite-bun': 4.0.0-rc.112 | ||
| 67 | '@effect/vitest': 4.0.0-rc.112 | 65 | '@effect/vitest': 4.0.0-rc.112 |
| 68 | '@effect/vitest>vitest': '-' | 66 | '@effect/vitest>vitest': '-' |
| 69 | '@expo/dom-webview': 57.0.1 | 67 | '@expo/dom-webview': 57.0.1 |
| ... | @@ -90,7 +88,7 @@ patchedDependencies: | ... | @@ -90,7 +88,7 @@ patchedDependencies: |
| 90 | '@clerk/expo@4.2.0': 72e426f44fc1cde16fc2cbba3d1e96cdca7c6d957faa73d0fe6b43948608a6c1 | 88 | '@clerk/expo@4.2.0': 72e426f44fc1cde16fc2cbba3d1e96cdca7c6d957faa73d0fe6b43948608a6c1 |
| 91 | '@effect/vitest@4.0.0-rc.112': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b | 89 | '@effect/vitest@4.0.0-rc.112': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b |
| 92 | '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 | 90 | '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 |
| 93 | '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368 | 91 | '@ff-labs/fff-node@0.9.4': c4e3cc2420ceb9dc650f9d189e9c24baf7e83f342998bacda5f459a4ce7927a8 |
| 94 | '@legendapp/list@3.3.5': 680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806 | 92 | '@legendapp/list@3.3.5': 680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806 |
| 95 | '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 | 93 | '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 |
| 96 | '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d | 94 | '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d |
| ... | @@ -499,21 +497,15 @@ importers: | ... | @@ -499,21 +497,15 @@ importers: |
| 499 | '@anthropic-ai/claude-agent-sdk': | 497 | '@anthropic-ai/claude-agent-sdk': |
| 500 | specifier: ^0.3.260 | 498 | specifier: ^0.3.260 |
| 501 | version: 0.3.260(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) | 499 | version: 0.3.260(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) |
| 502 | '@effect/platform-bun': | ||
| 503 | specifier: 4.0.0-rc.112 | ||
| 504 | version: 4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(utf-8-validate@6.0.6) | ||
| 505 | '@effect/platform-node': | 500 | '@effect/platform-node': |
| 506 | specifier: 4.0.0-rc.112 | 501 | specifier: 4.0.0-rc.112 |
| 507 | version: 4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(redis@6.2.1)(utf-8-validate@6.0.6) | 502 | version: 4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(redis@6.2.1)(utf-8-validate@6.0.6) |
| 508 | '@effect/platform-node-shared': | 503 | '@effect/platform-node-shared': |
| 509 | specifier: 4.0.0-rc.112 | 504 | specifier: 4.0.0-rc.112 |
| 510 | version: 4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(utf-8-validate@6.0.6) | 505 | version: 4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(utf-8-validate@6.0.6) |
| 511 | '@effect/sql-sqlite-bun': | ||
| 512 | specifier: 4.0.0-rc.112 | ||
| 513 | version: 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)) | ||
| 514 | '@ff-labs/fff-node': | 506 | '@ff-labs/fff-node': |
| 515 | specifier: 0.9.4 | 507 | specifier: 0.9.4 |
| 516 | version: 0.9.4(patch_hash=ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368) | 508 | version: 0.9.4(patch_hash=c4e3cc2420ceb9dc650f9d189e9c24baf7e83f342998bacda5f459a4ce7927a8) |
| 517 | '@opencode-ai/sdk': | 509 | '@opencode-ai/sdk': |
| 518 | specifier: ^1.3.15 | 510 | specifier: ^1.3.15 |
| 519 | version: 1.15.13 | 511 | version: 1.15.13 |
| ... | @@ -557,9 +549,6 @@ importers: | ... | @@ -557,9 +549,6 @@ importers: |
| 557 | '@t3tools/web': | 549 | '@t3tools/web': |
| 558 | specifier: workspace:* | 550 | specifier: workspace:* |
| 559 | version: link:../web | 551 | version: link:../web |
| 560 | '@types/bun': | ||
| 561 | specifier: 1.3.14 | ||
| 562 | version: 1.3.14 | ||
| 563 | '@types/node': | 552 | '@types/node': |
| 564 | specifier: 24.12.4 | 553 | specifier: 24.12.4 |
| 565 | version: 24.12.4 | 554 | version: 24.12.4 |
| ... | @@ -1069,7 +1058,7 @@ packages: | ... | @@ -1069,7 +1058,7 @@ packages: |
| 1069 | resolution: {integrity: sha512-rH5BFh0Xq7aExpSpITyQbC+Jqfc9XgwJXidHVHTi1tNRC6/KAWg4tOzBfwiFIxljFLLYcApzlYFvLa/fTYcEjw==} | 1058 | resolution: {integrity: sha512-rH5BFh0Xq7aExpSpITyQbC+Jqfc9XgwJXidHVHTi1tNRC6/KAWg4tOzBfwiFIxljFLLYcApzlYFvLa/fTYcEjw==} |
| 1070 | peerDependencies: | 1059 | peerDependencies: |
| 1071 | '@distilled.cloud/cloudflare': 1.0.0-rc.8 | 1060 | '@distilled.cloud/cloudflare': 1.0.0-rc.8 |
| 1072 | '@effect/platform-bun': 4.0.0-rc.112 | 1061 | '@effect/platform-bun': '>=4.0.0-rc.112 || >=4.0.0' |
| 1073 | '@effect/platform-node': 4.0.0-rc.112 | 1062 | '@effect/platform-node': 4.0.0-rc.112 |
| 1074 | effect: 4.0.0-rc.112 | 1063 | effect: 4.0.0-rc.112 |
| 1075 | rolldown: 1.2.5 | 1064 | rolldown: 1.2.5 |
| ... | @@ -5353,9 +5342,6 @@ packages: | ... | @@ -5353,9 +5342,6 @@ packages: |
| 5353 | '@types/body-parser@1.19.6': | 5342 | '@types/body-parser@1.19.6': |
| 5354 | resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} | 5343 | resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} |
| 5355 | 5344 | ||
| 5356 | '@types/bun@1.3.14': | ||
| 5357 | resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} | ||
| 5358 | |||
| 5359 | '@types/cacheable-request@6.0.3': | 5345 | '@types/cacheable-request@6.0.3': |
| 5360 | resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} | 5346 | resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} |
| 5361 | 5347 | ||
| ... | @@ -6093,7 +6079,7 @@ packages: | ... | @@ -6093,7 +6079,7 @@ packages: |
| 6093 | peerDependencies: | 6079 | peerDependencies: |
| 6094 | '@alchemy.run/frontend-frameworks': 2.0.0-beta.76 | 6080 | '@alchemy.run/frontend-frameworks': 2.0.0-beta.76 |
| 6095 | '@aws/durable-execution-sdk-js': ^2.1.0 | 6081 | '@aws/durable-execution-sdk-js': ^2.1.0 |
| 6096 | '@effect/platform-bun': 4.0.0-rc.112 | 6082 | '@effect/platform-bun': '>=4.0.0-rc.112 || >=4.0.0' |
| 6097 | '@effect/platform-node': 4.0.0-rc.112 | 6083 | '@effect/platform-node': 4.0.0-rc.112 |
| 6098 | '@effect/sql-mysql2': '>=4.0.0-rc.112 || >=4.0.0' | 6084 | '@effect/sql-mysql2': '>=4.0.0-rc.112 || >=4.0.0' |
| 6099 | '@effect/sql-pg': 4.0.0-rc.112 | 6085 | '@effect/sql-pg': 4.0.0-rc.112 |
| ... | @@ -6973,7 +6959,7 @@ packages: | ... | @@ -6973,7 +6959,7 @@ packages: |
| 6973 | '@effect/sql-mysql2': '>=4.0.0-beta.105 || >=4.0.0' | 6959 | '@effect/sql-mysql2': '>=4.0.0-beta.105 || >=4.0.0' |
| 6974 | '@effect/sql-pg': 4.0.0-rc.112 | 6960 | '@effect/sql-pg': 4.0.0-rc.112 |
| 6975 | '@effect/sql-pglite': '>=4.0.0-beta.105 || >=4.0.0' | 6961 | '@effect/sql-pglite': '>=4.0.0-beta.105 || >=4.0.0' |
| 6976 | '@effect/sql-sqlite-bun': 4.0.0-rc.112 | 6962 | '@effect/sql-sqlite-bun': '>=4.0.0-beta.105 || >=4.0.0' |
| 6977 | '@effect/sql-sqlite-do': '>=4.0.0-beta.105 || >=4.0.0' | 6963 | '@effect/sql-sqlite-do': '>=4.0.0-beta.105 || >=4.0.0' |
| 6978 | '@effect/sql-sqlite-node': '>=4.0.0-beta.105 || >=4.0.0' | 6964 | '@effect/sql-sqlite-node': '>=4.0.0-beta.105 || >=4.0.0' |
| 6979 | '@effect/sql-sqlite-wasm': '>=4.0.0-beta.105 || >=4.0.0' | 6965 | '@effect/sql-sqlite-wasm': '>=4.0.0-beta.105 || >=4.0.0' |
| ... | @@ -12675,6 +12661,7 @@ snapshots: | ... | @@ -12675,6 +12661,7 @@ snapshots: |
| 12675 | transitivePeerDependencies: | 12661 | transitivePeerDependencies: |
| 12676 | - bufferutil | 12662 | - bufferutil |
| 12677 | - utf-8-validate | 12663 | - utf-8-validate |
| 12664 | optional: true | ||
| 12678 | 12665 | ||
| 12679 | '@effect/platform-node-shared@4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(utf-8-validate@6.0.6)': | 12666 | '@effect/platform-node-shared@4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(utf-8-validate@6.0.6)': |
| 12680 | dependencies: | 12667 | dependencies: |
| ... | @@ -12715,6 +12702,7 @@ snapshots: | ... | @@ -12715,6 +12702,7 @@ snapshots: |
| 12715 | '@effect/sql-sqlite-bun@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))': | 12702 | '@effect/sql-sqlite-bun@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))': |
| 12716 | dependencies: | 12703 | dependencies: |
| 12717 | effect: 4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2) | 12704 | effect: 4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2) |
| 12705 | optional: true | ||
| 12718 | 12706 | ||
| 12719 | '@effect/sql-sqlite-do@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))': | 12707 | '@effect/sql-sqlite-do@4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))': |
| 12720 | dependencies: | 12708 | dependencies: |
| ... | @@ -13664,7 +13652,7 @@ snapshots: | ... | @@ -13664,7 +13652,7 @@ snapshots: |
| 13664 | '@ff-labs/fff-bin-win32-x64@0.9.4': | 13652 | '@ff-labs/fff-bin-win32-x64@0.9.4': |
| 13665 | optional: true | 13653 | optional: true |
| 13666 | 13654 | ||
| 13667 | '@ff-labs/fff-node@0.9.4(patch_hash=ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368)': | 13655 | '@ff-labs/fff-node@0.9.4(patch_hash=c4e3cc2420ceb9dc650f9d189e9c24baf7e83f342998bacda5f459a4ce7927a8)': |
| 13668 | dependencies: | 13656 | dependencies: |
| 13669 | ffi-rs: 1.3.2 | 13657 | ffi-rs: 1.3.2 |
| 13670 | optionalDependencies: | 13658 | optionalDependencies: |
| ... | @@ -15848,10 +15836,6 @@ snapshots: | ... | @@ -15848,10 +15836,6 @@ snapshots: |
| 15848 | '@types/connect': 3.4.38 | 15836 | '@types/connect': 3.4.38 |
| 15849 | '@types/node': 24.12.4 | 15837 | '@types/node': 24.12.4 |
| 15850 | 15838 | ||
| 15851 | '@types/bun@1.3.14': | ||
| 15852 | dependencies: | ||
| 15853 | bun-types: 1.3.14 | ||
| 15854 | |||
| 15855 | '@types/cacheable-request@6.0.3': | 15839 | '@types/cacheable-request@6.0.3': |
| 15856 | dependencies: | 15840 | dependencies: |
| 15857 | '@types/http-cache-semantics': 4.2.0 | 15841 | '@types/http-cache-semantics': 4.2.0 |
| ... | @@ -17009,6 +16993,7 @@ snapshots: | ... | @@ -17009,6 +16993,7 @@ snapshots: |
| 17009 | bun-types@1.3.14: | 16993 | bun-types@1.3.14: |
| 17010 | dependencies: | 16994 | dependencies: |
| 17011 | '@types/node': 24.12.4 | 16995 | '@types/node': 24.12.4 |
| 16996 | optional: true | ||
| 17012 | 16997 | ||
| 17013 | bytes@3.1.2: {} | 16998 | bytes@3.1.2: {} |
| 17014 | 16999 |
pnpm-workspace.yaml-6| ... | @@ -32,11 +32,9 @@ catalog: | ... | @@ -32,11 +32,9 @@ catalog: |
| 32 | "@clerk/shared": 4.30.1 | 32 | "@clerk/shared": 4.30.1 |
| 33 | "@effect/atom-react": 4.0.0-rc.112 | 33 | "@effect/atom-react": 4.0.0-rc.112 |
| 34 | "@effect/openapi-generator": 4.0.0-rc.112 | 34 | "@effect/openapi-generator": 4.0.0-rc.112 |
| 35 | "@effect/platform-bun": 4.0.0-rc.112 | ||
| 36 | "@effect/platform-node": 4.0.0-rc.112 | 35 | "@effect/platform-node": 4.0.0-rc.112 |
| 37 | "@effect/platform-node-shared": 4.0.0-rc.112 | 36 | "@effect/platform-node-shared": 4.0.0-rc.112 |
| 38 | "@effect/sql-pg": 4.0.0-rc.112 | 37 | "@effect/sql-pg": 4.0.0-rc.112 |
| 39 | "@effect/sql-sqlite-bun": 4.0.0-rc.112 | ||
| 40 | "@effect/tsgo": 0.41.0 | 38 | "@effect/tsgo": 0.41.0 |
| 41 | "@effect/vitest": 4.0.0-rc.112 | 39 | "@effect/vitest": 4.0.0-rc.112 |
| 42 | "@legendapp/list": 3.3.5 | 40 | "@legendapp/list": 3.3.5 |
| ... | @@ -71,11 +69,9 @@ minimumReleaseAgeExclude: | ... | @@ -71,11 +69,9 @@ minimumReleaseAgeExclude: |
| 71 | - "@distilled.cloud/planetscale@0.30.2" | 69 | - "@distilled.cloud/planetscale@0.30.2" |
| 72 | - "@effect/atom-react@4.0.0-rc.112" | 70 | - "@effect/atom-react@4.0.0-rc.112" |
| 73 | - "@effect/openapi-generator@4.0.0-rc.112" | 71 | - "@effect/openapi-generator@4.0.0-rc.112" |
| 74 | - "@effect/platform-bun@4.0.0-rc.112" | ||
| 75 | - "@effect/platform-node-shared@4.0.0-rc.112" | 72 | - "@effect/platform-node-shared@4.0.0-rc.112" |
| 76 | - "@effect/platform-node@4.0.0-rc.112" | 73 | - "@effect/platform-node@4.0.0-rc.112" |
| 77 | - "@effect/sql-pg@4.0.0-rc.112" | 74 | - "@effect/sql-pg@4.0.0-rc.112" |
| 78 | - "@effect/sql-sqlite-bun@4.0.0-rc.112" | ||
| 79 | - "@effect/vitest@4.0.0-rc.112" | 75 | - "@effect/vitest@4.0.0-rc.112" |
| 80 | - alchemy@2.0.0-beta.76 | 76 | - alchemy@2.0.0-beta.76 |
| 81 | - effect@4.0.0-rc.112 | 77 | - effect@4.0.0-rc.112 |
| ... | @@ -121,11 +117,9 @@ overrides: | ... | @@ -121,11 +117,9 @@ overrides: |
| 121 | "@clerk/react": "catalog:" | 117 | "@clerk/react": "catalog:" |
| 122 | "@clerk/shared": "catalog:" | 118 | "@clerk/shared": "catalog:" |
| 123 | "@effect/atom-react": "catalog:" | 119 | "@effect/atom-react": "catalog:" |
| 124 | "@effect/platform-bun": "catalog:" | ||
| 125 | "@effect/platform-node": "catalog:" | 120 | "@effect/platform-node": "catalog:" |
| 126 | "@effect/platform-node-shared": "catalog:" | 121 | "@effect/platform-node-shared": "catalog:" |
| 127 | "@effect/sql-pg": "catalog:" | 122 | "@effect/sql-pg": "catalog:" |
| 128 | "@effect/sql-sqlite-bun": "catalog:" | ||
| 129 | "@effect/vitest": "catalog:" | 123 | "@effect/vitest": "catalog:" |
| 130 | "@effect/vitest>vitest": "-" | 124 | "@effect/vitest>vitest": "-" |
| 131 | "@expo/dom-webview": 57.0.1 | 125 | "@expo/dom-webview": 57.0.1 |
scripts/lib/cli-external-packages.test.ts+39-6| ... | @@ -11,6 +11,7 @@ import serverPackageJson from "../../apps/server/package.json" with { type: "jso | ... | @@ -11,6 +11,7 @@ import serverPackageJson from "../../apps/server/package.json" with { type: "jso |
| 11 | 11 | ||
| 12 | import { | 12 | import { |
| 13 | CLI_RUNTIME_EXTERNAL_PREFIXES, | 13 | CLI_RUNTIME_EXTERNAL_PREFIXES, |
| 14 | findEsmImportsOfExternalPackages, | ||
| 14 | findInlinedExternalPackages, | 15 | findInlinedExternalPackages, |
| 15 | selectCliRuntimeExternalDependencies, | 16 | selectCliRuntimeExternalDependencies, |
| 16 | shouldBundleCliDependency, | 17 | shouldBundleCliDependency, |
| ... | @@ -55,11 +56,6 @@ describe("shouldBundleCliDependency", () => { | ... | @@ -55,11 +56,6 @@ describe("shouldBundleCliDependency", () => { |
| 55 | } | 56 | } |
| 56 | }); | 57 | }); |
| 57 | 58 | ||
| 58 | it("leaves bun-only entry points external", () => { | ||
| 59 | assert.strictEqual(shouldBundleCliDependency("@effect/platform-bun"), false); | ||
| 60 | assert.strictEqual(shouldBundleCliDependency("@effect/sql-sqlite-bun"), false); | ||
| 61 | }); | ||
| 62 | |||
| 63 | // The real package is `node-gyp-build-optional-packages`, reached by prefix. | 59 | // The real package is `node-gyp-build-optional-packages`, reached by prefix. |
| 64 | // It is transitive to a selected dependency root, so the runtime closure test | 60 | // It is transitive to a selected dependency root, so the runtime closure test |
| 65 | // below ensures it follows that root into the sidecar. | 61 | // below ensures it follows that root into the sidecar. |
| ... | @@ -72,7 +68,6 @@ describe("selectCliRuntimeExternalDependencies", () => { | ... | @@ -72,7 +68,6 @@ describe("selectCliRuntimeExternalDependencies", () => { |
| 72 | it("keeps only runtime-external dependency roots for the Windows sidecar", () => { | 68 | it("keeps only runtime-external dependency roots for the Windows sidecar", () => { |
| 73 | assert.deepStrictEqual( | 69 | assert.deepStrictEqual( |
| 74 | selectCliRuntimeExternalDependencies({ | 70 | selectCliRuntimeExternalDependencies({ |
| 75 | "@effect/platform-bun": "1.0.0", | ||
| 76 | "@ff-labs/fff-node": "2.0.0", | 71 | "@ff-labs/fff-node": "2.0.0", |
| 77 | effect: "3.0.0", | 72 | effect: "3.0.0", |
| 78 | "node-pty": "4.0.0", | 73 | "node-pty": "4.0.0", |
| ... | @@ -281,3 +276,41 @@ var x = 1; | ... | @@ -281,3 +276,41 @@ var x = 1; |
| 281 | assert.deepStrictEqual(result.inlined, []); | 276 | assert.deepStrictEqual(result.inlined, []); |
| 282 | }); | 277 | }); |
| 283 | }); | 278 | }); |
| 279 | |||
| 280 | // The single-executable build can only `import` built-ins. A file-backed | ||
| 281 | // import of an external package passes every bundler check and the regular | ||
| 282 | // `node dist/bin.mjs` path, then fails inside the executable, so the scan | ||
| 283 | // reads the emitted module graph instead. | ||
| 284 | describe("findEsmImportsOfExternalPackages", () => { | ||
| 285 | it("flags static and dynamic imports of file-backed packages", () => { | ||
| 286 | const source = [ | ||
| 287 | 'import { FileFinder } from "@ff-labs/fff-node";', | ||
| 288 | 'import * as fs from "fs";', | ||
| 289 | 'import { createRequire } from "node:module";', | ||
| 290 | 'const pty = () => import("node-pty");', | ||
| 291 | 'const data = () => import("@ff-labs/fff-bin-linux-x64-gnu", { with: { type: "json" } });', | ||
| 292 | 'const lazy = () => import(/* @vite-ignore */ "ffi-rs");', | ||
| 293 | 'const local = () => import("./chunk-abc.mjs");', | ||
| 294 | ].join("\n"); | ||
| 295 | |||
| 296 | assert.deepStrictEqual(findEsmImportsOfExternalPackages(source), [ | ||
| 297 | "@ff-labs/fff-bin-linux-x64-gnu", | ||
| 298 | "@ff-labs/fff-node", | ||
| 299 | "ffi-rs", | ||
| 300 | "node-pty", | ||
| 301 | ]); | ||
| 302 | }); | ||
| 303 | |||
| 304 | it("flags side-effect imports and re-exports too", () => { | ||
| 305 | const source = ['import "msgpackr-extract";', 'export { load } from "ffi-rs";'].join("\n"); | ||
| 306 | assert.deepStrictEqual(findEsmImportsOfExternalPackages(source), [ | ||
| 307 | "ffi-rs", | ||
| 308 | "msgpackr-extract", | ||
| 309 | ]); | ||
| 310 | }); | ||
| 311 | |||
| 312 | it("does not mistake createRequire calls for imports", () => { | ||
| 313 | const source = 'const { FileFinder } = createRequire(import.meta.url)("@ff-labs/fff-node");'; | ||
| 314 | assert.deepStrictEqual(findEsmImportsOfExternalPackages(source), []); | ||
| 315 | }); | ||
| 316 | }); |
scripts/lib/cli-external-packages.ts+38-19| ... | @@ -1,3 +1,6 @@ | ... | @@ -1,3 +1,6 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off | ||
| 2 | import * as NodeModule from "node:module"; | ||
| 3 | |||
| 1 | /** | 4 | /** |
| 2 | * The single source of truth for packages the server CLI bundle must NOT inline. | 5 | * The single source of truth for packages the server CLI bundle must NOT inline. |
| 3 | * | 6 | * |
| ... | @@ -50,24 +53,6 @@ export const CLI_RUNTIME_EXTERNAL_PREFIXES = [ | ... | @@ -50,24 +53,6 @@ export const CLI_RUNTIME_EXTERNAL_PREFIXES = [ |
| 50 | "utf-8-validate", | 53 | "utf-8-validate", |
| 51 | ] as const; | 54 | ] as const; |
| 52 | 55 | ||
| 53 | /** | ||
| 54 | * External only so the bundler never has to resolve them. | ||
| 55 | * | ||
| 56 | * These are reached through a runtime-conditional dynamic import that Node | ||
| 57 | * never takes, and they resolve `bun:*` specifiers that do not exist when | ||
| 58 | * bundling for Node. Because Node never loads them, their dependency closure | ||
| 59 | * does not need to be external — only the entry point must stay unbundled. | ||
| 60 | */ | ||
| 61 | export const CLI_BUILD_ONLY_EXTERNAL_PREFIXES = [ | ||
| 62 | "@effect/platform-bun", | ||
| 63 | "@effect/sql-sqlite-bun", | ||
| 64 | ] as const; | ||
| 65 | |||
| 66 | export const CLI_EXTERNAL_PACKAGE_PREFIXES = [ | ||
| 67 | ...CLI_RUNTIME_EXTERNAL_PREFIXES, | ||
| 68 | ...CLI_BUILD_ONLY_EXTERNAL_PREFIXES, | ||
| 69 | ] as const; | ||
| 70 | |||
| 71 | export function isRuntimeExternalCliDependency(id: string): boolean { | 56 | export function isRuntimeExternalCliDependency(id: string): boolean { |
| 72 | return CLI_RUNTIME_EXTERNAL_PREFIXES.some((prefix) => id.startsWith(prefix)); | 57 | return CLI_RUNTIME_EXTERNAL_PREFIXES.some((prefix) => id.startsWith(prefix)); |
| 73 | } | 58 | } |
| ... | @@ -83,7 +68,7 @@ export function isRuntimeExternalCliDependency(id: string): boolean { | ... | @@ -83,7 +68,7 @@ export function isRuntimeExternalCliDependency(id: string): boolean { |
| 83 | * inlined while node-pty (a declared dependency) stayed external. | 68 | * inlined while node-pty (a declared dependency) stayed external. |
| 84 | */ | 69 | */ |
| 85 | export function isExternalCliDependency(id: string): boolean { | 70 | export function isExternalCliDependency(id: string): boolean { |
| 86 | return CLI_EXTERNAL_PACKAGE_PREFIXES.some((prefix) => id.startsWith(prefix)); | 71 | return isRuntimeExternalCliDependency(id); |
| 87 | } | 72 | } |
| 88 | 73 | ||
| 89 | /** True when the CLI bundle should inline `id` rather than leave it external. */ | 74 | /** True when the CLI bundle should inline `id` rather than leave it external. */ |
| ... | @@ -101,6 +86,40 @@ export function selectCliRuntimeExternalDependencies( | ... | @@ -101,6 +86,40 @@ export function selectCliRuntimeExternalDependencies( |
| 101 | ); | 86 | ); |
| 102 | } | 87 | } |
| 103 | 88 | ||
| 89 | /** | ||
| 90 | * Scan an emitted bundle chunk for ESM imports of packages that are not Node | ||
| 91 | * built-ins. | ||
| 92 | * | ||
| 93 | * Inside a Node single-executable, `import` statements and `import()` can only | ||
| 94 | * resolve built-in modules; any file-backed specifier throws at module | ||
| 95 | * evaluation (static) or at first use (dynamic). External packages therefore | ||
| 96 | * have to be reached through `createRequire`, which reads the real filesystem | ||
| 97 | * in every runtime. The bundler cannot enforce this, so the check reads what it | ||
| 98 | * produced. | ||
| 99 | */ | ||
| 100 | export function findEsmImportsOfExternalPackages(source: string): ReadonlyArray<string> { | ||
| 101 | const specifiers = new Set<string>(); | ||
| 102 | // `import x from`, `import "side-effect"`, `export ... from`, and `import()` | ||
| 103 | // all resolve through the module loader. | ||
| 104 | const patterns = [ | ||
| 105 | /^import\s[^;]*?\sfrom\s+["']([^"']+)["']/gm, | ||
| 106 | /^import\s+["']([^"']+)["']/gm, | ||
| 107 | /^export\s[^;]*?\sfrom\s+["']([^"']+)["']/gm, | ||
| 108 | // Rolldown may leave a `/* @vite-ignore */` style comment before the specifier. | ||
| 109 | /\bimport\(\s*(?:\/\*[\s\S]*?\*\/\s*)*["']([^"']+)["']\s*[,)]/g, | ||
| 110 | ]; | ||
| 111 | for (const pattern of patterns) { | ||
| 112 | for (const match of source.matchAll(pattern)) { | ||
| 113 | const specifier = match[1]; | ||
| 114 | if (specifier === undefined) continue; | ||
| 115 | if (NodeModule.isBuiltin(specifier)) continue; | ||
| 116 | if (specifier.startsWith("./") || specifier.startsWith("../")) continue; | ||
| 117 | specifiers.add(specifier); | ||
| 118 | } | ||
| 119 | } | ||
| 120 | return [...specifiers].sort(); | ||
| 121 | } | ||
| 122 | |||
| 104 | /** | 123 | /** |
| 105 | * Scan an emitted bundle chunk for runtime-external packages that were inlined. | 124 | * Scan an emitted bundle chunk for runtime-external packages that were inlined. |
| 106 | * | 125 | * |