@pikku/bun-server 0.12.9 → 0.12.11

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/CHANGELOG.md CHANGED
@@ -1,5 +1,73 @@
1
1
  # @pikku/bun-server
2
2
 
3
+ ## 0.12.11
4
+
5
+ ### Patch Changes
6
+
7
+ - 3d75643: Run the app's server lifecycle in generated entries, and make an out-of-band
8
+ account signable-in.
9
+
10
+ `pikkuServerLifecycle` was only ever called by `pikku dev` and `pikku serve`, so
11
+ an app that seeds its first admin account, probes a dependency, or warms a cache
12
+ in `beforeStart` did all of that in development and silently skipped it
13
+ everywhere it was actually deployed. The standalone entry and the shared node
14
+ server entry — the one behind every `target: 'server'` unit — now import the
15
+ app's lifecycle and call it: `beforeStart` after `init` and before the port
16
+ opens, so work that must finish before the first request has, `afterStart` once
17
+ the server is listening, and the stop hooks handed to the signal handler that
18
+ already owns shutdown rather than a second listener racing it. Each shutdown
19
+ step is isolated from the ones after it, so a hook that throws is logged without
20
+ taking the service teardown and the socket close down with it.
21
+
22
+ Separately, `createAuthUser` and `setAuthUserPassword` wrote credential accounts
23
+ with no `issuer`. From better-auth 1.7 a credential account is matched by its
24
+ issuer as well as its provider, so those accounts were invisible to sign-in,
25
+ `updatePassword` and `findCredentialAccount` — a user who plainly existed in the
26
+ table was reported as "user not found". The field is written only when the
27
+ resolved schema has it, so older better-auth keeps working, and
28
+ `setAuthUserPassword` repairs an account that predates the fix.
29
+
30
+ ## 0.12.10
31
+
32
+ ### Patch Changes
33
+
34
+ - 80eb5c0: Generate a desktop shell from `pikku deploy apply --desktop`
35
+
36
+ `pikku deploy apply --provider standalone --runtime bun --desktop` now emits a
37
+ `src-tauri/` crate that ships the compiled binary as a sidecar and opens a
38
+ window on the server's own HTTP origin, so cookies, CORS and OAuth behave
39
+ exactly as they do in a browser. Regeneration is idempotent and leaves an
40
+ edited file alone rather than overwriting it.
41
+
42
+ `--desktop-url https://app.example.com` builds the other shape: a shell that
43
+ points at an already-deployed server. Nothing is bundled — no sidecar, no
44
+ binary, and so no bun runtime to compile one — and the window is declared in
45
+ `tauri.conf.json` rather than opened from Rust, because the origin is known up
46
+ front. The url can also live in `pikku.config.json` as `deploy.desktop.url`,
47
+ alongside `deploy.desktop.identifier`.
48
+
49
+ Supporting changes: `SERVER_READY_MARKER` moved to `@pikku/deploy` (the CLI
50
+ re-exports it from its old path), both HTTP runtimes expose the port they
51
+ actually bound so `--port 0` reports a real port, and the generated server
52
+ entry exits when its parent process goes away.
53
+
54
+ - 80eb5c0: feat: serve a built frontend from the pikku server's own origin
55
+
56
+ A new `frontend` key in `pikku.config.json` names a directory of built
57
+ frontend output. `pikku serve` mounts it, and `pikku deploy` ships it inside
58
+ the distributable — into a directory beside the bundle for the node runtime,
59
+ and embedded in the binary for a `bun build --compile` standalone. `pikku dev`
60
+ deliberately ignores it and says so, because the frontend's own dev server owns
61
+ that job.
62
+
63
+ Pikku reads the frontend's output and never builds it, so an unbuilt directory
64
+ fails with a message that says which build to run rather than booting a server
65
+ that answers every page with a 404.
66
+
67
+ - Updated dependencies [80eb5c0]
68
+ - Updated dependencies [2252016]
69
+ - @pikku/core@0.12.98
70
+
3
71
  ## 0.12.9
4
72
 
5
73
  ### Patch Changes
