@briancray/belte 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@briancray/belte",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
6
6
  "license": "MIT",
package/src/bundleApp.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { dirname } from 'node:path'
1
2
  import { buildDisconnected } from './buildDisconnected.ts'
2
3
  import { compile } from './compile.ts'
3
4
  import { ensureWebviewLib } from './lib/bundle/ensureWebviewLib.ts'
@@ -11,6 +12,7 @@ import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
11
12
  import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
12
13
  import { log } from './lib/shared/log.ts'
13
14
  import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
15
+ import { shippedEnvPath } from './lib/shared/shippedEnvPath.ts'
14
16
  import { serverBuildPlugins } from './serverBuildPlugins.ts'
15
17
 
16
18
  const APP_ENTRY = new URL('./appEntry.ts', import.meta.url).pathname
@@ -57,15 +59,19 @@ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}):
57
59
  await compile({ cwd, target, outfile: `${binDir}/${serverBinaryFilename()}` })
58
60
 
59
61
  /*
60
- Opt-in: ship the project's `.env.bundle` as the binary-dir `.env`, which the
62
+ Opt-in: ship the project's `.env.bundle` as the shipped `.env`, which the
61
63
  server loads at boot (loadEnvFromBinaryDir) as its default config layer. A
62
64
  dedicated file, never the working `.env` — a compiled bundle is extractable,
63
65
  so only ship-safe defaults belong here; user-specific/secret values come from
64
- the data-dir `.env` instead. Skipped when absent.
66
+ the data-dir `.env` instead. shippedEnvPath places it under Contents/Resources
67
+ in a macOS `.app` (sealed as a resource, so it survives codesign) and beside
68
+ the binaries otherwise. Skipped when absent.
65
69
  */
66
70
  const bundleEnv = Bun.file(`${cwd}/.env.bundle`)
67
71
  if (await bundleEnv.exists()) {
68
- await Bun.write(`${binDir}/.env`, bundleEnv)
72
+ const envPath = shippedEnvPath(binDir)
73
+ await Bun.$`mkdir -p ${dirname(envPath)}`.quiet()
74
+ await Bun.write(envPath, bundleEnv)
69
75
  }
70
76
 
71
77
  // 2. Connect screen — bake dist/bundle-disconnected.html before the launcher
@@ -9,10 +9,12 @@ import { resolveServerBinary } from './lib/bundle/resolveServerBinary.ts'
9
9
  import { resolveWebviewLib } from './lib/bundle/resolveWebviewLib.ts'
10
10
  import { stableLocalPort } from './lib/bundle/stableLocalPort.ts'
11
11
  import { waitForServer } from './lib/bundle/waitForServer.ts'
12
+ import { parsePort } from './lib/server/runtime/parsePort.ts'
12
13
  import { appDataDir } from './lib/shared/appDataDir.ts'
13
14
  import { log } from './lib/shared/log.ts'
14
15
  import { readEnvFile } from './lib/shared/readEnvFile.ts'
15
16
  import { serializeEnv } from './lib/shared/serializeEnv.ts'
17
+ import { shippedEnvPath } from './lib/shared/shippedEnvPath.ts'
16
18
 
17
19
  /*
18
20
  The bundle's control server, run in a Worker so it owns its own thread.
@@ -160,9 +162,26 @@ Spawns the sibling server binary on a free port and waits for it to answer,
160
162
  returning the URL to point the window at. Any previous child is reaped first so
161
163
  only one embedded server runs at a time.
162
164
  */
