@abide/abide 0.104.0 → 0.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/abide.ts +80 -173
  3. package/docs/REGISTRY.md +17 -17
  4. package/docs/RULEBOOK.md +50 -12
  5. package/docs/abide.md +9 -9
  6. package/package.json +1 -1
  7. package/src/compiler/Lowering.ts +15 -5
  8. package/src/compiler/Refusals.ts +57 -0
  9. package/src/compiler/TypeSession.ts +45 -86
  10. package/src/compiler/appLayout.ts +447 -0
  11. package/src/compiler/appMap.ts +21 -20
  12. package/src/compiler/checkApp.ts +61 -97
  13. package/src/compiler/checkedShadow.ts +11 -9
  14. package/src/compiler/classifyRead.ts +3 -6
  15. package/src/compiler/compileApp.ts +180 -512
  16. package/src/compiler/componentGraph.ts +2 -71
  17. package/src/compiler/declaredExports.ts +79 -60
  18. package/src/compiler/emitClient.ts +11 -13
  19. package/src/compiler/emitComponent.ts +21 -6
  20. package/src/compiler/emitSource.ts +20 -20
  21. package/src/compiler/handlersUnder.ts +46 -36
  22. package/src/compiler/hookCalls.ts +152 -0
  23. package/src/compiler/mountsOf.ts +31 -54
  24. package/src/compiler/readSources.ts +27 -42
  25. package/src/compiler/readsIn.ts +5 -16
  26. package/src/compiler/resolverIn.ts +77 -0
  27. package/src/server/abideModules.ts +92 -123
  28. package/src/server/answerRequest.ts +18 -4
  29. package/src/server/answerSocket.ts +136 -76
  30. package/src/server/answerValue.ts +28 -15
  31. package/src/server/appTree.ts +49 -71
  32. package/src/server/claudeApi.ts +2 -5
  33. package/src/server/clientBundle.ts +198 -0
  34. package/src/server/coerceArgs.ts +1 -1
  35. package/src/server/commands.ts +93 -57
  36. package/src/server/createApp.ts +90 -16
  37. package/src/server/deferredBuilds.ts +175 -0
  38. package/src/server/devWorker.ts +107 -319
  39. package/src/server/faceOf.ts +120 -0
  40. package/src/server/fenceServer.ts +10 -18
  41. package/src/server/index.ts +3 -0
  42. package/src/server/placement.ts +167 -0
  43. package/src/server/remote.ts +14 -42
  44. package/src/server/render.ts +25 -17
  45. package/src/server/renderComponent.ts +9 -5
  46. package/src/server/requestScope.ts +10 -4
  47. package/src/server/responseRelay.ts +389 -0
  48. package/src/server/runCompiled.ts +9 -12
  49. package/src/server/runDesktop.ts +6 -9
  50. package/src/server/serveBuild.ts +26 -73
  51. package/src/server/serveDev.ts +19 -176
  52. package/src/server/streamEvent.ts +29 -0
  53. package/src/server/withAbideSources.ts +2 -1
  54. package/src/shared/HandlerRoute.ts +68 -0
  55. package/src/shared/REFUSAL_KEY.ts +5 -0
  56. package/src/shared/answerOf.ts +105 -78
  57. package/src/shared/filledPath.ts +1 -9
  58. package/src/shared/framing.ts +127 -128
  59. package/src/shared/queryText.ts +15 -7
  60. package/src/shared/reactive/Cursor.ts +4 -2
  61. package/src/shared/reactive/Reactive.ts +285 -197
  62. package/src/shared/reactive/index.ts +11 -8
  63. package/src/shared/reactive/pairReactive.ts +1 -1
  64. package/src/shared/reactive/scoped.ts +4 -3
  65. package/src/shared/reactive/share.ts +11 -1
  66. package/src/shared/routePattern.ts +0 -123
  67. package/src/shared/rpc.ts +17 -24
  68. package/src/shared/rpcRequest.ts +18 -47
  69. package/src/shared/takeEarlyCall.ts +12 -37
  70. package/src/shared/url.ts +1 -1
  71. package/src/ui/Level.ts +276 -0
  72. package/src/ui/List.ts +17 -17
  73. package/src/ui/Router.ts +20 -87
  74. package/src/ui/applyHead.ts +92 -79
  75. package/src/ui/index.ts +4 -2
  76. package/src/ui/reconcile.ts +4 -110
  77. package/src/ui/rows.ts +123 -2
  78. package/src/ui/wire.ts +12 -27
  79. package/src/compiler/hooksCalled.ts +0 -35
  80. package/src/server/clientBuildOptions.ts +0 -54
  81. package/src/server/refuseAddressCollision.ts +0 -37
  82. package/src/shared/reactive/chunksOfStream.ts +0 -33
  83. package/src/ui/MOUNTING_LEVEL.ts +0 -18
