@briancray/belte 0.1.0 → 0.2.0
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 +217 -194
- 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/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/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 +2 -1
- 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,11 @@
|
|
|
2
2
|
import { existsSync, statSync } from 'node:fs'
|
|
3
3
|
import type { BunPlugin } from 'bun'
|
|
4
4
|
import { Glob } from 'bun'
|
|
5
|
+
import { fileStem } from './lib/shared/fileStem.ts'
|
|
6
|
+
import { jsonSchemaForPromptArguments } from './lib/shared/jsonSchemaForPromptArguments.ts'
|
|
5
7
|
import { log } from './lib/shared/log.ts'
|
|
6
8
|
import { pageUrlForFile } from './lib/shared/pageUrlForFile.ts'
|
|
7
|
-
import {
|
|
9
|
+
import { parsePromptMarkdown } from './lib/shared/parsePromptMarkdown.ts'
|
|
8
10
|
import { prepareRpcModule } from './lib/shared/prepareRpcModule.ts'
|
|
9
11
|
import { prepareSocketModule } from './lib/shared/prepareSocketModule.ts'
|
|
10
12
|
import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
|
|
@@ -57,6 +59,17 @@ function escapeRegex(value: string): string {
|
|
|
57
59
|
return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/* Memoises a zero-arg async producer so repeat calls reuse the first in-flight promise. */
|
|
63
|
+
function once<T>(produce: () => Promise<T>): () => Promise<T> {
|
|
64
|
+
let promise: Promise<T> | undefined
|
|
65
|
+
return () => {
|
|
66
|
+
if (!promise) {
|
|
67
|
+
promise = produce()
|
|
68
|
+
}
|
|
69
|
+
return promise
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
/*
|
|
61
74
|
Bun plugin that wires every virtual import belte produces at build time:
|
|
62
75
|
- `belte:rpc` — { rpcUrl: () => import(rpc-module) } HTTP-verb manifest
|
|
@@ -81,12 +94,10 @@ export function belteResolverPlugin({
|
|
|
81
94
|
cwd = process.cwd(),
|
|
82
95
|
embedAssets = false,
|
|
83
96
|
target = 'server',
|
|
84
|
-
thin,
|
|
85
97
|
}: {
|
|
86
98
|
cwd?: string
|
|
87
99
|
embedAssets?: boolean
|
|
88
100
|
target?: 'server' | 'client'
|
|
89
|
-
thin?: boolean
|
|
90
101
|
} = {}): BunPlugin {
|
|
91
102
|
const serverDir = `${cwd}/src/server`
|
|
92
103
|
const browserDir = `${cwd}/src/browser`
|
|
@@ -107,58 +118,30 @@ export function belteResolverPlugin({
|
|
|
107
118
|
re-globbing the trees. The shell read is memoised the same way so two
|
|
108
119
|
passes don't re-read app.html from disk.
|
|
109
120
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
return pagesScanPromise
|
|
123
|
-
}
|
|
124
|
-
function scanRpcOnce(): Promise<string[]> {
|
|
125
|
-
if (!rpcScanPromise) {
|
|
126
|
-
rpcScanPromise = scanRpc(rpcDir)
|
|
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
|
-
}
|
|
121
|
+
const scanPagesOnce = once(() =>
|
|
122
|
+
scanPages(pagesDir).then(async (scan) => {
|
|
123
|
+
await writeRoutesDts({ cwd, pageFiles: scan.pageFiles })
|
|
124
|
+
return scan
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
const scanRpcOnce = once(() => scanRpc(rpcDir))
|
|
128
|
+
const scanSocketsOnce = once(() => scanSockets(socketsDir))
|
|
129
|
+
const scanPromptsOnce = once(() => scanPrompts(promptsDir))
|
|
130
|
+
const loadShellOnce = once(() => loadShell(cwd))
|
|
148
131
|
|
|
149
132
|
const rpcFilter = new RegExp(`^${escapeRegex(rpcDir)}/.*\\.ts$`)
|
|
150
133
|
const socketsFilter = new RegExp(`^${escapeRegex(socketsDir)}/.*\\.ts$`)
|
|
151
|
-
const promptsFilter = new RegExp(`^${escapeRegex(promptsDir)}/.*\\.
|
|
134
|
+
const promptsFilter = new RegExp(`^${escapeRegex(promptsDir)}/.*\\.md$`)
|
|
152
135
|
|
|
153
136
|
return {
|
|
154
137
|
name: 'belte-resolver',
|
|
155
138
|
setup(build) {
|
|
156
139
|
build.onResolve(
|
|
157
140
|
{
|
|
158
|
-
filter: /\/_virtual\/(rpc|sockets|prompts|pages|layouts|app|mcp-resources|mcp|assets|public-assets|shell|app-info|cli-manifest|cli-name|cli-chrome|
|
|
141
|
+
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
142
|
},
|
|
160
143
|
(args) => {
|
|
161
|
-
const name = args.path
|
|
144
|
+
const name = fileStem(args.path)
|
|
162
145
|
if (!name) {
|
|
163
146
|
return undefined
|
|
164
147
|
}
|
|
@@ -172,30 +155,19 @@ export function belteResolverPlugin({
|
|
|
172
155
|
`$browser/pages/...`, `$mcp/prompts/...`, `$mcp/resources/...`.
|
|
173
156
|
`lib/` is userland — projects declare their own lib aliases.
|
|
174
157
|
*/
|
|
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
|
-
})
|
|
158
|
+
const dirAliases: Record<string, string> = {
|
|
159
|
+
$server: serverDir,
|
|
160
|
+
$browser: browserDir,
|
|
161
|
+
$shared: sharedDir,
|
|
162
|
+
$mcp: mcpDir,
|
|
163
|
+
$cli: cliDir,
|
|
164
|
+
}
|
|
165
|
+
for (const [alias, baseDir] of Object.entries(dirAliases)) {
|
|
166
|
+
build.onResolve({ filter: new RegExp(`^\\${alias}(\\/.*)?$`) }, (args) => {
|
|
167
|
+
const subpath = args.path.slice(alias.length)
|
|
168
|
+
return { path: resolveExtension(subpath ? `${baseDir}${subpath}` : baseDir) }
|
|
169
|
+
})
|
|
170
|
+
}
|
|
199
171
|
|
|
200
172
|
build.onLoad({ filter: rpcFilter }, async (args) => {
|
|
201
173
|
if (!args.path.startsWith(`${rpcDir}/`)) {
|
|
@@ -210,7 +182,7 @@ export function belteResolverPlugin({
|
|
|
210
182
|
`[belte] src/server/rpc/${relativePath} has no \`export const <name> = <VERB>(...)\` — every $rpc module must declare exactly one remote function`,
|
|
211
183
|
)
|
|
212
184
|
}
|
|
213
|
-
const expectedName = relativePath
|
|
185
|
+
const expectedName = fileStem(relativePath)
|
|
214
186
|
if (prepared.exportName !== expectedName) {
|
|
215
187
|
throw new Error(
|
|
216
188
|
`[belte] src/server/rpc/${relativePath} exports \`${prepared.exportName}\` but the filename expects \`${expectedName}\` — the export name must match the file's stem`,
|
|
@@ -257,7 +229,7 @@ export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prep
|
|
|
257
229
|
`[belte] src/server/sockets/${relativePath} has no \`export const <name> = socket(...)\` — every $sockets module must declare exactly one socket`,
|
|
258
230
|
)
|
|
259
231
|
}
|
|
260
|
-
const expectedName = relativePath
|
|
232
|
+
const expectedName = fileStem(relativePath)
|
|
261
233
|
if (prepared.exportName !== expectedName) {
|
|
262
234
|
throw new Error(
|
|
263
235
|
`[belte] src/server/sockets/${relativePath} exports \`${prepared.exportName}\` but the filename expects \`${expectedName}\` — the export name must match the file's stem`,
|
|
@@ -290,32 +262,40 @@ export const ${prepared.exportName} = __belteSocketProxy__(${JSON.stringify(name
|
|
|
290
262
|
Prompts are MCP-only — no client-side counterpart. The
|
|
291
263
|
client bundle never imports a prompts module, but emit an
|
|
292
264
|
empty stub for the client target defensively so a stray
|
|
293
|
-
import can't drag the
|
|
265
|
+
import can't drag the prompt body into the browser bundle.
|
|
294
266
|
*/
|
|
295
267
|
if (target === 'client') {
|
|
296
268
|
return { contents: 'export {}', loader: 'ts' }
|
|
297
269
|
}
|
|
270
|
+
/*
|
|
271
|
+
Server target: a `.md` prompt is data, not code. Parse the
|
|
272
|
+
frontmatter (description + arguments) and body once, then
|
|
273
|
+
generate a module that registers the prompt via definePrompt
|
|
274
|
+
— the body is embedded as a string literal and the render
|
|
275
|
+
closure interpolates `{{name}}` placeholders at call time.
|
|
276
|
+
*/
|
|
298
277
|
const relativePath = args.path.slice(promptsDir.length + 1)
|
|
299
278
|
const source = await Bun.file(args.path).text()
|
|
300
279
|
const name = promptNameForFile(relativePath)
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
|
|
280
|
+
const parsed = parsePromptMarkdown(source)
|
|
281
|
+
const jsonSchema = jsonSchemaForPromptArguments(parsed.arguments)
|
|
282
|
+
const optionLines = [
|
|
283
|
+
parsed.description
|
|
284
|
+
? ` description: ${JSON.stringify(parsed.description)},`
|
|
285
|
+
: undefined,
|
|
286
|
+
jsonSchema ? ` jsonSchema: ${JSON.stringify(jsonSchema)},` : undefined,
|
|
287
|
+
` render: (args) => __belteRenderPromptTemplate__(__template__, args),`,
|
|
288
|
+
]
|
|
289
|
+
.filter((line) => line !== undefined)
|
|
290
|
+
.join('\n')
|
|
291
|
+
const contents = `import { definePrompt as __belteDefinePrompt__ } from 'belte/server/prompts/definePrompt'
|
|
292
|
+
import { renderPromptTemplate as __belteRenderPromptTemplate__ } from 'belte/server/prompts/renderPromptTemplate'
|
|
293
|
+
const __template__ = ${JSON.stringify(parsed.body)}
|
|
294
|
+
export const prompt = __belteDefinePrompt__(${JSON.stringify(name)}, {
|
|
295
|
+
${optionLines}
|
|
296
|
+
})
|
|
314
297
|
`
|
|
315
|
-
return {
|
|
316
|
-
contents: `${banner}${prepared.rewriteForServer(name)}`,
|
|
317
|
-
loader: 'ts',
|
|
318
|
-
}
|
|
298
|
+
return { contents, loader: 'ts' }
|
|
319
299
|
})
|
|
320
300
|
|
|
321
301
|
build.onLoad({ filter: /.*/, namespace: NS }, async (args) => {
|
|
@@ -474,6 +454,71 @@ export const ${prepared.exportName} = __belteSocketProxy__(${JSON.stringify(name
|
|
|
474
454
|
return { contents: `export default ${JSON.stringify(name)}`, loader: 'js' }
|
|
475
455
|
}
|
|
476
456
|
|
|
457
|
+
if (args.path === 'belte:bundle-window') {
|
|
458
|
+
/*
|
|
459
|
+
Optional bundle window config (title/size/menu) baked into
|
|
460
|
+
the bundled launcher. Re-exports the default from
|
|
461
|
+
src/bundle/window.ts when present; otherwise an empty
|
|
462
|
+
object so the launcher falls back to its defaults.
|
|
463
|
+
*/
|
|
464
|
+
const userFile = `${cwd}/src/bundle/window.ts`
|
|
465
|
+
if (existsSync(userFile)) {
|
|
466
|
+
log.info('using custom src/bundle/window.ts')
|
|
467
|
+
return {
|
|
468
|
+
contents: `export { default } from ${JSON.stringify(userFile)}`,
|
|
469
|
+
loader: 'js',
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return { contents: 'export default {}', loader: 'js' }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (args.path === 'belte:bundle-disconnected') {
|
|
476
|
+
/*
|
|
477
|
+
The connect screen HTML baked into the launcher. buildDisconnected
|
|
478
|
+
writes `${cwd}/dist/bundle-disconnected.html`; this virtual splices
|
|
479
|
+
it in as a string export. A minimal inline fallback keeps the
|
|
480
|
+
launcher buildable when the file is missing (the screen still loads,
|
|
481
|
+
just unstyled) — bundleApp always builds it first.
|
|
482
|
+
*/
|
|
483
|
+
const htmlPath = `${cwd}/dist/bundle-disconnected.html`
|
|
484
|
+
if (!existsSync(htmlPath)) {
|
|
485
|
+
const fallback =
|
|
486
|
+
'<!doctype html><html><body><div id="app">belte</div></body></html>'
|
|
487
|
+
return {
|
|
488
|
+
contents: `export const disconnectedHtml = ${JSON.stringify(fallback)}`,
|
|
489
|
+
loader: 'js',
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const html = await Bun.file(htmlPath).text()
|
|
493
|
+
return {
|
|
494
|
+
contents: `export const disconnectedHtml = ${JSON.stringify(html)}`,
|
|
495
|
+
loader: 'js',
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (args.path === 'belte:bundle-disconnected-component') {
|
|
500
|
+
/*
|
|
501
|
+
The Svelte component the connect-screen build mounts: the project's
|
|
502
|
+
src/bundle/disconnected.svelte override when present, otherwise the
|
|
503
|
+
lib default. Re-exports the default like belte:bundle-window; the
|
|
504
|
+
svelte loader plugin compiles the .svelte target either way.
|
|
505
|
+
*/
|
|
506
|
+
const userFile = `${cwd}/src/bundle/disconnected.svelte`
|
|
507
|
+
if (existsSync(userFile)) {
|
|
508
|
+
log.info('using custom src/bundle/disconnected.svelte')
|
|
509
|
+
return {
|
|
510
|
+
contents: `export { default } from ${JSON.stringify(userFile)}`,
|
|
511
|
+
loader: 'js',
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const defaultFile = new URL('./lib/bundle/disconnected.svelte', import.meta.url)
|
|
515
|
+
.pathname
|
|
516
|
+
return {
|
|
517
|
+
contents: `export { default } from ${JSON.stringify(defaultFile)}`,
|
|
518
|
+
loader: 'js',
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
477
522
|
if (args.path === 'belte:cli-chrome') {
|
|
478
523
|
/*
|
|
479
524
|
Optional CLI help chrome baked into the binary: src/cli/
|
|
@@ -522,30 +567,6 @@ export const footer = ${JSON.stringify(footer)}
|
|
|
522
567
|
}
|
|
523
568
|
}
|
|
524
569
|
|
|
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
570
|
if (args.path === 'belte:mcp') {
|
|
550
571
|
/*
|
|
551
572
|
The MCP server is fully framework-generated — tools from
|
|
@@ -568,29 +589,16 @@ export const footer = ${JSON.stringify(footer)}
|
|
|
568
589
|
const files = await Array.fromAsync(
|
|
569
590
|
new Glob('**/*.zst').scan({ cwd: appDir, onlyFiles: true }),
|
|
570
591
|
)
|
|
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
|
-
}
|
|
592
|
+
const contents = await embedZstdDir({
|
|
593
|
+
dir: appDir,
|
|
594
|
+
files,
|
|
595
|
+
keyFor: (file) => `/_app/${file.replace(/\.zst$/, '')}`,
|
|
596
|
+
precompressed: true,
|
|
597
|
+
exportName: 'assets',
|
|
598
|
+
label: 'zstd assets',
|
|
599
|
+
source: 'dist/_app/',
|
|
600
|
+
})
|
|
601
|
+
return { contents, loader: 'js' }
|
|
594
602
|
}
|
|
595
603
|
|
|
596
604
|
if (args.path === 'belte:public-assets') {
|
|
@@ -616,28 +624,16 @@ ${entries.join('\n')}
|
|
|
616
624
|
loader: 'js',
|
|
617
625
|
}
|
|
618
626
|
}
|
|
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
|
-
}
|
|
627
|
+
const contents = await embedZstdDir({
|
|
628
|
+
dir: publicDir,
|
|
629
|
+
files,
|
|
630
|
+
keyFor: (file) => `/${file}`,
|
|
631
|
+
precompressed: false,
|
|
632
|
+
exportName: 'publicAssets',
|
|
633
|
+
label: 'public files',
|
|
634
|
+
source: 'public/',
|
|
635
|
+
})
|
|
636
|
+
return { contents, loader: 'js' }
|
|
641
637
|
}
|
|
642
638
|
|
|
643
639
|
if (args.path === 'belte:mcp-resources') {
|
|
@@ -663,28 +659,16 @@ ${encoded.map((entry) => entry.line).join('\n')}
|
|
|
663
659
|
loader: 'js',
|
|
664
660
|
}
|
|
665
661
|
}
|
|
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
|
-
}
|
|
662
|
+
const contents = await embedZstdDir({
|
|
663
|
+
dir: resourcesDir,
|
|
664
|
+
files,
|
|
665
|
+
keyFor: (file) => file,
|
|
666
|
+
precompressed: false,
|
|
667
|
+
exportName: 'mcpResources',
|
|
668
|
+
label: 'mcp resources',
|
|
669
|
+
source: 'src/mcp/resources/',
|
|
670
|
+
})
|
|
671
|
+
return { contents, loader: 'js' }
|
|
688
672
|
}
|
|
689
673
|
|
|
690
674
|
if (args.path === 'belte:shell') {
|
|
@@ -701,6 +685,54 @@ ${encoded.map((entry) => entry.line).join('\n')}
|
|
|
701
685
|
}
|
|
702
686
|
}
|
|
703
687
|
|
|
688
|
+
/*
|
|
689
|
+
Encodes every file in `files` (relative to `dir`) into a base64 zstd map and
|
|
690
|
+
emits `export const <exportName> = { "<key>": _d("<base64>") }`. `keyFor` maps
|
|
691
|
+
a relative path to its lookup key; `precompressed` true means the files are
|
|
692
|
+
already `.zst` on disk (read + base64 as-is), false means compress here at
|
|
693
|
+
level 22. Shared by the belte:assets / belte:public-assets / belte:mcp-resources
|
|
694
|
+
virtuals, which differ only in source dir, key shape, and whether the inputs
|
|
695
|
+
are pre-compressed.
|
|
696
|
+
*/
|
|
697
|
+
async function embedZstdDir({
|
|
698
|
+
dir,
|
|
699
|
+
files,
|
|
700
|
+
keyFor,
|
|
701
|
+
precompressed,
|
|
702
|
+
exportName,
|
|
703
|
+
label,
|
|
704
|
+
source,
|
|
705
|
+
}: {
|
|
706
|
+
dir: string
|
|
707
|
+
files: string[]
|
|
708
|
+
keyFor: (file: string) => string
|
|
709
|
+
precompressed: boolean
|
|
710
|
+
exportName: string
|
|
711
|
+
label: string
|
|
712
|
+
source: string
|
|
713
|
+
}): Promise<string> {
|
|
714
|
+
const encoded = await Promise.all(
|
|
715
|
+
files.map(async (file) => {
|
|
716
|
+
const raw = await Bun.file(`${dir}/${file}`).bytes()
|
|
717
|
+
const bytes = precompressed ? raw : await Bun.zstdCompress(raw, { level: 22 })
|
|
718
|
+
return {
|
|
719
|
+
line: ` ${JSON.stringify(keyFor(file))}: _d(${JSON.stringify(bytes.toBase64())}),`,
|
|
720
|
+
bytes: bytes.byteLength,
|
|
721
|
+
}
|
|
722
|
+
}),
|
|
723
|
+
)
|
|
724
|
+
const totalBytes = encoded.reduce((total, entry) => total + entry.bytes, 0)
|
|
725
|
+
const unit = precompressed ? 'KiB' : 'KiB zstd'
|
|
726
|
+
log.info(
|
|
727
|
+
`embedded ${encoded.length} ${label} from ${source} (${(totalBytes / 1024).toFixed(1)} ${unit})`,
|
|
728
|
+
)
|
|
729
|
+
return `const _d = (s) => Uint8Array.fromBase64(s)
|
|
730
|
+
export const ${exportName} = {
|
|
731
|
+
${encoded.map((entry) => entry.line).join('\n')}
|
|
732
|
+
}
|
|
733
|
+
`
|
|
734
|
+
}
|
|
735
|
+
|
|
704
736
|
type PagesScan = {
|
|
705
737
|
pageFiles: string[]
|
|
706
738
|
layoutFiles: string[]
|
|
@@ -765,15 +797,15 @@ async function scanSockets(socketsDir: string): Promise<string[]> {
|
|
|
765
797
|
}
|
|
766
798
|
|
|
767
799
|
/*
|
|
768
|
-
Walks src/mcp/prompts once. Each `.
|
|
769
|
-
|
|
770
|
-
prompts builds the same.
|
|
800
|
+
Walks src/mcp/prompts once. Each `.md` file declares one MCP prompt —
|
|
801
|
+
frontmatter for metadata, body for the template. Returns an empty list
|
|
802
|
+
when the directory doesn't exist so an app without prompts builds the same.
|
|
771
803
|
*/
|
|
772
804
|
async function scanPrompts(promptsDir: string): Promise<string[]> {
|
|
773
805
|
if (!existsSync(promptsDir)) {
|
|
774
806
|
return []
|
|
775
807
|
}
|
|
776
|
-
return await Array.fromAsync(new Glob('**/*.
|
|
808
|
+
return await Array.fromAsync(new Glob('**/*.md').scan({ cwd: promptsDir }))
|
|
777
809
|
}
|
|
778
810
|
|
|
779
811
|
/*
|
|
@@ -810,17 +842,8 @@ async function rewriteHashedClientEntries(shell: string, cwd: string): Promise<s
|
|
|
810
842
|
const entries = await Array.fromAsync(
|
|
811
843
|
new Glob('client-*').scan({ cwd: appDir, onlyFiles: true }),
|
|
812
844
|
)
|
|
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
|
-
}
|
|
845
|
+
const jsEntry = entries.find((file) => /^client-[a-z0-9]+\.js$/i.test(file))
|
|
846
|
+
const cssEntry = entries.find((file) => /^client-[a-z0-9]+\.css$/i.test(file))
|
|
824
847
|
let result = shell
|
|
825
848
|
if (jsEntry) {
|
|
826
849
|
result = result.replace('/_app/client.js', `/_app/${jsEntry}`)
|
package/src/build.ts
CHANGED
|
@@ -1,68 +1,12 @@
|
|
|
1
1
|
import type { BunPlugin } from 'bun'
|
|
2
2
|
import { belteResolverPlugin } from './belteResolverPlugin.ts'
|
|
3
|
+
import { dedupeSveltePlugin } from './dedupeSveltePlugin.ts'
|
|
3
4
|
import type { SvelteConfig } from './lib/server/runtime/types/SvelteConfig.ts'
|
|
5
|
+
import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
|
|
4
6
|
import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
|
|
5
7
|
import { log } from './lib/shared/log.ts'
|
|
6
8
|
import { sveltePlugin } from './sveltePlugin.ts'
|
|
7
9
|
|
|
8
|
-
type ExportEntry = string | { [condition: string]: ExportEntry }
|
|
9
|
-
|
|
10
|
-
/*
|
|
11
|
-
Walks a package.json `exports` entry, returning the first leaf string that
|
|
12
|
-
matches the supplied condition list in order. Returns undefined when no
|
|
13
|
-
branch resolves.
|
|
14
|
-
*/
|
|
15
|
-
function pickExport(entry: ExportEntry, conditions: string[]): string | undefined {
|
|
16
|
-
if (typeof entry === 'string') {
|
|
17
|
-
return entry
|
|
18
|
-
}
|
|
19
|
-
for (const condition of conditions) {
|
|
20
|
-
if (entry[condition]) {
|
|
21
|
-
const resolved = pickExport(entry[condition], conditions)
|
|
22
|
-
if (resolved) {
|
|
23
|
-
return resolved
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return undefined
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/*
|
|
31
|
-
Forces every `import 'svelte/...'` (from belte's own source, the consumer's
|
|
32
|
-
source, or any transitive dep) to resolve against the consumer app's svelte
|
|
33
|
-
install, picking the export condition that matches the build target.
|
|
34
|
-
Without this, belte's symlinked source can pick up a second svelte from its
|
|
35
|
-
install location, ship both runtimes, and break hydration.
|
|
36
|
-
*/
|
|
37
|
-
function dedupeSveltePlugin({ cwd, conditions }: { cwd: string; conditions: string[] }): BunPlugin {
|
|
38
|
-
const consumerSvelte = `${cwd}/node_modules/svelte`
|
|
39
|
-
return {
|
|
40
|
-
name: 'belte-dedupe-svelte',
|
|
41
|
-
async setup(build) {
|
|
42
|
-
const pkgFile = Bun.file(`${consumerSvelte}/package.json`)
|
|
43
|
-
if (!(await pkgFile.exists())) {
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
const consumerPackage = (await pkgFile.json()) as {
|
|
47
|
-
exports: Record<string, ExportEntry>
|
|
48
|
-
}
|
|
49
|
-
build.onResolve({ filter: /^svelte(\/.*)?$/ }, (args) => {
|
|
50
|
-
const subpath =
|
|
51
|
-
args.path === 'svelte' ? '.' : `.${args.path.slice('svelte'.length)}`
|
|
52
|
-
const entry = consumerPackage.exports[subpath]
|
|
53
|
-
if (!entry) {
|
|
54
|
-
return undefined
|
|
55
|
-
}
|
|
56
|
-
const resolvedFile = pickExport(entry, conditions)
|
|
57
|
-
if (!resolvedFile) {
|
|
58
|
-
return undefined
|
|
59
|
-
}
|
|
60
|
-
return { path: `${consumerSvelte}/${resolvedFile.replace(/^\.\//, '')}` }
|
|
61
|
-
})
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
10
|
const CLIENT_ENTRY = new URL('./clientEntry.ts', import.meta.url).pathname
|
|
67
11
|
|
|
68
12
|
/*
|
|
@@ -118,17 +62,12 @@ export async function build({
|
|
|
118
62
|
plugins,
|
|
119
63
|
})
|
|
120
64
|
|
|
121
|
-
|
|
122
|
-
for (const entry of result.logs) {
|
|
123
|
-
log.error(entry)
|
|
124
|
-
}
|
|
125
|
-
process.exit(1)
|
|
126
|
-
}
|
|
65
|
+
exitOnBuildFailure(result)
|
|
127
66
|
|
|
128
67
|
const compressedByteLengths = await Promise.all(
|
|
129
68
|
result.outputs.map(async (output) => {
|
|
130
69
|
const bytes = await Bun.file(output.path).bytes()
|
|
131
|
-
const compressed = Bun.
|
|
70
|
+
const compressed = await Bun.zstdCompress(bytes, { level: 22 })
|
|
132
71
|
await Bun.write(`${output.path}.zst`, compressed)
|
|
133
72
|
return compressed.byteLength
|
|
134
73
|
}),
|
|
@@ -138,7 +77,7 @@ export async function build({
|
|
|
138
77
|
log.success(
|
|
139
78
|
`wrote ${result.outputs.length} files to ${outDir} (+${result.outputs.length} .zst, ${(compressedBytes / 1024).toFixed(1)} KiB total)`,
|
|
140
79
|
)
|
|
141
|
-
|
|
80
|
+
result.outputs.forEach((output) => {
|
|
142
81
|
log.detail(` - ${output.path}`)
|
|
143
|
-
}
|
|
82
|
+
})
|
|
144
83
|
}
|