@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.
Files changed (98) hide show
  1. package/bin/belte.ts +25 -12
  2. package/package.json +2 -1
  3. package/src/appEntry.ts +124 -0
  4. package/src/belteResolverPlugin.ts +217 -194
  5. package/src/build.ts +6 -67
  6. package/src/buildCli.ts +36 -63
  7. package/src/buildDisconnected.ts +127 -0
  8. package/src/bundleApp.ts +123 -0
  9. package/src/bundleDisconnectedEntry.ts +17 -0
  10. package/src/cliEntry.ts +3 -9
  11. package/src/compile.ts +4 -15
  12. package/src/controlServerWorker.ts +261 -0
  13. package/src/dedupeSveltePlugin.ts +66 -0
  14. package/src/discoveryEntry.ts +12 -11
  15. package/src/lib/browser/cache.ts +3 -6
  16. package/src/lib/browser/page.svelte.ts +19 -21
  17. package/src/lib/browser/socketChannel.ts +11 -1
  18. package/src/lib/browser/types/Pages.ts +1 -1
  19. package/src/lib/bundle/BundleMenu.ts +11 -0
  20. package/src/lib/bundle/BundleMenuItem.ts +24 -0
  21. package/src/lib/bundle/BundleWindow.ts +20 -0
  22. package/src/lib/bundle/bindConnectedFlag.ts +29 -0
  23. package/src/lib/bundle/bindRequestNavigate.ts +31 -0
  24. package/src/lib/bundle/buildWebviewLib.ts +111 -0
  25. package/src/lib/bundle/disconnected.css +9 -0
  26. package/src/lib/bundle/disconnected.svelte +192 -0
  27. package/src/lib/bundle/ensureWebviewLib.ts +20 -0
  28. package/src/lib/bundle/exitWithParent.ts +28 -0
  29. package/src/lib/bundle/findFreePort.ts +14 -0
  30. package/src/lib/bundle/infoPlist.ts +46 -0
  31. package/src/lib/bundle/installMacMenu.ts +39 -0
  32. package/src/lib/bundle/listenLocalControlServer.ts +19 -0
  33. package/src/lib/bundle/native/belteMenu.mm +298 -0
  34. package/src/lib/bundle/native/webview.h +4557 -0
  35. package/src/lib/bundle/onMenu.ts +26 -0
  36. package/src/lib/bundle/openWebview.ts +81 -0
  37. package/src/lib/bundle/pngToIcns.ts +47 -0
  38. package/src/lib/bundle/probeBelteServer.ts +34 -0
  39. package/src/lib/bundle/resolveServerBinary.ts +12 -0
  40. package/src/lib/bundle/resolveWebviewLib.ts +51 -0
  41. package/src/lib/bundle/serverBinaryFilename.ts +8 -0
  42. package/src/lib/bundle/stableLocalPort.ts +19 -0
  43. package/src/lib/bundle/waitForServer.ts +23 -0
  44. package/src/lib/bundle/webviewBuildRevision.ts +9 -0
  45. package/src/lib/bundle/webviewCachePath.ts +23 -0
  46. package/src/lib/bundle/webviewLibName.ts +11 -0
  47. package/src/lib/bundle/webviewVersion.ts +7 -0
  48. package/src/lib/cli/createClient.ts +34 -36
  49. package/src/lib/cli/printHelp.ts +45 -2
  50. package/src/lib/cli/runCli.ts +12 -3
  51. package/src/lib/mcp/createMcpResourceServer.ts +1 -1
  52. package/src/lib/mcp/dispatchMcpRequest.ts +53 -73
  53. package/src/lib/server/AppModule.ts +2 -2
  54. package/src/lib/server/cli/handleCliDownload.ts +4 -5
  55. package/src/lib/server/cli/handleCliInstall.ts +17 -0
  56. package/src/lib/server/error.ts +23 -9
  57. package/src/lib/server/json.ts +5 -5
  58. package/src/lib/server/jsonl.ts +10 -5
  59. package/src/lib/server/prompts/definePrompt.ts +6 -6
  60. package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
  61. package/src/lib/server/prompts/types/Prompt.ts +8 -9
  62. package/src/lib/server/prompts/types/PromptOptions.ts +7 -12
  63. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +3 -5
  64. package/src/lib/server/prompts/types/PromptRoutes.ts +4 -4
  65. package/src/lib/server/redirect.ts +13 -8
  66. package/src/lib/server/rpc/defineVerb.ts +4 -3
  67. package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
  68. package/src/lib/server/rpc/types/RemoteFunction.ts +1 -1
  69. package/src/lib/server/rpc/types/RemoteHandler.ts +4 -0
  70. package/src/lib/server/runtime/acceptsZstd.ts +8 -0
  71. package/src/lib/server/runtime/buildOpenApiSpec.ts +2 -0
  72. package/src/lib/server/runtime/cacheControlForAsset.ts +7 -2
  73. package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
  74. package/src/lib/server/runtime/createPublicAssetServer.ts +6 -20
  75. package/src/lib/server/runtime/createServer.ts +50 -58
  76. package/src/lib/server/runtime/registryManifests.ts +33 -15
  77. package/src/lib/server/runtime/types/RequestStore.ts +2 -3
  78. package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
  79. package/src/lib/server/sockets/createSocketDispatcher.ts +10 -7
  80. package/src/lib/server/sse.ts +10 -5
  81. package/src/lib/shared/cacheControlValues.ts +10 -2
  82. package/src/lib/shared/canonicalJson.ts +1 -5
  83. package/src/lib/shared/createCacheStore.ts +29 -20
  84. package/src/lib/shared/exitOnBuildFailure.ts +17 -0
  85. package/src/lib/shared/fileStem.ts +9 -0
  86. package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
  87. package/src/lib/shared/keyForRemoteCall.ts +7 -5
  88. package/src/lib/shared/parsePromptMarkdown.ts +34 -0
  89. package/src/lib/shared/promptNameForFile.ts +5 -5
  90. package/src/lib/shared/subscribableFromResponse.ts +104 -215
  91. package/src/lib/shared/types/PromptArgument.ts +12 -0
  92. package/src/lib/shared/writeRoutesDts.ts +5 -7
  93. package/src/serverBuildPlugins.ts +25 -0
  94. package/src/serverEntry.ts +4 -0
  95. package/template/package.json +2 -1
  96. package/src/lib/server/prompt.ts +0 -30
  97. package/src/lib/server/prompts/types/PromptMessage.ts +0 -10
  98. package/src/lib/shared/preparePromptModule.ts +0 -36
