@abide/abide 0.101.0 → 0.102.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 (157) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/abide.ts +223 -164
  3. package/docs/REGISTRY.md +726 -0
  4. package/docs/RULEBOOK.md +2753 -0
  5. package/docs/abide.md +12878 -0
  6. package/package.json +5 -3
  7. package/scaffold/AGENTS.md +31 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +262 -67
  13. package/src/compiler/argsSchemaOf.ts +1 -1
  14. package/src/compiler/checkApp.ts +43 -15
  15. package/src/compiler/checkedShadow.ts +39 -5
  16. package/src/compiler/classifyRead.ts +6 -4
  17. package/src/compiler/classifyTemplate.ts +1 -1
  18. package/src/compiler/compileApp.ts +433 -193
  19. package/src/compiler/compiledEntry.ts +43 -39
  20. package/src/compiler/componentGraph.ts +58 -71
  21. package/src/compiler/declaredExports.ts +24 -7
  22. package/src/compiler/emitClient.ts +141 -71
  23. package/src/compiler/emitComponent.ts +37 -8
  24. package/src/compiler/emitSource.ts +106 -13
  25. package/src/compiler/handlersUnder.ts +106 -59
  26. package/src/compiler/index.ts +7 -0
  27. package/src/compiler/mountsOf.ts +138 -0
  28. package/src/compiler/nearest.ts +14 -0
  29. package/src/compiler/parseAbide.ts +157 -20
  30. package/src/compiler/projectShadow.ts +213 -7
  31. package/src/compiler/reactiveTypes.ts +20 -7
  32. package/src/compiler/readSources.ts +15 -2
  33. package/src/compiler/readsIn.ts +61 -7
  34. package/src/compiler/renderMarkup.ts +3 -3
  35. package/src/compiler/scaffold.ts +30 -11
  36. package/src/compiler/serveLanguage.ts +125 -34
  37. package/src/compiler/sourceMapOf.ts +193 -0
  38. package/src/compiler/splitImports.ts +205 -83
  39. package/src/compiler/syncTypes.ts +32 -0
  40. package/src/compiler/tokenize.ts +14 -0
  41. package/src/compiler/writeDefaultProject.ts +1 -1
  42. package/src/compiler/writeTypes.ts +82 -0
  43. package/src/server/CLIENT_BUILD.ts +11 -13
  44. package/src/server/DEFAULT_PORT.ts +1 -1
  45. package/src/server/LAUNCHER_FILES.ts +9 -0
  46. package/src/server/MODULE_QUERIES.ts +18 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +391 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +235 -106
  52. package/src/server/answerRpc.ts +102 -30
  53. package/src/server/answerSocket.ts +65 -20
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +179 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +37 -37
  65. package/src/server/componentModules.d.ts +3 -1
  66. package/src/server/compressAnswer.ts +4 -4
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +59 -73
  70. package/src/server/devWorker.ts +114 -181
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +37 -35
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +124 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +11 -7
  77. package/src/server/installSourceMaps.ts +88 -0
  78. package/src/server/launcherOptions.ts +34 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +5 -5
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/principal.ts +10 -5
  85. package/src/server/realOrAsGiven.ts +10 -0
  86. package/src/server/refuseAddressCollision.ts +21 -14
  87. package/src/server/remote.ts +49 -52
  88. package/src/server/render.ts +65 -94
  89. package/src/server/renderComponent.ts +5 -0
  90. package/src/server/reportHealth.ts +2 -2
  91. package/src/server/requestScope.ts +12 -1
  92. package/src/server/runBuild.ts +24 -0
  93. package/src/server/runCompiled.ts +5 -5
  94. package/src/server/runDesktop.ts +52 -27
  95. package/src/server/serveBuild.ts +43 -52
  96. package/src/server/serveDev.ts +73 -37
  97. package/src/server/shell.ts +7 -22
  98. package/src/server/sourceMappings.ts +139 -0
  99. package/src/server/surfaceOf.ts +18 -3
  100. package/src/server/testBuild.ts +1 -19
  101. package/src/server/withAbideSources.ts +117 -0
  102. package/src/shared/ADDRESSES.ts +7 -18
  103. package/src/shared/APP_META.ts +7 -0
  104. package/src/shared/CopyOptions.ts +1 -1
  105. package/src/shared/DEBUG_META.ts +1 -1
  106. package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
  107. package/src/shared/cookies.ts +2 -2
  108. package/src/shared/csp.ts +1 -1
  109. package/src/shared/escapeText.ts +1 -1
  110. package/src/shared/filledPath.ts +85 -0
  111. package/src/shared/framing.ts +79 -6
  112. package/src/shared/headKeyOf.ts +2 -0
  113. package/src/shared/index.ts +1 -1
  114. package/src/shared/log.ts +27 -13
  115. package/src/shared/matchRoute.ts +23 -4
  116. package/src/shared/menuItem.ts +3 -3
  117. package/src/shared/reactive/Keyed.ts +2 -2
  118. package/src/shared/reactive/NotLanded.ts +1 -1
  119. package/src/shared/reactive/Reactive.ts +43 -43
  120. package/src/shared/reactive/argsContract.ts +4 -4
  121. package/src/shared/reactive/canonicalKey.ts +4 -4
  122. package/src/shared/reactive/chunksOfStream.ts +6 -0
  123. package/src/shared/reactive/index.ts +1 -1
  124. package/src/shared/reactive/recordWrites.ts +103 -5
  125. package/src/shared/reactive/structural.ts +1 -1
  126. package/src/shared/reactive/tracking.ts +1 -1
  127. package/src/shared/reactive/watch.ts +3 -3
  128. package/src/shared/refuse.ts +1 -1
  129. package/src/shared/routePattern.ts +82 -46
  130. package/src/shared/rpc.ts +37 -8
  131. package/src/shared/rpcRequest.ts +27 -3
  132. package/src/shared/url.ts +4 -44
  133. package/src/shared/writePath.ts +1 -1
  134. package/src/ui/LAYER_NAMES.ts +1 -1
  135. package/src/ui/List.ts +3 -3
  136. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  137. package/src/ui/Mismatch.ts +1 -1
  138. package/src/ui/RawRun.ts +1 -1
  139. package/src/ui/Router.ts +38 -31
  140. package/src/ui/Scope.ts +1 -1
  141. package/src/ui/TryRegion.ts +2 -2
  142. package/src/ui/applyHead.ts +33 -0
  143. package/src/ui/index.ts +1 -1
  144. package/src/ui/menuBridge.ts +1 -1
  145. package/src/ui/moveNode.ts +1 -1
  146. package/src/ui/reconcile.ts +2 -2
  147. package/src/ui/reportMenu.ts +2 -2
  148. package/src/ui/routing.ts +2 -2
  149. package/src/ui/setAttribute.ts +1 -1
  150. package/src/ui/setText.ts +1 -1
  151. package/src/ui/socketWire.ts +9 -7
  152. package/src/ui/wire.ts +1 -1
  153. package/scaffold/src/server/app.ts +0 -6
  154. package/scaffold/src/shared/.gitkeep +0 -0
  155. package/scaffold/src/ui/app.html +0 -8
  156. package/src/server/componentImports.ts +0 -56
  157. /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