@@ -21,6 +21,19 @@ import { basename, dirname, resolve } from 'node:path' // bun exposes no path he
21
21
  import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
22
22
  import type { ServerTable } from './answerRequest.ts'
23
23
  import type { MountRow } from './answerRpc.ts'
24
+ import {
25
+ type Asked,
26
+ BOUND_NAMESPACE,
27
+ CLIENT_NAMESPACE,
28
+ componentNamespaceOf,
29
+ type Face,
30
+ faceOf,
31
+ type HandlerFace,
32
+ type Lane,
33
+ realFileOf,
34
+ STYLE_NAMESPACE,
35
+ TEXT_NAMESPACE,
36
+ } from './faceOf.ts'
24
37
  import { installSourceMaps, type SourceMaps } from './installSourceMaps.ts'
25
38
  import {
26
39
  CLIENT_QUERY,
@@ -68,24 +81,8 @@ export type Compilation = {
68
81
  mounted: Record<string, string>
69
82
  }
70
83
 
71
- // A VIRTUAL FACE'S PATH: its file's, with a suffix that is not a query. Both registries key a module
72
- // by its path whatever its namespace, and the runtime's drops a query from it — so a file's text, its
73
- // stylesheet or its bound face under `?…` took the place of the module the file compiles to.
74
- const BOUND = '::bound'
75
- const CLIENT = '::client'
76
- const TEXT = '::text'
77
- const STYLE = '::style'
78
-
79
- /** The namespace a bundle's component module is answered in, at its file's own path. */
80
- export const COMPONENT_NAMESPACE = 'abide-component'
81
- /** The namespaces a file's text and a component's stylesheet are loaded in. */
82
- export const TEXT_NAMESPACE = 'abide-text'
83
- export const STYLE_NAMESPACE = 'abide-style'
84
84
  const RELATIVE_OR_SUBPATH = /^(?:\.\.?\/|#)/
85
85
 
86
- /** One handler file, as `Compilation.handlers` holds it. */
87
- type HandlerFace = { scope: string; client: string }
88
-
89
86
  /** The plugin serving `compilation`'s files and tables, for one bundle. */
90
87
  export function abideModules(
91
88
  root: string,
@@ -235,79 +232,59 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
235
232
  name: 'abide-modules',
236
233
  setup(build) {
237
234
  // THE LANE IS THE BUILD'S TARGET: a browser bundle gets each file's browser module, and
238
- // the runtime — which has no config — and a server bundle get its server module. A
239
- // browser module the runtime was asked for (`CLIENT_QUERY`) is the one exception, and
240
- // what it imports is in its lane.
235
+ // the runtime — which has no config — and a server bundle get its server module.
241
236
  const bundling: boolean = 'config' in build
242
- const browser = bundling && build.config.target === 'browser'
243
- const inBrowser = (importer: string): boolean =>
244
- browser || pathOf(importer).endsWith(CLIENT)
245
- // A BUNDLE'S COMPONENT MODULE IS IN A NAMESPACE OF ITS OWN: the bundler refuses a
246
- // `file` path a plugin answers with `..` anywhere in it, which a `[...rest]` directory
247
- // writes (23.5). The runtime has no such check, and its stack frames name the path.
248
- const componentNamespace = bundling ? COMPONENT_NAMESPACE : 'file'
249
- // The module `path` compiles to, in the lane its importer is in.
250
- const componentFor = (path: string, importer: string) =>
251
- inBrowser(importer) && !browser
252
- ? { path: `${path}${CLIENT}`, namespace: 'abide-client' }
253
- : { path, namespace: componentNamespace }
237
+ const lane: Lane = {
238
+ bundling,
239
+ browser: bundling && build.config.target === 'browser',
240
+ }
254
241
  const resolved = (specifier: string, importer: string): string =>
255
242
  resolvedFile(specifier, dirname(pathOf(importer)))
243
+ // Each hook below only adapts bun's ask to `faceOf`'s: the file the specifier resolved to,
244
+ // its query taken off, and the answer handed back — a refusal thrown.
245
+ const answer = (
246
+ asked: Asked,
247
+ file: string,
248
+ importer: string,
249
+ ): Face | undefined => {
250
+ const face = faceOf(
251
+ asked,
252
+ file,
253
+ pathOf(importer),
254
+ lane,
255
+ served.handlers,
256
+ )
257
+ if (face instanceof Error) throw face
258
+ return face
259
+ }
256
260
  // A filter off the query's own constant, so renaming one cannot silently stop a match.
261
+ const asking = (asked: Asked, query: string, filter: RegExp) =>
262
+ build.onResolve({ filter }, (args) =>
263
+ resolving
264
+ ? undefined
265
+ : answer(
266
+ asked,
267
+ resolved(
268
+ args.path.slice(
269
+ 0,
270
+ args.path.length - query.length,
271
+ ),
272
+ args.importer,
273
+ ),
274
+ args.importer,
275
+ ),
276
+ )
257
277
  const abideWith = (query: string) =>
258
278
  new RegExp(`\\.abide${RegExp.escape(query)}$`)
259
-
260
- build.onResolve(
261
- { filter: new RegExp(`${RegExp.escape(TEXT_QUERY)}$`) },
262
- (args) => {
263
- if (resolving) return undefined
264
- const specifier = args.path.slice(0, -TEXT_QUERY.length)
265
- // A path of its own (`TEXT`): a file is often imported as code and as text at once
266
- // — an example's Files tab.
267
- return {
268
- path: `${resolved(specifier, args.importer)}${TEXT}`,
269
- namespace: TEXT_NAMESPACE,
270
- }
271
- },
279
+ asking(
280
+ 'text',
281
+ TEXT_QUERY,
282
+ new RegExp(`${RegExp.escape(TEXT_QUERY)}$`),
272
283
  )
273
- build.onResolve({ filter: abideWith(STYLE_QUERY) }, (args) => {
274
- if (resolving) return undefined
275
- const specifier = args.path.slice(0, -STYLE_QUERY.length)
276
- return {
277
- path: `${resolved(specifier, args.importer)}${STYLE}`,
278
- namespace: STYLE_NAMESPACE,
279
- }
280
- })
281
- build.onResolve({ filter: abideWith(COMPONENT_QUERY) }, (args) => {
282
- if (resolving) return undefined
283
- const specifier = args.path.slice(0, -COMPONENT_QUERY.length)
284
- return componentFor(
285
- resolved(specifier, args.importer),
286
- args.importer,
287
- )
288
- })
289
- build.onResolve({ filter: abideWith(CLIENT_QUERY) }, (args) => {
290
- if (resolving) return undefined
291
- const path = resolved(
292
- args.path.slice(0, -CLIENT_QUERY.length),
293
- args.importer,
294
- )
295
- return browser
296
- ? { path, namespace: componentNamespace }
297
- : { path: `${path}${CLIENT}`, namespace: 'abide-client' }
298
- })
299
- build.onResolve({ filter: /\.abide$/ }, (args) => {
300
- if (resolving) return undefined
301
- const path = resolved(args.path, args.importer)
302
- // A bound face's own import of its component, and every browser import, is the
303
- // component module.
304
- if (
305
- inBrowser(args.importer) ||
306
- pathOf(args.importer) === `${path}${BOUND}`
307
- )
308
- return componentFor(path, args.importer)
309
- return { path: `${path}${BOUND}`, namespace: 'abide-bound' }
310
- })
284
+ asking('style', STYLE_QUERY, abideWith(STYLE_QUERY))
285
+ asking('component', COMPONENT_QUERY, abideWith(COMPONENT_QUERY))
286
+ asking('client', CLIENT_QUERY, abideWith(CLIENT_QUERY))
287
+ asking('abide', '', /\.abide$/)
311
288
  build.onResolve(
312
289
  { filter: new RegExp(`/${VIRTUAL_DIRECTORY}/[^/]+\\.ts$`) },
313
290
  (args) => {
@@ -322,13 +299,15 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
322
299
  },
323
300
  )
324
301
 
325
- // A HANDLER FILE IS FOUND BY WHERE ITS IMPORT RESOLVES, however the import spells it, and
326
- // only from the app's own modules: a package's never names one, and asking bun again for
327
- // each of a package's relative imports is a resolve per module of the bundle. THE FILTER
328
- // IS THE HANDLER FILES' NAMES: a hook matching every relative import costs the bundle
329
- // abide's own `sideEffects`, and the barrel's rest — the validator, the csp — moved into
330
- // the chunk every page loads (38.94). ONLY IN A BUNDLE: the runtime finds a handler file
331
- // by the path it loads at (`handlerFilesPlugin`), which costs no resolve per import.
302
+ // A HANDLER FILE IS FOUND BY WHERE ITS IMPORT RESOLVES, however the import spells it — the
303
+ // app's own, or a mounted package's own page importing a handler file of its (23.19).
304
+ // THE FILTER IS THE HANDLER FILES' NAMES: a hook matching every relative import costs the
305
+ // bundle abide's own `sideEffects`, and the barrel's rest — the validator, the csp — moved
306
+ // into the chunk every page loads (38.94); and since only an import ending in one of those
307
+ // names is resolved again, a package's other imports cost nothing, so a package's are not
308
+ // passed over by where they lie. ONLY IN A BUNDLE: the runtime finds a handler file by the
309
+ // path it loads at (`handlerFilesPlugin`), and asks no `onResolve` of an extensionless
310
+ // specifier at all.
332
311
  // Each handler file's name without `.ts` — what an import of one ends in.
333
312
  const stems = new Set<string>()
334
313
  if (bundling)
@@ -344,23 +323,13 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
344
323
  (args) => {
345
324
  if (resolving || !RELATIVE_OR_SUBPATH.test(args.path))
346
325
  return undefined
347
- const importer = pathOf(args.importer)
348
- if (importer.includes('/node_modules/'))
349
- return undefined
350
- let path: string
326
+ let file: string
351
327
  try {
352
- path = resolved(args.path, args.importer)
328
+ file = resolved(args.path, args.importer)
353
329
  } catch {
354
330
  return undefined
355
331
  }
356
- const face = served.handlers.get(path)
357
- if (face === undefined) return undefined
358
- // 20.4 — a handler the browser is handed no wrapper for is no import of its.
359
- if (inBrowser(args.importer) && face.client === '')
360
- throw new Error(
361
- `abide: ${pathOf(args.importer)} imports ${args.path}, whose handler withholds itself from the browser (\`clients: { ui: false }\`). Call it from a <script server>, or let the browser have it. RULEBOOK 20.4.`,
362
- )
363
- return { path: `${path}${HANDLER_SUFFIX}` }
332
+ return answer('handler', file, args.importer)
364
333
  },
365
334
  )
366
335
 
@@ -385,11 +354,13 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
385
354
  }
386
355
 
387
356
  build.onLoad(
388
- { filter: /\.abide$/, namespace: componentNamespace },
357
+ { filter: /\.abide$/, namespace: componentNamespaceOf(lane) },
389
358
  (args) => {
390
359
  const compiled = compiledAt(served, args.path)
391
360
  return {
392
- contents: browser ? compiled.client : compiled.server,
361
+ contents: lane.browser
362
+ ? compiled.client
363
+ : compiled.server,
393
364
  loader: 'ts',
394
365
  }
395
366
  },
@@ -401,9 +372,9 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
401
372
  // resolves from its directory as the file's own imports do. A namespace of abide's own
402
373
  // had bun resolve a bare one from the working directory: a second abide, run from `/`.
403
374
  build.onLoad({ filter: HANDLER_FACE }, async (args) => {
404
- const path = args.path.slice(0, -HANDLER_SUFFIX.length)
375
+ const path = realFileOf(args.path)
405
376
  const face = served.handlers.get(path) as HandlerFace
406
- if (browser) return { contents: face.client, loader: 'js' }
377
+ if (lane.browser) return { contents: face.client, loader: 'js' }
407
378
  const text = await Bun.file(path).text()
408
379
  return {
409
380
  contents: `import { fileRoute as __fileRoute } from '${SERVER_SPECIFIER}';__fileRoute(${JSON.stringify(face.scope)});${text}\n;__fileRoute(undefined)\n`,
@@ -411,22 +382,22 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
411
382
  }
412
383
  })
413
384
  build.onLoad(
414
- { filter: /.*/, namespace: 'abide-client' },
385
+ { filter: /.*/, namespace: CLIENT_NAMESPACE },
415
386
  (args) => ({
416
- contents: compiledAt(
417
- served,
418
- args.path.slice(0, -CLIENT.length),
419
- ).client,
387
+ contents: compiledAt(served, realFileOf(args.path)).client,
420
388
  loader: 'ts',
421
389
  }),
422
390
  )
423
- build.onLoad({ filter: /.*/, namespace: 'abide-bound' }, (args) => {
424
- const path = args.path.slice(0, -BOUND.length)
425
- return {
426
- contents: boundModule(path),
427
- loader: 'js',
428
- }
429
- })
391
+ build.onLoad(
392
+ { filter: /.*/, namespace: BOUND_NAMESPACE },
393
+ (args) => {
394
+ const path = realFileOf(args.path)
395
+ return {
396
+ contents: boundModule(path),
397
+ loader: 'js',
398
+ }
399
+ },
400
+ )
430
401
  // A stylesheet is the bundle's; outside one there is no document to apply it to.
431
402
  build.onLoad(
432
403
  { filter: /.*/, namespace: STYLE_NAMESPACE },
@@ -434,10 +405,8 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
434
405
  bundling
435
406
  ? {
436
407
  contents:
437
- compiledAt(
438
- served,
439
- args.path.slice(0, -STYLE.length),
440
- ).sheet ?? '',
408
+ compiledAt(served, realFileOf(args.path))
409
+ .sheet ?? '',
441
410
  loader: 'css',
442
411
  }
443
412
  : { contents: '', loader: 'js' },
@@ -445,7 +414,7 @@ function modulesPlugin(served: Held): Bun.BunPlugin {
445
414
  build.onLoad(
446
415
  { filter: /.*/, namespace: TEXT_NAMESPACE },
447
416
  async (args) => ({
448
- contents: `export default ${JSON.stringify(await Bun.file(args.path.slice(0, -TEXT.length)).text())}\n`,
417
+ contents: `export default ${JSON.stringify(await Bun.file(realFileOf(args.path)).text())}\n`,
449
418
  loader: 'js',
450
419
  }),
451
420
  )
@@ -20,6 +20,7 @@ import { DEBUG_META } from '#shared/DEBUG_META.ts'
20
20
  import { decodedOrKept } from '#shared/decodedOrKept.ts'
21
21
  import { escapeAttribute } from '#shared/escapeText.ts'
22
22
  import { type Failed, isFailed } from '#shared/Failed.ts'
23
+ import { HandlerRoute } from '#shared/HandlerRoute.ts'
23
24
  import { health } from '#shared/health.ts'
24
25
  import { appName } from '#shared/log.ts'
25
26
  import { compose } from '#shared/Middleware.ts'
@@ -50,6 +51,7 @@ import { CLIENT_ENTRY } from './CLIENT_BUILD.ts'
50
51
  import { callerOrMint } from './callerOf.ts'
51
52
  import { compressAnswer } from './compressAnswer.ts'
52
53
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
54
+ import { noteCallInRender } from './deferredBuilds.ts'
53
55
  import { answerMcp, openApi } from './generatedSurfaces.ts'
54
56
  import { type AppRung, failureOf, rungsOver } from './hooks.ts'
55
57
  import { chunksOf, type Manifest } from './manifestOf.ts'
@@ -208,8 +210,8 @@ export async function answerRequest(
208
210
  }
209
211
 
210
212
  // 36.11 — A PUBLIC FILE IS ITS PATH BELOW THE DIRECTORY, so `/favicon.ico` is answered
211
- // before any route is tried. The order is not load-bearing: `abide build` and `abide dev`
212
- // run `refuseAddressCollision` (36.13) over the same two trees before anything is served,
213
+ // before any route is tried. The order is not load-bearing: the compile `abide build` and
214
+ // `abide dev` share refuses a collision (36.13) over the same two trees before anything is served,
213
215
  // so at run time there can never be a collision to resolve. 36.11 — keyed by its name on disk
214
216
  // and asked for percent-encoded: `my photo.jpg` is `/my%20photo.jpg`.
215
217
  const file = app.published.get(decodedOrKept(asked))
@@ -383,6 +385,10 @@ const SURFACES = new Map<
383
385
  [BRIDGE_ADDRESS, (_app, request) => answerBridge(request)],
384
386
  ])
385
387
 
388
+ // Each handler's route, compiled once rather than per render that calls it — as its `Rpc` keeps its
389
+ // own; compiled again only where it was mounted at another address since.
390
+ const ROUTE_OF = new WeakMap<object, HandlerRoute>()
391
+
386
392
  // What a call made in a page's render reports into: the seed buffer (41.13), and — where the handler
387
393
  // is mounted — the request the browser will make for it, for the early-call script (41.20). Once per
388
394
  // render, as the browser's `Rpc` makes it once, and the seed buffer is what says which is the once.
@@ -392,18 +398,26 @@ export function reachedInRender(
392
398
  reached: unknown,
393
399
  ): void {
394
400
  const page = currentRequestScope()?.page
395
- if (page === undefined || !page.seed(rpc, args, reached)) return
401
+ if (page === undefined) return
402
+ noteCallInRender()
403
+ if (!page.seed(rpc, args, reached)) return
396
404
  // 16.10 — the address the browser's own call is made at, stamped under the mount by
397
405
  // `mountHandlers` — and `undefined` on a handler nothing mounted.
398
406
  const { address, method } = rpc as Rpc<unknown, unknown>
399
407
  if (address === undefined) return
408
+ let route = ROUTE_OF.get(rpc)
409
+ if (route?.address !== address) {
410
+ route = new HandlerRoute(address)
411
+ ROUTE_OF.set(rpc, route)
412
+ }
400
413
  page.calls.push({
401
414
  request: rpcRequest(
402
415
  method,
403
- address,
416
+ route,
404
417
  args as Record<string, unknown> | undefined,
405
418
  ),
406
419
  reached,
420
+ painted: undefined,
407
421
  })
408
422
  }
409
423