@briancray/belte 0.1.0 → 0.2.1
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/bin/belte.ts +25 -12
- package/package.json +2 -1
- package/src/appEntry.ts +124 -0
- package/src/belteResolverPlugin.ts +236 -202
- package/src/build.ts +6 -67
- package/src/buildCli.ts +36 -63
- package/src/buildDisconnected.ts +127 -0
- package/src/bundleApp.ts +123 -0
- package/src/bundleDisconnectedEntry.ts +17 -0
- package/src/cliEntry.ts +3 -9
- package/src/compile.ts +4 -15
- package/src/controlServerWorker.ts +261 -0
- package/src/dedupeSveltePlugin.ts +66 -0
- package/src/discoveryEntry.ts +12 -11
- package/src/lib/browser/cache.ts +3 -6
- package/src/lib/browser/page.svelte.ts +19 -21
- package/src/lib/browser/socketChannel.ts +11 -1
- package/src/lib/browser/types/Pages.ts +1 -1
- package/src/lib/bundle/BundleMenu.ts +11 -0
- package/src/lib/bundle/BundleMenuItem.ts +24 -0
- package/src/lib/bundle/BundleWindow.ts +20 -0
- package/src/lib/bundle/bindConnectedFlag.ts +29 -0
- package/src/lib/bundle/bindRequestNavigate.ts +31 -0
- package/src/lib/bundle/buildWebviewLib.ts +111 -0
- package/src/lib/bundle/disconnected.css +9 -0
- package/src/lib/bundle/disconnected.svelte +192 -0
- package/src/lib/bundle/ensureWebviewLib.ts +20 -0
- package/src/lib/bundle/exitWithParent.ts +28 -0
- package/src/lib/bundle/findFreePort.ts +14 -0
- package/src/lib/bundle/infoPlist.ts +46 -0
- package/src/lib/bundle/installMacMenu.ts +39 -0
- package/src/lib/bundle/listenLocalControlServer.ts +19 -0
- package/src/lib/bundle/native/belteMenu.mm +298 -0
- package/src/lib/bundle/native/webview.h +4557 -0
- package/src/lib/bundle/onMenu.ts +26 -0
- package/src/lib/bundle/openWebview.ts +81 -0
- package/src/lib/bundle/pngToIcns.ts +47 -0
- package/src/lib/bundle/probeBelteServer.ts +34 -0
- package/src/lib/bundle/resolveServerBinary.ts +12 -0
- package/src/lib/bundle/resolveWebviewLib.ts +51 -0
- package/src/lib/bundle/serverBinaryFilename.ts +8 -0
- package/src/lib/bundle/stableLocalPort.ts +19 -0
- package/src/lib/bundle/waitForServer.ts +23 -0
- package/src/lib/bundle/webviewBuildRevision.ts +9 -0
- package/src/lib/bundle/webviewCachePath.ts +23 -0
- package/src/lib/bundle/webviewLibName.ts +11 -0
- package/src/lib/bundle/webviewVersion.ts +7 -0
- package/src/lib/cli/createClient.ts +34 -36
- package/src/lib/cli/printHelp.ts +45 -2
- package/src/lib/cli/runCli.ts +12 -3
- package/src/lib/mcp/createMcpResourceServer.ts +1 -1
- package/src/lib/mcp/dispatchMcpRequest.ts +53 -73
- package/src/lib/server/AppModule.ts +2 -2
- package/src/lib/server/cli/handleCliDownload.ts +4 -5
- package/src/lib/server/cli/handleCliInstall.ts +17 -0
- package/src/lib/server/error.ts +23 -9
- package/src/lib/server/json.ts +5 -5
- package/src/lib/server/jsonl.ts +10 -5
- package/src/lib/server/prompts/definePrompt.ts +6 -6
- package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
- package/src/lib/server/prompts/types/Prompt.ts +8 -9
- package/src/lib/server/prompts/types/PromptOptions.ts +7 -12
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +3 -5
- package/src/lib/server/prompts/types/PromptRoutes.ts +4 -4
- package/src/lib/server/redirect.ts +13 -8
- package/src/lib/server/rpc/defineVerb.ts +4 -3
- package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
- package/src/lib/server/rpc/types/RemoteFunction.ts +1 -1
- package/src/lib/server/rpc/types/RemoteHandler.ts +4 -0
- package/src/lib/server/runtime/acceptsZstd.ts +8 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +2 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +7 -2
- package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +6 -20
- package/src/lib/server/runtime/createServer.ts +50 -58
- package/src/lib/server/runtime/registryManifests.ts +33 -15
- package/src/lib/server/runtime/types/RequestStore.ts +2 -3
- package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +10 -7
- package/src/lib/server/sse.ts +10 -5
- package/src/lib/shared/belteImportName.test.ts +58 -0
- package/src/lib/shared/belteImportName.ts +45 -0
- package/src/lib/shared/beltePackageName.ts +7 -0
- package/src/lib/shared/cacheControlValues.ts +10 -2
- package/src/lib/shared/canonicalJson.ts +1 -5
- package/src/lib/shared/createCacheStore.ts +29 -20
- package/src/lib/shared/exitOnBuildFailure.ts +17 -0
- package/src/lib/shared/fileStem.ts +9 -0
- package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
- package/src/lib/shared/keyForRemoteCall.ts +7 -5
- package/src/lib/shared/parsePromptMarkdown.ts +34 -0
- package/src/lib/shared/prepareRpcModule.ts +14 -4
- package/src/lib/shared/prepareSocketModule.ts +16 -2
- package/src/lib/shared/promptNameForFile.ts +5 -5
- package/src/lib/shared/subscribableFromResponse.ts +104 -215
- package/src/lib/shared/types/PromptArgument.ts +12 -0
- package/src/lib/shared/writeRoutesDts.ts +5 -7
- package/src/serverBuildPlugins.ts +25 -0
- package/src/serverEntry.ts +4 -0
- package/template/package.json +3 -2
- package/src/lib/server/prompt.ts +0 -30
- package/src/lib/server/prompts/types/PromptMessage.ts +0 -10
- package/src/lib/shared/preparePromptModule.ts +0 -36
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { existsSync, statSync } from 'node:fs'
|
|
3
3
|
import type { BunPlugin } from 'bun'
|
|
4
4
|
import { Glob } from 'bun'
|
|
5
|
+
import { belteImportName } from './lib/shared/belteImportName.ts'
|
|
6
|
+
import { fileStem } from './lib/shared/fileStem.ts'
|
|
7
|
+
import { jsonSchemaForPromptArguments } from './lib/shared/jsonSchemaForPromptArguments.ts'
|
|
5
8
|
import { log } from './lib/shared/log.ts'
|
|
6
9
|
import { pageUrlForFile } from './lib/shared/pageUrlForFile.ts'
|
|
7
|
-
import {
|
|
10
|
+
import { parsePromptMarkdown } from './lib/shared/parsePromptMarkdown.ts'
|
|
8
11
|
import { prepareRpcModule } from './lib/shared/prepareRpcModule.ts'
|
|
9
12
|
import { prepareSocketModule } from './lib/shared/prepareSocketModule.ts'
|
|
10
13
|
import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
|
|
@@ -57,6 +60,17 @@ function escapeRegex(value: string): string {
|
|
|
57
60
|
return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
/* Memoises a zero-arg async producer so repeat calls reuse the first in-flight promise. */
|
|
64
|
+
function once<T>(produce: () => Promise<T>): () => Promise<T> {
|
|
65
|
+
let promise: Promise<T> | undefined
|
|
66
|
+
return () => {
|
|
67
|
+
if (!promise) {
|
|
68
|
+
promise = produce()
|
|
69
|
+
}
|
|
70
|
+
return promise
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
/*
|
|
61
75
|
Bun plugin that wires every virtual import belte produces at build time:
|
|
62
76
|
- `belte:rpc` — { rpcUrl: () => import(rpc-module) } HTTP-verb manifest
|
|
@@ -81,12 +95,10 @@ export function belteResolverPlugin({
|
|
|
81
95
|
cwd = process.cwd(),
|
|
82
96
|
embedAssets = false,
|
|
83
97
|
target = 'server',
|
|
84
|
-
thin,
|
|
85
98
|
}: {
|
|
86
99
|
cwd?: string
|
|
87
100
|
embedAssets?: boolean
|
|
88
101
|
target?: 'server' | 'client'
|
|
89
|
-
thin?: boolean
|
|
90
102
|
} = {}): BunPlugin {
|
|
91
103
|
const serverDir = `${cwd}/src/server`
|
|
92
104
|
const browserDir = `${cwd}/src/browser`
|
|
@@ -107,58 +119,37 @@ export function belteResolverPlugin({
|
|
|
107
119
|
re-globbing the trees. The shell read is memoised the same way so two
|
|
108
120
|
passes don't re-read app.html from disk.
|
|
109
121
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
return rpcScanPromise
|
|
129
|
-
}
|
|
130
|
-
function scanSocketsOnce(): Promise<string[]> {
|
|
131
|
-
if (!socketsScanPromise) {
|
|
132
|
-
socketsScanPromise = scanSockets(socketsDir)
|
|
133
|
-
}
|
|
134
|
-
return socketsScanPromise
|
|
135
|
-
}
|
|
136
|
-
function scanPromptsOnce(): Promise<string[]> {
|
|
137
|
-
if (!promptsScanPromise) {
|
|
138
|
-
promptsScanPromise = scanPrompts(promptsDir)
|
|
139
|
-
}
|
|
140
|
-
return promptsScanPromise
|
|
141
|
-
}
|
|
142
|
-
function loadShellOnce(): Promise<string> {
|
|
143
|
-
if (!shellContentsPromise) {
|
|
144
|
-
shellContentsPromise = loadShell(cwd)
|
|
145
|
-
}
|
|
146
|
-
return shellContentsPromise
|
|
147
|
-
}
|
|
122
|
+
const scanPagesOnce = once(() =>
|
|
123
|
+
scanPages(pagesDir).then(async (scan) => {
|
|
124
|
+
await writeRoutesDts({ cwd, pageFiles: scan.pageFiles })
|
|
125
|
+
return scan
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
const scanRpcOnce = once(() => scanRpc(rpcDir))
|
|
129
|
+
const scanSocketsOnce = once(() => scanSockets(socketsDir))
|
|
130
|
+
const scanPromptsOnce = once(() => scanPrompts(promptsDir))
|
|
131
|
+
const loadShellOnce = once(() => loadShell(cwd))
|
|
132
|
+
/*
|
|
133
|
+
The bare specifier the project imports belte under (canonical
|
|
134
|
+
`@briancray/belte` or a package alias). Resolved once from the project's
|
|
135
|
+
package.json and threaded into every generated module so the codegen's
|
|
136
|
+
imports resolve regardless of which install style the project uses.
|
|
137
|
+
*/
|
|
138
|
+
const belteImportNameOnce = once(() => belteImportName(cwd))
|
|
148
139
|
|
|
149
140
|
const rpcFilter = new RegExp(`^${escapeRegex(rpcDir)}/.*\\.ts$`)
|
|
150
141
|
const socketsFilter = new RegExp(`^${escapeRegex(socketsDir)}/.*\\.ts$`)
|
|
151
|
-
const promptsFilter = new RegExp(`^${escapeRegex(promptsDir)}/.*\\.
|
|
142
|
+
const promptsFilter = new RegExp(`^${escapeRegex(promptsDir)}/.*\\.md$`)
|
|
152
143
|
|
|
153
144
|
return {
|
|
154
145
|
name: 'belte-resolver',
|
|
155
146
|
setup(build) {
|
|
156
147
|
build.onResolve(
|
|
157
148
|
{
|
|
158
|
-
filter: /\/_virtual\/(rpc|sockets|prompts|pages|layouts|app|mcp-resources|mcp|assets|public-assets|shell|app-info|cli-manifest|cli-name|cli-chrome|
|
|
149
|
+
filter: /\/_virtual\/(rpc|sockets|prompts|pages|layouts|app|mcp-resources|mcp|assets|public-assets|shell|app-info|cli-manifest|cli-name|cli-chrome|bundle-window|bundle-disconnected-component|bundle-disconnected)\.ts$/,
|
|
159
150
|
},
|
|
160
151
|
(args) => {
|
|
161
|
-
const name = args.path
|
|
152
|
+
const name = fileStem(args.path)
|
|
162
153
|
if (!name) {
|
|
163
154
|
return undefined
|
|
164
155
|
}
|
|
@@ -172,30 +163,19 @@ export function belteResolverPlugin({
|
|
|
172
163
|
`$browser/pages/...`, `$mcp/prompts/...`, `$mcp/resources/...`.
|
|
173
164
|
`lib/` is userland — projects declare their own lib aliases.
|
|
174
165
|
*/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
build.onResolve({ filter: /^\$mcp(\/.*)?$/ }, (args) => {
|
|
191
|
-
const subpath = args.path.slice('$mcp'.length)
|
|
192
|
-
return { path: resolveExtension(subpath ? `${mcpDir}${subpath}` : mcpDir) }
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
build.onResolve({ filter: /^\$cli(\/.*)?$/ }, (args) => {
|
|
196
|
-
const subpath = args.path.slice('$cli'.length)
|
|
197
|
-
return { path: resolveExtension(subpath ? `${cliDir}${subpath}` : cliDir) }
|
|
198
|
-
})
|
|
166
|
+
const dirAliases: Record<string, string> = {
|
|
167
|
+
$server: serverDir,
|
|
168
|
+
$browser: browserDir,
|
|
169
|
+
$shared: sharedDir,
|
|
170
|
+
$mcp: mcpDir,
|
|
171
|
+
$cli: cliDir,
|
|
172
|
+
}
|
|
173
|
+
for (const [alias, baseDir] of Object.entries(dirAliases)) {
|
|
174
|
+
build.onResolve({ filter: new RegExp(`^\\${alias}(\\/.*)?$`) }, (args) => {
|
|
175
|
+
const subpath = args.path.slice(alias.length)
|
|
176
|
+
return { path: resolveExtension(subpath ? `${baseDir}${subpath}` : baseDir) }
|
|
177
|
+
})
|
|
178
|
+
}
|
|
199
179
|
|
|
200
180
|
build.onLoad({ filter: rpcFilter }, async (args) => {
|
|
201
181
|
if (!args.path.startsWith(`${rpcDir}/`)) {
|
|
@@ -204,13 +184,14 @@ export function belteResolverPlugin({
|
|
|
204
184
|
const relativePath = args.path.slice(rpcDir.length + 1)
|
|
205
185
|
const source = await Bun.file(args.path).text()
|
|
206
186
|
const url = rpcUrlForFile(relativePath)
|
|
207
|
-
const
|
|
187
|
+
const importName = await belteImportNameOnce()
|
|
188
|
+
const prepared = prepareRpcModule(source, importName)
|
|
208
189
|
if (!prepared) {
|
|
209
190
|
throw new Error(
|
|
210
191
|
`[belte] src/server/rpc/${relativePath} has no \`export const <name> = <VERB>(...)\` — every $rpc module must declare exactly one remote function`,
|
|
211
192
|
)
|
|
212
193
|
}
|
|
213
|
-
const expectedName = relativePath
|
|
194
|
+
const expectedName = fileStem(relativePath)
|
|
214
195
|
if (prepared.exportName !== expectedName) {
|
|
215
196
|
throw new Error(
|
|
216
197
|
`[belte] src/server/rpc/${relativePath} exports \`${prepared.exportName}\` but the filename expects \`${expectedName}\` — the export name must match the file's stem`,
|
|
@@ -224,7 +205,7 @@ export function belteResolverPlugin({
|
|
|
224
205
|
so page imports resolve identically on both sides.
|
|
225
206
|
*/
|
|
226
207
|
if (target === 'client') {
|
|
227
|
-
const contents = `import { remoteProxy as __belteRemoteProxy__ } from '
|
|
208
|
+
const contents = `import { remoteProxy as __belteRemoteProxy__ } from '${importName}/browser/remoteProxy';
|
|
228
209
|
export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prepared.verb)}, ${JSON.stringify(url)});
|
|
229
210
|
`
|
|
230
211
|
return { contents, loader: 'ts' }
|
|
@@ -239,7 +220,7 @@ export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prep
|
|
|
239
220
|
tokenizer-driven so `GET` mentions inside strings and
|
|
240
221
|
comments are left alone.
|
|
241
222
|
*/
|
|
242
|
-
const banner = `import { defineVerb as __belteDefineVerb__ } from '
|
|
223
|
+
const banner = `import { defineVerb as __belteDefineVerb__ } from '${importName}/server/rpc/defineVerb';
|
|
243
224
|
`
|
|
244
225
|
return { contents: `${banner}${prepared.rewriteForServer(url)}`, loader: 'ts' }
|
|
245
226
|
})
|
|
@@ -251,13 +232,14 @@ export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prep
|
|
|
251
232
|
const relativePath = args.path.slice(socketsDir.length + 1)
|
|
252
233
|
const source = await Bun.file(args.path).text()
|
|
253
234
|
const name = socketNameForFile(relativePath)
|
|
254
|
-
const
|
|
235
|
+
const importName = await belteImportNameOnce()
|
|
236
|
+
const prepared = prepareSocketModule(source, importName)
|
|
255
237
|
if (!prepared) {
|
|
256
238
|
throw new Error(
|
|
257
239
|
`[belte] src/server/sockets/${relativePath} has no \`export const <name> = socket(...)\` — every $sockets module must declare exactly one socket`,
|
|
258
240
|
)
|
|
259
241
|
}
|
|
260
|
-
const expectedName = relativePath
|
|
242
|
+
const expectedName = fileStem(relativePath)
|
|
261
243
|
if (prepared.exportName !== expectedName) {
|
|
262
244
|
throw new Error(
|
|
263
245
|
`[belte] src/server/sockets/${relativePath} exports \`${prepared.exportName}\` but the filename expects \`${expectedName}\` — the export name must match the file's stem`,
|
|
@@ -269,12 +251,12 @@ export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prep
|
|
|
269
251
|
clientPublish) are server-side state and don't
|
|
270
252
|
affect the client's wire behaviour.
|
|
271
253
|
*/
|
|
272
|
-
const contents = `import { socketProxy as __belteSocketProxy__ } from '
|
|
254
|
+
const contents = `import { socketProxy as __belteSocketProxy__ } from '${importName}/browser/socketProxy';
|
|
273
255
|
export const ${prepared.exportName} = __belteSocketProxy__(${JSON.stringify(name)});
|
|
274
256
|
`
|
|
275
257
|
return { contents, loader: 'ts' }
|
|
276
258
|
}
|
|
277
|
-
const banner = `import { defineSocket as __belteDefineSocket__ } from '
|
|
259
|
+
const banner = `import { defineSocket as __belteDefineSocket__ } from '${importName}/server/sockets/defineSocket';
|
|
278
260
|
`
|
|
279
261
|
return {
|
|
280
262
|
contents: `${banner}${prepared.rewriteForServer(name)}`,
|
|
@@ -290,32 +272,41 @@ export const ${prepared.exportName} = __belteSocketProxy__(${JSON.stringify(name
|
|
|
290
272
|
Prompts are MCP-only — no client-side counterpart. The
|
|
291
273
|
client bundle never imports a prompts module, but emit an
|
|
292
274
|
empty stub for the client target defensively so a stray
|
|
293
|
-
import can't drag the
|
|
275
|
+
import can't drag the prompt body into the browser bundle.
|
|
294
276
|
*/
|
|
295
277
|
if (target === 'client') {
|
|
296
278
|
return { contents: 'export {}', loader: 'ts' }
|
|
297
279
|
}
|
|
280
|
+
/*
|
|
281
|
+
Server target: a `.md` prompt is data, not code. Parse the
|
|
282
|
+
frontmatter (description + arguments) and body once, then
|
|
283
|
+
generate a module that registers the prompt via definePrompt
|
|
284
|
+
— the body is embedded as a string literal and the render
|
|
285
|
+
closure interpolates `{{name}}` placeholders at call time.
|
|
286
|
+
*/
|
|
298
287
|
const relativePath = args.path.slice(promptsDir.length + 1)
|
|
299
288
|
const source = await Bun.file(args.path).text()
|
|
300
289
|
const name = promptNameForFile(relativePath)
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
const
|
|
290
|
+
const importName = await belteImportNameOnce()
|
|
291
|
+
const parsed = parsePromptMarkdown(source)
|
|
292
|
+
const jsonSchema = jsonSchemaForPromptArguments(parsed.arguments)
|
|
293
|
+
const optionLines = [
|
|
294
|
+
parsed.description
|
|
295
|
+
? ` description: ${JSON.stringify(parsed.description)},`
|
|
296
|
+
: undefined,
|
|
297
|
+
jsonSchema ? ` jsonSchema: ${JSON.stringify(jsonSchema)},` : undefined,
|
|
298
|
+
` render: (args) => __belteRenderPromptTemplate__(__template__, args),`,
|
|
299
|
+
]
|
|
300
|
+
.filter((line) => line !== undefined)
|
|
301
|
+
.join('\n')
|
|
302
|
+
const contents = `import { definePrompt as __belteDefinePrompt__ } from '${importName}/server/prompts/definePrompt'
|
|
303
|
+
import { renderPromptTemplate as __belteRenderPromptTemplate__ } from '${importName}/server/prompts/renderPromptTemplate'
|
|
304
|
+
const __template__ = ${JSON.stringify(parsed.body)}
|
|
305
|
+
export const prompt = __belteDefinePrompt__(${JSON.stringify(name)}, {
|
|
306
|
+
${optionLines}
|
|
307
|
+
})
|
|
314
308
|
`
|
|
315
|
-
return {
|
|
316
|
-
contents: `${banner}${prepared.rewriteForServer(name)}`,
|
|
317
|
-
loader: 'ts',
|
|
318
|
-
}
|
|
309
|
+
return { contents, loader: 'ts' }
|
|
319
310
|
})
|
|
320
311
|
|
|
321
312
|
build.onLoad({ filter: /.*/, namespace: NS }, async (args) => {
|
|
@@ -474,6 +465,71 @@ export const ${prepared.exportName} = __belteSocketProxy__(${JSON.stringify(name
|
|
|
474
465
|
return { contents: `export default ${JSON.stringify(name)}`, loader: 'js' }
|
|
475
466
|
}
|
|
476
467
|
|
|
468
|
+
if (args.path === 'belte:bundle-window') {
|
|
469
|
+
/*
|
|
470
|
+
Optional bundle window config (title/size/menu) baked into
|
|
471
|
+
the bundled launcher. Re-exports the default from
|
|
472
|
+
src/bundle/window.ts when present; otherwise an empty
|
|
473
|
+
object so the launcher falls back to its defaults.
|
|
474
|
+
*/
|
|
475
|
+
const userFile = `${cwd}/src/bundle/window.ts`
|
|
476
|
+
if (existsSync(userFile)) {
|
|
477
|
+
log.info('using custom src/bundle/window.ts')
|
|
478
|
+
return {
|
|
479
|
+
contents: `export { default } from ${JSON.stringify(userFile)}`,
|
|
480
|
+
loader: 'js',
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return { contents: 'export default {}', loader: 'js' }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (args.path === 'belte:bundle-disconnected') {
|
|
487
|
+
/*
|
|
488
|
+
The connect screen HTML baked into the launcher. buildDisconnected
|
|
489
|
+
writes `${cwd}/dist/bundle-disconnected.html`; this virtual splices
|
|
490
|
+
it in as a string export. A minimal inline fallback keeps the
|
|
491
|
+
launcher buildable when the file is missing (the screen still loads,
|
|
492
|
+
just unstyled) — bundleApp always builds it first.
|
|
493
|
+
*/
|
|
494
|
+
const htmlPath = `${cwd}/dist/bundle-disconnected.html`
|
|
495
|
+
if (!existsSync(htmlPath)) {
|
|
496
|
+
const fallback =
|
|
497
|
+
'<!doctype html><html><body><div id="app">belte</div></body></html>'
|
|
498
|
+
return {
|
|
499
|
+
contents: `export const disconnectedHtml = ${JSON.stringify(fallback)}`,
|
|
500
|
+
loader: 'js',
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const html = await Bun.file(htmlPath).text()
|
|
504
|
+
return {
|
|
505
|
+
contents: `export const disconnectedHtml = ${JSON.stringify(html)}`,
|
|
506
|
+
loader: 'js',
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (args.path === 'belte:bundle-disconnected-component') {
|
|
511
|
+
/*
|
|
512
|
+
The Svelte component the connect-screen build mounts: the project's
|
|
513
|
+
src/bundle/disconnected.svelte override when present, otherwise the
|
|
514
|
+
lib default. Re-exports the default like belte:bundle-window; the
|
|
515
|
+
svelte loader plugin compiles the .svelte target either way.
|
|
516
|
+
*/
|
|
517
|
+
const userFile = `${cwd}/src/bundle/disconnected.svelte`
|
|
518
|
+
if (existsSync(userFile)) {
|
|
519
|
+
log.info('using custom src/bundle/disconnected.svelte')
|
|
520
|
+
return {
|
|
521
|
+
contents: `export { default } from ${JSON.stringify(userFile)}`,
|
|
522
|
+
loader: 'js',
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const defaultFile = new URL('./lib/bundle/disconnected.svelte', import.meta.url)
|
|
526
|
+
.pathname
|
|
527
|
+
return {
|
|
528
|
+
contents: `export { default } from ${JSON.stringify(defaultFile)}`,
|
|
529
|
+
loader: 'js',
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
477
533
|
if (args.path === 'belte:cli-chrome') {
|
|
478
534
|
/*
|
|
479
535
|
Optional CLI help chrome baked into the binary: src/cli/
|
|
@@ -522,30 +578,6 @@ export const footer = ${JSON.stringify(footer)}
|
|
|
522
578
|
}
|
|
523
579
|
}
|
|
524
580
|
|
|
525
|
-
if (args.path === 'belte:cli-rpcs') {
|
|
526
|
-
/*
|
|
527
|
-
Eager-import side-effect bundle for the FULL CLI
|
|
528
|
-
binary. Importing every rpc module fires defineVerb
|
|
529
|
-
so the verbRegistry is populated and createClient's
|
|
530
|
-
in-process fallback can dispatch. Thin builds emit
|
|
531
|
-
an empty module — the binary speaks remote-only.
|
|
532
|
-
|
|
533
|
-
`thin` is set by buildCli (default full — it passes
|
|
534
|
-
`thin: false` unless `--thin`). Defaults to full here
|
|
535
|
-
too so a stray APP_URL in the build environment can't
|
|
536
|
-
silently thin the bundle.
|
|
537
|
-
*/
|
|
538
|
-
const isThin = thin ?? false
|
|
539
|
-
if (isThin) {
|
|
540
|
-
return { contents: 'export {}', loader: 'js' }
|
|
541
|
-
}
|
|
542
|
-
const files = await scanRpcOnce()
|
|
543
|
-
const lines = files.map(
|
|
544
|
-
(file) => `import ${JSON.stringify(`${rpcDir}/${file}`)}`,
|
|
545
|
-
)
|
|
546
|
-
return { contents: `${lines.join('\n')}\nexport {}`, loader: 'js' }
|
|
547
|
-
}
|
|
548
|
-
|
|
549
581
|
if (args.path === 'belte:mcp') {
|
|
550
582
|
/*
|
|
551
583
|
The MCP server is fully framework-generated — tools from
|
|
@@ -553,9 +585,9 @@ export const footer = ${JSON.stringify(footer)}
|
|
|
553
585
|
from src/mcp/resources. createMcpServer is internal; there
|
|
554
586
|
is no user-authored server module.
|
|
555
587
|
*/
|
|
588
|
+
const importName = await belteImportNameOnce()
|
|
556
589
|
return {
|
|
557
|
-
contents:
|
|
558
|
-
"import { createMcpServer } from 'belte/mcp/createMcpServer'\nexport default createMcpServer()\n",
|
|
590
|
+
contents: `import { createMcpServer } from '${importName}/mcp/createMcpServer'\nexport default createMcpServer()\n`,
|
|
559
591
|
loader: 'js',
|
|
560
592
|
}
|
|
561
593
|
}
|
|
@@ -568,29 +600,16 @@ export const footer = ${JSON.stringify(footer)}
|
|
|
568
600
|
const files = await Array.fromAsync(
|
|
569
601
|
new Glob('**/*.zst').scan({ cwd: appDir, onlyFiles: true }),
|
|
570
602
|
)
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const entries = encoded.map((entry) => entry.line)
|
|
582
|
-
const totalBytes = encoded.reduce((total, entry) => total + entry.bytes, 0)
|
|
583
|
-
log.info(
|
|
584
|
-
`embedded ${encoded.length} zstd assets from dist/_app/ (${(totalBytes / 1024).toFixed(1)} KiB)`,
|
|
585
|
-
)
|
|
586
|
-
return {
|
|
587
|
-
contents: `const _d = (s) => Uint8Array.fromBase64(s)
|
|
588
|
-
export const assets = {
|
|
589
|
-
${entries.join('\n')}
|
|
590
|
-
}
|
|
591
|
-
`,
|
|
592
|
-
loader: 'js',
|
|
593
|
-
}
|
|
603
|
+
const contents = await embedZstdDir({
|
|
604
|
+
dir: appDir,
|
|
605
|
+
files,
|
|
606
|
+
keyFor: (file) => `/_app/${file.replace(/\.zst$/, '')}`,
|
|
607
|
+
precompressed: true,
|
|
608
|
+
exportName: 'assets',
|
|
609
|
+
label: 'zstd assets',
|
|
610
|
+
source: 'dist/_app/',
|
|
611
|
+
})
|
|
612
|
+
return { contents, loader: 'js' }
|
|
594
613
|
}
|
|
595
614
|
|
|
596
615
|
if (args.path === 'belte:public-assets') {
|
|
@@ -616,28 +635,16 @@ ${entries.join('\n')}
|
|
|
616
635
|
loader: 'js',
|
|
617
636
|
}
|
|
618
637
|
}
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const totalBytes = encoded.reduce((total, entry) => total + entry.bytes, 0)
|
|
630
|
-
log.info(
|
|
631
|
-
`embedded ${encoded.length} public files from public/ (${(totalBytes / 1024).toFixed(1)} KiB zstd)`,
|
|
632
|
-
)
|
|
633
|
-
return {
|
|
634
|
-
contents: `const _d = (s) => Uint8Array.fromBase64(s)
|
|
635
|
-
export const publicAssets = {
|
|
636
|
-
${encoded.map((entry) => entry.line).join('\n')}
|
|
637
|
-
}
|
|
638
|
-
`,
|
|
639
|
-
loader: 'js',
|
|
640
|
-
}
|
|
638
|
+
const contents = await embedZstdDir({
|
|
639
|
+
dir: publicDir,
|
|
640
|
+
files,
|
|
641
|
+
keyFor: (file) => `/${file}`,
|
|
642
|
+
precompressed: false,
|
|
643
|
+
exportName: 'publicAssets',
|
|
644
|
+
label: 'public files',
|
|
645
|
+
source: 'public/',
|
|
646
|
+
})
|
|
647
|
+
return { contents, loader: 'js' }
|
|
641
648
|
}
|
|
642
649
|
|
|
643
650
|
if (args.path === 'belte:mcp-resources') {
|
|
@@ -663,28 +670,16 @@ ${encoded.map((entry) => entry.line).join('\n')}
|
|
|
663
670
|
loader: 'js',
|
|
664
671
|
}
|
|
665
672
|
}
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const totalBytes = encoded.reduce((total, entry) => total + entry.bytes, 0)
|
|
677
|
-
log.info(
|
|
678
|
-
`embedded ${encoded.length} mcp resources from src/mcp/resources/ (${(totalBytes / 1024).toFixed(1)} KiB zstd)`,
|
|
679
|
-
)
|
|
680
|
-
return {
|
|
681
|
-
contents: `const _d = (s) => Uint8Array.fromBase64(s)
|
|
682
|
-
export const mcpResources = {
|
|
683
|
-
${encoded.map((entry) => entry.line).join('\n')}
|
|
684
|
-
}
|
|
685
|
-
`,
|
|
686
|
-
loader: 'js',
|
|
687
|
-
}
|
|
673
|
+
const contents = await embedZstdDir({
|
|
674
|
+
dir: resourcesDir,
|
|
675
|
+
files,
|
|
676
|
+
keyFor: (file) => file,
|
|
677
|
+
precompressed: false,
|
|
678
|
+
exportName: 'mcpResources',
|
|
679
|
+
label: 'mcp resources',
|
|
680
|
+
source: 'src/mcp/resources/',
|
|
681
|
+
})
|
|
682
|
+
return { contents, loader: 'js' }
|
|
688
683
|
}
|
|
689
684
|
|
|
690
685
|
if (args.path === 'belte:shell') {
|
|
@@ -701,6 +696,54 @@ ${encoded.map((entry) => entry.line).join('\n')}
|
|
|
701
696
|
}
|
|
702
697
|
}
|
|
703
698
|
|
|
699
|
+
/*
|
|
700
|
+
Encodes every file in `files` (relative to `dir`) into a base64 zstd map and
|
|
701
|
+
emits `export const <exportName> = { "<key>": _d("<base64>") }`. `keyFor` maps
|
|
702
|
+
a relative path to its lookup key; `precompressed` true means the files are
|
|
703
|
+
already `.zst` on disk (read + base64 as-is), false means compress here at
|
|
704
|
+
level 22. Shared by the belte:assets / belte:public-assets / belte:mcp-resources
|
|
705
|
+
virtuals, which differ only in source dir, key shape, and whether the inputs
|
|
706
|
+
are pre-compressed.
|
|
707
|
+
*/
|
|
708
|
+
async function embedZstdDir({
|
|
709
|
+
dir,
|
|
710
|
+
files,
|
|
711
|
+
keyFor,
|
|
712
|
+
precompressed,
|
|
713
|
+
exportName,
|
|
714
|
+
label,
|
|
715
|
+
source,
|
|
716
|
+
}: {
|
|
717
|
+
dir: string
|
|
718
|
+
files: string[]
|
|
719
|
+
keyFor: (file: string) => string
|
|
720
|
+
precompressed: boolean
|
|
721
|
+
exportName: string
|
|
722
|
+
label: string
|
|
723
|
+
source: string
|
|
724
|
+
}): Promise<string> {
|
|
725
|
+
const encoded = await Promise.all(
|
|
726
|
+
files.map(async (file) => {
|
|
727
|
+
const raw = await Bun.file(`${dir}/${file}`).bytes()
|
|
728
|
+
const bytes = precompressed ? raw : await Bun.zstdCompress(raw, { level: 22 })
|
|
729
|
+
return {
|
|
730
|
+
line: ` ${JSON.stringify(keyFor(file))}: _d(${JSON.stringify(bytes.toBase64())}),`,
|
|
731
|
+
bytes: bytes.byteLength,
|
|
732
|
+
}
|
|
733
|
+
}),
|
|
734
|
+
)
|
|
735
|
+
const totalBytes = encoded.reduce((total, entry) => total + entry.bytes, 0)
|
|
736
|
+
const unit = precompressed ? 'KiB' : 'KiB zstd'
|
|
737
|
+
log.info(
|
|
738
|
+
`embedded ${encoded.length} ${label} from ${source} (${(totalBytes / 1024).toFixed(1)} ${unit})`,
|
|
739
|
+
)
|
|
740
|
+
return `const _d = (s) => Uint8Array.fromBase64(s)
|
|
741
|
+
export const ${exportName} = {
|
|
742
|
+
${encoded.map((entry) => entry.line).join('\n')}
|
|
743
|
+
}
|
|
744
|
+
`
|
|
745
|
+
}
|
|
746
|
+
|
|
704
747
|
type PagesScan = {
|
|
705
748
|
pageFiles: string[]
|
|
706
749
|
layoutFiles: string[]
|
|
@@ -765,15 +808,15 @@ async function scanSockets(socketsDir: string): Promise<string[]> {
|
|
|
765
808
|
}
|
|
766
809
|
|
|
767
810
|
/*
|
|
768
|
-
Walks src/mcp/prompts once. Each `.
|
|
769
|
-
|
|
770
|
-
prompts builds the same.
|
|
811
|
+
Walks src/mcp/prompts once. Each `.md` file declares one MCP prompt —
|
|
812
|
+
frontmatter for metadata, body for the template. Returns an empty list
|
|
813
|
+
when the directory doesn't exist so an app without prompts builds the same.
|
|
771
814
|
*/
|
|
772
815
|
async function scanPrompts(promptsDir: string): Promise<string[]> {
|
|
773
816
|
if (!existsSync(promptsDir)) {
|
|
774
817
|
return []
|
|
775
818
|
}
|
|
776
|
-
return await Array.fromAsync(new Glob('**/*.
|
|
819
|
+
return await Array.fromAsync(new Glob('**/*.md').scan({ cwd: promptsDir }))
|
|
777
820
|
}
|
|
778
821
|
|
|
779
822
|
/*
|
|
@@ -810,17 +853,8 @@ async function rewriteHashedClientEntries(shell: string, cwd: string): Promise<s
|
|
|
810
853
|
const entries = await Array.fromAsync(
|
|
811
854
|
new Glob('client-*').scan({ cwd: appDir, onlyFiles: true }),
|
|
812
855
|
)
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
for (const file of entries) {
|
|
816
|
-
if (!jsEntry && /^client-[a-z0-9]+\.js$/i.test(file)) {
|
|
817
|
-
jsEntry = file
|
|
818
|
-
continue
|
|
819
|
-
}
|
|
820
|
-
if (!cssEntry && /^client-[a-z0-9]+\.css$/i.test(file)) {
|
|
821
|
-
cssEntry = file
|
|
822
|
-
}
|
|
823
|
-
}
|
|
856
|
+
const jsEntry = entries.find((file) => /^client-[a-z0-9]+\.js$/i.test(file))
|
|
857
|
+
const cssEntry = entries.find((file) => /^client-[a-z0-9]+\.css$/i.test(file))
|
|
824
858
|
let result = shell
|
|
825
859
|
if (jsEntry) {
|
|
826
860
|
result = result.replace('/_app/client.js', `/_app/${jsEntry}`)
|