package/src/buildCli.ts CHANGED
@@ -1,17 +1,19 @@
1
- import type { BunPlugin } from 'bun'
2
- import { belteResolverPlugin } from './belteResolverPlugin.ts'
3
1
  import type { CompileTarget } from './lib/server/runtime/types/CompileTarget.ts'
4
2
  import { detectTarget } from './lib/shared/detectTarget.ts'
3
+ import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
5
4
  import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
6
5
  import { log } from './lib/shared/log.ts'
7
6
  import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
8
- import { sveltePlugin } from './sveltePlugin.ts'
7
+ import { serverBuildPlugins } from './serverBuildPlugins.ts'
9
8
 
10
9
  const DISCOVERY_ENTRY = new URL('./discoveryEntry.ts', import.meta.url).pathname
11
10
  const CLI_ENTRY = new URL('./cliEntry.ts', import.meta.url).pathname
12
11
 
13
12
  /*
14
- Two-pass CLI binary build:
13
+ Two-pass CLI binary build. The CLI is always a thin remote client — it
14
+ bakes in the per-rpc manifest and talks to a running server over HTTP
15
+ (APP_URL at runtime); no handler code is bundled. For an embedded
16
+ backend, `belte compile` produces the standalone server binary instead.
15
17
 
16
18
  1. Discovery: build the discovery entry into a temporary JS bundle and
17
19
  run it. It imports every rpc/socket module so defineVerb /
@@ -21,28 +23,19 @@ Two-pass CLI binary build:
21
23
  resolver plugin's `belte:cli-manifest` virtual reads the manifest
22
24
  JSON written in step 1 and splices it into the bundle.
23
25
 
24
- The `thin` flag decides thin vs full (default full):
25
- - thin: empty `belte:cli-rpcs` virtual no handlers bundled, the
26
- manifest is the only RPC surface; requires APP_URL at runtime.
27
- - full: `belte:cli-rpcs` emits eager imports for every rpc module so the
28
- verbRegistry is populated and the binary runs in-process (and
29
- still reaches a remote server when APP_URL is set at runtime).
30
- `platforms` cross-compiles in either mode; thin per-platform binaries land
31
- in `dist/cli-thin/<platform>/` (the layout the /__belte/cli download endpoint
32
- serves), full ones in `dist/cli/<platform>/`.
26
+ `platforms` cross-compiles per target into `dist/cli-thin/<platform>/`
27
+ the layout the /__belte/cli download endpoint serves.
33
28
  */