@@ -7,6 +7,7 @@ export type StaticMount = {
7
7
  urlPrefix: string;
8
8
  directory: string;
9
9
  spaFallback?: boolean;
10
+ assets?: Record<string, string>;
10
11
  };
11
12
  export type BunServerConfig = CoreConfig & {
12
13
  port: number;
@@ -25,6 +26,10 @@ export type PikkuBunServerOptions = RunHTTPWiringOptions & {
25
26
  mcpPath?: string;
26
27
  contentSigningJWT?: JWTService;
27
28
  };
29
+ export type ShutdownHooks = {
30
+ beforeStop?: () => void | Promise<void>;
31
+ afterStop?: () => void | Promise<void>;
32
+ };
28
33
  export declare class PikkuBunServer {
29
34
  private readonly config;
30
35
  private readonly logger;
@@ -40,8 +45,11 @@ export declare class PikkuBunServer {
40
45
  private initMCP;
41
46
  start(): Promise<void>;
42
47
  get port(): number;
43
- private serveStaticMounts;
48
+ private matchesPrefix;
49
+ private staticMountCandidates;
50
+ private serveStaticFiles;
51
+ private serveSpaFallback;
44
52
  private resolveStaticFile;
45
53
  stop(): Promise<void>;
46
- enableExitOnSignals(): void;
54
+ enableExitOnSignals(hooks?: ShutdownHooks): void;
47
55
  }
@@ -118,7 +118,7 @@ export class PikkuBunServer {
118
118
  if (contentResponse) {
119
119
  return contentResponse;
120
120
  }
121
- const staticResponse = await this.serveStaticMounts(req);
121
+ const staticResponse = await this.serveStaticFiles(req);
122
122
  if (staticResponse) {
123
123
  return staticResponse;
124
124
  }
@@ -128,7 +128,14 @@ export class PikkuBunServer {
128
128
  respondWith404: true,
129
129
  ...options,
130
130
  });
131
- return pikkuRes.toResponse();
131
+ const response = pikkuRes.toResponse();
132
+ if (response.status === 404) {
133
+ const fallback = await this.serveSpaFallback(req);
134
+ if (fallback) {
135
+ return fallback;
136
+ }
137
+ }
138
+ return response;
132
139
  },
133
140
  websocket: {
134
141
  open: (ws) => {
@@ -169,48 +176,82 @@ export class PikkuBunServer {
169
176
  },
170
177
  });
171
178
  eventHub.setServer(this.server);
172
- logger.info(`pikku-bun-server: listening on http://${config.hostname ?? 'localhost'}:${config.port}`);
179
+ logger.info(`pikku-bun-server: listening on http://${config.hostname ?? 'localhost'}:${this.port}`);
173
180
  }
174
181
  get port() {
175
182
  return this.server?.port ?? this.config.port;
176
183
  }
