@briancray/belte 0.1.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/LICENSE +21 -0
- package/README.md +49 -0
- package/bin/belte.ts +136 -0
- package/package.json +80 -0
- package/src/App.svelte +31 -0
- package/src/assets/app.html +14 -0
- package/src/belteResolverPlugin.ts +832 -0
- package/src/build.ts +144 -0
- package/src/buildCli.ts +160 -0
- package/src/cliEntry.ts +31 -0
- package/src/clientEntry.ts +7 -0
- package/src/compile.ts +64 -0
- package/src/devEntry.ts +33 -0
- package/src/discoveryEntry.ts +33 -0
- package/src/lib/browser/cache.ts +191 -0
- package/src/lib/browser/page.svelte.ts +215 -0
- package/src/lib/browser/remoteProxy.ts +44 -0
- package/src/lib/browser/socketChannel.ts +182 -0
- package/src/lib/browser/socketProxy.ts +64 -0
- package/src/lib/browser/startClient.ts +132 -0
- package/src/lib/browser/subscribe.ts +131 -0
- package/src/lib/browser/types/Layouts.ts +7 -0
- package/src/lib/browser/types/Pages.ts +7 -0
- package/src/lib/cli/createClient.ts +126 -0
- package/src/lib/cli/loadEnvFromBinaryDir.ts +44 -0
- package/src/lib/cli/parseArgvForRpc.ts +97 -0
- package/src/lib/cli/printHelp.ts +70 -0
- package/src/lib/cli/runCli.ts +88 -0
- package/src/lib/cli/types/CliManifest.ts +9 -0
- package/src/lib/cli/types/CliManifestEntry.ts +12 -0
- package/src/lib/mcp/createMcpResourceServer.ts +101 -0
- package/src/lib/mcp/createMcpServer.ts +40 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +294 -0
- package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
- package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
- package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
- package/src/lib/mcp/types/McpResourceContents.ts +10 -0
- package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
- package/src/lib/mcp/types/McpResourceServer.ts +12 -0
- package/src/lib/mcp/types/McpServer.ts +9 -0
- package/src/lib/mcp/types/McpServerOptions.ts +16 -0
- package/src/lib/server/AppModule.ts +25 -0
- package/src/lib/server/DELETE.ts +9 -0
- package/src/lib/server/GET.ts +9 -0
- package/src/lib/server/HEAD.ts +9 -0
- package/src/lib/server/HttpError.ts +19 -0
- package/src/lib/server/PATCH.ts +9 -0
- package/src/lib/server/POST.ts +9 -0
- package/src/lib/server/PUT.ts +9 -0
- package/src/lib/server/cli/buildEnvContent.ts +18 -0
- package/src/lib/server/cli/createTarGz.ts +76 -0
- package/src/lib/server/cli/handleCliDownload.ts +124 -0
- package/src/lib/server/cli/handleCliInstall.ts +20 -0
- package/src/lib/server/cli/installScript.ts +29 -0
- package/src/lib/server/cli/maxSourceMtime.ts +27 -0
- package/src/lib/server/error.ts +56 -0
- package/src/lib/server/json.ts +28 -0
- package/src/lib/server/jsonl.ts +40 -0
- package/src/lib/server/prompt.ts +30 -0
- package/src/lib/server/prompts/definePrompt.ts +20 -0
- package/src/lib/server/prompts/promptRegistry.ts +9 -0
- package/src/lib/server/prompts/registerPrompt.ts +6 -0
- package/src/lib/server/prompts/types/Prompt.ts +14 -0
- package/src/lib/server/prompts/types/PromptMessage.ts +10 -0
- package/src/lib/server/prompts/types/PromptOptions.ts +17 -0
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +15 -0
- package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
- package/src/lib/server/redirect.ts +37 -0
- package/src/lib/server/request.ts +18 -0
- package/src/lib/server/rpc/defineVerb.ts +103 -0
- package/src/lib/server/rpc/parseArgs.ts +60 -0
- package/src/lib/server/rpc/registerVerb.ts +6 -0
- package/src/lib/server/rpc/types/HttpVerb.ts +1 -0
- package/src/lib/server/rpc/types/RawRemoteFunction.ts +13 -0
- package/src/lib/server/rpc/types/RemoteFunction.ts +35 -0
- package/src/lib/server/rpc/types/RemoteHandler.ts +22 -0
- package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
- package/src/lib/server/rpc/types/StandardSchemaV1.ts +57 -0
- package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
- package/src/lib/server/rpc/types/VerbHelper.ts +39 -0
- package/src/lib/server/rpc/types/VerbRegistryEntry.ts +17 -0
- package/src/lib/server/rpc/unprocessed.ts +14 -0
- package/src/lib/server/rpc/verbRegistry.ts +11 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +66 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +17 -0
- package/src/lib/server/runtime/containsTraversal.ts +37 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +66 -0
- package/src/lib/server/runtime/createServer.ts +555 -0
- package/src/lib/server/runtime/getActiveServer.ts +6 -0
- package/src/lib/server/runtime/mimeForExtension.ts +20 -0
- package/src/lib/server/runtime/registryManifests.ts +48 -0
- package/src/lib/server/runtime/requestContext.ts +5 -0
- package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +84 -0
- package/src/lib/server/runtime/serverSlot.ts +13 -0
- package/src/lib/server/runtime/setActiveServer.ts +6 -0
- package/src/lib/server/runtime/streamFromIterator.ts +76 -0
- package/src/lib/server/runtime/types/Assets.ts +1 -0
- package/src/lib/server/runtime/types/CompileTarget.ts +6 -0
- package/src/lib/server/runtime/types/RequestStore.ts +15 -0
- package/src/lib/server/runtime/types/SvelteConfig.ts +5 -0
- package/src/lib/server/server.ts +19 -0
- package/src/lib/server/socket.ts +31 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +267 -0
- package/src/lib/server/sockets/defineSocket.ts +160 -0
- package/src/lib/server/sockets/lookupSocket.ts +6 -0
- package/src/lib/server/sockets/registerSocket.ts +6 -0
- package/src/lib/server/sockets/socketRegistry.ts +9 -0
- package/src/lib/server/sockets/types/Socket.ts +21 -0
- package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
- package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +18 -0
- package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
- package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
- package/src/lib/server/sse.ts +47 -0
- package/src/lib/shared/activeCacheStore.ts +20 -0
- package/src/lib/shared/buildRpcRequest.ts +61 -0
- package/src/lib/shared/cacheControlValues.ts +8 -0
- package/src/lib/shared/cacheStoreSlot.ts +16 -0
- package/src/lib/shared/canonicalJson.ts +24 -0
- package/src/lib/shared/commandNameForUrl.ts +17 -0
- package/src/lib/shared/createCacheStore.ts +42 -0
- package/src/lib/shared/createPushIterator.ts +77 -0
- package/src/lib/shared/createRemoteFunction.ts +89 -0
- package/src/lib/shared/decodeResponse.ts +47 -0
- package/src/lib/shared/detectTarget.ts +27 -0
- package/src/lib/shared/findExportCallSite.ts +479 -0
- package/src/lib/shared/forwardHeaders.ts +28 -0
- package/src/lib/shared/getRemoteMeta.ts +5 -0
- package/src/lib/shared/isDebugEnabled.ts +23 -0
- package/src/lib/shared/jsonSchemaForSchema.ts +38 -0
- package/src/lib/shared/keyForRemoteCall.ts +38 -0
- package/src/lib/shared/loadSvelteConfig.ts +18 -0
- package/src/lib/shared/log.ts +104 -0
- package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
- package/src/lib/shared/normalizeTarget.ts +10 -0
- package/src/lib/shared/pageUrlForFile.ts +14 -0
- package/src/lib/shared/parseRouteSegments.ts +22 -0
- package/src/lib/shared/preparePromptModule.ts +36 -0
- package/src/lib/shared/prepareRpcModule.ts +51 -0
- package/src/lib/shared/prepareSocketModule.ts +37 -0
- package/src/lib/shared/programNameForPackage.ts +14 -0
- package/src/lib/shared/promptNameForFile.ts +10 -0
- package/src/lib/shared/recordRemoteMeta.ts +5 -0
- package/src/lib/shared/remoteMetaStore.ts +16 -0
- package/src/lib/shared/resolveClientFlags.ts +18 -0
- package/src/lib/shared/rpcUrlForFile.ts +19 -0
- package/src/lib/shared/setCacheStoreResolver.ts +6 -0
- package/src/lib/shared/socketNameForFile.ts +11 -0
- package/src/lib/shared/streamingContentTypes.ts +11 -0
- package/src/lib/shared/stripImport.ts +27 -0
- package/src/lib/shared/subscribableFromResponse.ts +333 -0
- package/src/lib/shared/toBunRoutePattern.ts +28 -0
- package/src/lib/shared/types/CacheEntry.ts +16 -0
- package/src/lib/shared/types/CacheOptions.ts +10 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
- package/src/lib/shared/types/CacheStore.ts +15 -0
- package/src/lib/shared/types/ClientFlags.ts +11 -0
- package/src/lib/shared/types/Subscribable.ts +15 -0
- package/src/lib/shared/writeRoutesDts.ts +64 -0
- package/src/preload.ts +20 -0
- package/src/scaffold.ts +92 -0
- package/src/serverEntry.ts +47 -0
- package/src/sveltePlugin.ts +58 -0
- package/src/tailwindStylePreprocessor.ts +62 -0
- package/template/package.json +16 -0
- package/template/src/app.ts +23 -0
- package/template/src/browser/app.css +21 -0
- package/template/src/browser/app.html +24 -0
- package/template/src/browser/pages/about/page.svelte +5 -0
- package/template/src/browser/pages/layout.svelte +26 -0
- package/template/src/browser/pages/page.svelte +20 -0
- package/template/src/cli/banner.txt +3 -0
- package/template/src/cli/footer.txt +1 -0
- package/template/src/server/rpc/getHello.ts +33 -0
- package/template/svelte.config.js +12 -0
- package/template/tsconfig.json +18 -0
- package/tsconfig.app.json +16 -0
package/src/build.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { BunPlugin } from 'bun'
|
|
2
|
+
import { belteResolverPlugin } from './belteResolverPlugin.ts'
|
|
3
|
+
import type { SvelteConfig } from './lib/server/runtime/types/SvelteConfig.ts'
|
|
4
|
+
import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
|
|
5
|
+
import { log } from './lib/shared/log.ts'
|
|
6
|
+
import { sveltePlugin } from './sveltePlugin.ts'
|
|
7
|
+
|
|
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
|
+
const CLIENT_ENTRY = new URL('./clientEntry.ts', import.meta.url).pathname
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
Builds the client-side bundle into `${cwd}/dist/_app`. Clears the dist
|
|
70
|
+
directory first, then runs Bun.build with the svelte-dedupe plugin, the
|
|
71
|
+
svelte loader, the virtual-module resolver, and (optionally) Tailwind.
|
|
72
|
+
Each emitted file is also written as a zstd-compressed `.zst` sibling
|
|
73
|
+
(level 22 — paid once at build time) so the server can stream the
|
|
74
|
+
precompressed bytes directly when the client supports it, and decompress
|
|
75
|
+
on the fly for older clients. Exits the process on build failure with
|
|
76
|
+
the build logs printed.
|
|
77
|
+
*/
|
|
78
|
+
export async function build({
|
|
79
|
+
cwd = process.cwd(),
|
|
80
|
+
svelteConfig,
|
|
81
|
+
minify = true,
|
|
82
|
+
}: {
|
|
83
|
+
cwd?: string
|
|
84
|
+
svelteConfig?: SvelteConfig
|
|
85
|
+
minify?: boolean
|
|
86
|
+
} = {}): Promise<void> {
|
|
87
|
+
const distDir = `${cwd}/dist`
|
|
88
|
+
const outDir = `${distDir}/_app`
|
|
89
|
+
|
|
90
|
+
// shell-rm is the impure boundary for "clear dist" — Bun.$ is first-party
|
|
91
|
+
await Bun.$`rm -rf ${distDir}`.quiet()
|
|
92
|
+
|
|
93
|
+
const config = svelteConfig ?? (await loadSvelteConfig(cwd))
|
|
94
|
+
const plugins: BunPlugin[] = [
|
|
95
|
+
dedupeSveltePlugin({ cwd, conditions: ['browser', 'default'] }),
|
|
96
|
+
sveltePlugin({ generate: 'client', svelteConfig: config }),
|
|
97
|
+
belteResolverPlugin({ cwd, target: 'client' }),
|
|
98
|
+
]
|
|
99
|
+
try {
|
|
100
|
+
const tailwind = (await import('bun-plugin-tailwind')).default
|
|
101
|
+
plugins.push(tailwind)
|
|
102
|
+
} catch {
|
|
103
|
+
log.warn('bun-plugin-tailwind not installed; building without Tailwind')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = await Bun.build({
|
|
107
|
+
entrypoints: [CLIENT_ENTRY],
|
|
108
|
+
outdir: outDir,
|
|
109
|
+
target: 'browser',
|
|
110
|
+
splitting: true,
|
|
111
|
+
minify,
|
|
112
|
+
sourcemap: 'linked',
|
|
113
|
+
naming: {
|
|
114
|
+
entry: 'client-[hash].[ext]',
|
|
115
|
+
chunk: '[name]-[hash].[ext]',
|
|
116
|
+
asset: '[name].[ext]',
|
|
117
|
+
},
|
|
118
|
+
plugins,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if (!result.success) {
|
|
122
|
+
for (const entry of result.logs) {
|
|
123
|
+
log.error(entry)
|
|
124
|
+
}
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const compressedByteLengths = await Promise.all(
|
|
129
|
+
result.outputs.map(async (output) => {
|
|
130
|
+
const bytes = await Bun.file(output.path).bytes()
|
|
131
|
+
const compressed = Bun.zstdCompressSync(bytes, { level: 22 })
|
|
132
|
+
await Bun.write(`${output.path}.zst`, compressed)
|
|
133
|
+
return compressed.byteLength
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
const compressedBytes = compressedByteLengths.reduce((total, length) => total + length, 0)
|
|
137
|
+
|
|
138
|
+
log.success(
|
|
139
|
+
`wrote ${result.outputs.length} files to ${outDir} (+${result.outputs.length} .zst, ${(compressedBytes / 1024).toFixed(1)} KiB total)`,
|
|
140
|
+
)
|
|
141
|
+
for (const output of result.outputs) {
|
|
142
|
+
log.detail(` - ${output.path}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/buildCli.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { BunPlugin } from 'bun'
|
|
2
|
+
import { belteResolverPlugin } from './belteResolverPlugin.ts'
|
|
3
|
+
import type { CompileTarget } from './lib/server/runtime/types/CompileTarget.ts'
|
|
4
|
+
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
5
|
+
import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
|
|
6
|
+
import { log } from './lib/shared/log.ts'
|
|
7
|
+
import { programNameForPackage } from './lib/shared/programNameForPackage.ts'
|
|
8
|
+
import { sveltePlugin } from './sveltePlugin.ts'
|
|
9
|
+
|
|
10
|
+
const DISCOVERY_ENTRY = new URL('./discoveryEntry.ts', import.meta.url).pathname
|
|
11
|
+
const CLI_ENTRY = new URL('./cliEntry.ts', import.meta.url).pathname
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
Two-pass CLI binary build:
|
|
15
|
+
|
|
16
|
+
1. Discovery: build the discovery entry into a temporary JS bundle and
|
|
17
|
+
run it. It imports every rpc/socket module so defineVerb /
|
|
18
|
+
defineSocket populate the registries, then prints the CLI manifest
|
|
19
|
+
to stdout. The manifest is written to `dist/cli-manifest.json`.
|
|
20
|
+
2. Compile: build the CLI binary via `Bun.build({ compile })`. The
|
|
21
|
+
resolver plugin's `belte:cli-manifest` virtual reads the manifest
|
|
22
|
+
JSON written in step 1 and splices it into the bundle.
|
|
23
|
+
|
|
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>/`.
|
|
33
|
+
*/
|
|
34
|
+
export async function buildCli({
|
|
35
|
+
cwd = process.cwd(),
|
|
36
|
+
target = detectTarget(),
|
|
37
|
+
outfile,
|
|
38
|
+
platforms,
|
|
39
|
+
thin: thinOverride,
|
|
40
|
+
}: {
|
|
41
|
+
cwd?: string
|
|
42
|
+
target?: CompileTarget
|
|
43
|
+
outfile?: string
|
|
44
|
+
platforms?: CompileTarget[]
|
|
45
|
+
thin?: boolean
|
|
46
|
+
} = {}): Promise<string[]> {
|
|
47
|
+
const distDir = `${cwd}/dist`
|
|
48
|
+
await Bun.$`mkdir -p ${distDir}`.quiet()
|
|
49
|
+
const manifestPath = `${distDir}/cli-manifest.json`
|
|
50
|
+
const discoveryOut = `${distDir}/_discovery.js`
|
|
51
|
+
|
|
52
|
+
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
|
+
]
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
Step 1 — discovery. Build a runnable bundle, execute it under bun,
|
|
61
|
+
capture stdout. We don't `bun build --compile` here because the
|
|
62
|
+
discovery output is throwaway; a plain JS bundle runs faster.
|
|
63
|
+
*/
|
|
64
|
+
const discoveryResult = await Bun.build({
|
|
65
|
+
entrypoints: [DISCOVERY_ENTRY],
|
|
66
|
+
target: 'bun',
|
|
67
|
+
outdir: distDir,
|
|
68
|
+
naming: '_discovery.js',
|
|
69
|
+
plugins: sharedPlugins(),
|
|
70
|
+
})
|
|
71
|
+
if (!discoveryResult.success) {
|
|
72
|
+
for (const entry of discoveryResult.logs) {
|
|
73
|
+
log.error(entry)
|
|
74
|
+
}
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const proc = Bun.spawn({
|
|
79
|
+
cmd: ['bun', discoveryOut],
|
|
80
|
+
cwd,
|
|
81
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
|
+
})
|
|
83
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
84
|
+
new Response(proc.stdout).text(),
|
|
85
|
+
new Response(proc.stderr).text(),
|
|
86
|
+
proc.exited,
|
|
87
|
+
])
|
|
88
|
+
if (exitCode !== 0) {
|
|
89
|
+
log.error(`discovery exited ${exitCode}:\n${stderr}`)
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
await Bun.write(manifestPath, stdout)
|
|
93
|
+
await Bun.$`rm -f ${discoveryOut}`.quiet()
|
|
94
|
+
const entryCount = Object.keys(JSON.parse(stdout) as Record<string, unknown>).length
|
|
95
|
+
log.info(`discovered ${entryCount} cli commands → ${manifestPath}`)
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
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).
|
|
104
|
+
*/
|
|
105
|
+
const programName = await readProgramName(cwd)
|
|
106
|
+
|
|
107
|
+
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
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const suffix = target.includes('windows') ? '.exe' : ''
|
|
134
|
+
const outPath = outfile ?? `${distDir}/cli${suffix}`
|
|
135
|
+
|
|
136
|
+
const cliResult = await Bun.build({
|
|
137
|
+
entrypoints: [CLI_ENTRY],
|
|
138
|
+
target: 'bun',
|
|
139
|
+
compile: { target, outfile: outPath },
|
|
140
|
+
plugins: sharedPlugins(),
|
|
141
|
+
})
|
|
142
|
+
if (!cliResult.success) {
|
|
143
|
+
for (const entry of cliResult.logs) {
|
|
144
|
+
log.error(entry)
|
|
145
|
+
}
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
log.success(`compiled ${isThin ? 'thin' : 'full'} cli binary: ${outPath} (target: ${target})`)
|
|
150
|
+
return [outPath]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function readProgramName(cwd: string): Promise<string> {
|
|
154
|
+
const pkgFile = Bun.file(`${cwd}/package.json`)
|
|
155
|
+
if (!(await pkgFile.exists())) {
|
|
156
|
+
return 'app'
|
|
157
|
+
}
|
|
158
|
+
const pkg = (await pkgFile.json()) as { name?: string }
|
|
159
|
+
return programNameForPackage(pkg.name)
|
|
160
|
+
}
|
package/src/cliEntry.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
2
|
+
import { banner, footer } from './_virtual/cli-chrome.ts'
|
|
3
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
4
|
+
import manifest from './_virtual/cli-manifest.ts'
|
|
5
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
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
|
+
import { runCli } from './lib/cli/runCli.ts'
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
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:
|
|
16
|
+
- belte:cli-manifest — the per-rpc manifest (method, url, jsonSchema)
|
|
17
|
+
- belte:cli-name — the program name from package.json
|
|
18
|
+
- 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
|
+
*/
|
|
24
|
+
const exitCode = await runCli({
|
|
25
|
+
programName,
|
|
26
|
+
manifest,
|
|
27
|
+
banner,
|
|
28
|
+
footer,
|
|
29
|
+
argv: process.argv.slice(2),
|
|
30
|
+
})
|
|
31
|
+
process.exit(exitCode)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
2
|
+
import { layouts } from './_virtual/layouts.ts'
|
|
3
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
4
|
+
import { pages } from './_virtual/pages.ts'
|
|
5
|
+
import { startClient } from './lib/browser/startClient.ts'
|
|
6
|
+
|
|
7
|
+
await startClient({ pages, layouts })
|
package/src/compile.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { BunPlugin } from 'bun'
|
|
2
|
+
import { belteResolverPlugin } from './belteResolverPlugin.ts'
|
|
3
|
+
import { build } from './build.ts'
|
|
4
|
+
import type { CompileTarget } from './lib/server/runtime/types/CompileTarget.ts'
|
|
5
|
+
import { detectTarget } from './lib/shared/detectTarget.ts'
|
|
6
|
+
import { loadSvelteConfig } from './lib/shared/loadSvelteConfig.ts'
|
|
7
|
+
import { log } from './lib/shared/log.ts'
|
|
8
|
+
import { sveltePlugin } from './sveltePlugin.ts'
|
|
9
|
+
|
|
10
|
+
const SERVER_ENTRY = new URL('./serverEntry.ts', import.meta.url).pathname
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
Produces a standalone Bun executable for the server. Runs the client `build`
|
|
14
|
+
first so the resolver plugin can embed the zstd-compressed assets into
|
|
15
|
+
the binary, then invokes Bun.build in compile mode against the server
|
|
16
|
+
entry. Defaults
|
|
17
|
+
the target to the host platform and appends `.exe` for windows targets.
|
|
18
|
+
Returns the path of the emitted binary; exits the process on build failure.
|
|
19
|
+
*/
|
|
20
|
+
export async function compile({
|
|
21
|
+
cwd = process.cwd(),
|
|
22
|
+
target = detectTarget(),
|
|
23
|
+
outfile,
|
|
24
|
+
}: {
|
|
25
|
+
cwd?: string
|
|
26
|
+
target?: CompileTarget
|
|
27
|
+
outfile?: string
|
|
28
|
+
} = {}): Promise<string> {
|
|
29
|
+
const svelteConfig = await loadSvelteConfig(cwd)
|
|
30
|
+
await build({ cwd, svelteConfig })
|
|
31
|
+
|
|
32
|
+
const outPath = outfile ?? `${cwd}/dist/app${target.includes('windows') ? '.exe' : ''}`
|
|
33
|
+
|
|
34
|
+
const plugins: BunPlugin[] = [
|
|
35
|
+
sveltePlugin({ generate: 'server', svelteConfig }),
|
|
36
|
+
belteResolverPlugin({ cwd, embedAssets: true, target: 'server' }),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const result = await Bun.build({
|
|
40
|
+
entrypoints: [SERVER_ENTRY],
|
|
41
|
+
target: 'bun',
|
|
42
|
+
format: 'esm',
|
|
43
|
+
minify: true,
|
|
44
|
+
/*
|
|
45
|
+
Bytecode embeds precompiled JS module metadata directly into the
|
|
46
|
+
standalone binary, dramatically cutting cold-start time for large
|
|
47
|
+
apps. Requires `target: 'bun'` + an explicit `format` because the
|
|
48
|
+
default for `bytecode` alone is CommonJS; we need ESM bytecode.
|
|
49
|
+
*/
|
|
50
|
+
bytecode: true,
|
|
51
|
+
compile: { target, outfile: outPath },
|
|
52
|
+
plugins,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
for (const entry of result.logs) {
|
|
57
|
+
log.error(entry)
|
|
58
|
+
}
|
|
59
|
+
process.exit(1)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
log.success(`compiled standalone binary: ${outPath} (target: ${target})`)
|
|
63
|
+
return outPath
|
|
64
|
+
}
|
package/src/devEntry.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
2
|
+
import { layouts } from './_virtual/layouts.ts'
|
|
3
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
4
|
+
import { pages } from './_virtual/pages.ts'
|
|
5
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
6
|
+
import { prompts } from './_virtual/prompts.ts'
|
|
7
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
8
|
+
import { rpc } from './_virtual/rpc.ts'
|
|
9
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
10
|
+
import { sockets } from './_virtual/sockets.ts'
|
|
11
|
+
import { build } from './build.ts'
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
Dev-only entry. Each `bun --watch` restart re-runs the client build (so the
|
|
15
|
+
browser-served bundle matches the freshly-evaluated server modules) and then
|
|
16
|
+
eagerly invokes every dynamic loader for pages, layouts, rpc handlers, and
|
|
17
|
+
sockets. That pulls those files into Bun's import graph from boot, so the
|
|
18
|
+
watcher sees edits to a page or component on the *first* save instead of
|
|
19
|
+
needing the page to be visited once to warm the dynamic import. Finally
|
|
20
|
+
hands off to the normal server entry, which expects the same virtual
|
|
21
|
+
modules — they're already cached, so it just runs createServer().
|
|
22
|
+
*/
|
|
23
|
+
await build({ cwd: process.cwd(), minify: false })
|
|
24
|
+
|
|
25
|
+
await Promise.all([
|
|
26
|
+
...Object.values(pages).map((loader) => (loader as () => Promise<unknown>)()),
|
|
27
|
+
...Object.values(layouts).map((loader) => (loader as () => Promise<unknown>)()),
|
|
28
|
+
...Object.values(rpc).map((loader) => (loader as () => Promise<unknown>)()),
|
|
29
|
+
...Object.values(sockets).map((loader) => (loader as () => Promise<unknown>)()),
|
|
30
|
+
...Object.values(prompts).map((loader) => (loader as () => Promise<unknown>)()),
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
await import('./serverEntry.ts')
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
2
|
+
import { rpc } from './_virtual/rpc.ts'
|
|
3
|
+
// @ts-expect-error virtual module resolved by belteResolverPlugin
|
|
4
|
+
import { sockets } from './_virtual/sockets.ts'
|
|
5
|
+
import { verbRegistry } from './lib/server/rpc/verbRegistry.ts'
|
|
6
|
+
import { commandNameForUrl } from './lib/shared/commandNameForUrl.ts'
|
|
7
|
+
import { jsonSchemaForSchema } from './lib/shared/jsonSchemaForSchema.ts'
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
One-shot script that imports every rpc + socket module so defineVerb /
|
|
11
|
+
defineSocket populate the process-wide registries, then prints the CLI
|
|
12
|
+
manifest to stdout as JSON. Used by buildCli to bake the manifest into
|
|
13
|
+
the standalone binary at build time without resorting to static source
|
|
14
|
+
parsing (which can't see toJsonSchema()/toJSONSchema() at compile time).
|
|
15
|
+
*/
|
|
16
|
+
await Promise.all([
|
|
17
|
+
...Object.values(rpc).map((loader) => (loader as () => Promise<unknown>)()),
|
|
18
|
+
...Object.values(sockets).map((loader) => (loader as () => Promise<unknown>)()),
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
const manifest: Record<string, unknown> = {}
|
|
22
|
+
for (const entry of verbRegistry.values()) {
|
|
23
|
+
if (!entry.clients.cli) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
manifest[commandNameForUrl(entry.remote.url)] = {
|
|
27
|
+
method: entry.remote.method,
|
|
28
|
+
url: entry.remote.url,
|
|
29
|
+
jsonSchema: jsonSchemaForSchema(entry.schema, entry.jsonSchema),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
process.stdout.write(JSON.stringify(manifest))
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { RawRemoteFunction } from '../server/rpc/types/RawRemoteFunction.ts'
|
|
2
|
+
import type { RemoteFunction } from '../server/rpc/types/RemoteFunction.ts'
|
|
3
|
+
import { activeCacheStore } from '../shared/activeCacheStore.ts'
|
|
4
|
+
import { canonicalJson } from '../shared/canonicalJson.ts'
|
|
5
|
+
import { decodeResponse } from '../shared/decodeResponse.ts'
|
|
6
|
+
import { getRemoteMeta } from '../shared/getRemoteMeta.ts'
|
|
7
|
+
import { keyForRemoteCall } from '../shared/keyForRemoteCall.ts'
|
|
8
|
+
import type { CacheOptions } from '../shared/types/CacheOptions.ts'
|
|
9
|
+
|
|
10
|
+
type AnyRemote<Args, Return> = RemoteFunction<Args, Return> | RawRemoteFunction<Args>
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
Curries a remote-function call against the request-scoped cache store.
|
|
14
|
+
`cache(fn, options?)` returns an invoker; calling that invoker with args
|
|
15
|
+
checks the store for a prior entry (keyed by fn.method + fn.url + args) and
|
|
16
|
+
returns a shared promise on hit, or invokes the underlying raw call once
|
|
17
|
+
and stores the resulting Response promise on miss. Splitting configuration
|
|
18
|
+
(the outer call) from invocation (the inner call) keeps options anchored
|
|
19
|
+
in a fixed position so they can't collide with arg shapes. TTL = undefined
|
|
20
|
+
→ forever; ttl = 0 → dedupe only; ttl > 0 → entry expires `ttl` ms after
|
|
21
|
+
the promise resolves.
|
|
22
|
+
|
|
23
|
+
The invoker's return type mirrors the function you passed:
|
|
24
|
+
|
|
25
|
+
cache(getPost)({ id }) // → Promise<Post> (decoded body)
|
|
26
|
+
cache(getPost.raw)({ id }) // → Promise<Response> (raw escape hatch)
|
|
27
|
+
|
|
28
|
+
Both share one stored entry — the cache only ever holds the underlying
|
|
29
|
+
Response promise; the decoded view is derived on the way out for callers
|
|
30
|
+
of the non-raw variant.
|
|
31
|
+
|
|
32
|
+
Reactivity is implicit: the invoker calls `store.subscribe(key)`, which
|
|
33
|
+
registers the surrounding $derived / $effect scope. Invalidating the key
|
|
34
|
+
then re-runs that scope, which calls cache() again and gets a fresh entry.
|
|
35
|
+
Outside a tracking scope subscribe() is a no-op, so cache() works the same
|
|
36
|
+
in server code and plain client code.
|
|
37
|
+
*/
|
|
38
|
+
export function cache<Args, Return>(
|
|
39
|
+
fn: RemoteFunction<Args, Return>,
|
|
40
|
+
options?: CacheOptions,
|
|
41
|
+
): (args?: Args) => Promise<Return>
|
|
42
|
+
export function cache<Args>(
|
|
43
|
+
fn: RawRemoteFunction<Args>,
|
|
44
|
+
options?: CacheOptions,
|
|
45
|
+
): (args?: Args) => Promise<Response>
|
|
46
|
+
export function cache<Args, Return>(
|
|
47
|
+
fn: AnyRemote<Args, Return>,
|
|
48
|
+
options?: CacheOptions,
|
|
49
|
+
): (args?: Args) => Promise<Return | Response> {
|
|
50
|
+
/*
|
|
51
|
+
The "raw" variant lacks its own `.raw` sibling; only the decoded
|
|
52
|
+
callable carries one. Tell them apart by that presence and dispatch the
|
|
53
|
+
decode step accordingly.
|
|
54
|
+
*/
|
|
55
|
+
const isRaw = !('raw' in fn)
|
|
56
|
+
const rawFn = isRaw ? (fn as RawRemoteFunction<Args>) : (fn as RemoteFunction<Args, Return>).raw
|
|
57
|
+
return (args) => {
|
|
58
|
+
const responsePromise = invokeWithCache(rawFn, args, options)
|
|
59
|
+
return isRaw ? responsePromise : (responsePromise.then(decodeResponse) as Promise<Return>)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function invokeWithCache<Args>(
|
|
64
|
+
rawFn: RawRemoteFunction<Args>,
|
|
65
|
+
args: Args | undefined,
|
|
66
|
+
options: CacheOptions | undefined,
|
|
67
|
+
): Promise<Response> {
|
|
68
|
+
const store = activeCacheStore()
|
|
69
|
+
const key = resolveKey(rawFn, args, options?.key)
|
|
70
|
+
store.subscribe(key)
|
|
71
|
+
const existing = store.entries.get(key)
|
|
72
|
+
if (existing) {
|
|
73
|
+
return shareable(existing.promise)
|
|
74
|
+
}
|
|
75
|
+
const promise = rawFn(args as Args)
|
|
76
|
+
const request = getRemoteMeta(promise)
|
|
77
|
+
if (!request) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
'[belte] cache() received a function whose call did not record metadata — was it produced by a verb helper?',
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
const ttl = options?.ttl
|
|
83
|
+
const entry = {
|
|
84
|
+
key,
|
|
85
|
+
promise,
|
|
86
|
+
request,
|
|
87
|
+
ttl,
|
|
88
|
+
expiresAt: undefined as number | undefined,
|
|
89
|
+
}
|
|
90
|
+
store.entries.set(key, entry)
|
|
91
|
+
function deleteIfCurrent() {
|
|
92
|
+
if (store.entries.get(key) === entry) {
|
|
93
|
+
store.entries.delete(key)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
promise.then(() => {
|
|
97
|
+
/*
|
|
98
|
+
On the server the cache store is request-scoped and gets GC'd
|
|
99
|
+
with the response; skip ttl=0 eviction so the SSR snapshot can
|
|
100
|
+
still pick the entry up. In the browser ttl=0 stays "dedupe
|
|
101
|
+
in-flight only" — evict the moment the promise settles.
|
|
102
|
+
*/
|
|
103
|
+
if (ttl === 0) {
|
|
104
|
+
if (typeof window !== 'undefined') {
|
|
105
|
+
deleteIfCurrent()
|
|
106
|
+
}
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if (ttl !== undefined) {
|
|
110
|
+
entry.expiresAt = Date.now() + ttl
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
if ((entry.expiresAt ?? 0) <= Date.now()) {
|
|
113
|
+
deleteIfCurrent()
|
|
114
|
+
}
|
|
115
|
+
}, ttl).unref?.()
|
|
116
|
+
}
|
|
117
|
+
}, deleteIfCurrent)
|
|
118
|
+
return shareable(promise)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/*
|
|
122
|
+
Returns a promise that resolves to a fresh clone of the underlying Response.
|
|
123
|
+
Multiple readers can each consume the body independently — the stored
|
|
124
|
+
promise's Response is never consumed directly, so clones always succeed.
|
|
125
|
+
*/
|
|
126
|
+
function shareable(promise: Promise<Response>): Promise<Response> {
|
|
127
|
+
return promise.then((response) => response.clone())
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
cache.invalidate = function invalidate<Args, Return>(
|
|
131
|
+
arg?: AnyRemote<Args, Return> | CacheOptions['key'],
|
|
132
|
+
): void {
|
|
133
|
+
const store = activeCacheStore()
|
|
134
|
+
if (arg === undefined) {
|
|
135
|
+
const keys = Array.from(store.entries.keys())
|
|
136
|
+
store.entries.clear()
|
|
137
|
+
emit(store, keys)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (typeof arg === 'function') {
|
|
141
|
+
/*
|
|
142
|
+
`arg.url` is the route template; per-call args appear as `?...`
|
|
143
|
+
(GET/DELETE) or after a space (canonical-json body) — see
|
|
144
|
+
keyForRemoteCall. Passing either `fn` or `fn.raw` invalidates the
|
|
145
|
+
same set because they share method+url.
|
|
146
|
+
*/
|
|
147
|
+
const prefix = `${arg.method} ${arg.url}`
|
|
148
|
+
const affected: string[] = []
|
|
149
|
+
for (const key of store.entries.keys()) {
|
|
150
|
+
if (key === prefix || key.startsWith(`${prefix}?`) || key.startsWith(`${prefix} `)) {
|
|
151
|
+
affected.push(key)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
affected.forEach((key) => store.entries.delete(key))
|
|
155
|
+
emit(store, affected)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
const target = canonicalKey(arg)
|
|
159
|
+
if (store.entries.delete(target)) {
|
|
160
|
+
emit(store, [target])
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveKey<Args>(
|
|
165
|
+
rawFn: RawRemoteFunction<Args>,
|
|
166
|
+
args: Args | undefined,
|
|
167
|
+
override: CacheOptions['key'],
|
|
168
|
+
): string {
|
|
169
|
+
if (override !== undefined) {
|
|
170
|
+
return canonicalKey(override)
|
|
171
|
+
}
|
|
172
|
+
return keyForRemoteCall(rawFn.method, rawFn.url, args)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function canonicalKey(value: CacheOptions['key']): string {
|
|
176
|
+
if (typeof value === 'string') {
|
|
177
|
+
return value
|
|
178
|
+
}
|
|
179
|
+
return canonicalJson(value)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/*
|
|
183
|
+
Detail is a Set so each subscriber's `has(key)` check is O(1) regardless of
|
|
184
|
+
how many keys a single invalidate touches.
|
|
185
|
+
*/
|
|
186
|
+
function emit(store: ReturnType<typeof activeCacheStore>, keys: string[]): void {
|
|
187
|
+
if (keys.length === 0) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
store.events.dispatchEvent(new CustomEvent('invalidate', { detail: new Set(keys) }))
|
|
191
|
+
}
|