@briancray/belte 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/belteResolverPlugin.ts +21 -10
- package/src/lib/shared/belteImportName.test.ts +58 -0
- package/src/lib/shared/belteImportName.ts +45 -0
- package/src/lib/shared/beltePackageName.ts +7 -0
- package/src/lib/shared/prepareRpcModule.ts +14 -4
- package/src/lib/shared/prepareSocketModule.ts +16 -2
- package/template/package.json +1 -1
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { existsSync, statSync } from 'node:fs'
|
|
3
3
|
import type { BunPlugin } from 'bun'
|
|
4
4
|
import { Glob } from 'bun'
|
|
5
|
+
import { belteImportName } from './lib/shared/belteImportName.ts'
|
|
5
6
|
import { fileStem } from './lib/shared/fileStem.ts'
|
|
6
7
|
import { jsonSchemaForPromptArguments } from './lib/shared/jsonSchemaForPromptArguments.ts'
|
|
7
8
|
import { log } from './lib/shared/log.ts'
|
|
@@ -128,6 +129,13 @@ export function belteResolverPlugin({
|
|
|
128
129
|
const scanSocketsOnce = once(() => scanSockets(socketsDir))
|
|
129
130
|
const scanPromptsOnce = once(() => scanPrompts(promptsDir))
|
|
130
131
|
const loadShellOnce = once(() => loadShell(cwd))
|
|
132
|
+
/*
|
|
133
|
+
The bare specifier the project imports belte under (canonical
|
|
134
|
+
`@briancray/belte` or a package alias). Resolved once from the project's
|
|
135
|
+
package.json and threaded into every generated module so the codegen's
|
|
136
|
+
imports resolve regardless of which install style the project uses.
|
|
137
|
+
*/
|
|
138
|
+
const belteImportNameOnce = once(() => belteImportName(cwd))
|
|
131
139
|
|
|
132
140
|
const rpcFilter = new RegExp(`^${escapeRegex(rpcDir)}/.*\\.ts$`)
|
|
133
141
|
const socketsFilter = new RegExp(`^${escapeRegex(socketsDir)}/.*\\.ts$`)
|
|
@@ -176,7 +184,8 @@ export function belteResolverPlugin({
|
|
|
176
184
|
const relativePath = args.path.slice(rpcDir.length + 1)
|
|
177
185
|
const source = await Bun.file(args.path).text()
|
|
178
186
|
const url = rpcUrlForFile(relativePath)
|
|
179
|
-
const
|
|
187
|
+
const importName = await belteImportNameOnce()
|
|
188
|
+
const prepared = prepareRpcModule(source, importName)
|
|
180
189
|
if (!prepared) {
|
|
181
190
|
throw new Error(
|
|
182
191
|
`[belte] src/server/rpc/${relativePath} has no \`export const <name> = <VERB>(...)\` — every $rpc module must declare exactly one remote function`,
|
|
@@ -196,7 +205,7 @@ export function belteResolverPlugin({
|
|
|
196
205
|
so page imports resolve identically on both sides.
|
|
197
206
|
*/
|
|
198
207
|
if (target === 'client') {
|
|
199
|
-
const contents = `import { remoteProxy as __belteRemoteProxy__ } from '
|
|
208
|
+
const contents = `import { remoteProxy as __belteRemoteProxy__ } from '${importName}/browser/remoteProxy';
|
|
200
209
|
export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prepared.verb)}, ${JSON.stringify(url)});
|
|
201
210
|
`
|
|
202
211
|
return { contents, loader: 'ts' }
|
|
@@ -211,7 +220,7 @@ export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prep
|
|
|
211
220
|
tokenizer-driven so `GET` mentions inside strings and
|
|
212
221
|
comments are left alone.
|
|
213
222
|
*/
|
|
214
|
-
const banner = `import { defineVerb as __belteDefineVerb__ } from '
|
|
223
|
+
const banner = `import { defineVerb as __belteDefineVerb__ } from '${importName}/server/rpc/defineVerb';
|
|
215
224
|
`
|
|
216
225
|
return { contents: `${banner}${prepared.rewriteForServer(url)}`, loader: 'ts' }
|
|
217
226
|
})
|
|
@@ -223,7 +232,8 @@ export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prep
|
|
|
223
232
|
const relativePath = args.path.slice(socketsDir.length + 1)
|
|
224
233
|
const source = await Bun.file(args.path).text()
|
|
225
234
|
const name = socketNameForFile(relativePath)
|
|
226
|
-
const
|
|
235
|
+
const importName = await belteImportNameOnce()
|
|
236
|
+
const prepared = prepareSocketModule(source, importName)
|
|
227
237
|
if (!prepared) {
|
|
228
238
|
throw new Error(
|
|
229
239
|
`[belte] src/server/sockets/${relativePath} has no \`export const <name> = socket(...)\` — every $sockets module must declare exactly one socket`,
|
|
@@ -241,12 +251,12 @@ export const ${prepared.exportName} = __belteRemoteProxy__(${JSON.stringify(prep
|
|
|
241
251
|
clientPublish) are server-side state and don't
|
|
242
252
|
affect the client's wire behaviour.
|
|
243
253
|
*/
|
|
244
|
-
const contents = `import { socketProxy as __belteSocketProxy__ } from '
|
|
254
|
+
const contents = `import { socketProxy as __belteSocketProxy__ } from '${importName}/browser/socketProxy';
|
|
245
255
|
export const ${prepared.exportName} = __belteSocketProxy__(${JSON.stringify(name)});
|
|
246
256
|
`
|
|
247
257
|
return { contents, loader: 'ts' }
|
|
248
258
|
}
|
|
249
|
-
const banner = `import { defineSocket as __belteDefineSocket__ } from '
|
|
259
|
+
const banner = `import { defineSocket as __belteDefineSocket__ } from '${importName}/server/sockets/defineSocket';
|
|
250
260
|
`
|
|
251
261
|
return {
|
|
252
262
|
contents: `${banner}${prepared.rewriteForServer(name)}`,
|
|
@@ -277,6 +287,7 @@ export const ${prepared.exportName} = __belteSocketProxy__(${JSON.stringify(name
|
|
|
277
287
|
const relativePath = args.path.slice(promptsDir.length + 1)
|
|
278
288
|
const source = await Bun.file(args.path).text()
|
|
279
289
|
const name = promptNameForFile(relativePath)
|
|
290
|
+
const importName = await belteImportNameOnce()
|
|
280
291
|
const parsed = parsePromptMarkdown(source)
|
|
281
292
|
const jsonSchema = jsonSchemaForPromptArguments(parsed.arguments)
|
|
282
293
|
const optionLines = [
|
|
@@ -288,8 +299,8 @@ export const ${prepared.exportName} = __belteSocketProxy__(${JSON.stringify(name
|
|
|
288
299
|
]
|
|
289
300
|
.filter((line) => line !== undefined)
|
|
290
301
|
.join('\n')
|
|
291
|
-
const contents = `import { definePrompt as __belteDefinePrompt__ } from '
|
|
292
|
-
import { renderPromptTemplate as __belteRenderPromptTemplate__ } from '
|
|
302
|
+
const contents = `import { definePrompt as __belteDefinePrompt__ } from '${importName}/server/prompts/definePrompt'
|
|
303
|
+
import { renderPromptTemplate as __belteRenderPromptTemplate__ } from '${importName}/server/prompts/renderPromptTemplate'
|
|
293
304
|
const __template__ = ${JSON.stringify(parsed.body)}
|
|
294
305
|
export const prompt = __belteDefinePrompt__(${JSON.stringify(name)}, {
|
|
295
306
|
${optionLines}
|
|
@@ -574,9 +585,9 @@ export const footer = ${JSON.stringify(footer)}
|
|
|
574
585
|
from src/mcp/resources. createMcpServer is internal; there
|
|
575
586
|
is no user-authored server module.
|
|
576
587
|
*/
|
|
588
|
+
const importName = await belteImportNameOnce()
|
|
577
589
|
return {
|
|
578
|
-
contents:
|
|
579
|
-
"import { createMcpServer } from 'belte/mcp/createMcpServer'\nexport default createMcpServer()\n",
|
|
590
|
+
contents: `import { createMcpServer } from '${importName}/mcp/createMcpServer'\nexport default createMcpServer()\n`,
|
|
580
591
|
loader: 'js',
|
|
581
592
|
}
|
|
582
593
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { afterAll, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { belteImportName } from './belteImportName.ts'
|
|
5
|
+
|
|
6
|
+
const roots: string[] = []
|
|
7
|
+
afterAll(() => roots.forEach((root) => rmSync(root, { recursive: true, force: true })))
|
|
8
|
+
|
|
9
|
+
// Writes a package.json into a fresh temp dir and returns the dir.
|
|
10
|
+
async function projectWith(packageJson: unknown): Promise<string> {
|
|
11
|
+
const root = mkdtempSync(`${tmpdir()}/belte-import-name-`)
|
|
12
|
+
roots.push(root)
|
|
13
|
+
await Bun.write(`${root}/package.json`, JSON.stringify(packageJson))
|
|
14
|
+
return root
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('uses the canonical name for a direct dependency', async () => {
|
|
18
|
+
const cwd = await projectWith({ dependencies: { '@briancray/belte': '^0.2.0' } })
|
|
19
|
+
expect(await belteImportName(cwd)).toBe('@briancray/belte')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('uses the `belte` alias key for an npm alias', async () => {
|
|
23
|
+
const cwd = await projectWith({ dependencies: { belte: 'npm:@briancray/belte@^0.2.0' } })
|
|
24
|
+
expect(await belteImportName(cwd)).toBe('belte')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('uses the `belte` alias key for a workspace alias', async () => {
|
|
28
|
+
const cwd = await projectWith({ dependencies: { belte: 'workspace:@briancray/belte@*' } })
|
|
29
|
+
expect(await belteImportName(cwd)).toBe('belte')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('uses a non-`belte` alias key when that is how belte is declared', async () => {
|
|
33
|
+
const cwd = await projectWith({ dependencies: { framework: 'npm:@briancray/belte' } })
|
|
34
|
+
expect(await belteImportName(cwd)).toBe('framework')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('prefers the `belte` alias over a direct canonical dependency', async () => {
|
|
38
|
+
const cwd = await projectWith({
|
|
39
|
+
dependencies: { '@briancray/belte': '^0.2.0', belte: 'npm:@briancray/belte@^0.2.0' },
|
|
40
|
+
})
|
|
41
|
+
expect(await belteImportName(cwd)).toBe('belte')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('finds the alias in devDependencies', async () => {
|
|
45
|
+
const cwd = await projectWith({ devDependencies: { belte: 'npm:@briancray/belte@^0.2.0' } })
|
|
46
|
+
expect(await belteImportName(cwd)).toBe('belte')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('falls back to the canonical name when belte is absent', async () => {
|
|
50
|
+
const cwd = await projectWith({ dependencies: { svelte: '^5.0.0' } })
|
|
51
|
+
expect(await belteImportName(cwd)).toBe('@briancray/belte')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('falls back to the canonical name when package.json is missing', async () => {
|
|
55
|
+
const root = mkdtempSync(`${tmpdir()}/belte-import-name-`)
|
|
56
|
+
roots.push(root)
|
|
57
|
+
expect(await belteImportName(root)).toBe('@briancray/belte')
|
|
58
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { beltePackageName } from './beltePackageName.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Resolves the bare specifier prefix a consuming project imports belte under —
|
|
5
|
+
the name belte is installed as in its package.json. A project may depend on
|
|
6
|
+
belte directly (`@briancray/belte`) or behind a package alias
|
|
7
|
+
(`"belte": "npm:@briancray/belte@..."`, or `workspace:@briancray/belte@*`
|
|
8
|
+
inside this repo). An alias-only install resolves only under the alias key and
|
|
9
|
+
a direct install only under the canonical name, so the generated rpc / socket
|
|
10
|
+
/ prompt modules must import under whichever name the project actually
|
|
11
|
+
declared.
|
|
12
|
+
|
|
13
|
+
Prefers a `belte` alias (the ergonomic surface the docs use) when present, then
|
|
14
|
+
a direct canonical dependency, then any other alias targeting belte. Falls back
|
|
15
|
+
to the canonical name when belte isn't found in package.json — the build can't
|
|
16
|
+
resolve belte at all in that case, and the canonical name yields the clearest
|
|
17
|
+
resolution error.
|
|
18
|
+
*/
|
|
19
|
+
export async function belteImportName(cwd: string): Promise<string> {
|
|
20
|
+
const packageJsonPath = `${cwd}/package.json`
|
|
21
|
+
if (!(await Bun.file(packageJsonPath).exists())) {
|
|
22
|
+
return beltePackageName
|
|
23
|
+
}
|
|
24
|
+
const packageJson = (await Bun.file(packageJsonPath).json()) as {
|
|
25
|
+
dependencies?: Record<string, string>
|
|
26
|
+
devDependencies?: Record<string, string>
|
|
27
|
+
}
|
|
28
|
+
const dependencies = { ...packageJson.devDependencies, ...packageJson.dependencies }
|
|
29
|
+
/*
|
|
30
|
+
Alias entries whose target is belte — `npm:` for a published install,
|
|
31
|
+
`workspace:` for the in-repo examples. The key is the name the project
|
|
32
|
+
imports under; the version suffix (`@^0.2.0`, `@*`) is optional.
|
|
33
|
+
*/
|
|
34
|
+
const aliasPattern = new RegExp(`^(npm|workspace):${beltePackageName}(@.*)?$`)
|
|
35
|
+
const aliasNames = Object.entries(dependencies)
|
|
36
|
+
.filter(([, specifier]) => aliasPattern.test(specifier))
|
|
37
|
+
.map(([name]) => name)
|
|
38
|
+
if (aliasNames.includes('belte')) {
|
|
39
|
+
return 'belte'
|
|
40
|
+
}
|
|
41
|
+
if (beltePackageName in dependencies) {
|
|
42
|
+
return beltePackageName
|
|
43
|
+
}
|
|
44
|
+
return aliasNames[0] ?? beltePackageName
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The package's published npm name. The codegen and the import-name resolver
|
|
3
|
+
match a consuming project's dependency (direct or aliased) against this to
|
|
4
|
+
decide which specifier to emit, so keeping it in one place means a future
|
|
5
|
+
rename touches a single constant.
|
|
6
|
+
*/
|
|
7
|
+
export const beltePackageName = '@briancray/belte'
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { HttpVerb } from '../server/rpc/types/HttpVerb.ts'
|
|
2
|
+
import { beltePackageName } from './beltePackageName.ts'
|
|
2
3
|
import { findExportCallSite } from './findExportCallSite.ts'
|
|
3
4
|
import { stripImport } from './stripImport.ts'
|
|
4
5
|
|
|
5
6
|
const VERB_NAMES = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] as const
|
|
6
7
|
const VERB_SET = new Set<string>(VERB_NAMES)
|
|
7
|
-
const VERB_IMPORT_PATHS = VERB_NAMES.map((verb) => `belte/server/${verb}`)
|
|
8
8
|
|
|
9
9
|
const SINGLE_EXPORT_ERROR =
|
|
10
10
|
'[belte] $rpc module contains more than one `<VERB>(...)` export — each file must declare exactly one remote function'
|
|
@@ -27,14 +27,24 @@ A regex pass would be tidier but it can't tell a `GET` mention inside a
|
|
|
27
27
|
docstring or template literal from the real call, and it can't follow
|
|
28
28
|
nested generics like `GET<Map<K, V>>(`.
|
|
29
29
|
*/
|
|
30
|
-
export function prepareRpcModule(
|
|
30
|
+
export function prepareRpcModule(
|
|
31
|
+
source: string,
|
|
32
|
+
importName: string,
|
|
33
|
+
): PreparedRpcModule | undefined {
|
|
31
34
|
/*
|
|
32
35
|
The "no barrels" surface places each verb at its own path
|
|
33
36
|
(`belte/server/GET`, `belte/server/POST`, …) — strip every one so
|
|
34
37
|
the user's verb import doesn't linger and side-effect-load the
|
|
35
|
-
stub module into the server bundle.
|
|
38
|
+
stub module into the server bundle. The user may import under the
|
|
39
|
+
project's chosen name or the canonical package name, so strip both.
|
|
36
40
|
*/
|
|
37
|
-
const
|
|
41
|
+
const importNames =
|
|
42
|
+
importName === beltePackageName ? [beltePackageName] : [importName, beltePackageName]
|
|
43
|
+
const stripped = importNames.reduce(
|
|
44
|
+
(current, name) =>
|
|
45
|
+
VERB_NAMES.reduce((acc, verb) => stripImport(acc, `${name}/server/${verb}`), current),
|
|
46
|
+
source,
|
|
47
|
+
)
|
|
38
48
|
const site = findExportCallSite(stripped, (ident) => VERB_SET.has(ident), SINGLE_EXPORT_ERROR)
|
|
39
49
|
if (!site) {
|
|
40
50
|
return undefined
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { beltePackageName } from './beltePackageName.ts'
|
|
1
2
|
import { findExportCallSite } from './findExportCallSite.ts'
|
|
2
3
|
import { stripImport } from './stripImport.ts'
|
|
3
4
|
|
|
@@ -17,8 +18,21 @@ original source). The single scan replaces the prior separate
|
|
|
17
18
|
extract + rewrite passes, so the resolver plugin only walks each source
|
|
18
19
|
character-by-character once.
|
|
19
20
|
*/
|
|
20
|
-
export function prepareSocketModule(
|
|
21
|
-
|
|
21
|
+
export function prepareSocketModule(
|
|
22
|
+
source: string,
|
|
23
|
+
importName: string,
|
|
24
|
+
): PreparedSocketModule | undefined {
|
|
25
|
+
/*
|
|
26
|
+
Strip the user's `socket` import under the project's chosen name and the
|
|
27
|
+
canonical package name so the dead import can't side-effect-load the
|
|
28
|
+
socket helper into the server bundle.
|
|
29
|
+
*/
|
|
30
|
+
const importNames =
|
|
31
|
+
importName === beltePackageName ? [beltePackageName] : [importName, beltePackageName]
|
|
32
|
+
const stripped = importNames.reduce(
|
|
33
|
+
(current, name) => stripImport(current, `${name}/server/socket`),
|
|
34
|
+
source,
|
|
35
|
+
)
|
|
22
36
|
const site = findExportCallSite(stripped, (ident) => ident === 'socket', SINGLE_EXPORT_ERROR)
|
|
23
37
|
if (!site) {
|
|
24
38
|
return undefined
|