@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
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 {
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
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
|
|
58
|
+
plugins,
|
|
70
59
|
})
|
|
71
|
-
|
|
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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
118
|
+
plugins,
|
|
141
119
|
})
|
|
142
|
-
|
|
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
|
|
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
|
+
}
|
package/src/bundleApp.ts
ADDED
|
@@ -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` (
|
|
15
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|