165
+ /*
166
+ The port the embedded server binds. A `PORT` configured in the data-dir `.env`
167
+ (where the config form writes), the shipped binary-dir `.env`, or the launcher's
168
+ own env is honored — so the server answers at a fixed, known address another
169
+ machine can reliably connect to. With none set, a free port is chosen (the
170
+ historical behaviour). Precedence matches the server's own env stack: shell >
171
+ data-dir > binary-dir. A configured port is used as-is and not second-guessed —
172
+ if it's taken, the bind failure surfaces rather than silently moving.
173
+ */
174
+ async function resolveEmbeddedPort(): Promise<number> {
175
+ const [dataDirEnv, binaryDirEnv] = await Promise.all([
176
+ readEnvFile(dataDirEnvPath()),
177
+ readEnvFile(binaryDirEnvPath()),
178
+ ])
179
+ return parsePort(process.env.PORT ?? dataDirEnv.PORT ?? binaryDirEnv.PORT) ?? findFreePort()
180
+ }
181
+
163
182
  async function startEmbeddedServer(timeoutMs?: number): Promise<string> {
164
183
  killServerChild()
165
- const port = findFreePort()
184
+ const port = await resolveEmbeddedPort()
166
185
  const url = `http://localhost:${port}`
167
186
  serverChild = Bun.spawn({
168
187
  cmd: [resolveServerBinary()],
@@ -267,6 +286,13 @@ function dataDirEnvPath(): string {
267
286
  return join(appDataDir(programName), '.env')
268
287
  }
269
288
 
289
+ // The bundle's shipped `.env` (its default config layer), resolved from the binary
290
+ // directory — same source loadEnvFromBinaryDir reads at boot (dirname of the running
291
+ // binary): beside the binary in the flat layout, under Resources in a `.app`.
292
+ function binaryDirEnvPath(): string {
293
+ return shippedEnvPath(dirname(process.execPath))
294
+ }
295
+
270
296
  /*
271
297
  Resolves the value to pre-fill each config field with, following the same
272
298
  precedence the server applies below the shell: the user's saved data-dir `.env`,
@@ -279,7 +305,7 @@ async function resolveConfigValues(): Promise<Record<string, string>> {
279
305
  // Independent reads — fetch together; precedence is applied in the merge below.
280
306
  const [dataDirEnv, binaryDirEnv] = await Promise.all([
281
307
  readEnvFile(dataDirEnvPath()),
282
- readEnvFile(join(dirname(resolveServerBinary()), '.env')),
308
+ readEnvFile(binaryDirEnvPath()),
283
309
  ])
284
310
  return Object.fromEntries(
285
311
  Object.keys(properties).map((key) => {
@@ -1,14 +1,16 @@
1
1
  import { dirname } from 'node:path'
2
2
  import { loadEnvFile } from '../shared/loadEnvFile.ts'
3
+ import { shippedEnvPath } from '../shared/shippedEnvPath.ts'
3
4
 
4
5
  /*
5
- Loads a `.env` sitting next to the running binary (resolved via
6
- `process.execPath`) into `process.env`. This is the file the install tarball
7
- ships beside the executable and, for a bundle, the one `bundleApp` copies
8
- from the project's `.env.bundle`. It carries the app's shipped defaults; the
6
+ Loads the bundle's shipped `.env` into `process.env`, resolved from the running
7
+ binary's directory (`process.execPath`) via shippedEnvPath beside the binary in
8
+ the flat layout, under `Contents/Resources` in a macOS `.app`. This is the file the
9
+ install tarball ships beside the executable and, for a bundle, the one `bundleApp`
10
+ copies from the project's `.env.bundle`. It carries the app's shipped defaults; the
9
11
  fill-when-unset merge (see loadEnvFile) lets per-shell exports, Bun's CWD
10
12
  `.env`, and the user's data-dir config all override it.
11
13
  */
12
14
  export async function loadEnvFromBinaryDir(): Promise<void> {
13
- await loadEnvFile(`${dirname(process.execPath)}/.env`)
15
+ await loadEnvFile(shippedEnvPath(dirname(process.execPath)))
14
16
  }
@@ -0,0 +1,21 @@
1
+ import { basename, dirname, join } from 'node:path'
2
+
3
+ /*
4
+ Path of the bundle's shipped `.env` — the default config layer written at build
5
+ time and read back at boot. Given the directory holding the binaries, returns
6
+ where that `.env` lives.
7
+
8
+ A macOS `.app` nests binaries under `Contents/MacOS`, but `codesign` seals that
9
+ directory as *code*: a data file there can't survive signing and reloading. So
10
+ for the `.app` layout the `.env` belongs beside the icon in `Contents/Resources`,
11
+ which is sealed as a resource. Every other platform keeps the flat layout, with
12
+ the `.env` next to the binaries. Pure: computes the path, never touches disk.
13
+ */
14
+ export function shippedEnvPath(binaryDir: string): string {
15
+ const isMacAppBinaryDir =
16
+ basename(binaryDir) === 'MacOS' && basename(dirname(binaryDir)) === 'Contents'
17
+ if (isMacAppBinaryDir) {
18
+ return join(dirname(binaryDir), 'Resources', '.env')
19
+ }
20
+ return join(binaryDir, '.env')
21
+ }