@abide/abide 0.100.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.
- package/CHANGELOG.md +31 -0
- package/bin/abide.ts +227 -163
- package/docs/REGISTRY.md +726 -0
- package/docs/RULEBOOK.md +2753 -0
- package/docs/abide.md +12878 -0
- package/package.json +10 -3
- package/scaffold/AGENTS.md +31 -0
- package/scaffold/CLAUDE.md +1 -0
- package/scaffold/src/layout.abide +18 -0
- package/src/compiler/Lowering.ts +78 -9
- package/src/compiler/ShadowMap.ts +2 -2
- package/src/compiler/TypeSession.ts +262 -67
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +43 -15
- package/src/compiler/checkedShadow.ts +39 -5
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +433 -193
- package/src/compiler/compiledEntry.ts +43 -39
- package/src/compiler/componentGraph.ts +58 -71
- package/src/compiler/declaredExports.ts +24 -7
- package/src/compiler/emitClient.ts +141 -71
- package/src/compiler/emitComponent.ts +37 -8
- package/src/compiler/emitSource.ts +106 -13
- package/src/compiler/handlersUnder.ts +106 -56
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +138 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +213 -7
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +61 -7
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +30 -11
- package/src/compiler/serveLanguage.ts +125 -34
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/syncTypes.ts +32 -0
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +1 -1
- package/src/compiler/writeTypes.ts +82 -0
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/LAUNCHER_FILES.ts +9 -0
- package/src/server/MODULE_QUERIES.ts +18 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +391 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +235 -106
- package/src/server/answerRpc.ts +124 -39
- package/src/server/answerSocket.ts +141 -48
- package/src/server/answerValue.ts +22 -9
- package/src/server/appDirectories.ts +1 -1
- package/src/server/appPlugins.ts +1 -1
- package/src/server/appTree.ts +179 -0
- package/src/server/argsFor.ts +2 -2
- package/src/server/assistant.ts +2 -2
- package/src/server/bridgeRelay.ts +6 -8
- package/src/server/claudeCode.ts +0 -6
- package/src/server/clientBuildOptions.ts +8 -3
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +37 -37
- package/src/server/componentModules.d.ts +3 -1
- package/src/server/compressAnswer.ts +48 -14
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +59 -73
- package/src/server/devWorker.ts +144 -183
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +37 -35
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +124 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +11 -7
- package/src/server/installSourceMaps.ts +88 -0
- package/src/server/launcherOptions.ts +34 -31
- package/src/server/launcherPage.ts +107 -64
- package/src/server/manifestOf.ts +16 -13
- package/src/server/namesUnder.ts +2 -2
- package/src/server/nativeMenu.ts +5 -5
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/principal.ts +10 -5
- package/src/server/realOrAsGiven.ts +10 -0
- package/src/server/refuseAddressCollision.ts +21 -14
- package/src/server/remote.ts +49 -52
- package/src/server/render.ts +65 -94
- package/src/server/renderComponent.ts +5 -0
- package/src/server/reportHealth.ts +2 -2
- package/src/server/requestScope.ts +12 -1
- package/src/server/runBuild.ts +24 -0
- package/src/server/runCompiled.ts +5 -5
- package/src/server/runDesktop.ts +52 -27
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +81 -39
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +18 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +7 -18
- package/src/shared/APP_META.ts +7 -0
- package/src/shared/CopyOptions.ts +1 -1
- package/src/shared/DEBUG_META.ts +1 -1
- package/src/shared/GLOBAL_GETS.ts +9 -3
- package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
- package/src/shared/RPC_SERVER.ts +2 -2
- package/src/shared/cookies.ts +2 -2
- package/src/shared/csp.ts +1 -1
- package/src/shared/escapeText.ts +1 -1
- package/src/shared/filledPath.ts +85 -0
- package/src/shared/framing.ts +79 -6
- package/src/shared/headKeyOf.ts +2 -0
- package/src/shared/index.ts +1 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/shared/menuItem.ts +3 -3
- package/src/shared/reactive/Cursor.ts +3 -9
- package/src/shared/reactive/Keyed.ts +4 -3
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +81 -60
- package/src/shared/reactive/argsContract.ts +4 -4
- package/src/shared/reactive/canonicalKey.ts +4 -4
- package/src/shared/reactive/chunksOfStream.ts +6 -0
- package/src/shared/reactive/index.ts +1 -1
- package/src/shared/reactive/recordWrites.ts +103 -5
- package/src/shared/reactive/structural.ts +1 -1
- package/src/shared/reactive/tracking.ts +155 -21
- package/src/shared/reactive/watch.ts +10 -6
- package/src/shared/refuse.ts +1 -1
- package/src/shared/routePattern.ts +82 -46
- package/src/shared/rpc.ts +47 -13
- package/src/shared/rpcRequest.ts +27 -3
- package/src/shared/url.ts +4 -44
- package/src/shared/writePath.ts +1 -1
- package/src/ui/LAYER_NAMES.ts +1 -1
- package/src/ui/List.ts +3 -3
- package/src/ui/MOUNTING_LEVEL.ts +18 -0
- package/src/ui/Mismatch.ts +1 -1
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +38 -31
- package/src/ui/Scope.ts +1 -1
- package/src/ui/TryRegion.ts +2 -2
- package/src/ui/applyHead.ts +33 -0
- package/src/ui/index.ts +1 -1
- package/src/ui/menuBridge.ts +1 -1
- package/src/ui/moveNode.ts +1 -1
- package/src/ui/reconcile.ts +2 -2
- package/src/ui/reportMenu.ts +2 -2
- package/src/ui/routing.ts +2 -2
- package/src/ui/setAttribute.ts +1 -1
- package/src/ui/setText.ts +1 -1
- package/src/ui/socketWire.ts +9 -7
- package/src/ui/wire.ts +1 -1
- package/scaffold/src/server/app.ts +0 -6
- package/scaffold/src/shared/.gitkeep +0 -0
- package/scaffold/src/ui/app.html +0 -8
- package/src/server/componentImports.ts +0 -56
- /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type Attribute,
|
|
9
9
|
type Branch,
|
|
10
10
|
bodiesOf,
|
|
11
|
+
type ElementNode,
|
|
11
12
|
type ForBlock,
|
|
12
13
|
type InlineComponent,
|
|
13
14
|
type Setup,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
nesting,
|
|
19
20
|
skipQuoted,
|
|
20
21
|
slotEnd,
|
|
22
|
+
startsToken,
|
|
21
23
|
type Token,
|
|
22
24
|
tokenize,
|
|
23
25
|
} from './tokenize.ts'
|
|
@@ -37,6 +39,11 @@ export type AbideFile = {
|
|
|
37
39
|
* `scripts` because the two run a different number of times, which is the whole of the clause.
|
|
38
40
|
*/
|
|
39
41
|
modules: string[]
|
|
42
|
+
/**
|
|
43
|
+
* `<script server>` bodies in source order — 34.20, module scope on a server alone. Apart from
|
|
44
|
+
* `modules` because the browser's module never carries them.
|
|
45
|
+
*/
|
|
46
|
+
servers: string[]
|
|
40
47
|
/**
|
|
41
48
|
* `<style>` bodies in source order, outside any block body — each scoped to the component (34.4). A
|
|
42
49
|
* body's own is on its block (34.11).
|
|
@@ -47,6 +54,12 @@ export type AbideFile = {
|
|
|
47
54
|
* template here so no walk over the body steps around it. In tree order, which is 39.5.
|
|
48
55
|
*/
|
|
49
56
|
head: TemplateNode[]
|
|
57
|
+
/**
|
|
58
|
+
* 35.15 — the document's `<html>` and `<body>`, where the file is written inside them: each with
|
|
59
|
+
* its attributes and none of its children, which are the head and the template. `undefined`
|
|
60
|
+
* where the template is not.
|
|
61
|
+
*/
|
|
62
|
+
document: { html: ElementNode; body: ElementNode } | undefined
|
|
50
63
|
template: TemplateNode[]
|
|
51
64
|
/** 32.43 — the inline components the template's own body declares, in source order. */
|
|
52
65
|
components: InlineComponent[]
|
|
@@ -68,11 +81,19 @@ export type AbideFile = {
|
|
|
68
81
|
origins: Origins
|
|
69
82
|
}
|
|
70
83
|
|
|
84
|
+
/** Every script body a file holds — module, server and per-instance — for a walk over its imports. */
|
|
85
|
+
export function scriptBodies(file: AbideFile): string[] {
|
|
86
|
+
return [...file.modules, ...file.servers, ...file.scripts]
|
|
87
|
+
}
|
|
88
|
+
|
|
71
89
|
type Origins = {
|
|
72
90
|
modules: number[]
|
|
91
|
+
servers: number[]
|
|
73
92
|
scripts: number[]
|
|
74
93
|
holders: Map<object, number>
|
|
75
94
|
keys: Map<object, number>
|
|
95
|
+
/** 38.39 — per `{#for}` block, where its item and its index start (`-1` where it has none). */
|
|
96
|
+
bindings: Map<object, { item: number; index: number }>
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
/**
|
|
@@ -111,13 +132,16 @@ function unwindTo(
|
|
|
111
132
|
export function parseAbide(source: string): AbideFile {
|
|
112
133
|
const scripts: string[] = []
|
|
113
134
|
const modules: string[] = []
|
|
135
|
+
const servers: string[] = []
|
|
114
136
|
const styles: string[] = []
|
|
115
137
|
let generics: string | undefined
|
|
116
138
|
const origins: Origins = {
|
|
117
139
|
modules: [],
|
|
140
|
+
servers: [],
|
|
118
141
|
scripts: [],
|
|
119
142
|
holders: new Map(),
|
|
120
143
|
keys: new Map(),
|
|
144
|
+
bindings: new Map(),
|
|
121
145
|
}
|
|
122
146
|
const root: TemplateNode[] = []
|
|
123
147
|
// The open elements, innermost last. `root` is the floor and is never popped.
|
|
@@ -152,11 +176,16 @@ export function parseAbide(source: string): AbideFile {
|
|
|
152
176
|
// the div — while the browser re-parsing that markup closes at `</DIV>`, so every
|
|
153
177
|
// position the client walks to after it is a different node and the page hydrates
|
|
154
178
|
// against the wrong tree.
|
|
179
|
+
const closing = source.slice(at + 2, close).trim()
|
|
180
|
+
// 34.34 — a `<script>` is raw text whose body takes its close tag with it, so one met
|
|
181
|
+
// here is what is left of a block that ended early: a `</script>` in a comment or a
|
|
182
|
+
// string in it, and the source after it would be rendered as the page.
|
|
183
|
+
if (sameTag(closing, 'script'))
|
|
184
|
+
throw new Error(
|
|
185
|
+
'abide: a </script> that closes no <script> — a block ends at the first `</script>` in it, even one in a comment or a string. Write it `<\\/script>` there. RULEBOOK 34.34.',
|
|
186
|
+
)
|
|
155
187
|
const innermost = open[open.length - 1]
|
|
156
|
-
if (
|
|
157
|
-
typeof innermost === 'string' &&
|
|
158
|
-
sameTag(innermost, source.slice(at + 2, close).trim())
|
|
159
|
-
) {
|
|
188
|
+
if (typeof innermost === 'string' && sameTag(innermost, closing)) {
|
|
160
189
|
stack.pop()
|
|
161
190
|
open.pop()
|
|
162
191
|
}
|
|
@@ -213,7 +242,7 @@ export function parseAbide(source: string): AbideFile {
|
|
|
213
242
|
lowerTag === 'style'
|
|
214
243
|
? styles
|
|
215
244
|
: lowerTag === 'script'
|
|
216
|
-
? scriptBlock(tag.attributes, scripts, modules)
|
|
245
|
+
? scriptBlock(tag.attributes, scripts, modules, servers)
|
|
217
246
|
: undefined
|
|
218
247
|
const declared =
|
|
219
248
|
block === undefined || block === styles
|
|
@@ -224,7 +253,7 @@ export function parseAbide(source: string): AbideFile {
|
|
|
224
253
|
// setup have no call to be generic over.
|
|
225
254
|
if (block !== scripts || blocks.length > 0)
|
|
226
255
|
throw new Error(
|
|
227
|
-
'abide: generics="…" belongs on the component\'s own <script>, and this one is a <script module> or a block body\'s. RULEBOOK 34.13.',
|
|
256
|
+
'abide: generics="…" belongs on the component\'s own <script>, and this one is a <script module>, a <script server> or a block body\'s. RULEBOOK 34.13.',
|
|
228
257
|
)
|
|
229
258
|
if (generics !== undefined)
|
|
230
259
|
throw new Error(
|
|
@@ -233,7 +262,12 @@ export function parseAbide(source: string): AbideFile {
|
|
|
233
262
|
generics = declared
|
|
234
263
|
}
|
|
235
264
|
if (lowerTag === 'script' || lowerTag === 'style') {
|
|
236
|
-
const raw = rawBody(
|
|
265
|
+
const raw = rawBody(
|
|
266
|
+
source,
|
|
267
|
+
lowerTag,
|
|
268
|
+
tag,
|
|
269
|
+
block !== undefined && block !== styles,
|
|
270
|
+
)
|
|
237
271
|
// 14.8 — A `<script>` INSIDE A BLOCK IS THAT BODY'S, set up once each time the body is
|
|
238
272
|
// built, and not the component's: lifted, it ran once for the page and a row's own
|
|
239
273
|
// names (`{#for item …}`) were not in scope.
|
|
@@ -272,6 +306,7 @@ export function parseAbide(source: string): AbideFile {
|
|
|
272
306
|
if (block !== undefined) {
|
|
273
307
|
if (block === scripts) origins.scripts.push(tag.end)
|
|
274
308
|
else if (block === modules) origins.modules.push(tag.end)
|
|
309
|
+
else if (block === servers) origins.servers.push(tag.end)
|
|
275
310
|
block.push(raw.body)
|
|
276
311
|
at = raw.end
|
|
277
312
|
continue
|
|
@@ -508,11 +543,16 @@ export function parseAbide(source: string): AbideFile {
|
|
|
508
543
|
}
|
|
509
544
|
|
|
510
545
|
const components = fileComponents(root, declared)
|
|
511
|
-
refuseImported(components, [...modules, ...scripts])
|
|
546
|
+
refuseImported(components, [...modules, ...servers, ...scripts])
|
|
512
547
|
collapseWhitespace(root)
|
|
513
548
|
const head: TemplateNode[] = []
|
|
514
549
|
const template: TemplateNode[] = []
|
|
550
|
+
let document: AbideFile['document']
|
|
515
551
|
for (const node of root) {
|
|
552
|
+
if (node.kind === 'element' && node.lowerTag === 'html') {
|
|
553
|
+
document = documentOf(node, head, template)
|
|
554
|
+
continue
|
|
555
|
+
}
|
|
516
556
|
if (node.kind === 'element' && node.lowerTag === 'head') {
|
|
517
557
|
assertNoNestedHead(node.children, '<head>')
|
|
518
558
|
for (const child of node.children) head.push(child)
|
|
@@ -521,11 +561,23 @@ export function parseAbide(source: string): AbideFile {
|
|
|
521
561
|
assertNoNestedHead([node], '')
|
|
522
562
|
template.push(node)
|
|
523
563
|
}
|
|
564
|
+
// 35.16 — the document is written once, around everything else.
|
|
565
|
+
if (document !== undefined)
|
|
566
|
+
for (const node of root)
|
|
567
|
+
if (
|
|
568
|
+
!(node.kind === 'element' && node.lowerTag === 'html') &&
|
|
569
|
+
!(node.kind === 'text' && node.text.trim() === '')
|
|
570
|
+
)
|
|
571
|
+
throw new Error(
|
|
572
|
+
`abide: ${node.kind === 'element' ? `<${node.tag}>` : 'markup'} stands beside <html>, and a file written inside <html> has nothing outside it. RULEBOOK 35.16.`,
|
|
573
|
+
)
|
|
524
574
|
return {
|
|
525
575
|
scripts,
|
|
526
576
|
modules,
|
|
577
|
+
servers,
|
|
527
578
|
styles,
|
|
528
579
|
head,
|
|
580
|
+
document,
|
|
529
581
|
template,
|
|
530
582
|
components,
|
|
531
583
|
generics,
|
|
@@ -578,7 +630,7 @@ function inlineComponent(header: {
|
|
|
578
630
|
}
|
|
579
631
|
if (close !== tokens.length - 1)
|
|
580
632
|
throw new Error(
|
|
581
|
-
`abide: {#component ${header.text}} is not a name and one
|
|
633
|
+
`abide: {#component ${header.text}} is not a name and one parenthesized props pattern. Write {#component ${name.text}(props)}. RULEBOOK 32.39.`,
|
|
582
634
|
)
|
|
583
635
|
const inside = (from: number, to: number): { text: string; at: number } =>
|
|
584
636
|
trimmed(header.text, from, to)
|
|
@@ -695,6 +747,43 @@ function refuseImported(components: InlineComponent[], blocks: string[]): void {
|
|
|
695
747
|
)
|
|
696
748
|
}
|
|
697
749
|
|
|
750
|
+
/**
|
|
751
|
+
* 35.15 — the `<html>` a file is written inside: its `<head>`'s children added to `head`, its
|
|
752
|
+
* `<body>`'s to `template`, and the two elements handed back with their attributes alone.
|
|
753
|
+
*/
|
|
754
|
+
function documentOf(
|
|
755
|
+
html: ElementNode,
|
|
756
|
+
head: TemplateNode[],
|
|
757
|
+
template: TemplateNode[],
|
|
758
|
+
): { html: ElementNode; body: ElementNode } {
|
|
759
|
+
let body: ElementNode | undefined
|
|
760
|
+
for (const child of html.children) {
|
|
761
|
+
if (child.kind === 'text' && child.text.trim() === '') continue
|
|
762
|
+
if (child.kind === 'element' && child.lowerTag === 'head') {
|
|
763
|
+
assertNoNestedHead(child.children, '<head>')
|
|
764
|
+
for (const node of child.children) head.push(node)
|
|
765
|
+
continue
|
|
766
|
+
}
|
|
767
|
+
if (child.kind === 'element' && child.lowerTag === 'body' && !body) {
|
|
768
|
+
body = child
|
|
769
|
+
assertNoNestedHead(child.children, '<body>')
|
|
770
|
+
for (const node of child.children) template.push(node)
|
|
771
|
+
continue
|
|
772
|
+
}
|
|
773
|
+
throw new Error(
|
|
774
|
+
`abide: <html> holds a <head> and a <body> and nothing else, and this one holds ${child.kind === 'element' ? `<${child.tag}>` : 'more'}. RULEBOOK 35.16.`,
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
if (body === undefined)
|
|
778
|
+
throw new Error(
|
|
779
|
+
'abide: <html> holds no <body>, so the page has nowhere to go. RULEBOOK 35.16.',
|
|
780
|
+
)
|
|
781
|
+
// The elements themselves, emptied, so where each attribute was written is still known.
|
|
782
|
+
html.children = []
|
|
783
|
+
body.children = []
|
|
784
|
+
return { html, body }
|
|
785
|
+
}
|
|
786
|
+
|
|
698
787
|
/**
|
|
699
788
|
* Throws where a `<head>` stands anywhere but the top level of the file.
|
|
700
789
|
*
|
|
@@ -712,6 +801,14 @@ function assertNoNestedHead(nodes: TemplateNode[], within: string): void {
|
|
|
712
801
|
throw new Error(
|
|
713
802
|
`abide: a <head> must be top level in a .abide file, and this one is inside ${within}. RULEBOOK 39.2.`,
|
|
714
803
|
)
|
|
804
|
+
// 35.16 — the document's own elements are written once, at the top, around the rest.
|
|
805
|
+
if (
|
|
806
|
+
node.kind === 'element' &&
|
|
807
|
+
(node.lowerTag === 'html' || node.lowerTag === 'body')
|
|
808
|
+
)
|
|
809
|
+
throw new Error(
|
|
810
|
+
`abide: <${node.tag}> ${within === '' ? 'stands among the markup' : `is inside ${within}`}, and the document's <html> and <body> are written once, at the top of the root layout.abide. RULEBOOK 35.16.`,
|
|
811
|
+
)
|
|
715
812
|
const label =
|
|
716
813
|
node.kind === 'element'
|
|
717
814
|
? `<${node.tag}>`
|
|
@@ -783,29 +880,49 @@ function sameTag(left: string, right: string): boolean {
|
|
|
783
880
|
/**
|
|
784
881
|
* The list a `<script>`'s body belongs in, or `undefined` where it is markup.
|
|
785
882
|
*
|
|
786
|
-
* 34.1 is the bare tag
|
|
883
|
+
* 34.1 is the bare tag, 34.2 the bare `module` attribute and 34.20 the bare `server` one; anything
|
|
884
|
+
* else on the tag — `src`, `type`, `defer`, `nonce` —
|
|
787
885
|
* is a `<script>` the author wrote for the DOCUMENT, and lifting it into component setup moves it
|
|
788
|
-
* out of the markup and throws its attributes away.
|
|
886
|
+
* out of the markup and throws its attributes away — save beside `server`, which 34.33 refuses.
|
|
789
887
|
*
|
|
790
|
-
* 34.
|
|
791
|
-
* so
|
|
792
|
-
*
|
|
888
|
+
* 34.13 — BUT `generics="…"` IS THE COMPONENT'S. 34.12 — `lang` is refused on any `<script>`: every
|
|
889
|
+
* block is TypeScript, so it says nothing, and read as markup it took the author's whole setup out of
|
|
890
|
+
* the component without a word.
|
|
793
891
|
*/
|
|
794
892
|
function scriptBlock(
|
|
795
893
|
attributes: Attribute[],
|
|
796
894
|
scripts: string[],
|
|
797
895
|
modules: string[],
|
|
896
|
+
servers: string[],
|
|
798
897
|
): string[] | undefined {
|
|
799
898
|
let module = false
|
|
899
|
+
let server = false
|
|
900
|
+
let markup = false
|
|
800
901
|
for (const attribute of attributes) {
|
|
801
902
|
// `<script module>` carries the marker and no value. `module="x"` names something else.
|
|
802
903
|
if (attribute.lowerName === 'module' && attribute.value === '')
|
|
803
904
|
module = true
|
|
804
|
-
else if (attribute.lowerName === '
|
|
805
|
-
|
|
806
|
-
|
|
905
|
+
else if (attribute.lowerName === 'server') {
|
|
906
|
+
server = true
|
|
907
|
+
if (attribute.value !== '') markup = true
|
|
908
|
+
} else if (attribute.lowerName === 'lang')
|
|
909
|
+
throw new Error(
|
|
910
|
+
'abide: every <script> in a .abide file is TypeScript, so `lang` says nothing — remove it. RULEBOOK 34.12.',
|
|
911
|
+
)
|
|
912
|
+
else if (attribute.lowerName !== 'generics') markup = true
|
|
807
913
|
}
|
|
808
|
-
|
|
914
|
+
// 34.33 — a `server` attribute on a tag that would be markup ships the server's source to the
|
|
915
|
+
// document and the browser's module, so it is refused rather than demoted.
|
|
916
|
+
if (server && markup)
|
|
917
|
+
throw new Error(
|
|
918
|
+
'abide: a <script> carrying `server` is a server block, written `<script server>` with nothing beside it. RULEBOOK 34.33.',
|
|
919
|
+
)
|
|
920
|
+
if (markup) return undefined
|
|
921
|
+
if (module && server)
|
|
922
|
+
throw new Error(
|
|
923
|
+
'abide: a <script> is `module` or `server`, not both — a server block is module scope already. RULEBOOK 34.20.',
|
|
924
|
+
)
|
|
925
|
+
return server ? servers : module ? modules : scripts
|
|
809
926
|
}
|
|
810
927
|
|
|
811
928
|
/**
|
|
@@ -826,13 +943,22 @@ function rawBody(
|
|
|
826
943
|
source: string,
|
|
827
944
|
lowerTag: string,
|
|
828
945
|
tag: { selfClosing: boolean; end: number },
|
|
946
|
+
code: boolean,
|
|
829
947
|
): { body: string; end: number } {
|
|
830
948
|
if (tag.selfClosing) return { body: '', end: tag.end }
|
|
831
|
-
// The body ends at the first literal close tag, so a `</script>` inside
|
|
832
|
-
// block ends it early — the
|
|
949
|
+
// The body ends at the first literal close tag, as a browser's does, so a `</script>` inside
|
|
950
|
+
// a string or a comment in a script block ends it early — and the real close tag after it
|
|
951
|
+
// is refused where the template meets it (34.34).
|
|
833
952
|
const close = closeTagAt(source, lowerTag, tag.end)
|
|
834
953
|
if (close === null)
|
|
835
954
|
return { body: source.slice(tag.end), end: source.length }
|
|
955
|
+
// 34.35 — a component block's early end is refused AT the early end, because what is left
|
|
956
|
+
// over can open a `<script>` of its own that takes the real close tag, and then 34.34 meets
|
|
957
|
+
// nothing to refuse while the block's source between them renders as the page.
|
|
958
|
+
if (code && !startsToken(source, tag.end, close.start))
|
|
959
|
+
throw new Error(
|
|
960
|
+
'abide: a </script> inside a string, a comment or a regex ends the <script> block there, as a browser would. Write it `<\\/script>`. RULEBOOK 34.35.',
|
|
961
|
+
)
|
|
836
962
|
return { body: source.slice(tag.end, close.start), end: close.end }
|
|
837
963
|
}
|
|
838
964
|
|
|
@@ -1005,9 +1131,20 @@ function forBlock(
|
|
|
1005
1131
|
}
|
|
1006
1132
|
origins.holders.set(block, subject.at)
|
|
1007
1133
|
if (key.text !== '') origins.keys.set(block, key.at)
|
|
1134
|
+
origins.bindings.set(block, {
|
|
1135
|
+
item: header.at + leadingSpace(source),
|
|
1136
|
+
index:
|
|
1137
|
+
comma === undefined
|
|
1138
|
+
? -1
|
|
1139
|
+
: header.at + comma.end + leadingSpace(source.slice(comma.end)),
|
|
1140
|
+
})
|
|
1008
1141
|
return block
|
|
1009
1142
|
}
|
|
1010
1143
|
|
|
1144
|
+
function leadingSpace(text: string): number {
|
|
1145
|
+
return text.length - text.trimStart().length
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1011
1148
|
function readTag(
|
|
1012
1149
|
source: string,
|
|
1013
1150
|
open: number,
|
|
@@ -8,6 +8,14 @@ import {
|
|
|
8
8
|
PositionTable,
|
|
9
9
|
} from './PositionTable.ts'
|
|
10
10
|
import type { AbideFile } from './parseAbide.ts'
|
|
11
|
+
import {
|
|
12
|
+
attributedImports,
|
|
13
|
+
IMPORT_ATTRIBUTES,
|
|
14
|
+
type ImportBinding,
|
|
15
|
+
importStatements,
|
|
16
|
+
newlinesIn,
|
|
17
|
+
SOURCE_SUFFIX,
|
|
18
|
+
} from './splitImports.ts'
|
|
11
19
|
import type {
|
|
12
20
|
Body,
|
|
13
21
|
Branch,
|
|
@@ -37,6 +45,8 @@ export type Span = { start: number; end: number }
|
|
|
37
45
|
export type Projection = {
|
|
38
46
|
text: string
|
|
39
47
|
modules: Span
|
|
48
|
+
/** 34.20 — the `<script server>` bodies, module scope like `modules` and the server's alone. */
|
|
49
|
+
servers: Span
|
|
40
50
|
scripts: Span
|
|
41
51
|
/** Every template position, ascending, each with its kind — what the checker's tree is asked. */
|
|
42
52
|
positions: PositionTable
|
|
@@ -83,6 +93,45 @@ export function projectShadow(file: AbideFile): Projection {
|
|
|
83
93
|
return projection
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
type ImportStatement = ReturnType<typeof importStatements>[number]
|
|
97
|
+
|
|
98
|
+
/** 34.26 — a binding as two blocks importing the same thing agree on it. */
|
|
99
|
+
function bindingKey(
|
|
100
|
+
statement: ImportStatement,
|
|
101
|
+
binding: ImportBinding,
|
|
102
|
+
): string {
|
|
103
|
+
return `${statement.specifier}|${statement.attributes}|${binding.imported}|${binding.local}`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 34.26 — what is left of an import once the bindings `drops` answers for are taken out:
|
|
108
|
+
* `undefined` where none is, `''` where every one is, and the statement over the rest otherwise,
|
|
109
|
+
* spelled from its tokens.
|
|
110
|
+
*/
|
|
111
|
+
function keptImport(
|
|
112
|
+
statement: ImportStatement,
|
|
113
|
+
drops: (statement: ImportStatement, binding: ImportBinding) => boolean,
|
|
114
|
+
): string | undefined {
|
|
115
|
+
const parts: string[] = []
|
|
116
|
+
const named: string[] = []
|
|
117
|
+
for (const binding of statement.bindings) {
|
|
118
|
+
if (drops(statement, binding)) continue
|
|
119
|
+
// The default's own slot and a namespace stand outside the braces; `{ default as x }` in.
|
|
120
|
+
if (
|
|
121
|
+
(binding.imported === 'default' &&
|
|
122
|
+
binding.written === binding.local) ||
|
|
123
|
+
binding.imported === '*'
|
|
124
|
+
)
|
|
125
|
+
parts.push(binding.written)
|
|
126
|
+
else named.push(binding.written)
|
|
127
|
+
}
|
|
128
|
+
const kept = parts.length + named.length
|
|
129
|
+
if (kept === statement.bindings.length) return undefined
|
|
130
|
+
if (kept === 0) return ''
|
|
131
|
+
if (named.length > 0) parts.push(`{ ${named.join(', ')} }`)
|
|
132
|
+
return `import ${statement.typeOnly ? 'type ' : ''}${parts.join(', ')} from '${statement.specifier}'${statement.attributes === '' ? '' : ` ${statement.attributes}`}`
|
|
133
|
+
}
|
|
134
|
+
|
|
86
135
|
function project(file: AbideFile): Projection {
|
|
87
136
|
const origins: Origin[] = []
|
|
88
137
|
// 34.13 — scaffolding ahead of every block, so each one and the template are in their scope.
|
|
@@ -102,17 +151,115 @@ function project(file: AbideFile): Projection {
|
|
|
102
151
|
}
|
|
103
152
|
|
|
104
153
|
// Blocks of one kind, joined as the file wrote them, each run placed where it stood.
|
|
105
|
-
const blocks = (
|
|
154
|
+
const blocks = (
|
|
155
|
+
bodies: string[],
|
|
156
|
+
starts: number[],
|
|
157
|
+
drops: (statement: ImportStatement, binding: ImportBinding) => boolean,
|
|
158
|
+
): Span => {
|
|
106
159
|
const start = text.length
|
|
107
160
|
for (let at = 0; at < bodies.length; at += 1) {
|
|
108
161
|
if (at > 0) text += '\n'
|
|
109
|
-
|
|
162
|
+
script(bodies[at] as string, starts[at], drops)
|
|
110
163
|
}
|
|
111
164
|
return { start, end: text.length }
|
|
112
165
|
}
|
|
113
|
-
|
|
166
|
+
// 34.25 — a block copied as written, save that a `.abide` imported as text names the module
|
|
167
|
+
// holding its text, and loses the attribute bun would otherwise read that module as text by.
|
|
168
|
+
//
|
|
169
|
+
// 34.26 — and a binding a `<script server>` and the file's `<script module>` or `<script>` both
|
|
170
|
+
// import is written once: the server's module holds the other blocks' imports too, and written
|
|
171
|
+
// twice it was a duplicate binding. `drops` says which copy goes. A blank line per line it
|
|
172
|
+
// spanned keeps the block line for line with the file.
|
|
173
|
+
const script = (
|
|
174
|
+
body: string,
|
|
175
|
+
origin: number | undefined,
|
|
176
|
+
drops: (statement: ImportStatement, binding: ImportBinding) => boolean,
|
|
177
|
+
): void => {
|
|
178
|
+
const edits: { start: number; end: number; written: string }[] = []
|
|
179
|
+
for (const one of attributedImports(body, 'text'))
|
|
180
|
+
edits.push({
|
|
181
|
+
start: one.literalStart,
|
|
182
|
+
end: one.end,
|
|
183
|
+
written: `'${one.specifier}${SOURCE_SUFFIX}'`,
|
|
184
|
+
})
|
|
185
|
+
// 23.19 — a mount is the build's to read, and no module of the checker's or bun's.
|
|
186
|
+
for (const one of attributedImports(body, 'routes'))
|
|
187
|
+
edits.push({
|
|
188
|
+
start: one.start,
|
|
189
|
+
end: one.end,
|
|
190
|
+
written: `;${'\n'.repeat(newlinesIn(body, one.start, one.end))}`,
|
|
191
|
+
})
|
|
192
|
+
for (const one of importStatements(body)) {
|
|
193
|
+
const kept = keptImport(one, drops)
|
|
194
|
+
if (kept === undefined) continue
|
|
195
|
+
edits.push({
|
|
196
|
+
start: one.start,
|
|
197
|
+
end: one.end,
|
|
198
|
+
written: `${kept === '' ? ';' : kept}${'\n'.repeat(newlinesIn(body, one.start, one.end))}`,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
edits.sort((one, two) => one.start - two.start)
|
|
202
|
+
let from = 0
|
|
203
|
+
for (const edit of edits) {
|
|
204
|
+
if (edit.start < from) continue
|
|
205
|
+
copied(
|
|
206
|
+
body.slice(from, edit.start),
|
|
207
|
+
origin === undefined ? undefined : origin + from,
|
|
208
|
+
)
|
|
209
|
+
place(origin === undefined ? undefined : origin + edit.start, 0)
|
|
210
|
+
text += edit.written
|
|
211
|
+
from = edit.end
|
|
212
|
+
}
|
|
213
|
+
copied(
|
|
214
|
+
body.slice(from),
|
|
215
|
+
origin === undefined ? undefined : origin + from,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
// 34.26 — per binding the blocks both sides run import, whether one of them imports a value;
|
|
219
|
+
// and the bindings the server block imports as values.
|
|
220
|
+
const shared = new Map<string, boolean>()
|
|
221
|
+
for (const body of [...file.modules, ...file.scripts])
|
|
222
|
+
for (const one of importStatements(body))
|
|
223
|
+
for (const binding of one.bindings) {
|
|
224
|
+
const key = bindingKey(one, binding)
|
|
225
|
+
shared.set(key, shared.get(key) === true || !binding.typeOnly)
|
|
226
|
+
}
|
|
227
|
+
const serverValues = new Set<string>()
|
|
228
|
+
for (const body of file.servers)
|
|
229
|
+
for (const one of importStatements(body))
|
|
230
|
+
for (const binding of one.bindings)
|
|
231
|
+
if (!binding.typeOnly)
|
|
232
|
+
serverValues.add(bindingKey(one, binding))
|
|
233
|
+
// A TYPE-ONLY IMPORT NEVER STANDS FOR A VALUE ONE: erased, it binds nothing at run time, so where
|
|
234
|
+
// the server block imports the value, it is the other block's type-only copy that goes.
|
|
235
|
+
const typeOnlyOfServerValue = (
|
|
236
|
+
statement: ImportStatement,
|
|
237
|
+
binding: ImportBinding,
|
|
238
|
+
): boolean =>
|
|
239
|
+
binding.typeOnly && serverValues.has(bindingKey(statement, binding))
|
|
240
|
+
const modules = blocks(
|
|
241
|
+
file.modules,
|
|
242
|
+
file.origins.modules,
|
|
243
|
+
typeOnlyOfServerValue,
|
|
244
|
+
)
|
|
245
|
+
text += '\n;\n'
|
|
246
|
+
const servers = blocks(
|
|
247
|
+
file.servers,
|
|
248
|
+
file.origins.servers,
|
|
249
|
+
(statement, binding) => {
|
|
250
|
+
const value = shared.get(bindingKey(statement, binding))
|
|
251
|
+
if (value === undefined || (value === false && !binding.typeOnly))
|
|
252
|
+
return false
|
|
253
|
+
if (value && !binding.typeOnly) refuseTwoFaces(statement, binding)
|
|
254
|
+
return true
|
|
255
|
+
},
|
|
256
|
+
)
|
|
114
257
|
text += '\n;\n'
|
|
115
|
-
const scripts = blocks(
|
|
258
|
+
const scripts = blocks(
|
|
259
|
+
file.scripts,
|
|
260
|
+
file.origins.scripts,
|
|
261
|
+
typeOnlyOfServerValue,
|
|
262
|
+
)
|
|
116
263
|
const scriptsText = text.slice(scripts.start, scripts.end)
|
|
117
264
|
text += '\n'
|
|
118
265
|
const positions: Position[] = []
|
|
@@ -161,7 +308,7 @@ function project(file: AbideFile): Projection {
|
|
|
161
308
|
// 32.17 — A COMPONENT TAG IS A CALL, AND IT IS PROJECTED AS ONE: the name it invokes, handed the
|
|
162
309
|
// record the build hands it (`invocationProps`), so the checker holds each value to the prop the
|
|
163
310
|
// child declares (33.4), reports a name nobody imported, and answers a hover or a jump on the
|
|
164
|
-
// tag with the import's (38.
|
|
311
|
+
// tag with the import's (38.39). Spreads first and explicit keys after, which is the order 33.12
|
|
165
312
|
// resolves them in.
|
|
166
313
|
const invocation = (node: ElementNode): void => {
|
|
167
314
|
// 32.36 — CALLED OPTIONALLY, so a value that may be absent is checked as one that may
|
|
@@ -386,8 +533,17 @@ function project(file: AbideFile): Projection {
|
|
|
386
533
|
),
|
|
387
534
|
)
|
|
388
535
|
text += ')\n'
|
|
389
|
-
|
|
390
|
-
|
|
536
|
+
// 38.39 — the item and the index copied from where they stand, so a hover on either is
|
|
537
|
+
// the binding's. The index is a `number`: `= 0` alone types it the literal `0`.
|
|
538
|
+
const bound = file.origins.bindings.get(node)
|
|
539
|
+
text += 'for (const '
|
|
540
|
+
copied(node.item, bound?.item)
|
|
541
|
+
text += ` of ${ROWS_OF}(${subject})) {\n`
|
|
542
|
+
if (node.index !== '') {
|
|
543
|
+
text += 'const '
|
|
544
|
+
copied(node.index, bound?.index)
|
|
545
|
+
text += ': number = 0\n'
|
|
546
|
+
}
|
|
391
547
|
// `inBody` but for the key, which stands after the row's setup and before its nodes.
|
|
392
548
|
setup(node.setup)
|
|
393
549
|
declare(node.components)
|
|
@@ -402,6 +558,9 @@ function project(file: AbideFile): Projection {
|
|
|
402
558
|
}
|
|
403
559
|
declare(file.components)
|
|
404
560
|
walk(file.head)
|
|
561
|
+
// 35.17 — the document's attributes are read as any element's are.
|
|
562
|
+
if (file.document !== undefined)
|
|
563
|
+
walk([file.document.html, file.document.body])
|
|
405
564
|
walk(file.template)
|
|
406
565
|
if (subjects > 0) text += ROWS_OF_DECLARATION
|
|
407
566
|
if (inlineProps) text += INLINE_PROPS_IMPORT
|
|
@@ -410,11 +569,14 @@ function project(file: AbideFile): Projection {
|
|
|
410
569
|
// does not parse; a `<script module>` is module scope, where it would, and holds the import of
|
|
411
570
|
// every file that reaches it until it settles — the render waiting on it in all but name.
|
|
412
571
|
refuseTopLevelAwait(file.modules.join('\n'), '<script module>')
|
|
572
|
+
refuseTopLevelAwait(file.servers.join('\n'), '<script server>')
|
|
413
573
|
refuseTopLevelAwait(scriptsText, '<script>')
|
|
414
574
|
refuseInstanceExport(scriptsText)
|
|
575
|
+
refuseServerDefault(file.servers.join('\n'))
|
|
415
576
|
return {
|
|
416
577
|
text,
|
|
417
578
|
modules,
|
|
579
|
+
servers,
|
|
418
580
|
scripts,
|
|
419
581
|
positions: new PositionTable(positions),
|
|
420
582
|
slots,
|
|
@@ -573,6 +735,50 @@ function refuseInstanceExport(source: string): void {
|
|
|
573
735
|
}
|
|
574
736
|
}
|
|
575
737
|
|
|
738
|
+
/**
|
|
739
|
+
* 34.22 — a `<script server>` exporting `default`, refused: the module's default is its component,
|
|
740
|
+
* and a handler is addressed by its name (16.2).
|
|
741
|
+
*/
|
|
742
|
+
/**
|
|
743
|
+
* 34.26 — throws where the binding both blocks import is a `.abide`'s default or namespace: the
|
|
744
|
+
* server block's is the function binding its props (34.16) and the other block's is the module it
|
|
745
|
+
* compiles to (32.17), and one name in one module scope cannot hold both.
|
|
746
|
+
*/
|
|
747
|
+
function refuseTwoFaces(
|
|
748
|
+
statement: ImportStatement,
|
|
749
|
+
binding: ImportBinding,
|
|
750
|
+
): void {
|
|
751
|
+
if (
|
|
752
|
+
statement.specifier.endsWith('.abide') &&
|
|
753
|
+
!IMPORT_ATTRIBUTES.text.test(statement.attributes) &&
|
|
754
|
+
(binding.imported === 'default' || binding.imported === '*')
|
|
755
|
+
)
|
|
756
|
+
throw new Error(
|
|
757
|
+
`abide: <script server> imports ${binding.local} from '${statement.specifier}' as another script block of its file does — a server block's .abide is the function binding its props (34.16), the other block's is its module (32.17), and one name cannot be both. Import it under another name in the <script server>. RULEBOOK 34.26.`,
|
|
758
|
+
)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function refuseServerDefault(source: string): void {
|
|
762
|
+
if (!source.includes(EXPORT)) return
|
|
763
|
+
let depth = 0
|
|
764
|
+
let previous = ''
|
|
765
|
+
for (const token of tokenize(source)) {
|
|
766
|
+
const text = token.text
|
|
767
|
+
if (text === '{' || text === '(' || text === '[') depth += 1
|
|
768
|
+
else if (text === '}' || text === ')' || text === ']') depth -= 1
|
|
769
|
+
// `export { x as default }` names it too.
|
|
770
|
+
else if (
|
|
771
|
+
text === 'default' &&
|
|
772
|
+
((depth === 0 && previous === EXPORT) ||
|
|
773
|
+
(depth === 1 && previous === 'as'))
|
|
774
|
+
)
|
|
775
|
+
throw new Error(
|
|
776
|
+
"abide: a <script server> cannot export `default` — the module's default is its component. Export the handler under a name; the name is its address's last segment. RULEBOOK 34.22.",
|
|
777
|
+
)
|
|
778
|
+
previous = text
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
576
782
|
/**
|
|
577
783
|
* 34.13 — each type parameter a component declares, as a type its body cannot see inside.
|
|
578
784
|
*
|