34
29
  export async function buildCli({
35
30
  cwd = process.cwd(),
36
31
  target = detectTarget(),
37
32
  outfile,
38
33
  platforms,
39
- thin: thinOverride,
40
34
  }: {
41
35
  cwd?: string
42
36
  target?: CompileTarget
43
37
  outfile?: string
44
38
  platforms?: CompileTarget[]
45
- thin?: boolean
46
39
  } = {}): Promise<string[]> {
47
40
  const distDir = `${cwd}/dist`
48
41
  await Bun.$`mkdir -p ${distDir}`.quiet()
@@ -50,11 +43,7 @@ export async function buildCli({
50
43
  const discoveryOut = `${distDir}/_discovery.js`
51
44
 
52
45
  const svelteConfig = await loadSvelteConfig(cwd)
53
- const isThin = thinOverride ?? false
54
- const sharedPlugins = (): BunPlugin[] => [
55
- sveltePlugin({ generate: 'server', svelteConfig }),
56
- belteResolverPlugin({ cwd, target: 'server', thin: isThin }),
57
- ]
46
+ const plugins = serverBuildPlugins({ cwd, svelteConfig })
58
47
 
59
48
  /*
60
49
  Step 1 — discovery. Build a runnable bundle, execute it under bun,
@@ -66,14 +55,9 @@ export async function buildCli({
66
55
  target: 'bun',
67
56
  outdir: distDir,
68
57
  naming: '_discovery.js',
69
- plugins: sharedPlugins(),
58
+ plugins,
70
59
  })
71
- if (!discoveryResult.success) {
72
- for (const entry of discoveryResult.logs) {
73
- log.error(entry)
74
- }
75
- process.exit(1)
76
- }
60
+ exitOnBuildFailure(discoveryResult)
77
61
 
78
62
  const proc = Bun.spawn({
79
63
  cmd: ['bun', discoveryOut],
@@ -96,38 +80,32 @@ export async function buildCli({
96
80
 
97
81
  /*
98
82
  Step 2 — compile. The cliEntry imports the now-populated
99
- belte:cli-manifest virtual + the eager rpc imports (full mode only,
100
- empty for thin). bun build --compile emits the standalone binary.
101
- When `platforms` is set, loops once per target and writes binaries
102
- into `dist/cli-thin/<platform>/<programName>` (thin — the layout the
103
- download route expects) or `dist/cli/<platform>/<programName>` (full).
83
+ belte:cli-manifest virtual; bun build --compile emits the standalone
84
+ binary. When `platforms` is set, loops once per target and writes
85
+ binaries into `dist/cli-thin/<platform>/<programName>` the layout
86
+ the download route expects.
104
87
  */
105
88
  const programName = await readProgramName(cwd)
106
89
 
107
90
  if (platforms && platforms.length > 0) {
108
- const platformDir = isThin ? 'cli-thin' : 'cli'
109
- const outPaths: string[] = []
110
- for (const platformTarget of platforms) {
111
- const shortName = platformTarget.replace(/^bun-/, '')
112
- const suffix = platformTarget.includes('windows') ? '.exe' : ''
113
- const platformOut = `${distDir}/${platformDir}/${shortName}/${programName}${suffix}`
114
- await Bun.$`mkdir -p ${`${distDir}/${platformDir}/${shortName}`}`.quiet()
115
- const result = await Bun.build({
116
- entrypoints: [CLI_ENTRY],
117
- target: 'bun',
118
- compile: { target: platformTarget, outfile: platformOut },
119
- plugins: sharedPlugins(),
120
- })
121
- if (!result.success) {
122
- for (const entry of result.logs) {
123
- log.error(entry)
124
- }
125
- process.exit(1)
126
- }
127
- log.success(`compiled ${isThin ? 'thin' : 'full'} cli binary: ${platformOut}`)
128
- outPaths.push(platformOut)
129
- }
130
- return outPaths
91
+ // Cross-compile every target in parallel each build is independent.
92
+ return Promise.all(
93
+ platforms.map(async (platformTarget) => {
94
+ const shortName = platformTarget.replace(/^bun-/, '')
95
+ const suffix = platformTarget.includes('windows') ? '.exe' : ''
96
+ const platformOut = `${distDir}/cli-thin/${shortName}/${programName}${suffix}`
97
+ await Bun.$`mkdir -p ${`${distDir}/cli-thin/${shortName}`}`.quiet()
98
+ const result = await Bun.build({
99
+ entrypoints: [CLI_ENTRY],
100
+ target: 'bun',
101
+ compile: { target: platformTarget, outfile: platformOut },
102
+ plugins,
103
+ })
104
+ exitOnBuildFailure(result)
105
+ log.success(`compiled thin cli binary: ${platformOut}`)
106
+ return platformOut
107
+ }),
108
+ )
131
109
  }
132
110
 
133
111
  const suffix = target.includes('windows') ? '.exe' : ''
@@ -137,16 +115,11 @@ export async function buildCli({
137
115
  entrypoints: [CLI_ENTRY],
138
116
  target: 'bun',
139
117
  compile: { target, outfile: outPath },
140
- plugins: sharedPlugins(),
118
+ plugins,
141
119
  })
142
- if (!cliResult.success) {
143
- for (const entry of cliResult.logs) {
144
- log.error(entry)
145
- }
146
- process.exit(1)
147
- }
120
+ exitOnBuildFailure(cliResult)
148
121
 
149
- log.success(`compiled ${isThin ? 'thin' : 'full'} cli binary: ${outPath} (target: ${target})`)
122
+ log.success(`compiled thin cli binary: ${outPath} (target: ${target})`)
150
123
  return [outPath]
151
124
  }
152
125
 
@@ -0,0 +1,127 @@
1
+ import type { BunPlugin } from 'bun'
2
+ import { belteResolverPlugin } from './belteResolverPlugin.ts'
3
+ import { dedupeSveltePlugin } from './dedupeSveltePlugin.ts'
4
+ import type { SvelteConfig } from './lib/server/runtime/types/SvelteConfig.ts'
5
+ import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
6
+ import { log } from './lib/shared/log.ts'
7
+ import { sveltePlugin } from './sveltePlugin.ts'
8
+
9
+ const ENTRY = new URL('./bundleDisconnectedEntry.ts', import.meta.url).pathname
10
+ const CSS_ENTRY = new URL('./lib/bundle/disconnected.css', import.meta.url).pathname
11
+
12
+ /*
13
+ Default screen logo: a minimal inline-SVG belte mark, used when the project ships
14
+ no src/bundle/logo.png. Inline SVG keeps the bundle self-contained with no asset
15
+ file to vendor; a project overrides it just by adding the PNG.
16
+ */
17
+ const DEFAULT_LOGO = `data:image/svg+xml,${encodeURIComponent(
18
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">' +
19
+ '<rect width="64" height="64" rx="16" fill="#111827"/>' +
20
+ '<text x="32" y="45" font-family="system-ui,sans-serif" font-size="38" ' +
21
+ 'font-weight="700" fill="#fff" text-anchor="middle">b</text></svg>',
22
+ )}`
23
+
24
+ /*
25
+ Builds the bundle connect screen into a single self-contained HTML string and
26
+ writes it to `dist/bundle-disconnected.html`, which the launcher bakes in via the
27
+ `belte:bundle-disconnected` virtual. The client bundle (Svelte component +
28
+ injected CSS + compiled Tailwind) is inlined into the page so the launcher serves
29
+ it with zero external requests; the logo is embedded as a data URI. The app title
30
+ (`window.__BELTE_TITLE__`) is injected by the launcher at serve time, so a
31
+ `<!--belte:connect-config-->` marker is left in <head> for it.
32
+
33
+ Must run after the client build has cleared and repopulated dist (it writes a
34
+ file into dist), and before the launcher build that reads it. Uses no outdir of
35
+ its own — Bun.build artifacts are read from memory — so it never touches dist
36
+ beyond the one file it writes.
37
+ */
38
+ export async function buildDisconnected({
39
+ cwd = process.cwd(),
40
+ svelteConfig,
41
+ }: {
42
+ cwd?: string
43
+ svelteConfig?: SvelteConfig
44
+ } = {}): Promise<string> {
45
+ const plugins: BunPlugin[] = [
46
+ dedupeSveltePlugin({ cwd, conditions: ['browser', 'default'] }),
47
+ sveltePlugin({ generate: 'client', svelteConfig }),
48
+ belteResolverPlugin({ cwd, target: 'client' }),
49
+ ]
50
+ try {
51
+ const tailwind = (await import('bun-plugin-tailwind')).default
52
+ plugins.push(tailwind)
53
+ } catch {
54
+ log.warn('bun-plugin-tailwind not installed; building connect screen without Tailwind')
55
+ }
56
+
57
+ /*
58
+ The Tailwind CSS rides as its own entrypoint rather than a `.css` import from
59
+ the entry (which TS can't type), so it compiles to a standalone artifact we
60
+ inline alongside the JS — the component carries no <style> of its own.
61
+ */
62
+ const result = await Bun.build({
63
+ entrypoints: [ENTRY, CSS_ENTRY],
64
+ target: 'browser',
65
+ minify: true,
66
+ plugins,
67
+ })
68
+ exitOnBuildFailure(result)
69
+
70
+ // Collect the JS bundle + extracted CSS from the in-memory artifacts.
71
+ let js = ''
72
+ let css = ''
73
+ for (const output of result.outputs) {
74
+ const text = await output.text()
75
+ if (output.path.endsWith('.css')) {
76
+ css += text
77
+ } else if (output.path.endsWith('.js')) {
78
+ js += text
79
+ }
80
+ }
81
+
82
+ const logo = await readLogo(cwd)
83
+ const html = composeHtml({ js, css, logo })
84
+ const outPath = `${cwd}/dist/bundle-disconnected.html`
85
+ await Bun.write(outPath, html)
86
+ log.success(`built connect screen: ${outPath} (${(html.length / 1024).toFixed(1)} KiB)`)
87
+ return outPath
88
+ }
89
+
90
+ // Reads the project's src/bundle/logo.png as a data URI, or the default mark.
91
+ async function readLogo(cwd: string): Promise<string> {
92
+ const userLogo = Bun.file(`${cwd}/src/bundle/logo.png`)
93
+ if (await userLogo.exists()) {
94
+ const bytes = await userLogo.bytes()
95
+ return `data:image/png;base64,${bytes.toBase64()}`
96
+ }
97
+ return DEFAULT_LOGO
98
+ }
99
+
100
+ /*
101
+ Escapes a closing `</script>` so an inline `<script>` body can't be terminated
102
+ early by content in the bundle (rare, but a `</script>` substring would break the
103
+ page). The browser still parses `<\/script>` as the intended characters.
104
+ */
105
+ function escapeScriptBody(value: string): string {
106
+ return value.replace(/<\/(script)/gi, '<\\/$1')
107
+ }
108
+
109
+ // Assembles the final standalone HTML document from the inlined pieces.
110
+ function composeHtml({ js, css, logo }: { js: string; css: string; logo: string }): string {
111
+ const logoScript = `window.__BELTE_LOGO__=${JSON.stringify(logo)}`
112
+ return `<!doctype html>
113
+ <html lang="en">
114
+ <head>
115
+ <meta charset="UTF-8" />
116
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
117
+ <!--belte:connect-config-->
118
+ <script>${escapeScriptBody(logoScript)}</script>
119
+ <style>${css}</style>
120
+ </head>
121
+ <body>
122
+ <div id="app"></div>
123
+ <script type="module">${escapeScriptBody(js)}</script>
124
+ </body>
125
+ </html>
126
+ `
127
+ }
@@ -0,0 +1,123 @@
1
+ import { buildDisconnected } from './buildDisconnected.ts'
2
+ import { compile } from './compile.ts'
3
+ import { ensureWebviewLib } from './lib/bundle/ensureWebviewLib.ts'
4
+ import { infoPlist } from './lib/bundle/infoPlist.ts'
5
+ import { pngToIcns } from './lib/bundle/pngToIcns.ts'
6
+ import { serverBinaryFilename } from './lib/bundle/serverBinaryFilename.ts'
7
+ import { webviewLibName } from './lib/bundle/webviewLibName.ts'
8
+ import { detectTarget } from './lib/shared/detectTarget.ts'
9
+ import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
10
+ import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
11
+ import { log } from './lib/shared/log.ts'
12
+ import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
13
+ import { serverBuildPlugins } from './serverBuildPlugins.ts'
14
+
15
+ const APP_ENTRY = new URL('./appEntry.ts', import.meta.url).pathname
16
+ const WORKER_ENTRY = new URL('./controlServerWorker.ts', import.meta.url).pathname
17
+
18
+ /*
19
+ Assembles a movable, self-contained app bundle for the host platform —
20
+ no signing, no cross-compilation. Three pieces travel together so the app
21
+ runs on another machine of the same OS with nothing installed:
22
+
23
+ - the standalone server binary (`compile()`, assets embedded)
24
+ - the launcher binary (appEntry — spawns the server, opens the webview)
25
+ - the native webview shared library
26
+
27
+ macOS gets a `.app` bundle (`Contents/MacOS/` + `Contents/Frameworks/` +
28
+ Info.plist); other platforms get a flat `<name>/` directory with the two
29
+ binaries and the lib side by side. The launcher finds both relatives at
30
+ runtime via resolveServerBinary / resolveWebviewLib.
31
+ */
32
+ export async function bundleApp({ cwd = process.cwd() }: { cwd?: string } = {}): Promise<string> {
33
+ const target = detectTarget()
34
+ const { name, version } = await readPackage(cwd)
35
+ const programName = programNameForPackage(name)
36
+ const svelteConfig = await loadSvelteConfig(cwd)
37
+
38
+ /*
39
+ Layout differs by OS: a macOS .app nests binaries under Contents/MacOS
40
+ and the lib under Contents/Frameworks; elsewhere everything sits flat
41
+ in one directory. binDir is where the launcher + server land, libDir
42
+ where the webview lib lands — matching resolveWebviewLib's candidates.
43
+ */
44
+ const isMac = process.platform === 'darwin'
45
+ const bundleRoot = isMac ? `${cwd}/dist/${programName}.app` : `${cwd}/dist/${programName}`
46
+ const binDir = isMac ? `${bundleRoot}/Contents/MacOS` : bundleRoot
47
+ const libDir = isMac ? `${bundleRoot}/Contents/Frameworks` : bundleRoot
48
+
49
+ await Bun.$`rm -rf ${bundleRoot}`.quiet()
50
+ await Bun.$`mkdir -p ${binDir} ${libDir}`.quiet()
51
+
52
+ // 1. Server binary — self-contained, embeds the client assets. compile()
53
+ // runs the client build, which clears dist first, so it must precede the
54
+ // connect-screen build that writes into dist.
55
+ await compile({ cwd, target, outfile: `${binDir}/${serverBinaryFilename()}` })
56
+
57
+ // 2. Connect screen — bake dist/bundle-disconnected.html before the launcher
58
+ // build, which inlines it via the belte:bundle-disconnected virtual.
59
+ await buildDisconnected({ cwd, svelteConfig })
60
+
61
+ // 3. Launcher binary — named after the program so CFBundleExecutable matches.
62
+ const launcherSuffix = target.includes('windows') ? '.exe' : ''
63
+ const launcherPath = `${binDir}/${programName}${launcherSuffix}`
64
+ const launcherResult = await Bun.build({
65
+ entrypoints: [APP_ENTRY],
66
+ target: 'bun',
67
+ compile: { target, outfile: launcherPath },
68
+ plugins: serverBuildPlugins({ cwd, svelteConfig }),
69
+ /*
70
+ Inject the worker's absolute path as a static literal. `new Worker()` is
71
+ embedded into the standalone binary only when its specifier is a build-time
72
+ literal, and a relative one would resolve against `cwd` (the consumer
73
+ project) rather than appEntry's directory — so the launcher passes the
74
+ absolute path through this define instead.
75
+ */
76
+ define: { __BELTE_WORKER_ENTRY__: JSON.stringify(WORKER_ENTRY) },
77
+ })
78
+ exitOnBuildFailure(launcherResult)
79
+
80
+ // 4. Webview lib — built from the vendored source if needed, then copied
81
+ // beside the binaries (or into Frameworks on macOS) so the bundle is self-contained.
82
+ const libSource = await ensureWebviewLib(cwd)
83
+ await Bun.write(`${libDir}/${webviewLibName()}`, Bun.file(libSource))
84
+
85
+ /*
86
+ macOS-only: produce Contents/Resources/icon.icns from an optional project
87
+ icon, then write the Info.plist that makes the .app launchable from
88
+ Finder, wiring CFBundleIconFile when an icon was produced. A ready-made
89
+ src/bundle/icon.icns is used as-is; otherwise src/bundle/icon.png is
90
+ converted via sips + iconutil so authors don't need to make an .icns.
91
+ */
92
+ if (isMac) {
93
+ const resourcesDir = `${bundleRoot}/Contents/Resources`
94
+ const icnsSource = `${cwd}/src/bundle/icon.icns`
95
+ const pngSource = `${cwd}/src/bundle/icon.png`
96
+ let hasIcon = false
97
+ if (await Bun.file(icnsSource).exists()) {
98
+ await Bun.$`mkdir -p ${resourcesDir}`.quiet()
99
+ await Bun.write(`${resourcesDir}/icon.icns`, Bun.file(icnsSource))
100
+ hasIcon = true
101
+ } else if (await Bun.file(pngSource).exists()) {
102
+ await Bun.$`mkdir -p ${resourcesDir}`.quiet()
103
+ hasIcon = await pngToIcns(pngSource, `${resourcesDir}/icon.icns`)
104
+ }
105
+ await Bun.write(
106
+ `${bundleRoot}/Contents/Info.plist`,
107
+ infoPlist({ name: programName, version, icon: hasIcon ? 'icon' : undefined }),
108
+ )
109
+ }
110
+
111
+ log.success(`bundled app: ${bundleRoot} (target: ${target})`)
112
+ return bundleRoot
113
+ }
114
+
115
+ // Reads name + version from package.json, with fallbacks when absent.
116
+ async function readPackage(cwd: string): Promise<{ name: string | undefined; version: string }> {
117
+ const pkgFile = Bun.file(`${cwd}/package.json`)
118
+ if (!(await pkgFile.exists())) {
119
+ return { name: undefined, version: '0.0.0' }
120
+ }
121
+ const pkg = (await pkgFile.json()) as { name?: string; version?: string }
122
+ return { name: pkg.name, version: pkg.version ?? '0.0.0' }
123
+ }
@@ -0,0 +1,17 @@
1
+ import { mount } from 'svelte'
2
+ // @ts-expect-error virtual module resolved by belteResolverPlugin
3
+ import Disconnected from './_virtual/bundle-disconnected-component.ts'
4
+
5
+ /*
6
+ Client entry for the bundle connect screen. Standalone — it mounts the
7
+ disconnected component (the user's src/bundle/disconnected.svelte override or the
8
+ lib default, picked by the resolver) into #app, with no router or SSR hydration.
9
+ buildDisconnected bundles this into a single self-contained HTML file (the Tailwind
10
+ CSS is a separate build entrypoint, not imported here). The `svelte` import sits
11
+ first so biome's import sorting leaves the `_virtual` component's `@ts-expect-error`
12
+ attached.
13
+ */
14
+ const target = document.getElementById('app')
15
+ if (target) {
16
+ mount(Disconnected, { target })
17
+ }
package/src/cliEntry.ts CHANGED
@@ -4,22 +4,16 @@ import { banner, footer } from './_virtual/cli-chrome.ts'
4
4
  import manifest from './_virtual/cli-manifest.ts'
5
5
  // @ts-expect-error virtual module resolved by belteResolverPlugin
6
6
  import programName from './_virtual/cli-name.ts'
7
- // @ts-expect-error virtual module resolved by belteResolverPlugin — side-effect import that
8
- // populates verbRegistry for in-process mode on full builds; empty on thin builds
9
- import './_virtual/cli-rpcs.ts'
10
7
  import { runCli } from './lib/cli/runCli.ts'
11
8
 
12
9
  /*
13
10
  Standalone CLI binary entry. Compiled with `bun build --compile` into
14
- `dist/cli` (full, default) or `dist/cli-thin` (with `--thin`). The
15
- bundler emits:
11
+ `dist/cli` (or `dist/cli-thin/<platform>/` for cross-builds). The CLI is
12
+ a thin remote client — no handler code is bundled; it talks to a running
13
+ server over HTTP (APP_URL at runtime). The bundler emits:
16
14
  - belte:cli-manifest — the per-rpc manifest (method, url, jsonSchema)
17
15
  - belte:cli-name — the program name from package.json
18
16
  - belte:cli-chrome — optional banner/footer text from src/cli/
19
-
20
- All are virtual modules so the same source file works for thin and
21
- full builds; what differs is whether the verbRegistry is also bundled
22
- in (full mode → in-process fallback; thin mode → APP_URL required).
23
17
  */
24
18
  const exitCode = await runCli({
25
19
  programName,
package/src/compile.ts CHANGED
@@ -1,11 +1,10 @@
1
- import type { BunPlugin } from 'bun'
2
- import { belteResolverPlugin } from './belteResolverPlugin.ts'
3
1
  import { build } from './build.ts'
4
2
  import type { CompileTarget } from './lib/server/runtime/types/CompileTarget.ts'
5
3
  import { detectTarget } from './lib/shared/detectTarget.ts'
4
+ import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
6
5
  import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
7
6
  import { log } from './lib/shared/log.ts'
8
- import { sveltePlugin } from './sveltePlugin.ts'
7
+ import { serverBuildPlugins } from './serverBuildPlugins.ts'
9
8
 
10
9
  const SERVER_ENTRY = new URL('./serverEntry.ts', import.meta.url).pathname
11
10
 
@@ -31,11 +30,6 @@ export async function compile({
31
30
 
32
31
  const outPath = outfile ?? `${cwd}/dist/app${target.includes('windows') ? '.exe' : ''}`
33
32
 
34
- const plugins: BunPlugin[] = [
35
- sveltePlugin({ generate: 'server', svelteConfig }),
36
- belteResolverPlugin({ cwd, embedAssets: true, target: 'server' }),
37
- ]
38
-
39
33
  const result = await Bun.build({
40
34
  entrypoints: [SERVER_ENTRY],
41
35
  target: 'bun',
@@ -49,15 +43,10 @@ export async function compile({
49
43
  */
50
44
  bytecode: true,
51
45
  compile: { target, outfile: outPath },
52
- plugins,
46
+ plugins: serverBuildPlugins({ cwd, svelteConfig, embedAssets: true }),
53
47
  })
54
48
 
55
- if (!result.success) {
56
- for (const entry of result.logs) {
57
- log.error(entry)
58
- }
59
- process.exit(1)
60
- }
49
+ exitOnBuildFailure(result)
61
50
 
62
51
  log.success(`compiled standalone binary: ${outPath} (target: ${target})`)
63
52
  return outPath