@@ -26,7 +26,7 @@ import { decodeCharacterReferences } from './decodeCharacterReferences.ts'
26
26
  import { moduleImports } from './emitComponent.ts'
27
27
  import { invocationProps, passedName } from './invocationProps.ts'
28
28
  import { isNamedAlone } from './isNamedAlone.ts'
29
- import { Lowering, PROPS } from './Lowering.ts'
29
+ import { type LoweredChunk, Lowering, PROPS } from './Lowering.ts'
30
30
  import type { AbideFile } from './parseAbide.ts'
31
31
  import {
32
32
  CLIENT_LANE,
@@ -143,11 +143,11 @@ export function emitClient(
143
143
  file: AbideFile,
144
144
  answer: CheckerAnswer = UNCHECKED,
145
145
  selfFill: SelfFill = 'none',
146
- /** Where each specifier this file imports landed in the build (`splitImports`). */
147
- landed: ReadonlyMap<string, string> = new Map(),
146
+ /** 38.97 — where given, every chunk the module is written around (`sourceMapOf`). */
147
+ chunks?: LoweredChunk[],
148
148
  ): string {
149
149
  const carriesOwnMarkup = selfFill !== 'none'
150
- const lowering = new Lowering(file, answer)
150
+ const lowering = new Lowering(file, answer, false, chunks)
151
151
  // THE TEMPLATE IS PLACED ONCE (`placeTemplate`) and everything this module emits is rendered
152
152
  // from what was placed: the mount's walk, a region's build, and the markup a region made once
153
153
  // fills from — the same positions the server's markup is rendered from.
@@ -163,11 +163,25 @@ export function emitClient(
163
163
  page,
164
164
  new RegionEmission(lowering),
165
165
  )
166
+ // 35.17 — THE DOCUMENT'S OWN `<html>` AND `<body>`, bound as any element is: the server wrote
167
+ // the values standing at render, and these keep writing them. Nothing to find, so no walk.
168
+ if (file.document !== undefined)
169
+ for (const [element, node, slot] of [
170
+ ['document.documentElement', file.document.html, 'html'],
171
+ ['document.body', file.document.body, 'body'],
172
+ ] as const) {
173
+ const classified = classifyAttributes(node, lowering)
174
+ if (!classified.some(isBound)) continue
175
+ const units = elementUnits(element, classified, slot, lowering)
176
+ for (const unit of units.handlers) lines.push(unit)
177
+ for (const unit of units.attributes) lines.push(unit)
178
+ for (const unit of units.elements) lines.push(unit)
179
+ }
166
180
 
167
181
  // 34.2 — A `<script module>` IS PROCESS-WIDE IN A BROWSER, which is MODULE SCOPE: it is
168
182
  // evaluated once when this module is first imported rather than once per `mount`, so a second
169
183
  // mount of the same page reads what the first one left.
170
- const moduleSetup = splitImports(lowering.modules(), '.client', landed)
184
+ const moduleSetup = splitImports(lowering.modules())
171
185
  // 32.17 — A COMPONENT FILLS ITS OWN REGION where the server wrote none. A row the list grew by
172
186
  // is built in the browser, and the child's markup is in the child's SERVER module — which the
173
187
  // browser may not reach. So the caller writes the anchor pair alone and the child fills it
@@ -210,14 +224,25 @@ export function emitClient(
210
224
  lines.push(
211
225
  `if (${ROUTED}) ${lowering.use('liveHead')}(${SCOPE}, ${ROUTED}.head)`,
212
226
  )
227
+ // 39.1, 39.6 — A COMPONENT'S HEAD IS ITS PAGE'S, on a navigation as on the server: added to the
228
+ // level the router is mounting (`contributeHead`), which a row grown later is not.
229
+ const ownHead = routed ? undefined : headSink(file)
230
+ const ownMarkup =
231
+ ownHead === undefined
232
+ ? "''"
233
+ : renderMarkup(ownHead, lowering, CLIENT_LANE)
234
+ const contributed =
235
+ ownMarkup === "''"
236
+ ? ''
237
+ : `${lowering.use('contributeHead')}(${REGION_ROOT}, () => ${ownMarkup})\n`
213
238
  const builds = carriesOwnMarkup && page.positions.length > 0
214
- let fills = ''
239
+ let fills = contributed
215
240
  if (applied !== '' || builds) {
216
241
  const build = builds
217
242
  ? `function __build(__document) {\n ${emitMaker(page, lowering)}\n}\n`
218
243
  : ''
219
244
  // THE HEAD GOES BEFORE THE FILL, so the guard is written out where there is one to apply.
220
- fills =
245
+ fills +=
221
246
  applied === ''
222
247
  ? `${registered}${build}${lowering.use('fillEmpty')}(${REGION_ROOT}, __build)\n`
223
248
  : `${registered}${build}if (${lowering.use('isEmptyRegion')}(${REGION_ROOT})) { ${applied}${builds ? `${lowering.use('fillRegion')}(${REGION_ROOT}, __build)` : ''} }\n`
@@ -236,12 +261,19 @@ export function emitClient(
236
261
  // A FILE MOUNTED AGAINST AN EMPTY REGION SHIPS A MODULE REGARDLESS: the router or the caller's
237
262
  // client module imports it by name, so the no-op is what it ships.
238
263
  //
239
- // 41.6 — BUT A `<script>` IS SOMETHING TO DO: hydration re-runs every setup, and a static
264
+ // 41.6 — BUT A `<script>` IS SOMETHING TO DO: hydration reruns every setup, and a static
240
265
  // component's can still open a watch, write a shared value or listen on the window.
241
266
  const live = lines.length > 0
242
- const setup = splitImports(lowering.scripts(), '.client', landed)
243
- if (!live && fills === '' && setup.body === '')
244
- return carriesOwnMarkup ? 'export default () => {}\n' : ''
267
+ const setup = splitImports(lowering.scripts())
268
+ if (!live && fills === '' && setup.body === '') {
269
+ const mount = carriesOwnMarkup ? 'export default () => {}\n' : ''
270
+ // 34.17 — WHAT A `<script module>` EXPORTS IS THE BROWSER'S TOO, live markup or none: a
271
+ // component nothing invokes is still a module something imports a name from.
272
+ if (moduleSetup.body === '' && moduleSetup.imports === '') return mount
273
+ return `${moduleImports(lowering, (name) => (SHARED_NAMES.has(name) ? SHARED_SPECIFIER : RUNTIME_SPECIFIER), [moduleSetup])}
274
+ ${moduleSetup.body}
275
+ ${mount}`
276
+ }
245
277
 
246
278
  // A SCOPE IS WHAT THERE IS TO STOP, so a module with nothing live opens none. A static child
247
279
  // still ships a module — it fills the anchors of a row the browser grew — and making it
@@ -606,64 +638,15 @@ ${SCOPE}.watch(() => {
606
638
  )
607
639
  continue
608
640
  }
609
- const element = `${lowering.use('elementHere')}(${at.name}, ${JSON.stringify(at.tag)})`
610
- // 32.3 — THE ATTRIBUTE BINDING. The server wrote the value standing at render; this is
611
- // what keeps writing it. ONCE PER ELEMENT, NOT PER ATTRIBUTE: `<a href={u} title={t}>` is
612
- // one element and two bindings, and a refusal loses exactly what it should either way,
613
- // because every binding in the group needs the element the check refused.
614
- const writes: string[] = []
615
- const binds: string[] = []
616
- const spreads: string[] = []
617
- for (const one of bound.get(at) as ClassifiedAttribute[]) {
618
- if (!isBound(one)) continue
619
- if (one.kind === 'handler')
620
- handlers.push(
621
- `${element}.addEventListener(${JSON.stringify(one.event)}, ${lowering.expression(one.attribute)})`,
622
- )
623
- // 32.5, 32.7, 32.8 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
624
- // region's scope writes the property, and a listener writes it back.
625
- else if (one.kind === 'bound')
626
- binds.push(
627
- `${lowering.use('bindProperty')}(${element}, ${JSON.stringify(one.bind)}, ${lowering.reference(one.attribute)}, ${SCOPE})`,
628
- )
629
- // 32.11 — THE SPREAD, KEPT WRITTEN: an attribute it stops carrying is removed.
630
- else if (one.kind === 'spread')
631
- spreads.push(
632
- `${lowering.use('bindSpread')}(${element}, () => (${lowering.expression(one.attribute)}), ${JSON.stringify(one.explicit)}, ${SCOPE})`,
633
- )
634
- // 32.9 — THE NODE, HANDED OVER, once. Not a watch: what the value takes is the element,
635
- // which is the same node on every run a watch would have made — and the callback
636
- // form's `Disposer` goes on this region's scope, so a widget it set up is torn down
637
- // when the nodes it was set up over leave.
638
- else if (one.kind === 'element')
639
- elements.push(
640
- `const __r${slot} = ${lowering.use('bindElement')}(${element}, ${lowering.reference(one.attribute)})
641
- if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
642
- )
643
- // 32.20 IS THE INTERPOLATION'S CLAUSE ON THIS SIDE TOO, and 32.37's absence with it:
644
- // `setAttribute` answers through `attributeText`, as the server's markup did, so a
645
- // `javascript:` the server refused is refused the moment the value moves too.
646
- // A WATCH PER ATTRIBUTE, not one over the group: two attributes reading two
647
- // different values would otherwise each wake for the other's write.
648
- else {
649
- const { write, name } =
650
- one.kind === 'merged' && one.toggles !== undefined
651
- ? one.toggles
652
- : {
653
- write: 'attribute' as const,
654
- name: one.attribute.name,
655
- }
656
- writes.push(
657
- `${SCOPE}.watch(() => ${lowering.use(WRITES[write])}(__a${slot}, ${JSON.stringify(name)}, ${lowering.expression(one.attribute)}))`,
658
- )
659
- }
660
- }
661
- if (writes.length > 0)
662
- attributes.push(
663
- `const __a${slot} = ${element}\n${writes.join('\n')}`,
664
- )
665
- for (const unit of binds) attributes.push(unit)
666
- for (const unit of spreads) attributes.push(unit)
641
+ const units = elementUnits(
642
+ `${lowering.use('elementHere')}(${at.name}, ${JSON.stringify(at.tag)})`,
643
+ bound.get(at) as ClassifiedAttribute[],
644
+ slot,
645
+ lowering,
646
+ )
647
+ for (const unit of units.handlers) handlers.push(unit)
648
+ for (const unit of units.attributes) attributes.push(unit)
649
+ for (const unit of units.elements) elements.push(unit)
667
650
  }
668
651
  for (const unit of texts) lines.push(unit)
669
652
  for (const unit of handlers) lines.push(unit)
@@ -688,6 +671,81 @@ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
688
671
  return region
689
672
  }
690
673
 
674
+ /**
675
+ * One bound element's units — its listeners, its attribute bindings and the node it hands over —
676
+ * against the element `element` evaluates to.
677
+ *
678
+ * 32.3 — THE ATTRIBUTE BINDING. The server wrote the value standing at render; this is what keeps
679
+ * writing it. ONCE PER ELEMENT, NOT PER ATTRIBUTE: `<a href={u} title={t}>` is one element and two
680
+ * bindings, and a refusal loses exactly what it should either way, because every binding in the
681
+ * group needs the element the check refused. The document's own `<html>` and `<body>` are bound
682
+ * through here too (35.17), an element being whatever `element` reaches.
683
+ */
684
+ function elementUnits(
685
+ element: string,
686
+ classified: ClassifiedAttribute[],
687
+ slot: string,
688
+ lowering: Lowering,
689
+ ): { handlers: string[]; attributes: string[]; elements: string[] } {
690
+ const handlers: string[] = []
691
+ const attributes: string[] = []
692
+ const elements: string[] = []
693
+ const writes: string[] = []
694
+ const binds: string[] = []
695
+ const spreads: string[] = []
696
+ for (const one of classified) {
697
+ if (!isBound(one)) continue
698
+ if (one.kind === 'handler')
699
+ handlers.push(
700
+ `${element}.addEventListener(${JSON.stringify(one.event)}, ${lowering.expression(one.attribute)})`,
701
+ )
702
+ // 32.5, 32.7, 32.8 — THE PROPERTY AND THE VALUE, IN STEP BOTH WAYS: a watch on this
703
+ // region's scope writes the property, and a listener writes it back.
704
+ else if (one.kind === 'bound')
705
+ binds.push(
706
+ `${lowering.use('bindProperty')}(${element}, ${JSON.stringify(one.bind)}, ${lowering.reference(one.attribute)}, ${SCOPE})`,
707
+ )
708
+ // 32.11 — THE SPREAD, KEPT WRITTEN: an attribute it stops carrying is removed.
709
+ else if (one.kind === 'spread')
710
+ spreads.push(
711
+ `${lowering.use('bindSpread')}(${element}, () => (${lowering.expression(one.attribute)}), ${JSON.stringify(one.explicit)}, ${SCOPE})`,
712
+ )
713
+ // 32.9 — THE NODE, HANDED OVER, once. Not a watch: what the value takes is the element,
714
+ // which is the same node on every run a watch would have made — and the callback
715
+ // form's `Disposer` goes on this region's scope, so a widget it set up is torn down
716
+ // when the nodes it was set up over leave.
717
+ else if (one.kind === 'element')
718
+ elements.push(
719
+ `const __r${slot} = ${lowering.use('bindElement')}(${element}, ${lowering.reference(one.attribute)})
720
+ if (__r${slot} !== undefined) ${SCOPE}.stops.push(__r${slot})`,
721
+ )
722
+ // 32.20 IS THE INTERPOLATION'S CLAUSE ON THIS SIDE TOO, and 32.37's absence with it:
723
+ // `setAttribute` answers through `attributeText`, as the server's markup did, so a
724
+ // `javascript:` the server refused is refused the moment the value moves too.
725
+ // A WATCH PER ATTRIBUTE, not one over the group: two attributes reading two
726
+ // different values would otherwise each wake for the other's write.
727
+ else {
728
+ const { write, name } =
729
+ one.kind === 'merged' && one.toggles !== undefined
730
+ ? one.toggles
731
+ : {
732
+ write: 'attribute' as const,
733
+ name: one.attribute.name,
734
+ }
735
+ writes.push(
736
+ `${SCOPE}.watch(() => ${lowering.use(WRITES[write])}(__a${slot}, ${JSON.stringify(name)}, ${lowering.expression(one.attribute)}))`,
737
+ )
738
+ }
739
+ }
740
+ if (writes.length > 0)
741
+ attributes.push(
742
+ `const __a${slot} = ${element}\n${writes.join('\n')}`,
743
+ )
744
+ for (const unit of binds) attributes.push(unit)
745
+ for (const unit of spreads) attributes.push(unit)
746
+ return { handlers, attributes, elements }
747
+ }
748
+
691
749
  // A region's bindings go through its SCOPE, so what replaced the region can stop them (see
692
750
  // `Scope`). In the framework's own `__` namespace because a row binds the AUTHOR's item name as a
693
751
  // parameter in the same scope.
@@ -934,6 +992,12 @@ function emitFillable(
934
992
  name: string,
935
993
  sink: Cursor,
936
994
  region: RegionEmission,
995
+ /**
996
+ * 10.5 — CHILDREN ARE MOUNTED BY THE CHILD, so they are its descendants: the scope current when
997
+ * it mounts them is the one their components find, as it is the one the server built them in.
998
+ * The caller's own is the fallback for a mount with none current.
999
+ */
1000
+ children = false,
937
1001
  ): void {
938
1002
  const lowering = region.lowering
939
1003
  const filled = emitRegion(sink, region.nested())
@@ -948,6 +1012,10 @@ function emitFillable(
948
1012
  `${lowering.use('fillRegion')}(${REGION_ROOT}, ${name}_build)`,
949
1013
  lowering,
950
1014
  sink.defersSetup,
1015
+ false,
1016
+ children
1017
+ ? `(${lowering.use('currentShare')}() ?? ${SHARE_AT})`
1018
+ : SHARE_AT,
951
1019
  ),
952
1020
  )
953
1021
  }
@@ -972,6 +1040,8 @@ function mountFunction(
972
1040
  * mount binds into it. A refusal stops only what the mount opened, past `__mark`.
973
1041
  */
974
1042
  declared = false,
1043
+ /** The share scope a `resumes` mount puts back. */
1044
+ resumed = SHARE_AT,
975
1045
  ): string {
976
1046
  // A BODY THAT BINDS NOTHING HAS NOTHING TO STOP AND NOTHING TO REFUSE, so it opens no scope
977
1047
  // and no `try`: every mount of it answers the one shared empty scope. A static row's two
@@ -995,7 +1065,7 @@ function mountFunction(
995
1065
  ].join('\n')
996
1066
  // 10.5 — the instance's own scope, put back for a mount that happens later than its caller's.
997
1067
  // Only where a component stands below this region: nothing else can read it.
998
- return `const ${name}_mount = (${parameters}, __rebuilt) => {\n${resumes ? insideShare(lowering, SHARE_AT, mount) : mount}\n}`
1068
+ return `const ${name}_mount = (${parameters}, __rebuilt) => {\n${resumes ? insideShare(lowering, resumed, mount) : mount}\n}`
999
1069
  }
1000
1070
 
1001
1071
  /**
@@ -1023,7 +1093,7 @@ function emitInvocation(
1023
1093
  const children = at.inner[0]
1024
1094
  if (children !== undefined) {
1025
1095
  const kids = region.claim('kd')
1026
- emitFillable(kids, children, region)
1096
+ emitFillable(kids, children, region, true)
1027
1097
  props.push(
1028
1098
  `${JSON.stringify(CHILDREN)}: () => ({ build: ${kids}_build, mount: ${kids}_mount })`,
1029
1099
  )
@@ -1,6 +1,6 @@
1
1
  import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
2
2
  import { Cursor, placeTemplate } from './Cursor.ts'
3
- import { Lowering, PROPS } from './Lowering.ts'
3
+ import { type LoweredChunk, Lowering, PROPS } from './Lowering.ts'
4
4
  import type { AbideFile } from './parseAbide.ts'
5
5
  import {
6
6
  concat,
@@ -14,6 +14,7 @@ import {
14
14
  import { SERVER_SPECIFIER } from './SPECIFIERS.ts'
15
15
  import { insideShare, SETTLED_AT, shareScope } from './shareScope.ts'
16
16
  import { splitImports, withoutProps } from './splitImports.ts'
17
+ import type { TemplateNode } from './TemplateNode.ts'
17
18
 
18
19
  // The name the body is bound to while a component's head contributions land (`HEADS`).
19
20
  const BODY = '__body'
@@ -42,12 +43,14 @@ export function emitComponent(
42
43
  file: AbideFile,
43
44
  answer: CheckerAnswer = UNCHECKED,
44
45
  settles = false,
45
- /** Where each specifier this file imports landed in the build (`splitImports`). */
46
- landed: ReadonlyMap<string, string> = new Map(),
46
+ /** The route a `<script server>` files its hooks at (37.1), where the file is under one. */
47
+ route?: string,
48
+ /** 38.97 — where given, every chunk the module is written around (`sourceMapOf`). */
49
+ chunks?: LoweredChunk[],
47
50
  ): string {
48
51
  // Only what the page actually uses. An emitted module carrying an import nothing calls is a
49
52
  // name the app's own tooling reports as unused, in a file nobody wrote.
50
- const lowering = new Lowering(file, answer)
53
+ const lowering = new Lowering(file, answer, false, chunks)
51
54
 
52
55
  const page = new Cursor()
53
56
  page.components = file.components
@@ -62,6 +65,12 @@ export function emitComponent(
62
65
  // `Lowering.used` (`lowering.use`), so emitting a contribution the merge then drops leaves the
63
66
  // module importing a name nothing calls. `headSink` is the client lane's too, which is what keeps the two from drifting.
64
67
  const head = renderMarkup(headSink(file), lowering, SERVER_LANE)
68
+ // 35.15, 35.17 — THE DOCUMENT'S OWN TWO ELEMENTS, rendered as any element is and empty, so
69
+ // `render` writes their attributes onto the document's and every live one is read per request.
70
+ const document =
71
+ file.document === undefined
72
+ ? 'undefined'
73
+ : `{ html: ${renderMarkup(placed([file.document.html]), lowering, SERVER_LANE)}, body: ${renderMarkup(placed([file.document.body]), lowering, SERVER_LANE)} }`
65
74
 
66
75
  // 31.1 and 31.4 hold INSIDE a `.abide` file, not just in its template, so the script block is
67
76
  // lowered with the same rewrite the slots get.
@@ -69,8 +78,19 @@ export function emitComponent(
69
78
  // 34.2 — A `<script module>` IS MODULE SCOPE, here as in the browser, so what it exports is the
70
79
  // module's (34.17). What must stay per request is a `reactive`, which is by 11.32 wherever it
71
80
  // is declared.
72
- const moduleSetup = splitImports(lowering.modules(), '', landed)
73
- const setup = splitImports(lowering.scripts(), '', landed)
81
+ const moduleSetup = splitImports(lowering.modules())
82
+ // 34.20 — A `<script server>` IS MODULE SCOPE ON THIS SIDE ALONE, run between two calls that
83
+ // file every hook it makes at the file's route (37.1). Its body cannot await at its top level
84
+ // (34.3), so nothing else runs inside the window.
85
+ const serverSetup = splitImports(lowering.servers(), 'source')
86
+ const serverBody =
87
+ serverSetup.body === ''
88
+ ? ''
89
+ : `${lowering.use('fileRoute')}(${JSON.stringify(route ?? '/')})
90
+ ${serverSetup.body}
91
+ ${lowering.use('fileRoute')}(undefined)
92
+ `
93
+ const setup = splitImports(lowering.scripts())
74
94
  const componentBody = setup.body
75
95
 
76
96
  // A child invoked inside `<head>` pushes to the same array from the head expression.
@@ -95,6 +115,7 @@ export function emitComponent(
95
115
  return {
96
116
  head: ${head}${heads ? ` + ${HEADS}.join('')` : ''},
97
117
  body: ${BODY},
118
+ document: ${document},
98
119
  }`
99
120
  // 41.19 — BUILT ON THE FIRST READ, and ONCE: `render` reads `head` and then `body`, and a
100
121
  // child the markup invokes must not be set up twice. The build resumes the share scope the
@@ -112,10 +133,11 @@ export function emitComponent(
112
133
  scope: ${SETTLED_AT},
113
134
  get head() { return ${BUILD}().head },
114
135
  get body() { return ${BUILD}().body },
136
+ get document() { return ${BUILD}().document },
115
137
  }`
116
138
  : build
117
- return `${moduleImports(lowering, () => SERVER_SPECIFIER, [moduleSetup, setup])}
118
- ${moduleSetup.body === '' ? '' : `${moduleSetup.body}\n`}export default function Component(${lowering.takesProps() ? `${PROPS} = {}` : ''}) {
139
+ return `${moduleImports(lowering, () => SERVER_SPECIFIER, [moduleSetup, serverSetup, setup])}
140
+ ${moduleSetup.body === '' ? '' : `${moduleSetup.body}\n`}${serverBody}export default function Component(${lowering.takesProps() ? `${PROPS} = {}` : ''}) {
119
141
  ${opened}${componentBody}
120
142
  ${level}${closed}
121
143
  }
@@ -160,3 +182,10 @@ function importClause(names: string[]): string {
160
182
  for (const name of names) written.push(`${name} as __${name}`)
161
183
  return written.join(', ')
162
184
  }
185
+
186
+ // `nodes` placed on a sink of their own, as `headSink` places a head.
187
+ function placed(nodes: TemplateNode[]): Cursor {
188
+ const sink = new Cursor()
189
+ placeTemplate(nodes, sink)
190
+ return sink
191
+ }
@@ -1,14 +1,18 @@
1
- import { fileKind } from '#shared/routePattern.ts'
1
+ import { STYLE_QUERY } from '#server/MODULE_QUERIES.ts'
2
+ import { fileKind, placeOf, routeOf } from '#shared/routePattern.ts'
2
3
  import type { CheckerAnswer } from './CheckerAnswer.ts'
3
4
  import { emitClient } from './emitClient.ts'
4
5
  import { emitComponent } from './emitComponent.ts'
6
+ import type { LoweredChunk } from './Lowering.ts'
5
7
  import type { AbideFile } from './parseAbide.ts'
8
+ import { projectShadow } from './projectShadow.ts'
6
9
  import {
7
10
  markScoped,
8
11
  namesViewTransition,
9
12
  scopeCss,
10
13
  scopeToken,
11
14
  } from './scopeStyles.ts'
15
+ import { sourceMapOf } from './sourceMapOf.ts'
12
16
 
13
17
  /**
14
18
  * One `.abide`'s modules, in the mode the build compiles it in: its server module, its client
@@ -22,18 +26,30 @@ export function emitSource(
22
26
  file: AbideFile,
23
27
  answer: CheckerAnswer,
24
28
  invoked: boolean,
25
- landed?: ReadonlyMap<string, string>,
29
+ /**
30
+ * 34.23 — what the browser's module exports in place of the server block: the wrapper of each
31
+ * handler it declares, under the handler's name (`standInsOf`).
32
+ */
33
+ standIns = '',
34
+ /** 38.97 — the `.abide`'s text, where the build wants each module's source map. */
35
+ source?: string,
36
+ /** 23.19 — the path it answers by, where a mount gives it one other than `relative`. */
37
+ position = relative,
26
38
  ): {
27
39
  server: string
28
40
  client: string
41
+ /** 38.97 — each module's `mappings` into the `.abide`, where `source` was given. */
42
+ serverMappings: string | undefined
43
+ clientMappings: string | undefined
29
44
  sheet: string | undefined
30
45
  /** 39.8 — whether a `<style>` names a view transition, read here rather than at run time. */
31
46
  transitions: boolean
32
47
  } {
33
- const kind = fileKind(relative)
48
+ const kind = fileKind(position)
49
+ const routed = placeOf(position) === 'address'
34
50
  // 34.4 — A COMPONENT WITH STYLES: its elements carry its token, and the styles, scoped,
35
51
  // are a stylesheet its client module imports — which the bundle collects into the
36
- // stylesheet the document links (38.24).
52
+ // stylesheet the document links (38.23).
37
53
  // 34.11 — each body's own `<style>`, scoped to the token its elements carry; the
38
54
  // component's token is added only where a component-level `<style>` requires it.
39
55
  const token = scopeToken(relative)
@@ -66,26 +82,103 @@ export function emitSource(
66
82
  // nothing imports it, the router is what invokes it, and a reader who navigated to a
67
83
  // page under it may arrive with the layout already standing or with nothing at all.
68
84
  // An `error.abide` is never navigated to — only the server holds the error table.
69
- const routed = kind === 'page' || kind === 'layout'
85
+ const selfFilled = kind === 'page' || kind === 'layout'
70
86
  // The client module is emitted only where there is something to hydrate, or where something
71
87
  // fills or imports it by name (`emitClient` answers which). A static component nothing invokes
72
88
  // has no `.client.ts`, so a file shipping no runtime is a fact about the build rather than a
73
89
  // claim about the bundler.
90
+ const serverChunks: LoweredChunk[] | undefined =
91
+ source === undefined ? undefined : []
92
+ const clientChunks: LoweredChunk[] | undefined =
93
+ source === undefined ? undefined : []
74
94
  const client = emitClient(
75
95
  file,
76
96
  answer,
77
- invoked ? (routed ? 'both' : 'invoked') : routed ? 'routed' : 'none',
78
- landed,
97
+ invoked
98
+ ? selfFilled
99
+ ? 'both'
100
+ : 'invoked'
101
+ : selfFilled
102
+ ? 'routed'
103
+ : 'none',
104
+ clientChunks,
105
+ )
106
+ // 41.19 — a page and a layout are built a level at a time by `render`.
107
+ const server = emitComponent(
108
+ file,
109
+ answer,
110
+ kind !== 'component',
111
+ routed ? routeOf(position) : undefined,
112
+ serverChunks,
79
113
  )
80
114
  const name = relative.slice(relative.lastIndexOf('/') + 1)
115
+ const browser = withStandIns(
116
+ client !== '' && sheet !== undefined
117
+ ? `import './${name}${STYLE_QUERY}'\n${client}`
118
+ : client,
119
+ standIns,
120
+ )
121
+ let mapped: Mapped | undefined
122
+ if (source !== undefined) {
123
+ mapped = MAPPED.get(relative)
124
+ if (
125
+ mapped === undefined ||
126
+ mapped.source !== source ||
127
+ mapped.server !== server ||
128
+ mapped.client !== browser
129
+ ) {
130
+ const projected = projectShadow(file).origins
131
+ mapped = {
132
+ source,
133
+ server,
134
+ client: browser,
135
+ serverMappings: sourceMapOf(
136
+ server,
137
+ serverChunks ?? [],
138
+ projected,
139
+ source,
140
+ ),
141
+ clientMappings:
142
+ browser === ''
143
+ ? undefined
144
+ : sourceMapOf(
145
+ browser,
146
+ clientChunks ?? [],
147
+ projected,
148
+ source,
149
+ ),
150
+ }
151
+ MAPPED.set(relative, mapped)
152
+ }
153
+ }
81
154
  return {
82
- // 41.19 — a page and a layout are built a level at a time by `render`.
83
- server: emitComponent(file, answer, kind !== 'component', landed),
84
- client:
85
- client !== '' && sheet !== undefined
86
- ? `import './${name.replace(/\.abide$/, '.style.css')}'\n${client}`
87
- : client,
155
+ server,
156
+ client: browser,
157
+ serverMappings: mapped?.serverMappings,
158
+ clientMappings: mapped?.clientMappings,
88
159
  sheet,
89
160
  transitions,
90
161
  }
91
162
  }
163
+
164
+ type Mapped = {
165
+ source: string
166
+ server: string
167
+ client: string
168
+ serverMappings: string
169
+ clientMappings: string | undefined
170
+ }
171
+
172
+ // THE LAST MAPS PER FILE, for a process that compiles again (`abide dev`, the language server): a
173
+ // module's maps are a function of its text and the `.abide`'s, and a save changes one file. Reused
174
+ // only where all three texts match, so two apps in one process sharing a path cannot cross.
175
+ const MAPPED = new Map<string, Mapped>()
176
+
177
+ /**
178
+ * The browser's module with each handler's wrapper after it, or the wrappers alone where it had
179
+ * nothing else to carry: a file a page imports for its handlers is imported all the same.
180
+ */
181
+ function withStandIns(client: string, standIns: string): string {
182
+ if (standIns === '') return client
183
+ return client === '' ? standIns : `${client}\n${standIns}`
184
+ }