@briancray/belte 0.5.1 → 0.5.3
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 +1 -1
- package/src/belteResolverPlugin.ts +39 -93
- package/src/buildCli.ts +6 -8
- package/src/bundleApp.ts +17 -15
- package/src/compile.ts +2 -1
- package/src/controlServerWorker.ts +75 -59
- package/src/lib/browser/page.svelte.ts +1 -1
- package/src/lib/browser/remoteProxy.ts +2 -9
- package/src/lib/browser/socketProxy.ts +2 -9
- package/src/lib/bundle/resolveWebviewLib.ts +5 -3
- package/src/lib/cli/loadEnvFromBinaryDir.ts +7 -5
- package/src/lib/cli/runCli.ts +4 -2
- package/src/lib/mcp/dispatchMcpRequest.ts +1 -2
- package/src/lib/server/cli/buildEnvContent.ts +6 -5
- package/src/lib/server/cli/handleCliDownload.ts +1 -2
- package/src/lib/server/cli/handleCliInstall.ts +1 -1
- package/src/lib/server/cli/maxSourceMtime.ts +16 -17
- package/src/lib/server/runtime/createServer.ts +13 -14
- package/src/lib/server/sockets/createSocketDispatcher.ts +12 -34
- package/src/lib/shared/belteImportName.ts +5 -6
- package/src/lib/shared/browserClientFlags.ts +10 -0
- package/src/lib/shared/bundleLayout.ts +36 -0
- package/src/lib/shared/exeSuffix.ts +9 -0
- package/src/lib/shared/manifestModule.ts +34 -0
- package/src/lib/shared/memoizeByKey.ts +24 -0
- package/src/lib/shared/readPackageJson.ts +9 -0
package/package.json
CHANGED
|
@@ -6,6 +6,7 @@ import { belteImportName } from './lib/shared/belteImportName.ts'
|
|
|
6
6
|
import { fileStem } from './lib/shared/fileStem.ts'
|
|
7
7
|
import { jsonSchemaForPromptArguments } from './lib/shared/jsonSchemaForPromptArguments.ts'
|
|
8
8
|
import { log } from './lib/shared/log.ts'
|
|
9
|
+
import { manifestModule } from './lib/shared/manifestModule.ts'
|
|
9
10
|
import { pageUrlForFile } from './lib/shared/pageUrlForFile.ts'
|
|
10
11
|
import { parsePromptMarkdown } from './lib/shared/parsePromptMarkdown.ts'
|
|
11
12
|
import { prepareRpcModule } from './lib/shared/prepareRpcModule.ts'
|
|
@@ -315,111 +316,56 @@ ${optionLines}
|
|
|
315
316
|
|
|
316
317
|
build.onLoad({ filter: /.*/, namespace: NS }, async (args) => {
|
|
317
318
|
if (args.path === 'belte:rpc') {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
` ${JSON.stringify(url)}: () => import(${JSON.stringify(`${rpcDir}/${file}`)}),`,
|
|
326
|
-
)
|
|
327
|
-
.join('\n')
|
|
328
|
-
if (byUrl.length > 0) {
|
|
329
|
-
log.info(
|
|
330
|
-
`resolved ${byUrl.length} rpc modules: ${byUrl.map((b) => b.url).join(', ')}`,
|
|
331
|
-
)
|
|
332
|
-
}
|
|
333
|
-
return {
|
|
334
|
-
contents: `export const rpc = {\n${entries}\n}\n`,
|
|
335
|
-
loader: 'js',
|
|
336
|
-
}
|
|
319
|
+
return manifestModule({
|
|
320
|
+
files: await scanRpcOnce(),
|
|
321
|
+
keyForFile: rpcUrlForFile,
|
|
322
|
+
importDir: rpcDir,
|
|
323
|
+
exportName: 'rpc',
|
|
324
|
+
label: 'rpc modules',
|
|
325
|
+
})
|
|
337
326
|
}
|
|
338
327
|
|
|
339
328
|
if (args.path === 'belte:sockets') {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
` ${JSON.stringify(name)}: () => import(${JSON.stringify(`${socketsDir}/${file}`)}),`,
|
|
348
|
-
)
|
|
349
|
-
.join('\n')
|
|
350
|
-
if (byName.length > 0) {
|
|
351
|
-
log.info(
|
|
352
|
-
`resolved ${byName.length} socket modules: ${byName.map((b) => b.name).join(', ')}`,
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
return {
|
|
356
|
-
contents: `export const sockets = {\n${entries}\n}\n`,
|
|
357
|
-
loader: 'js',
|
|
358
|
-
}
|
|
329
|
+
return manifestModule({
|
|
330
|
+
files: await scanSocketsOnce(),
|
|
331
|
+
keyForFile: socketNameForFile,
|
|
332
|
+
importDir: socketsDir,
|
|
333
|
+
exportName: 'sockets',
|
|
334
|
+
label: 'socket modules',
|
|
335
|
+
})
|
|
359
336
|
}
|
|
360
337
|
|
|
361
338
|
if (args.path === 'belte:prompts') {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
` ${JSON.stringify(name)}: () => import(${JSON.stringify(`${promptsDir}/${file}`)}),`,
|
|
370
|
-
)
|
|
371
|
-
.join('\n')
|
|
372
|
-
if (byName.length > 0) {
|
|
373
|
-
log.info(
|
|
374
|
-
`resolved ${byName.length} prompt modules: ${byName.map((b) => b.name).join(', ')}`,
|
|
375
|
-
)
|
|
376
|
-
}
|
|
377
|
-
return {
|
|
378
|
-
contents: `export const prompts = {\n${entries}\n}\n`,
|
|
379
|
-
loader: 'js',
|
|
380
|
-
}
|
|
339
|
+
return manifestModule({
|
|
340
|
+
files: await scanPromptsOnce(),
|
|
341
|
+
keyForFile: promptNameForFile,
|
|
342
|
+
importDir: promptsDir,
|
|
343
|
+
exportName: 'prompts',
|
|
344
|
+
label: 'prompt modules',
|
|
345
|
+
})
|
|
381
346
|
}
|
|
382
347
|
|
|
383
348
|
if (args.path === 'belte:pages') {
|
|
384
|
-
const { pageFiles
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
.join('\n')
|
|
394
|
-
log.info(
|
|
395
|
-
`resolved ${byUrl.length} pages: ${byUrl.map((b) => b.url).join(', ')}`,
|
|
396
|
-
)
|
|
397
|
-
return {
|
|
398
|
-
contents: `export const pages = {\n${entries}\n}\n`,
|
|
399
|
-
loader: 'js',
|
|
400
|
-
}
|
|
349
|
+
const { pageFiles } = await scanPagesOnce()
|
|
350
|
+
return manifestModule({
|
|
351
|
+
files: pageFiles,
|
|
352
|
+
keyForFile: pageUrlForFile,
|
|
353
|
+
importDir: pagesDir,
|
|
354
|
+
exportName: 'pages',
|
|
355
|
+
label: 'pages',
|
|
356
|
+
logWhenEmpty: true,
|
|
357
|
+
})
|
|
401
358
|
}
|
|
402
359
|
|
|
403
360
|
if (args.path === 'belte:layouts') {
|
|
404
|
-
const { layoutFiles
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
)
|
|
413
|
-
.join('\n')
|
|
414
|
-
if (byPrefix.length > 0) {
|
|
415
|
-
log.info(
|
|
416
|
-
`resolved ${byPrefix.length} layouts: ${byPrefix.map((b) => b.prefix).join(', ')}`,
|
|
417
|
-
)
|
|
418
|
-
}
|
|
419
|
-
return {
|
|
420
|
-
contents: `export const layouts = {\n${entries}\n}\n`,
|
|
421
|
-
loader: 'js',
|
|
422
|
-
}
|
|
361
|
+
const { layoutFiles } = await scanPagesOnce()
|
|
362
|
+
return manifestModule({
|
|
363
|
+
files: layoutFiles,
|
|
364
|
+
keyForFile: pageUrlForFile,
|
|
365
|
+
importDir: pagesDir,
|
|
366
|
+
exportName: 'layouts',
|
|
367
|
+
label: 'layouts',
|
|
368
|
+
})
|
|
423
369
|
}
|
|
424
370
|
|
|
425
371
|
if (args.path === 'belte:app') {
|
package/src/buildCli.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { CompileTarget } from './lib/server/runtime/types/CompileTarget.ts'
|
|
2
2
|
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
3
|
+
import { exeSuffix } from './lib/shared/exeSuffix.ts'
|
|
3
4
|
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
4
5
|
import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
|
|
5
6
|
import { log } from './lib/shared/log.ts'
|
|
6
7
|
import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
|
|
8
|
+
import { readPackageJson } from './lib/shared/readPackageJson.ts'
|
|
7
9
|
import { serverBuildPlugins } from './serverBuildPlugins.ts'
|
|
8
10
|
|
|
9
11
|
const DISCOVERY_ENTRY = new URL('./discoveryEntry.ts', import.meta.url).pathname
|
|
@@ -92,7 +94,7 @@ export async function buildCli({
|
|
|
92
94
|
return Promise.all(
|
|
93
95
|
platforms.map(async (platformTarget) => {
|
|
94
96
|
const shortName = platformTarget.replace(/^bun-/, '')
|
|
95
|
-
const suffix = platformTarget
|
|
97
|
+
const suffix = exeSuffix(platformTarget)
|
|
96
98
|
const platformOut = `${distDir}/cli-thin/${shortName}/${programName}${suffix}`
|
|
97
99
|
await Bun.$`mkdir -p ${`${distDir}/cli-thin/${shortName}`}`.quiet()
|
|
98
100
|
const result = await Bun.build({
|
|
@@ -108,7 +110,7 @@ export async function buildCli({
|
|
|
108
110
|
)
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
const suffix = target
|
|
113
|
+
const suffix = exeSuffix(target)
|
|
112
114
|
const outPath = outfile ?? `${distDir}/cli${suffix}`
|
|
113
115
|
|
|
114
116
|
const cliResult = await Bun.build({
|
|
@@ -124,10 +126,6 @@ export async function buildCli({
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
async function readProgramName(cwd: string): Promise<string> {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
return 'app'
|
|
130
|
-
}
|
|
131
|
-
const pkg = (await pkgFile.json()) as { name?: string }
|
|
132
|
-
return programNameForPackage(pkg.name)
|
|
129
|
+
const pkg = (await readPackageJson(cwd)) as { name?: string } | undefined
|
|
130
|
+
return programNameForPackage(pkg?.name)
|
|
133
131
|
}
|
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'
|
|
@@ -6,11 +7,13 @@ import { pngToIcns } from './lib/bundle/pngToIcns.ts'
|
|
|
6
7
|
import { serverBinaryFilename } from './lib/bundle/serverBinaryFilename.ts'
|
|
7
8
|
import { signMacApp } from './lib/bundle/signMacApp.ts'
|
|
8
9
|
import { webviewLibName } from './lib/bundle/webviewLibName.ts'
|
|
10
|
+
import { bundleLayout } from './lib/shared/bundleLayout.ts'
|
|
9
11
|
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
10
12
|
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
11
13
|
import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
|
|
12
14
|
import { log } from './lib/shared/log.ts'
|
|
13
15
|
import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
|
|
16
|
+
import { readPackageJson } from './lib/shared/readPackageJson.ts'
|
|
14
17
|
import { serverBuildPlugins } from './serverBuildPlugins.ts'
|
|
15
18
|
|
|
16
19
|
const APP_ENTRY = new URL('./appEntry.ts', import.meta.url).pathname
|
|
@@ -38,15 +41,16 @@ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}):
|
|
|
38
41
|
const svelteConfig = await loadSvelteConfig(cwd)
|
|
39
42
|
|
|
40
43
|
/*
|
|
41
|
-
Layout differs by OS: a macOS .app nests binaries under Contents/MacOS
|
|
42
|
-
|
|
43
|
-
in one directory.
|
|
44
|
-
|
|
44
|
+
Layout differs by OS: a macOS .app nests binaries under Contents/MacOS, the
|
|
45
|
+
lib under Contents/Frameworks, and data under Contents/Resources; elsewhere
|
|
46
|
+
everything sits flat in one directory. bundleLayout derives the rest from
|
|
47
|
+
binDir — the same source the boot readers resolve from — so build and runtime
|
|
48
|
+
agree on where the lib, resources, and shipped `.env` land.
|
|
45
49
|
*/
|
|
46
50
|
const isMac = process.platform === 'darwin'
|
|
47
51
|
const bundleRoot = isMac ? `${cwd}/dist/${programName}.app` : `${cwd}/dist/${programName}`
|
|
48
52
|
const binDir = isMac ? `${bundleRoot}/Contents/MacOS` : bundleRoot
|
|
49
|
-
const libDir
|
|
53
|
+
const { libDir, resourcesDir, envPath } = bundleLayout(binDir)
|
|
50
54
|
|
|
51
55
|
await Bun.$`rm -rf ${bundleRoot}`.quiet()
|
|
52
56
|
await Bun.$`mkdir -p ${binDir} ${libDir}`.quiet()
|
|
@@ -57,15 +61,18 @@ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}):
|
|
|
57
61
|
await compile({ cwd, target, outfile: `${binDir}/${serverBinaryFilename()}` })
|
|
58
62
|
|
|
59
63
|
/*
|
|
60
|
-
Opt-in: ship the project's `.env.bundle` as the
|
|
64
|
+
Opt-in: ship the project's `.env.bundle` as the shipped `.env`, which the
|
|
61
65
|
server loads at boot (loadEnvFromBinaryDir) as its default config layer. A
|
|
62
66
|
dedicated file, never the working `.env` — a compiled bundle is extractable,
|
|
63
67
|
so only ship-safe defaults belong here; user-specific/secret values come from
|
|
64
|
-
the data-dir `.env` instead.
|
|
68
|
+
the data-dir `.env` instead. bundleLayout places it under Contents/Resources
|
|
69
|
+
in a macOS `.app` (sealed as a resource, so it survives codesign) and beside
|
|
70
|
+
the binaries otherwise. Skipped when absent.
|
|
65
71
|
*/
|
|
66
72
|
const bundleEnv = Bun.file(`${cwd}/.env.bundle`)
|
|
67
73
|
if (await bundleEnv.exists()) {
|
|
68
|
-
await Bun
|
|
74
|
+
await Bun.$`mkdir -p ${dirname(envPath)}`.quiet()
|
|
75
|
+
await Bun.write(envPath, bundleEnv)
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
// 2. Connect screen — bake dist/bundle-disconnected.html before the launcher
|
|
@@ -104,7 +111,6 @@ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}):
|
|
|
104
111
|
converted via sips + iconutil so authors don't need to make an .icns.
|
|
105
112
|
*/
|
|
106
113
|
if (isMac) {
|
|
107
|
-
const resourcesDir = `${bundleRoot}/Contents/Resources`
|
|
108
114
|
const icnsSource = `${cwd}/src/bundle/icon.icns`
|
|
109
115
|
const pngSource = `${cwd}/src/bundle/icon.png`
|
|
110
116
|
let hasIcon = false
|
|
@@ -136,10 +142,6 @@ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}):
|
|
|
136
142
|
|
|
137
143
|
// Reads name + version from package.json, with fallbacks when absent.
|
|
138
144
|
async function readPackage(cwd: string): Promise<{ name: string | undefined; version: string }> {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
return { name: undefined, version: '0.0.0' }
|
|
142
|
-
}
|
|
143
|
-
const pkg = (await pkgFile.json()) as { name?: string; version?: string }
|
|
144
|
-
return { name: pkg.name, version: pkg.version ?? '0.0.0' }
|
|
145
|
+
const pkg = (await readPackageJson(cwd)) as { name?: string; version?: string } | undefined
|
|
146
|
+
return { name: pkg?.name, version: pkg?.version ?? '0.0.0' }
|
|
145
147
|
}
|
package/src/compile.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { build } from './build.ts'
|
|
2
2
|
import type { CompileTarget } from './lib/server/runtime/types/CompileTarget.ts'
|
|
3
3
|
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
4
|
+
import { exeSuffix } from './lib/shared/exeSuffix.ts'
|
|
4
5
|
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
5
6
|
import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
|
|
6
7
|
import { log } from './lib/shared/log.ts'
|
|
@@ -28,7 +29,7 @@ export async function compile({
|
|
|
28
29
|
const svelteConfig = await loadSvelteConfig(cwd)
|
|
29
30
|
await build({ cwd, svelteConfig })
|
|
30
31
|
|
|
31
|
-
const outPath = outfile ?? `${cwd}/dist/app${target
|
|
32
|
+
const outPath = outfile ?? `${cwd}/dist/app${exeSuffix(target)}`
|
|
32
33
|
|
|
33
34
|
const result = await Bun.build({
|
|
34
35
|
entrypoints: [SERVER_ENTRY],
|
|
@@ -11,6 +11,7 @@ import { stableLocalPort } from './lib/bundle/stableLocalPort.ts'
|
|
|
11
11
|
import { waitForServer } from './lib/bundle/waitForServer.ts'
|
|
12
12
|
import { parsePort } from './lib/server/runtime/parsePort.ts'
|
|
13
13
|
import { appDataDir } from './lib/shared/appDataDir.ts'
|
|
14
|
+
import { bundleLayout } from './lib/shared/bundleLayout.ts'
|
|
14
15
|
import { log } from './lib/shared/log.ts'
|
|
15
16
|
import { readEnvFile } from './lib/shared/readEnvFile.ts'
|
|
16
17
|
import { serializeEnv } from './lib/shared/serializeEnv.ts'
|
|
@@ -285,9 +286,11 @@ function dataDirEnvPath(): string {
|
|
|
285
286
|
return join(appDataDir(programName), '.env')
|
|
286
287
|
}
|
|
287
288
|
|
|
288
|
-
// The shipped `.env`
|
|
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`.
|
|
289
292
|
function binaryDirEnvPath(): string {
|
|
290
|
-
return
|
|
293
|
+
return bundleLayout(dirname(process.execPath)).envPath
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
/*
|
|
@@ -362,68 +365,81 @@ async function clearLastConnection(): Promise<void> {
|
|
|
362
365
|
await rm(lastConnectionPath(), { force: true })
|
|
363
366
|
}
|
|
364
367
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
async function handleControlRequest(request: Request): Promise<Response> {
|
|
370
|
-
const url = new URL(request.url)
|
|
371
|
-
if (request.method === 'GET' && url.pathname === '/') {
|
|
372
|
-
return renderConnectScreen()
|
|
373
|
-
}
|
|
374
|
-
if (request.method === 'GET' && url.pathname === '/__belte/config') {
|
|
375
|
-
// No schema declared → null tells the form to skip the gate entirely.
|
|
376
|
-
if (!configSchema) {
|
|
377
|
-
return Response.json({ schema: null, values: {} })
|
|
378
|
-
}
|
|
379
|
-
return Response.json({ schema: configSchema, values: await resolveConfigValues() })
|
|
368
|
+
// GET /__belte/config — the form's schema + current values, or null schema to skip the gate.
|
|
369
|
+
async function handleConfigGet(): Promise<Response> {
|
|
370
|
+
if (!configSchema) {
|
|
371
|
+
return Response.json({ schema: null, values: {} })
|
|
380
372
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
373
|
+
return Response.json({ schema: configSchema, values: await resolveConfigValues() })
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// POST /__belte/config — persist the form's answers to the data-dir `.env`.
|
|
377
|
+
async function handleConfigPost(request: Request): Promise<Response> {
|
|
378
|
+
const { values } = (await request.json()) as { values: Record<string, string> }
|
|
379
|
+
await writeConfig(values)
|
|
380
|
+
return new Response(undefined, { status: 204 })
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// POST /connect — point the window at a remote belte server after probing it.
|
|
384
|
+
async function handleConnect(request: Request): Promise<Response> {
|
|
385
|
+
const { url: target } = (await request.json()) as { url: string }
|
|
386
|
+
// Verify it's actually a belte server before pointing the window at it.
|
|
387
|
+
const identity = await probeBelteServer(target)
|
|
388
|
+
if (!identity) {
|
|
389
|
+
log.warn(`no belte server responded at ${target}`)
|
|
390
|
+
return Response.json({ error: `No belte server responded at ${target}` }, { status: 502 })
|
|
385
391
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
392
|
+
flag?.setConnected(true)
|
|
393
|
+
startLivenessWatch(target)
|
|
394
|
+
// Record the choice so the next launch reconnects here before opening.
|
|
395
|
+
await writeLastConnection({ kind: 'url', url: target })
|
|
396
|
+
log.info(`connecting to ${identity.name} at ${target}`)
|
|
397
|
+
return Response.json({ redirect: target })
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// POST /start — boot the embedded server and point the window at it.
|
|
401
|
+
async function handleStart(): Promise<Response> {
|
|
402
|
+
try {
|
|
403
|
+
const localUrl = await startEmbeddedServer()
|
|
397
404
|
flag?.setConnected(true)
|
|
398
|
-
startLivenessWatch(
|
|
399
|
-
// Record the choice so the next launch
|
|
400
|
-
await writeLastConnection({ kind: '
|
|
401
|
-
log.info(`
|
|
402
|
-
return Response.json({ redirect:
|
|
403
|
-
}
|
|
404
|
-
if (request.method === 'POST' && url.pathname === '/start') {
|
|
405
|
-
try {
|
|
406
|
-
const localUrl = await startEmbeddedServer()
|
|
407
|
-
flag?.setConnected(true)
|
|
408
|
-
startLivenessWatch(localUrl)
|
|
409
|
-
// Record the choice so the next launch boots the embedded server first.
|
|
410
|
-
await writeLastConnection({ kind: 'embedded' })
|
|
411
|
-
log.info(`started embedded server at ${localUrl}`)
|
|
412
|
-
return Response.json({ redirect: localUrl })
|
|
413
|
-
} catch (error) {
|
|
414
|
-
killServerChild()
|
|
415
|
-
return Response.json({ error: String(error) }, { status: 500 })
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
if (request.method === 'GET' && url.pathname === '/__belte/disconnect') {
|
|
419
|
-
stopLivenessWatch()
|
|
405
|
+
startLivenessWatch(localUrl)
|
|
406
|
+
// Record the choice so the next launch boots the embedded server first.
|
|
407
|
+
await writeLastConnection({ kind: 'embedded' })
|
|
408
|
+
log.info(`started embedded server at ${localUrl}`)
|
|
409
|
+
return Response.json({ redirect: localUrl })
|
|
410
|
+
} catch (error) {
|
|
420
411
|
killServerChild()
|
|
421
|
-
|
|
422
|
-
// Forget the auto-resume choice so the next launch lands on the connect screen.
|
|
423
|
-
await clearLastConnection()
|
|
424
|
-
return new Response(undefined, { status: 204 })
|
|
412
|
+
return Response.json({ error: String(error) }, { status: 500 })
|
|
425
413
|
}
|
|
426
|
-
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// GET /__belte/disconnect — tear down the embedded server and forget the auto-resume choice.
|
|
417
|
+
async function handleDisconnect(): Promise<Response> {
|
|
418
|
+
stopLivenessWatch()
|
|
419
|
+
killServerChild()
|
|
420
|
+
flag?.setConnected(false)
|
|
421
|
+
await clearLastConnection()
|
|
422
|
+
return new Response(undefined, { status: 204 })
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/*
|
|
426
|
+
The control server's routes, keyed by `${method} ${pathname}` (exact match). The
|
|
427
|
+
connect screen owns localStorage + navigation; this worker owns the embedded-
|
|
428
|
+
server process and the native flag.
|
|
429
|
+
*/
|
|
430
|
+
const controlRoutes: Record<string, (request: Request) => Promise<Response> | Response> = {
|
|
431
|
+
'GET /': () => renderConnectScreen(),
|
|
432
|
+
'GET /__belte/config': handleConfigGet,
|
|
433
|
+
'POST /__belte/config': handleConfigPost,
|
|
434
|
+
'POST /connect': handleConnect,
|
|
435
|
+
'POST /start': handleStart,
|
|
436
|
+
'GET /__belte/disconnect': handleDisconnect,
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function handleControlRequest(request: Request): Promise<Response> | Response {
|
|
440
|
+
const { pathname } = new URL(request.url)
|
|
441
|
+
const route = controlRoutes[`${request.method} ${pathname}`]
|
|
442
|
+
return route ? route(request) : new Response('not found', { status: 404 })
|
|
427
443
|
}
|
|
428
444
|
|
|
429
445
|
/*
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import type { HttpVerb } from '../server/rpc/types/HttpVerb.ts'
|
|
2
2
|
import type { RemoteFunction } from '../server/rpc/types/RemoteFunction.ts'
|
|
3
|
+
import { browserClientFlags } from '../shared/browserClientFlags.ts'
|
|
3
4
|
import { buildRpcRequest } from '../shared/buildRpcRequest.ts'
|
|
4
5
|
import { createRemoteFunction } from '../shared/createRemoteFunction.ts'
|
|
5
6
|
|
|
6
|
-
/*
|
|
7
|
-
The browser stub is only emitted when the verb has `clients.browser:
|
|
8
|
-
true`, so the value is always true here. mcp/cli flags are server-only
|
|
9
|
-
discovery state and the browser bundle has no use for them; default
|
|
10
|
-
false so the public RemoteFunction shape stays the same on both sides.
|
|
11
|
-
*/
|
|
12
|
-
const BROWSER_CLIENT_FLAGS = { browser: true, mcp: false, cli: false } as const
|
|
13
|
-
|
|
14
7
|
/*
|
|
15
8
|
Client-side substitute for a verb-defined handler. The bundler emits one
|
|
16
9
|
call per verb export inside an `$rpc/**` module (GET / POST / …): server
|
|
@@ -31,7 +24,7 @@ export function remoteProxy<Args, Return>(
|
|
|
31
24
|
return createRemoteFunction<Args, Return>({
|
|
32
25
|
method,
|
|
33
26
|
url,
|
|
34
|
-
clients:
|
|
27
|
+
clients: browserClientFlags,
|
|
35
28
|
buildRequest: (args) =>
|
|
36
29
|
buildRpcRequest({ method, url, args, baseUrl: window.location.href }),
|
|
37
30
|
/*
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import type { Socket } from '../server/sockets/types/Socket.ts'
|
|
2
|
+
import { browserClientFlags } from '../shared/browserClientFlags.ts'
|
|
2
3
|
import { createPushIterator } from '../shared/createPushIterator.ts'
|
|
3
4
|
import { getSocketChannel } from './socketChannel.ts'
|
|
4
5
|
|
|
5
6
|
let nextId = 0
|
|
6
7
|
|
|
7
|
-
/*
|
|
8
|
-
Browser stub is only emitted when `clients.browser: true`, so the value
|
|
9
|
-
is always true here. mcp/cli flags are server-only discovery state; the
|
|
10
|
-
browser bundle has no use for them. Default false so the public Socket
|
|
11
|
-
shape stays consistent on both sides.
|
|
12
|
-
*/
|
|
13
|
-
const BROWSER_CLIENT_FLAGS = { browser: true, mcp: false, cli: false } as const
|
|
14
|
-
|
|
15
8
|
/*
|
|
16
9
|
Client-side substitute for a server-declared Socket. The bundler emits
|
|
17
10
|
one call per socket export under `src/server/sockets/`: server target uses
|
|
@@ -54,7 +47,7 @@ export function socketProxy<T>(name: string): Socket<T> {
|
|
|
54
47
|
|
|
55
48
|
return {
|
|
56
49
|
name,
|
|
57
|
-
clients:
|
|
50
|
+
clients: browserClientFlags,
|
|
58
51
|
publish(message: T) {
|
|
59
52
|
getSocketChannel().publish(name, message)
|
|
60
53
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path'
|
|
2
|
+
import { bundleLayout } from '../shared/bundleLayout.ts'
|
|
2
3
|
import { webviewCachePath } from './webviewCachePath.ts'
|
|
3
4
|
import { webviewLibName } from './webviewLibName.ts'
|
|
4
5
|
|
|
@@ -29,10 +30,11 @@ export async function resolveWebviewLib(cwd: string = process.cwd()): Promise<st
|
|
|
29
30
|
/*
|
|
30
31
|
Bundle-relative candidates. In dev `process.execPath` is the `bun`
|
|
31
32
|
binary, so these miss and we fall through to the build cache; in a
|
|
32
|
-
shipped bundle the launcher's own directory holds the lib
|
|
33
|
+
shipped bundle the launcher's own directory holds the lib (flat layout)
|
|
34
|
+
or its sibling Frameworks dir (macOS `.app`) — bundleLayout knows which.
|
|
33
35
|
*/
|
|
34
|
-
const binDir = dirname(process.execPath)
|
|
35
|
-
const bundledCandidates = [join(binDir, libName), join(
|
|
36
|
+
const { binDir, libDir } = bundleLayout(dirname(process.execPath))
|
|
37
|
+
const bundledCandidates = [join(binDir, libName), join(libDir, libName)]
|
|
36
38
|
for (const candidate of bundledCandidates) {
|
|
37
39
|
if (await Bun.file(candidate).exists()) {
|
|
38
40
|
return candidate
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { dirname } from 'node:path'
|
|
2
|
+
import { bundleLayout } from '../shared/bundleLayout.ts'
|
|
2
3
|
import { loadEnvFile } from '../shared/loadEnvFile.ts'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
|
-
Loads
|
|
6
|
-
`process.execPath`)
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
Loads the bundle's shipped `.env` into `process.env`, resolved from the running
|
|
7
|
+
binary's directory (`process.execPath`) via bundleLayout — 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(
|
|
15
|
+
await loadEnvFile(bundleLayout(dirname(process.execPath)).envPath)
|
|
14
16
|
}
|
package/src/lib/cli/runCli.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { parseArgvForRpc } from './parseArgvForRpc.ts'
|
|
|
8
8
|
import { printCommandHelp, printTopLevelHelp } from './printHelp.ts'
|
|
9
9
|
import type { CliManifest } from './types/CliManifest.ts'
|
|
10
10
|
|
|
11
|
+
const isHelpFlag = (arg: string): boolean => arg === '--help' || arg === '-h'
|
|
12
|
+
|
|
11
13
|
// String results print verbatim (with a trailing newline); everything else as a JSON line.
|
|
12
14
|
function printValue(value: unknown, pretty: boolean): void {
|
|
13
15
|
if (typeof value === 'string') {
|
|
@@ -54,12 +56,12 @@ export async function runCli({
|
|
|
54
56
|
await loadEnvFromBinaryDir()
|
|
55
57
|
|
|
56
58
|
const first = argv[0]
|
|
57
|
-
if (!first || first
|
|
59
|
+
if (!first || isHelpFlag(first)) {
|
|
58
60
|
printTopLevelHelp(programName, manifest, banner, footer)
|
|
59
61
|
return 0
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
if (argv.
|
|
64
|
+
if (argv.some(isHelpFlag)) {
|
|
63
65
|
printCommandHelp(programName, first, manifest)
|
|
64
66
|
return 0
|
|
65
67
|
}
|
|
@@ -224,8 +224,7 @@ function dispatchVerb(
|
|
|
224
224
|
args: Record<string, unknown> | undefined,
|
|
225
225
|
inbound: Request,
|
|
226
226
|
): Promise<Response> {
|
|
227
|
-
const
|
|
228
|
-
const baseUrl = `${inboundUrl.protocol}//${inboundUrl.host}/`
|
|
227
|
+
const baseUrl = `${new URL(inbound.url).origin}/`
|
|
229
228
|
const request = buildRpcRequest({
|
|
230
229
|
method: entry.remote.method,
|
|
231
230
|
url: entry.remote.url,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { serializeEnv } from '../../shared/serializeEnv.ts'
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
Generates the `.env` file content shipped alongside the CLI binary in
|
|
3
5
|
the download tarball. APP_URL is always present (derived from the
|
|
@@ -10,9 +12,8 @@ user's auth code at the actual RPC endpoints validates whatever value
|
|
|
10
12
|
arrives back in subsequent calls.
|
|
11
13
|
*/
|
|
12
14
|
export function buildEnvContent(appUrl: string, bearerToken: string | undefined): string {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
return `${lines.join('\n')}\n`
|
|
15
|
+
return serializeEnv({
|
|
16
|
+
APP_URL: appUrl,
|
|
17
|
+
...(bearerToken ? { APP_TOKEN: bearerToken } : {}),
|
|
18
|
+
})
|
|
18
19
|
}
|
|
@@ -101,8 +101,7 @@ export async function handleCliDownload(
|
|
|
101
101
|
headers: { 'Cache-Control': NO_STORE },
|
|
102
102
|
})
|
|
103
103
|
}
|
|
104
|
-
const
|
|
105
|
-
const appUrl = `${url.protocol}//${url.host}`
|
|
104
|
+
const appUrl = new URL(request.url).origin
|
|
106
105
|
const auth = request.headers.get('authorization')
|
|
107
106
|
const bearer =
|
|
108
107
|
auth && auth.toLowerCase().startsWith('bearer ') ? auth.slice('bearer '.length) : undefined
|
|
@@ -26,7 +26,7 @@ export function handleCliInstall(request: Request, programName: string): Respons
|
|
|
26
26
|
headers: { 'Cache-Control': NO_STORE },
|
|
27
27
|
})
|
|
28
28
|
}
|
|
29
|
-
const appUrl =
|
|
29
|
+
const appUrl = url.origin
|
|
30
30
|
const script = installScript(appUrl, programName)
|
|
31
31
|
return new Response(script, {
|
|
32
32
|
headers: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
2
|
import { Glob } from 'bun'
|
|
3
3
|
|
|
4
4
|
/*
|
|
@@ -6,22 +6,21 @@ Returns the most-recent mtime across every rpc + socket source file in
|
|
|
6
6
|
the project, or 0 when both directories are absent. The lazy CLI
|
|
7
7
|
download path compares this to the binary's mtime to decide whether to
|
|
8
8
|
rebuild — covers the common dev iteration of "user edited an rpc
|
|
9
|
-
handler" without needing to scan transitively-imported modules.
|
|
9
|
+
handler" without needing to scan transitively-imported modules. Globs and
|
|
10
|
+
stats run concurrently since each file is independent.
|
|
10
11
|
*/
|
|
11
12
|
export async function maxSourceMtime(cwd: string): Promise<number> {
|
|
12
|
-
const roots = [`${cwd}/src/server/rpc`, `${cwd}/src/server/sockets`]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
return newest
|
|
13
|
+
const roots = [`${cwd}/src/server/rpc`, `${cwd}/src/server/sockets`].filter(existsSync)
|
|
14
|
+
const perRoot = await Promise.all(
|
|
15
|
+
roots.map(async (root) => {
|
|
16
|
+
const files = await Array.fromAsync(
|
|
17
|
+
new Glob('**/*.ts').scan({ cwd: root, onlyFiles: true }),
|
|
18
|
+
)
|
|
19
|
+
return files.map((file) => `${root}/${file}`)
|
|
20
|
+
}),
|
|
21
|
+
)
|
|
22
|
+
const mtimes = await Promise.all(
|
|
23
|
+
perRoot.flat().map(async (path) => (await Bun.file(path).stat()).mtimeMs),
|
|
24
|
+
)
|
|
25
|
+
return mtimes.reduce((newest, mtime) => Math.max(newest, mtime), 0)
|
|
27
26
|
}
|
|
@@ -11,6 +11,7 @@ import { NO_STORE, SSR_CACHE_CONTROL } from '../../shared/cacheControlValues.ts'
|
|
|
11
11
|
import { createCacheStore } from '../../shared/createCacheStore.ts'
|
|
12
12
|
import { isDebugEnabled } from '../../shared/isDebugEnabled.ts'
|
|
13
13
|
import { log } from '../../shared/log.ts'
|
|
14
|
+
import { memoizeByKey } from '../../shared/memoizeByKey.ts'
|
|
14
15
|
import { nearestLayoutPrefix, normalizeLayoutPrefixes } from '../../shared/nearestLayoutPrefix.ts'
|
|
15
16
|
import { toBunRoutePattern } from '../../shared/toBunRoutePattern.ts'
|
|
16
17
|
import type { AppModule } from '../AppModule.ts'
|
|
@@ -43,6 +44,9 @@ function wantsJson(req: Request): boolean {
|
|
|
43
44
|
return (req.headers.get('accept') ?? '').includes('application/json')
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// SSR placeholders the shell carries; filled in a single pass per render.
|
|
48
|
+
const SSR_MARKER = /<!--ssr:(head|body|state)-->/g
|
|
49
|
+
|
|
46
50
|
/*
|
|
47
51
|
The framework's default 500 response — a `<pre>` stack dump. Shared by the
|
|
48
52
|
per-request catch and Bun.serve's global error() fallback so the two can't
|
|
@@ -146,12 +150,7 @@ export async function createServer({
|
|
|
146
150
|
(file) => `/_app/${file.replace(/\.zst$/, '')}`,
|
|
147
151
|
)
|
|
148
152
|
|
|
149
|
-
const
|
|
150
|
-
function loadRpc(url: string): Promise<AnyRemoteFunction | undefined> | undefined {
|
|
151
|
-
const existing = rpcModuleCache.get(url)
|
|
152
|
-
if (existing) {
|
|
153
|
-
return existing
|
|
154
|
-
}
|
|
153
|
+
const loadRpc = memoizeByKey((url): Promise<AnyRemoteFunction | undefined> | undefined => {
|
|
155
154
|
const loader = rpc[url]
|
|
156
155
|
if (!loader) {
|
|
157
156
|
return undefined
|
|
@@ -161,7 +160,7 @@ export async function createServer({
|
|
|
161
160
|
time. Pick the first export that looks like a RemoteFunction so the
|
|
162
161
|
framework stays tolerant of incidental re-exports.
|
|
163
162
|
*/
|
|
164
|
-
|
|
163
|
+
return loader().then((mod) => {
|
|
165
164
|
for (const value of Object.values(mod)) {
|
|
166
165
|
if (typeof value === 'function' && 'method' in value && 'url' in value) {
|
|
167
166
|
return value as AnyRemoteFunction
|
|
@@ -169,9 +168,7 @@ export async function createServer({
|
|
|
169
168
|
}
|
|
170
169
|
return undefined
|
|
171
170
|
})
|
|
172
|
-
|
|
173
|
-
return promise
|
|
174
|
-
}
|
|
171
|
+
})
|
|
175
172
|
|
|
176
173
|
const logRequests = isDebugEnabled('belte')
|
|
177
174
|
|
|
@@ -258,10 +255,12 @@ export async function createServer({
|
|
|
258
255
|
params,
|
|
259
256
|
cache: cacheSnapshot,
|
|
260
257
|
})};</script>`
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
const fills: Record<string, string> = {
|
|
259
|
+
head: rendered.head,
|
|
260
|
+
body: rendered.body,
|
|
261
|
+
state: stateTag,
|
|
262
|
+
}
|
|
263
|
+
const html = shell.replace(SSR_MARKER, (_match, key: string) => fills[key])
|
|
265
264
|
return new Response(html, {
|
|
266
265
|
headers: {
|
|
267
266
|
'Content-Type': 'text/html; charset=utf-8',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ServerWebSocket } from 'bun'
|
|
2
2
|
import { log } from '../../shared/log.ts'
|
|
3
|
+
import { memoizeByKey } from '../../shared/memoizeByKey.ts'
|
|
3
4
|
import { error } from '../error.ts'
|
|
4
5
|
import { json } from '../json.ts'
|
|
5
6
|
import { sse } from '../sse.ts'
|
|
@@ -52,22 +53,12 @@ module triggers its `defineSocket` call, which inserts into the
|
|
|
52
53
|
registry. After that the dispatcher just reads the registry.
|
|
53
54
|
*/
|
|
54
55
|
export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher {
|
|
55
|
-
const moduleCache = new Map<string, Promise<void>>()
|
|
56
56
|
const connections = new WeakMap<ServerWebSocket<unknown>, ConnectionState>()
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
const existing = moduleCache.get(name)
|
|
60
|
-
if (existing) {
|
|
61
|
-
return existing
|
|
62
|
-
}
|
|
58
|
+
const ensureLoaded = memoizeByKey((name): Promise<void> | undefined => {
|
|
63
59
|
const loader = sockets[name]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
const promise = loader().then(() => undefined)
|
|
68
|
-
moduleCache.set(name, promise)
|
|
69
|
-
return promise
|
|
70
|
-
}
|
|
60
|
+
return loader ? loader().then(() => undefined) : undefined
|
|
61
|
+
})
|
|
71
62
|
|
|
72
63
|
function send(ws: ServerWebSocket<unknown>, frame: SocketServerFrame): void {
|
|
73
64
|
if (ws.readyState !== WebSocket.OPEN) {
|
|
@@ -111,37 +102,24 @@ export function createSocketDispatcher(sockets: SocketRoutes): SocketDispatcher
|
|
|
111
102
|
state: ConnectionState,
|
|
112
103
|
frame: Extract<SocketClientFrame, { type: 'sub' }>,
|
|
113
104
|
): Promise<void> {
|
|
105
|
+
// Reject this sub: emit the error then the terminal end frame for its id.
|
|
106
|
+
function fail(message: string): void {
|
|
107
|
+
send(ws, { type: 'err', sub: frame.sub, message })
|
|
108
|
+
send(ws, { type: 'end', sub: frame.sub })
|
|
109
|
+
}
|
|
114
110
|
const loader = ensureLoaded(frame.socket)
|
|
115
111
|
if (!loader) {
|
|
116
|
-
|
|
117
|
-
type: 'err',
|
|
118
|
-
sub: frame.sub,
|
|
119
|
-
message: `[belte] no socket registered at ${frame.socket}`,
|
|
120
|
-
})
|
|
121
|
-
send(ws, { type: 'end', sub: frame.sub })
|
|
122
|
-
return
|
|
112
|
+
return fail(`[belte] no socket registered at ${frame.socket}`)
|
|
123
113
|
}
|
|
124
114
|
try {
|
|
125
115
|
await loader
|
|
126
116
|
} catch (error) {
|
|
127
117
|
log.error(error)
|
|
128
|
-
|
|
129
|
-
type: 'err',
|
|
130
|
-
sub: frame.sub,
|
|
131
|
-
message: error instanceof Error ? error.message : String(error),
|
|
132
|
-
})
|
|
133
|
-
send(ws, { type: 'end', sub: frame.sub })
|
|
134
|
-
return
|
|
118
|
+
return fail(error instanceof Error ? error.message : String(error))
|
|
135
119
|
}
|
|
136
120
|
const entry = lookupSocket(frame.socket)
|
|
137
121
|
if (!entry) {
|
|
138
|
-
|
|
139
|
-
type: 'err',
|
|
140
|
-
sub: frame.sub,
|
|
141
|
-
message: `[belte] socket module at ${frame.socket} did not register a Socket export`,
|
|
142
|
-
})
|
|
143
|
-
send(ws, { type: 'end', sub: frame.sub })
|
|
144
|
-
return
|
|
122
|
+
return fail(`[belte] socket module at ${frame.socket} did not register a Socket export`)
|
|
145
123
|
}
|
|
146
124
|
const isFirstLocalSub = addSub(state, frame.socket, frame.sub)
|
|
147
125
|
if (isFirstLocalSub) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { beltePackageName } from './beltePackageName.ts'
|
|
2
|
+
import { readPackageJson } from './readPackageJson.ts'
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
Resolves the bare specifier prefix a consuming project imports belte under —
|
|
@@ -17,14 +18,12 @@ resolve belte at all in that case, and the canonical name yields the clearest
|
|
|
17
18
|
resolution error.
|
|
18
19
|
*/
|
|
19
20
|
export async function belteImportName(cwd: string): Promise<string> {
|
|
20
|
-
const
|
|
21
|
-
|
|
21
|
+
const packageJson = (await readPackageJson(cwd)) as
|
|
22
|
+
| { dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
23
|
+
| undefined
|
|
24
|
+
if (!packageJson) {
|
|
22
25
|
return beltePackageName
|
|
23
26
|
}
|
|
24
|
-
const packageJson = (await Bun.file(packageJsonPath).json()) as {
|
|
25
|
-
dependencies?: Record<string, string>
|
|
26
|
-
devDependencies?: Record<string, string>
|
|
27
|
-
}
|
|
28
27
|
const dependencies = { ...packageJson.devDependencies, ...packageJson.dependencies }
|
|
29
28
|
/*
|
|
30
29
|
Alias entries whose target is belte — `npm:` for a published install,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ClientFlags } from './types/ClientFlags.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Client surface flags for a browser-emitted RPC/socket stub. The bundler only
|
|
5
|
+
emits the browser proxy when clients.browser is true, so browser is always true
|
|
6
|
+
here; mcp/cli are server-only discovery state with no meaning in the browser
|
|
7
|
+
bundle, defaulted false so the public RemoteFunction/Socket shape matches the
|
|
8
|
+
server side. Single source shared by remoteProxy and socketProxy.
|
|
9
|
+
*/
|
|
10
|
+
export const browserClientFlags: ClientFlags = { browser: true, mcp: false, cli: false }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { basename, dirname, join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Where a bundle's files live relative to its binary directory (the dir holding the
|
|
5
|
+
launcher + server — `dirname(process.execPath)` at runtime). A macOS `.app` nests
|
|
6
|
+
binaries under `Contents/MacOS`, the webview lib under `Contents/Frameworks`, and
|
|
7
|
+
data under `Contents/Resources` — the only one `codesign` seals as a *resource*,
|
|
8
|
+
so the shipped `.env` survives signing. Every other platform keeps everything
|
|
9
|
+
flat beside the binaries. The single source the bundler writes against and the
|
|
10
|
+
boot readers resolve from, so build and runtime can't disagree. Pure: computes
|
|
11
|
+
paths, never touches disk.
|
|
12
|
+
*/
|
|
13
|
+
export function bundleLayout(binaryDir: string): {
|
|
14
|
+
binDir: string
|
|
15
|
+
libDir: string
|
|
16
|
+
resourcesDir: string
|
|
17
|
+
envPath: string
|
|
18
|
+
} {
|
|
19
|
+
const isMacApp = basename(binaryDir) === 'MacOS' && basename(dirname(binaryDir)) === 'Contents'
|
|
20
|
+
if (isMacApp) {
|
|
21
|
+
const contents = dirname(binaryDir)
|
|
22
|
+
const resourcesDir = join(contents, 'Resources')
|
|
23
|
+
return {
|
|
24
|
+
binDir: binaryDir,
|
|
25
|
+
libDir: join(contents, 'Frameworks'),
|
|
26
|
+
resourcesDir,
|
|
27
|
+
envPath: join(resourcesDir, '.env'),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
binDir: binaryDir,
|
|
32
|
+
libDir: binaryDir,
|
|
33
|
+
resourcesDir: binaryDir,
|
|
34
|
+
envPath: join(binaryDir, '.env'),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CompileTarget } from '../server/runtime/types/CompileTarget.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Executable filename suffix for a compile target — `.exe` on Windows targets,
|
|
5
|
+
empty elsewhere. Single source so every cross-compile output path agrees.
|
|
6
|
+
*/
|
|
7
|
+
export function exeSuffix(target: CompileTarget): string {
|
|
8
|
+
return target.includes('windows') ? '.exe' : ''
|
|
9
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { log } from './log.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Builds one of belte's virtual manifest modules — the `{ key: () => import(...) }`
|
|
5
|
+
map the bundler emits for rpc / sockets / prompts / pages / layouts. They differ
|
|
6
|
+
only in their files, the key derived per file, the import dir, the export name,
|
|
7
|
+
and the log label; this is the single shape they share.
|
|
8
|
+
*/
|
|
9
|
+
export function manifestModule(options: {
|
|
10
|
+
files: string[]
|
|
11
|
+
keyForFile: (file: string) => string
|
|
12
|
+
importDir: string
|
|
13
|
+
exportName: string
|
|
14
|
+
label: string
|
|
15
|
+
// pages logs its count even at zero (a route-less app is worth surfacing);
|
|
16
|
+
// the other manifests stay quiet when empty.
|
|
17
|
+
logWhenEmpty?: boolean
|
|
18
|
+
}): { contents: string; loader: 'js' } {
|
|
19
|
+
const entries = options.files
|
|
20
|
+
.toSorted()
|
|
21
|
+
.map((file) => ({ key: options.keyForFile(file), file }))
|
|
22
|
+
const lines = entries
|
|
23
|
+
.map(
|
|
24
|
+
({ key, file }) =>
|
|
25
|
+
` ${JSON.stringify(key)}: () => import(${JSON.stringify(`${options.importDir}/${file}`)}),`,
|
|
26
|
+
)
|
|
27
|
+
.join('\n')
|
|
28
|
+
if (entries.length > 0 || options.logWhenEmpty) {
|
|
29
|
+
log.info(
|
|
30
|
+
`resolved ${entries.length} ${options.label}: ${entries.map((entry) => entry.key).join(', ')}`,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
return { contents: `export const ${options.exportName} = {\n${lines}\n}\n`, loader: 'js' }
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Memoises an async loader keyed by string: the first call for a key starts the
|
|
3
|
+
load and caches its promise; later calls reuse it. `load` returns undefined when
|
|
4
|
+
the key has no loader, which is passed through (and not cached) so the caller can
|
|
5
|
+
treat "unknown key" distinctly from "loaded value". Used by the rpc-module and
|
|
6
|
+
socket-module load caches, which share this exact shape.
|
|
7
|
+
*/
|
|
8
|
+
export function memoizeByKey<T>(
|
|
9
|
+
load: (key: string) => Promise<T> | undefined,
|
|
10
|
+
): (key: string) => Promise<T> | undefined {
|
|
11
|
+
const cache = new Map<string, Promise<T>>()
|
|
12
|
+
return (key) => {
|
|
13
|
+
const existing = cache.get(key)
|
|
14
|
+
if (existing) {
|
|
15
|
+
return existing
|
|
16
|
+
}
|
|
17
|
+
const started = load(key)
|
|
18
|
+
if (!started) {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
cache.set(key, started)
|
|
22
|
+
return started
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Reads and parses a project's `package.json`, or undefined when absent. Callers
|
|
3
|
+
apply their own field defaults — this centralizes only the exists-check + parse
|
|
4
|
+
boilerplate. Bun.file().json() so no Node fs.
|
|
5
|
+
*/
|
|
6
|
+
export async function readPackageJson(cwd: string): Promise<Record<string, unknown> | undefined> {
|
|
7
|
+
const file = Bun.file(`${cwd}/package.json`)
|
|
8
|
+
return (await file.exists()) ? ((await file.json()) as Record<string, unknown>) : undefined
|
|
9
|
+
}
|