177
- async serveStaticMounts(req) {
184
+ matchesPrefix(pathname, prefix) {
185
+ if (prefix === '' || prefix === '/') {
186
+ return true;
187
+ }
188
+ return pathname === prefix || pathname.startsWith(`${prefix}/`);
189
+ }
190
+ staticMountCandidates(req) {
178
191
  const mounts = this.config.staticMounts;
179
192
  if (!mounts?.length || (req.method !== 'GET' && req.method !== 'HEAD')) {
180
193
  return null;
181
194
  }
182
195
  const pathname = decodeURIComponent(new URL(req.url).pathname);
183
- for (const mount of mounts) {
184
- if (pathname !== mount.urlPrefix &&
185
- !pathname.startsWith(`${mount.urlPrefix}/`)) {
186
- continue;
196
+ return {
197
+ pathname,
198
+ mounts: mounts.filter((mount) => this.matchesPrefix(pathname, mount.urlPrefix)),
199
+ };
200
+ }
201
+ async serveStaticFiles(req) {
202
+ const candidates = this.staticMountCandidates(req);
203
+ if (!candidates) {
204
+ return null;
205
+ }
206
+ for (const mount of candidates.mounts) {
207
+ const key = candidates.pathname
208
+ .slice(mount.urlPrefix.length)
209
+ .replace(/^\/+/, '');
210
+ const result = await this.resolveStaticFile(mount, key);
211
+ if (result === 'rejected') {
212
+ return new Response('Not Found', { status: 404 });
187
213
  }
188
- const key = pathname.slice(mount.urlPrefix.length).replace(/^\/+/, '');
189
- const file = await this.resolveStaticFile(mount, key);
190
- if (file) {
191
- return new Response(req.method === 'HEAD' ? null : file);
214
+ if (result) {
215
+ return new Response(req.method === 'HEAD' ? null : result);
192
216
  }
193
- if (mount.spaFallback) {
194
- const index = await this.resolveStaticFile(mount, 'index.html');
195
- if (index) {
196
- return new Response(req.method === 'HEAD' ? null : index);
197
- }
217
+ }
218
+ return null;
219
+ }
220
+ async serveSpaFallback(req) {
221
+ const candidates = this.staticMountCandidates(req);
222
+ if (!candidates) {
223
+ return null;
224
+ }
225
+ for (const mount of candidates.mounts) {
226
+ if (!mount.spaFallback) {
227
+ continue;
228
+ }
229
+ const index = await this.resolveStaticFile(mount, 'index.html');
230
+ if (index && index !== 'rejected') {
231
+ return new Response(req.method === 'HEAD' ? null : index);
198
232
  }
199
- return new Response('Not Found', { status: 404 });
200
233
  }
201
234
  return null;
202
235
  }
203
236
  async resolveStaticFile(mount, key) {
237
+ if (mount.assets) {
238
+ const assetPath = mount.assets[key === '' ? 'index.html' : key];
239
+ if (!assetPath) {
240
+ return null;
241
+ }
242
+ const asset = Bun.file(assetPath);
243
+ return (await asset.exists()) ? asset : null;
244
+ }
204
245
  const directory = resolve(mount.directory);
205
246
  const targetPath = key === '' ? resolve(directory, 'index.html') : resolve(directory, key);
206
247
  if (targetPath !== directory && !targetPath.startsWith(`${directory}/`)) {
207
- return null;
248
+ return 'rejected';
208
249
  }
209
250
  let file = Bun.file(targetPath);
210
251
  if (!(await file.exists())) {
211
252
  const indexPath = resolve(targetPath, 'index.html');
212
253
  if (!indexPath.startsWith(`${directory}/`)) {
213
- return null;
254
+ return 'rejected';
214
255
  }
215
256
  file = Bun.file(indexPath);
216
257
  if (!(await file.exists())) {
@@ -223,12 +264,22 @@ export class PikkuBunServer {
223
264
  await this.server?.stop();
224
265
  this.server = null;
225
266
  }
226
- enableExitOnSignals() {
267
+ enableExitOnSignals(hooks) {
268
+ const phase = async (name, run) => {
269
+ try {
270
+ await run();
271
+ }
272
+ catch (error) {
273
+ this.logger.error(`pikku-bun-server: ${name} failed during shutdown`, error);
274
+ }
275
+ };
227
276
  const shutdown = async (signal) => {
228
277
  this.logger.info(`pikku-bun-server: ${signal} received, stopping`);
229
278
  try {
230
- await stopSingletonServices();
231
- await this.stop();
279
+ await phase('beforeStop', () => hooks?.beforeStop?.());
280
+ await phase('stopping singleton services', () => stopSingletonServices());
281
+ await phase('stopping the server', () => this.stop());
282
+ await phase('afterStop', () => hooks?.afterStop?.());
232
283
  }
233
284
  finally {
234
285
  process.exit(0);
@@ -1 +1 @@
1
- {"version":3,"file":"pikku-bun-server.js","sourceRoot":"","sources":["../src/pikku-bun-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pikku-bun-server.js","sourceRoot":"","sources":["../src/pikku-bun-server.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/bun-server",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "description": "Pikku server adapter for Bun (Bun.serve)",
5
5
  "license": "MIT",
6
6
  "author": "yasser.fadl@gmail.com",
@@ -16,7 +16,7 @@
16
16
  "prepublishOnly": "bun run build"
17
17
  },
18
18
  "peerDependencies": {
19
- "@pikku/core": "^0.12.85",
19
+ "@pikku/core": "^0.12.98",
20
20
  "@pikku/modelcontextprotocol": "^0.12.10"
21
21
  },
22
22
  "peerDependenciesMeta": {
@@ -25,7 +25,7 @@
25
25
  }
26
26
  },
27
27
  "devDependencies": {
28
- "@pikku/core": "^0.12.85",
28
+ "@pikku/core": "^0.12.98",
29
29
  "@pikku/modelcontextprotocol": "^0.12.10",
30
30
  "@types/bun": "^1.4.0",
31
31
  "typescript": "^6.0.3"
@@ -31,6 +31,17 @@ export type StaticMount = {
31
31
  urlPrefix: string
32
32
  directory: string
33
33
  spaFallback?: boolean
34
+ /**
35
+ * Serve from an explicit key → path map instead of from `directory`, which is
36
+ * then unused. Keys are mount-relative request paths (`index.html`,
37
+ * `assets/app-a1b2c3.js`).
38
+ *
39
+ * This exists for assets that are not laid out as a directory at all: a
40
+ * `bun build --compile` binary embeds each file and hands back an opaque
41
+ * `/$bunfs/…` path, so the only way to find one is to have recorded it at
42
+ * build time.
43
+ */
44
+ assets?: Record<string, string>
34
45
  }
35
46
 
36
47
  export type BunServerConfig = CoreConfig & {
@@ -100,6 +111,16 @@ const isSerializable = (data: unknown): boolean =>
100
111
  * Handles HTTP via the fetch handler and WebSocket via Bun.serve's native
101
112
  * websocket handler (which is backed by uWebSockets internally).
102
113
  */
114
+ /**
115
+ * Work the process owes its app on the way down, run around the service
116
+ * teardown `enableExitOnSignals` already does. A failing hook is logged and
117
+ * shutdown continues: a process that has been told to stop has to stop.
118
+ */
119
+ export type ShutdownHooks = {
120
+ beforeStop?: () => void | Promise<void>
121
+ afterStop?: () => void | Promise<void>
122
+ }
123
+
103
124
  export class PikkuBunServer {
104
125
  private server: BunServer<WsData> | null = null
105
126
  private readonly eventHub: BunEventHubService
@@ -220,7 +241,7 @@ export class PikkuBunServer {
220
241
  return contentResponse
221
242
  }
222
243
 
223
- const staticResponse = await this.serveStaticMounts(req)
244
+ const staticResponse = await this.serveStaticFiles(req)
224
245
  if (staticResponse) {
225
246
  return staticResponse
226
247
  }
@@ -231,7 +252,14 @@ export class PikkuBunServer {
231
252
  respondWith404: true,
232
253
  ...options,
233
254
  })
234
- return pikkuRes.toResponse()
255
+ const response = pikkuRes.toResponse()
256
+ if (response.status === 404) {
257
+ const fallback = await this.serveSpaFallback(req)
258
+ if (fallback) {
259
+ return fallback
260
+ }
261
+ }
262
+ return response
235
263
  },
236
264
 
237
265
  websocket: {
@@ -279,60 +307,133 @@ export class PikkuBunServer {
279
307
 
280
308
  eventHub.setServer(this.server as BunServer<unknown>)
281
309
  logger.info(
282
- `pikku-bun-server: listening on http://${config.hostname ?? 'localhost'}:${config.port}`
310
+ `pikku-bun-server: listening on http://${config.hostname ?? 'localhost'}:${this.port}`
283
311
  )
284
312
  }
285
313
 
314
+ /**
315
+ * The port the server is actually listening on.
316
+ *
317
+ * Not the same as `config.port` whenever that is `0`: the OS picks a free
318
+ * port at bind time, and a parent process that was told `0` has no other way
319
+ * to find out which one. Falls back to the requested port before `start()`.
320
+ */
286
321
  public get port(): number {
287
322
  return this.server?.port ?? this.config.port
288
323
  }
289
324
 
290
- private async serveStaticMounts(req: Request): Promise<Response | null> {
325
+ private matchesPrefix(pathname: string, prefix: string): boolean {
326
+ // A root mount covers the whole tree; the generic test below would match
327
+ // only `/` itself, since nothing starts with `//`.
328
+ if (prefix === '' || prefix === '/') {
329
+ return true
330
+ }
331
+ return pathname === prefix || pathname.startsWith(`${prefix}/`)
332
+ }
333
+
334
+ /**
335
+ * Resolve the mounts a GET/HEAD request could be served from, or null when
336
+ * static serving does not apply to this request at all.
337
+ */
338
+ private staticMountCandidates(
339
+ req: Request
340
+ ): { pathname: string; mounts: StaticMount[] } | null {
291
341
  const mounts = this.config.staticMounts
292
342
  if (!mounts?.length || (req.method !== 'GET' && req.method !== 'HEAD')) {
293
343
  return null
294
344
  }
295
345
  const pathname = decodeURIComponent(new URL(req.url).pathname)
346
+ return {
347
+ pathname,
348
+ mounts: mounts.filter((mount) =>
349
+ this.matchesPrefix(pathname, mount.urlPrefix)
350
+ ),
351
+ }
352
+ }
296
353
 
297
- for (const mount of mounts) {
298
- if (
299
- pathname !== mount.urlPrefix &&
300
- !pathname.startsWith(`${mount.urlPrefix}/`)
301
- ) {
302
- continue
354
+ /**
355
+ * Serve an exact file hit only. A miss falls through to route dispatch, so a
356
+ * mount at `/` cannot swallow the API — the app shell is served afterwards by
357
+ * `serveSpaFallback`, once dispatch has had its chance and produced a 404.
358
+ */
359
+ private async serveStaticFiles(req: Request): Promise<Response | null> {
360
+ const candidates = this.staticMountCandidates(req)
361
+ if (!candidates) {
362
+ return null
363
+ }
364
+
365
+ for (const mount of candidates.mounts) {
366
+ const key = candidates.pathname
367
+ .slice(mount.urlPrefix.length)
368
+ .replace(/^\/+/, '')
369
+ const result = await this.resolveStaticFile(mount, key)
370
+ if (result === 'rejected') {
371
+ // A key that escaped the mount directory is refused outright rather
372
+ // than falling through, which would answer a traversal attempt with a
373
+ // 200 and the app shell.
374
+ return new Response('Not Found', { status: 404 })
303
375
  }
304
- const key = pathname.slice(mount.urlPrefix.length).replace(/^\/+/, '')
305
- const file = await this.resolveStaticFile(mount, key)
306
- if (file) {
307
- return new Response(req.method === 'HEAD' ? null : file)
376
+ if (result) {
377
+ return new Response(req.method === 'HEAD' ? null : result)
308
378
  }
309
- if (mount.spaFallback) {
310
- const index = await this.resolveStaticFile(mount, 'index.html')
311
- if (index) {
312
- return new Response(req.method === 'HEAD' ? null : index)
313
- }
379
+ }
380
+
381
+ return null
382
+ }
383
+
384
+ /**
385
+ * Last resort for a client-side route: dispatch has already 404'd, so an
386
+ * unmatched path under a `spaFallback` mount is the SPA's own routing.
387
+ */
388
+ private async serveSpaFallback(req: Request): Promise<Response | null> {
389
+ const candidates = this.staticMountCandidates(req)
390
+ if (!candidates) {
391
+ return null
392
+ }
393
+
394
+ for (const mount of candidates.mounts) {
395
+ if (!mount.spaFallback) {
396
+ continue
397
+ }
398
+ const index = await this.resolveStaticFile(mount, 'index.html')
399
+ if (index && index !== 'rejected') {
400
+ return new Response(req.method === 'HEAD' ? null : index)
314
401
  }
315
- return new Response('Not Found', { status: 404 })
316
402
  }
317
403
 
318
404
  return null
319
405
  }
320
406
 
407
+ /**
408
+ * `'rejected'` is kept distinct from `null` because the two must not share a
409
+ * fate: a missing file is a candidate for the SPA fallback, while a key that
410
+ * resolved outside the mount directory must never be. An `assets` mount never
411
+ * produces `'rejected'` at all — a map lookup has no directory to escape.
412
+ */
321
413
  private async resolveStaticFile(
322
414
  mount: StaticMount,
323
415
  key: string
324
- ): Promise<ReturnType<typeof Bun.file> | null> {
416
+ ): Promise<ReturnType<typeof Bun.file> | 'rejected' | null> {
417
+ if (mount.assets) {
418
+ const assetPath = mount.assets[key === '' ? 'index.html' : key]
419
+ if (!assetPath) {
420
+ return null
421
+ }
422
+ const asset = Bun.file(assetPath)
423
+ return (await asset.exists()) ? asset : null
424
+ }
425
+
325
426
  const directory = resolve(mount.directory)
326
427
  const targetPath =
327
428
  key === '' ? resolve(directory, 'index.html') : resolve(directory, key)
328
429
  if (targetPath !== directory && !targetPath.startsWith(`${directory}/`)) {
329
- return null
430
+ return 'rejected'
330
431
  }
331
432
  let file = Bun.file(targetPath)
332
433
  if (!(await file.exists())) {
333
434
  const indexPath = resolve(targetPath, 'index.html')
334
435
  if (!indexPath.startsWith(`${directory}/`)) {
335
- return null
436
+ return 'rejected'
336
437
  }
337
438
  file = Bun.file(indexPath)
338
439
  if (!(await file.exists())) {
@@ -347,12 +448,32 @@ export class PikkuBunServer {
347
448
  this.server = null
348
449
  }
349
450
 
350
- public enableExitOnSignals(): void {
451
+ public enableExitOnSignals(hooks?: ShutdownHooks): void {
452
+ /**
453
+ * Every step of the shutdown is isolated from the ones after it. Sharing
454
+ * one catch means a hook that throws takes the service teardown and the
455
+ * socket close down with it, leaving the process to exit with connections
456
+ * still open and services still holding their handles.
457
+ */
458
+ const phase = async (name: string, run: () => void | Promise<void>) => {
459
+ try {
460
+ await run()
461
+ } catch (error) {
462
+ this.logger.error(
463
+ `pikku-bun-server: ${name} failed during shutdown`,
464
+ error
465
+ )
466
+ }
467
+ }
351
468
  const shutdown = async (signal: string) => {
352
469
  this.logger.info(`pikku-bun-server: ${signal} received, stopping`)
353
470
  try {
354
- await stopSingletonServices()
355
- await this.stop()
471
+ await phase('beforeStop', () => hooks?.beforeStop?.())
472
+ await phase('stopping singleton services', () =>
473
+ stopSingletonServices()
474
+ )
475
+ await phase('stopping the server', () => this.stop())
476
+ await phase('afterStop', () => hooks?.afterStop?.())
356
477
  } finally {
357
478
  process.exit(0)
358
479
  }
@@ -4,7 +4,9 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { join } from 'node:path'
6
6
  import { setSingletonServices } from '@pikku/core/state'
7
- import { resetPikkuState } from '@pikku/core/state'
7
+ import { pikkuState, resetPikkuState } from '@pikku/core/state'
8
+ import { httpRouter, wireHTTP } from '@pikku/core/http'
9
+ import { addFunction } from '@pikku/core/function'
8
10
  import type { Logger } from '@pikku/core/services'
9
11
  import { PikkuBunServer } from './pikku-bun-server.js'
10
12
 
@@ -106,3 +108,223 @@ describe('PikkuBunServer static mounts', () => {
106
108
  }
107
109
  })
108
110
  })
111
+
112
+ describe('PikkuBunServer root-mounted SPA', () => {
113
+ let server: PikkuBunServer
114
+ let tmpDir: string
115
+ let origin: string
116
+
117
+ before(async () => {
118
+ resetPikkuState()
119
+ httpRouter.reset()
120
+ setSingletonServices({
121
+ logger: noopLogger,
122
+ schema: {
123
+ compileSchema: () => {},
124
+ getSchemaNames: () => new Set<string>(),
125
+ },
126
+ } as any)
127
+ tmpDir = mkdtempSync(join(tmpdir(), 'pikku-bun-spa-'))
128
+ writeFileSync(
129
+ join(tmpDir, 'index.html'),
130
+ '<!doctype html><title>app shell</title>'
131
+ )
132
+ mkdirSync(join(tmpDir, 'assets'))
133
+ writeFileSync(join(tmpDir, 'assets', 'app.js'), 'console.log("app")')
134
+
135
+ pikkuState(null, 'function', 'meta', {
136
+ things_func: {
137
+ pikkuFuncId: 'things_func',
138
+ inputSchemaName: null,
139
+ outputSchemaName: null,
140
+ sessionless: true,
141
+ },
142
+ } as any)
143
+ pikkuState(null, 'http', 'meta', {
144
+ get: {
145
+ '/api/things': {
146
+ pikkuFuncId: 'things_func',
147
+ route: '/api/things',
148
+ method: 'get',
149
+ },
150
+ },
151
+ post: {},
152
+ delete: {},
153
+ patch: {},
154
+ head: {},
155
+ put: {},
156
+ options: {},
157
+ } as any)
158
+ addFunction('things_func', { func: async () => ({ things: ['one'] }) })
159
+ wireHTTP({
160
+ route: '/api/things',
161
+ method: 'get',
162
+ auth: false,
163
+ func: { func: async () => ({ things: ['one'] }) },
164
+ } as any)
165
+ httpRouter.initialize()
166
+
167
+ server = new PikkuBunServer(
168
+ {
169
+ port: 0,
170
+ hostname: '127.0.0.1',
171
+ staticMounts: [
172
+ { urlPrefix: '/', directory: tmpDir, spaFallback: true },
173
+ ],
174
+ } as any,
175
+ noopLogger
176
+ )
177
+ await server.init()
178
+ await server.start()
179
+ origin = `http://127.0.0.1:${server.port}`
180
+ })
181
+
182
+ after(async () => {
183
+ await server.stop()
184
+ rmSync(tmpDir, { recursive: true, force: true })
185
+ })
186
+
187
+ test('a wired API route still dispatches under a root SPA mount', async () => {
188
+ const response = await fetch(`${origin}/api/things`)
189
+ assert.equal(response.status, 200)
190
+ assert.match(
191
+ response.headers.get('content-type') ?? '',
192
+ /application\/json/,
193
+ 'API route returned the SPA shell instead of dispatching'
194
+ )
195
+ assert.deepEqual(await response.json(), { things: ['one'] })
196
+ })
197
+
198
+ test('static files still win over the SPA fallback', async () => {
199
+ const response = await fetch(`${origin}/assets/app.js`)
200
+ assert.equal(response.status, 200)
201
+ assert.equal(await response.text(), 'console.log("app")')
202
+ })
203
+
204
+ test('unknown client routes still fall back to the app shell', async () => {
205
+ const response = await fetch(`${origin}/records/42`)
206
+ assert.equal(response.status, 200)
207
+ assert.match(await response.text(), /<title>app shell<\/title>/)
208
+ })
209
+ })
210
+
211
+ describe('PikkuBunServer asset-mapped mount', () => {
212
+ let server: PikkuBunServer
213
+ let tmpDir: string
214
+ let origin: string
215
+
216
+ before(async () => {
217
+ resetPikkuState()
218
+ httpRouter.reset()
219
+ setSingletonServices({
220
+ logger: noopLogger,
221
+ schema: {
222
+ compileSchema: () => {},
223
+ getSchemaNames: () => new Set<string>(),
224
+ },
225
+ } as any)
226
+ tmpDir = mkdtempSync(join(tmpdir(), 'pikku-bun-assets-'))
227
+
228
+ // The files live in a `store` subdirectory that the mount's own `directory`
229
+ // does not contain, so a hit can only come from the map.
230
+ const store = join(tmpDir, 'store')
231
+ mkdirSync(store)
232
+ writeFileSync(
233
+ join(store, 'shell-abc123.html'),
234
+ '<!doctype html><title>embedded shell</title>'
235
+ )
236
+ writeFileSync(join(store, 'app-def456.js'), 'console.log("embedded")')
237
+
238
+ pikkuState(null, 'function', 'meta', {
239
+ things_func: {
240
+ pikkuFuncId: 'things_func',
241
+ inputSchemaName: null,
242
+ outputSchemaName: null,
243
+ sessionless: true,
244
+ },
245
+ } as any)
246
+ pikkuState(null, 'http', 'meta', {
247
+ get: {
248
+ '/api/things': {
249
+ pikkuFuncId: 'things_func',
250
+ route: '/api/things',
251
+ method: 'get',
252
+ },
253
+ },
254
+ post: {},
255
+ delete: {},
256
+ patch: {},
257
+ head: {},
258
+ put: {},
259
+ options: {},
260
+ } as any)
261
+ addFunction('things_func', { func: async () => ({ things: ['one'] }) })
262
+ wireHTTP({
263
+ route: '/api/things',
264
+ method: 'get',
265
+ auth: false,
266
+ func: { func: async () => ({ things: ['one'] }) },
267
+ } as any)
268
+ httpRouter.initialize()
269
+
270
+ server = new PikkuBunServer(
271
+ {
272
+ port: 0,
273
+ hostname: '127.0.0.1',
274
+ staticMounts: [
275
+ {
276
+ urlPrefix: '/',
277
+ directory: join(tmpDir, 'nonexistent'),
278
+ spaFallback: true,
279
+ assets: {
280
+ 'index.html': join(store, 'shell-abc123.html'),
281
+ 'assets/app.js': join(store, 'app-def456.js'),
282
+ },
283
+ },
284
+ ],
285
+ } as any,
286
+ noopLogger
287
+ )
288
+ await server.init()
289
+ await server.start()
290
+ origin = `http://127.0.0.1:${server.port}`
291
+ })
292
+
293
+ after(async () => {
294
+ await server.stop()
295
+ rmSync(tmpDir, { recursive: true, force: true })
296
+ })
297
+
298
+ test('serves a mapped asset from the path the map names', async () => {
299
+ const response = await fetch(`${origin}/assets/app.js`)
300
+ assert.equal(response.status, 200)
301
+ assert.match(response.headers.get('content-type') ?? '', /javascript/)
302
+ assert.equal(await response.text(), 'console.log("embedded")')
303
+ })
304
+
305
+ test('serves the mapped index.html at the mount root', async () => {
306
+ const response = await fetch(`${origin}/`)
307
+ assert.equal(response.status, 200)
308
+ assert.match(await response.text(), /<title>embedded shell<\/title>/)
309
+ })
310
+
311
+ test('a wired API route still dispatches under a root asset mount', async () => {
312
+ const response = await fetch(`${origin}/api/things`)
313
+ assert.equal(response.status, 200)
314
+ assert.deepEqual(await response.json(), { things: ['one'] })
315
+ })
316
+
317
+ test('an unmapped key falls back to the app shell', async () => {
318
+ const response = await fetch(`${origin}/records/42`)
319
+ assert.equal(response.status, 200)
320
+ assert.match(await response.text(), /<title>embedded shell<\/title>/)
321
+ })
322
+
323
+ test('a traversal-shaped key is a plain miss, not a rejection', async () => {
324
+ // A map lookup cannot escape a directory, so there is nothing to reject:
325
+ // the key is simply absent and the SPA fallback is allowed to answer.
326
+ const response = await fetch(`${origin}/%2e%2e/pikku-bun-assets-secret.txt`)
327
+ assert.equal(response.status, 200)
328
+ assert.match(await response.text(), /<title>embedded shell<\/title>/)
329
+ })
330
+ })