@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
|
@@ -76,8 +76,8 @@ async function everyPresentMember(
|
|
|
76
76
|
return present
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// Where abide declares `Reactive`, `props`, `route` and `
|
|
80
|
-
// `METHODS_SOURCE` is.
|
|
79
|
+
// Where abide declares `Reactive`, `props`, `route`, `Selection` and `onConfig`, found in the project
|
|
80
|
+
// the way `METHODS_SOURCE` is.
|
|
81
81
|
const REACTIVE_SOURCE = Bun.fileURLToPath(
|
|
82
82
|
new URL('../shared/reactive/Reactive.ts', import.meta.url),
|
|
83
83
|
)
|
|
@@ -90,16 +90,20 @@ const ROUTE_SOURCE = Bun.fileURLToPath(
|
|
|
90
90
|
const SELECTION_SOURCE = Bun.fileURLToPath(
|
|
91
91
|
new URL('../shared/reactive/index.ts', import.meta.url),
|
|
92
92
|
)
|
|
93
|
+
const HOOKS_SOURCE = Bun.fileURLToPath(
|
|
94
|
+
new URL('../server/hooks.ts', import.meta.url),
|
|
95
|
+
)
|
|
93
96
|
|
|
94
97
|
/**
|
|
95
|
-
* Abide's own `Reactive`, `Selection`, `props` and `
|
|
96
|
-
* names them.
|
|
98
|
+
* Abide's own `Reactive`, `Selection`, `props`, `route` and `onConfig` declarations, as this
|
|
99
|
+
* project's checker names them.
|
|
97
100
|
*
|
|
98
101
|
* 31.3 — A `Reactive` IS TOLD BY ITS DECLARATION, never by how it prints. Every instantiation of
|
|
99
102
|
* abide's `Reactive` carries the symbol of the one type literal it is declared as, whatever alias
|
|
100
103
|
* or re-export the app reached it through — so `type Counter = Reactive<number>` is one, and an
|
|
101
104
|
* app's own type that happens to be spelled `Reactive` is not. `undefined` where the project never
|
|
102
|
-
* reaches abide, and then nothing in it is one. `props`
|
|
105
|
+
* reaches abide, and then nothing in it is one. `props` and `onConfig` are abide's files as the
|
|
106
|
+
* checker names them.
|
|
103
107
|
*/
|
|
104
108
|
export type Abide = {
|
|
105
109
|
/** 23.9 — abide's `route`, as the file the checker names it. */
|
|
@@ -110,14 +114,17 @@ export type Abide = {
|
|
|
110
114
|
*/
|
|
111
115
|
selection: CheckedSymbol | undefined
|
|
112
116
|
props: string | undefined
|
|
117
|
+
/** 27.13 — abide's `onConfig`, as the file the checker names it. */
|
|
118
|
+
onConfig: string | undefined
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
export async function abideIn(project: Project): Promise<Abide> {
|
|
116
|
-
const [declares, props, selects, route] = await Promise.all([
|
|
122
|
+
const [declares, props, selects, route, hooks] = await Promise.all([
|
|
117
123
|
project.program.getSourceFile(REACTIVE_SOURCE),
|
|
118
124
|
project.program.getSourceFile(PROPS_SOURCE),
|
|
119
125
|
project.program.getSourceFile(SELECTION_SOURCE),
|
|
120
126
|
project.program.getSourceFile(ROUTE_SOURCE),
|
|
127
|
+
project.program.getSourceFile(HOOKS_SOURCE),
|
|
121
128
|
])
|
|
122
129
|
let selection: CheckedSymbol | undefined
|
|
123
130
|
for (const statement of selects?.statements ?? [])
|
|
@@ -142,7 +149,13 @@ export async function abideIn(project: Project): Promise<Abide> {
|
|
|
142
149
|
reactive = await literal?.getSymbol()
|
|
143
150
|
break
|
|
144
151
|
}
|
|
145
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
route: route?.path,
|
|
154
|
+
reactive,
|
|
155
|
+
selection,
|
|
156
|
+
props: props?.path,
|
|
157
|
+
onConfig: hooks?.path,
|
|
158
|
+
}
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
/**
|
|
@@ -13,6 +13,8 @@ export type ParsedSource = {
|
|
|
13
13
|
text: string
|
|
14
14
|
file: AbideFile
|
|
15
15
|
quiet: boolean
|
|
16
|
+
/** 23.19 — where a mounted package's file stands in the app; `undefined` for one at its own path. */
|
|
17
|
+
position: string | undefined
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/** One `.abide` as the emitters and the checked lowering take it: parsed, and answered. */
|
|
@@ -25,7 +27,12 @@ export type ReadSource = ParsedSource & { answer: CheckerAnswer }
|
|
|
25
27
|
* parsed around it — a quiet one's is nobody's. Without it the refusal is thrown, as a build stops.
|
|
26
28
|
*/
|
|
27
29
|
export function parseSources(
|
|
28
|
-
sources: readonly {
|
|
30
|
+
sources: readonly {
|
|
31
|
+
path: string
|
|
32
|
+
text: string
|
|
33
|
+
quiet?: boolean
|
|
34
|
+
position?: string
|
|
35
|
+
}[],
|
|
29
36
|
refused?: Diagnosis[],
|
|
30
37
|
): ParsedSource[] {
|
|
31
38
|
const parsed: ParsedSource[] = []
|
|
@@ -39,7 +46,13 @@ export function parseSources(
|
|
|
39
46
|
if (!quiet) refused.push(refusal(source.path, thrown))
|
|
40
47
|
continue
|
|
41
48
|
}
|
|
42
|
-
parsed.push({
|
|
49
|
+
parsed.push({
|
|
50
|
+
path: source.path,
|
|
51
|
+
text: source.text,
|
|
52
|
+
file,
|
|
53
|
+
quiet,
|
|
54
|
+
position: source.position,
|
|
55
|
+
})
|
|
43
56
|
}
|
|
44
57
|
return parsed
|
|
45
58
|
}
|
package/src/compiler/readsIn.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isCallExpression,
|
|
9
9
|
isClassDeclaration,
|
|
10
10
|
isEnumDeclaration,
|
|
11
|
+
isExpressionStatement,
|
|
11
12
|
isFunctionDeclaration,
|
|
12
13
|
isFunctionExpression,
|
|
13
14
|
isIdentifier,
|
|
@@ -66,8 +67,9 @@ import { trackingWarnings } from './trackingWarnings.ts'
|
|
|
66
67
|
export type ShadowSpans = {
|
|
67
68
|
/** Where the shadow's scripts end and its template begins. */
|
|
68
69
|
scriptsEnd: number
|
|
69
|
-
/** The `<script module>`
|
|
70
|
+
/** The `<script module>`, `<script server>` and `<script>` bodies, in the shadow. */
|
|
70
71
|
modules: Span
|
|
72
|
+
servers: Span
|
|
71
73
|
scripts: Span
|
|
72
74
|
positions: PositionTable
|
|
73
75
|
}
|
|
@@ -123,7 +125,8 @@ export async function readsIn(
|
|
|
123
125
|
sourceFile: SourceFile,
|
|
124
126
|
shadowPath: string,
|
|
125
127
|
spans: ShadowSpans,
|
|
126
|
-
|
|
128
|
+
// The file below the app's root, which its route is read off (23.14).
|
|
129
|
+
routedPath: string,
|
|
127
130
|
file: AbideFile,
|
|
128
131
|
): Promise<{ answer: CheckerAnswer; warnings: Finding[] }> {
|
|
129
132
|
// A destructuring pattern binds names this does not see, and that is 31.12's case.
|
|
@@ -185,7 +188,14 @@ export async function readsIn(
|
|
|
185
188
|
// Awaited after the round trips below, so a 33.14 refusal would reject with nothing
|
|
186
189
|
// listening and be reported unhandled; `await props` still throws it.
|
|
187
190
|
props.catch(() => {})
|
|
188
|
-
const params = routeReads(checker, abide, sourceFile,
|
|
191
|
+
const params = routeReads(checker, abide, sourceFile, routedPath, reads)
|
|
192
|
+
const configs = configReads(
|
|
193
|
+
checker,
|
|
194
|
+
abide,
|
|
195
|
+
sourceFile,
|
|
196
|
+
spans.servers,
|
|
197
|
+
reads,
|
|
198
|
+
)
|
|
189
199
|
// No name is no round trip to ask about none.
|
|
190
200
|
const symbols =
|
|
191
201
|
names.length === 0 ? [] : await checker.getSymbolAtLocation(names)
|
|
@@ -249,6 +259,7 @@ export async function readsIn(
|
|
|
249
259
|
await Promise.all(holding)
|
|
250
260
|
await props
|
|
251
261
|
await params
|
|
262
|
+
await configs
|
|
252
263
|
const statics = new Set<number>()
|
|
253
264
|
const expressions = spans.positions.nodesIn(
|
|
254
265
|
sourceFile,
|
|
@@ -683,7 +694,7 @@ const ROUTE_MEMBERS = new Set(['url', 'params', 'name', 'navigating'])
|
|
|
683
694
|
* a name, and this read is spelled as a member.
|
|
684
695
|
*
|
|
685
696
|
* 23.9 — AND `route.params` IN A PAGE OR A LAYOUT IS WHAT THAT FILE'S OWN ROUTE PROMISES. The file
|
|
686
|
-
* is the route: `
|
|
697
|
+
* is the route: `media/[id]/page.abide` only ever renders where `id` matched, so its
|
|
687
698
|
* `route.params` has an `id`, and a layout's has its own segments and whatever a page below adds.
|
|
688
699
|
* Only the checker reads that — the running module reads the one `route` every page shares.
|
|
689
700
|
*/
|
|
@@ -691,11 +702,11 @@ async function routeReads(
|
|
|
691
702
|
checker: Checker,
|
|
692
703
|
abide: Abide,
|
|
693
704
|
sourceFile: Node,
|
|
694
|
-
|
|
705
|
+
routedPath: string,
|
|
695
706
|
reads: Read[],
|
|
696
707
|
): Promise<void> {
|
|
697
708
|
if (abide.route === undefined) return
|
|
698
|
-
const routed = fileKind(
|
|
709
|
+
const routed = fileKind(routedPath)
|
|
699
710
|
const kind = routed === 'page' || routed === 'layout' ? routed : undefined
|
|
700
711
|
const accesses: Node[] = []
|
|
701
712
|
const owners: Node[] = []
|
|
@@ -714,7 +725,7 @@ async function routeReads(
|
|
|
714
725
|
sourceFile.forEachChild(visit)
|
|
715
726
|
if (owners.length === 0) return
|
|
716
727
|
const symbols = await checker.getSymbolAtLocation(owners)
|
|
717
|
-
const pattern = routeOf(
|
|
728
|
+
const pattern = routeOf(routedPath)
|
|
718
729
|
for (let at = 0; at < accesses.length; at += 1) {
|
|
719
730
|
const symbol = symbols[at]
|
|
720
731
|
if (symbol === undefined) continue
|
|
@@ -777,6 +788,49 @@ function refuseDeclared(
|
|
|
777
788
|
)
|
|
778
789
|
}
|
|
779
790
|
|
|
791
|
+
/**
|
|
792
|
+
* 27.13 — each `onConfig(…)` standing as a statement at the top of a `<script server>`, as a read of
|
|
793
|
+
* its callee, pushed onto `reads`.
|
|
794
|
+
*
|
|
795
|
+
* The callee says which are abide's, as `props`'s does. A call anywhere else — inside a function,
|
|
796
|
+
* or handing its unfiling back to a name — is filed all the same, and declares nothing.
|
|
797
|
+
*/
|
|
798
|
+
async function configReads(
|
|
799
|
+
checker: Checker,
|
|
800
|
+
abide: Abide,
|
|
801
|
+
sourceFile: Node & { statements: readonly Node[] },
|
|
802
|
+
servers: Span,
|
|
803
|
+
reads: Read[],
|
|
804
|
+
): Promise<void> {
|
|
805
|
+
if (abide.onConfig === undefined) return
|
|
806
|
+
const callees: Node[] = []
|
|
807
|
+
for (const statement of sourceFile.statements) {
|
|
808
|
+
const start = statement.getStart()
|
|
809
|
+
if (start < servers.start) continue
|
|
810
|
+
if (start >= servers.end) break
|
|
811
|
+
if (
|
|
812
|
+
isExpressionStatement(statement) &&
|
|
813
|
+
isCallExpression(statement.expression) &&
|
|
814
|
+
isIdentifier(statement.expression.expression)
|
|
815
|
+
)
|
|
816
|
+
callees.push(statement.expression.expression)
|
|
817
|
+
}
|
|
818
|
+
if (callees.length === 0) return
|
|
819
|
+
const named = await checker.getSymbolAtLocation(callees)
|
|
820
|
+
for (let at = 0; at < callees.length; at += 1) {
|
|
821
|
+
const symbol = named[at]
|
|
822
|
+
if (symbol === undefined) continue
|
|
823
|
+
const callee = await aliasedSymbol(checker, symbol)
|
|
824
|
+
if (
|
|
825
|
+
callee.name !== 'onConfig' ||
|
|
826
|
+
callee.valueDeclaration?.path !== abide.onConfig
|
|
827
|
+
)
|
|
828
|
+
continue
|
|
829
|
+
const node = callees[at] as Node
|
|
830
|
+
reads.push(newRead(node.getStart(), node.end, 'config'))
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
780
834
|
/**
|
|
781
835
|
* The reads a `props<Props>()` destructuring contributes, pushed onto `reads`.
|
|
782
836
|
*
|
|
@@ -108,7 +108,7 @@ export const HEADS = '__heads'
|
|
|
108
108
|
* EVERY MARKUP POSITION THAT EVALUATES AN AUTHOR EXPRESSION, rather than the two that were found
|
|
109
109
|
* first. A GUARD HERE IS HALF A MECHANISM — it trades a throw for a HOLE, and something on the
|
|
110
110
|
* client has to fill that hole once the value lands: a text slot has `runHere` and `textHere`, a
|
|
111
|
-
* `{#for}` has `List.hydrate`'s count check, an attribute has its own binding
|
|
111
|
+
* `{#for}` has `List.hydrate`'s count check, an attribute has its own binding rerunning on the
|
|
112
112
|
* value that landed (32.3), and the `{#if}` chain has `isEmptyRegion` — which is why that one is
|
|
113
113
|
* guarded at the REGION rather than per subject, and says so at its own arm. The ESCAPER STAYS
|
|
114
114
|
* OUTSIDE: absence is `undefined` here and what it means is already decided where it lands (see
|
|
@@ -296,7 +296,7 @@ function renderPosition(
|
|
|
296
296
|
`abide: a ${at.kind} position holds a ${node.kind} node.`,
|
|
297
297
|
)
|
|
298
298
|
|
|
299
|
-
// 32.17 — A
|
|
299
|
+
// 32.17 — A CAPITALIZED TAG IS AN INVOCATION, and the capital is the whole of the
|
|
300
300
|
// distinction. It is emitted as a CALL, so the child's own module renders it.
|
|
301
301
|
if (isComponentTag(node.tag)) return invocationMarkup(node, at, lowering)
|
|
302
302
|
|
|
@@ -536,7 +536,7 @@ function serverComponent(
|
|
|
536
536
|
// standing side by side are two instances, each creating what no ancestor holds (10.6).
|
|
537
537
|
const { opened, closed } = shareScope(lowering, '')
|
|
538
538
|
return `function ${component.name}(${PROPS} = {}) {
|
|
539
|
-
${opened}${head}${setup}${definitions(sink, lowering)}return { head: '', body: ${renderMarkup(sink, lowering, SERVER_LANE)} }${closed}
|
|
539
|
+
${opened}${head}${setup}${definitions(sink, lowering)}return { head: '', body: ${renderMarkup(sink, lowering, SERVER_LANE)}, document: undefined }${closed}
|
|
540
540
|
}`
|
|
541
541
|
}
|
|
542
542
|
|
package/src/compiler/scaffold.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 38.1 — `abide scaffold <name>`: THE LAYOUT EVERY ABIDE APP HAS, written into `<name>`. There is
|
|
2
|
-
// nothing to choose, because what is scaffolded is the
|
|
2
|
+
// nothing to choose, because what is scaffolded is a route, the shell and the hooks — the files the
|
|
3
|
+
// commands read.
|
|
3
4
|
// A file that is already there is left as it is and reported skipped, so a scaffold run over a
|
|
4
5
|
// half-made project finishes it rather than overwriting it.
|
|
5
6
|
|
|
@@ -28,20 +29,26 @@ function filesFor(name: string): Record<string, string> {
|
|
|
28
29
|
name,
|
|
29
30
|
private: true,
|
|
30
31
|
type: 'module',
|
|
32
|
+
// 34.24 — `prepare` runs on `bun install`, so a `.ts` importing a `.abide` is typed
|
|
33
|
+
// before any other abide command has run; and never fails it, as it would where
|
|
34
|
+
// `package.json` is installed before `src` is there to sync (38.26).
|
|
31
35
|
scripts: {
|
|
36
|
+
prepare: 'abide sync || true',
|
|
37
|
+
// 38.103 — the checker over `.abide`, which `tsc` does not read.
|
|
38
|
+
check: 'abide check',
|
|
32
39
|
dev: 'abide dev',
|
|
33
40
|
build: 'abide build',
|
|
34
41
|
start: 'abide start',
|
|
35
42
|
},
|
|
36
|
-
//
|
|
37
|
-
// written and only bun probes for the extension:
|
|
38
|
-
// target as the exact file, so `#server/
|
|
39
|
-
// imports `any` and the checker could not type one of
|
|
40
|
-
// target is what bun resolves, and the one a `.ts` or
|
|
43
|
+
// ONE ALIAS FOR THE SOURCE, and two targets for it, because an extensionless
|
|
44
|
+
// specifier is how an import is written and only bun probes for the extension:
|
|
45
|
+
// TypeScript takes an `imports` target as the exact file, so `#src/media/server/db`
|
|
46
|
+
// alone left every name it imports `any` and the checker could not type one of
|
|
47
|
+
// them (31.3). The first target is what bun resolves, and the one a `.ts` or
|
|
48
|
+
// `.abide` specifier needs. Which side a module is on is its directory's to say
|
|
49
|
+
// (36.10), not its alias's.
|
|
41
50
|
imports: {
|
|
42
|
-
'#
|
|
43
|
-
'#server/*': ['./src/server/*', './src/server/*.ts'],
|
|
44
|
-
'#shared/*': ['./src/shared/*', './src/shared/*.ts'],
|
|
51
|
+
'#src/*': ['./src/*', './src/*.ts'],
|
|
45
52
|
},
|
|
46
53
|
dependencies: { '@abide/abide': ABIDE_DEPENDENCY },
|
|
47
54
|
// `Bun`, `bun:sqlite` and the rest of the runtime a server module reaches for are
|
|
@@ -53,8 +60,21 @@ function filesFor(name: string): Record<string, string> {
|
|
|
53
60
|
)}\n`,
|
|
54
61
|
'tsconfig.json': `${JSON.stringify(
|
|
55
62
|
// TypeScript 7 includes no `@types` package it is not named, so the runtime's is named.
|
|
63
|
+
// 34.24 — `rootDirs` and the `#src/*` path reach what abide writes to `.abide/types`,
|
|
64
|
+
// where a `.ts` importing a `.abide` finds the names it exports.
|
|
56
65
|
{
|
|
57
|
-
compilerOptions: {
|
|
66
|
+
compilerOptions: {
|
|
67
|
+
...COMPILER_OPTIONS,
|
|
68
|
+
types: ['bun'],
|
|
69
|
+
rootDirs: ['.', '.abide/types'],
|
|
70
|
+
paths: {
|
|
71
|
+
'#src/*': [
|
|
72
|
+
'./src/*',
|
|
73
|
+
'./src/*.ts',
|
|
74
|
+
'./.abide/types/src/*',
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
58
78
|
include: ['src'],
|
|
59
79
|
},
|
|
60
80
|
null,
|
|
@@ -81,7 +101,6 @@ export async function scaffold(
|
|
|
81
101
|
const written: string[] = []
|
|
82
102
|
const skipped: string[] = []
|
|
83
103
|
const files: [string, string | Blob][] = Object.entries(filesFor(name))
|
|
84
|
-
// The seam both sides import from is there from the first commit, `.gitkeep` and all.
|
|
85
104
|
for (const path of new Bun.Glob('**/*').scanSync({
|
|
86
105
|
cwd: STARTER,
|
|
87
106
|
dot: true,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { type FSWatcher, watch } from 'node:fs' // bun exposes no file watcher of its own
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, type FSWatcher, watch } from 'node:fs' // bun exposes no file watcher or sync existence check of its own
|
|
2
|
+
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
3
|
+
import { realOrAsGiven } from '#server/realOrAsGiven.ts'
|
|
3
4
|
import { checkSource } from './checkApp.ts'
|
|
4
5
|
import { type AppChecker, openChecker } from './compileApp.ts'
|
|
6
|
+
import { nearest } from './nearest.ts'
|
|
7
|
+
import { parseAbide } from './parseAbide.ts'
|
|
8
|
+
import { syncTypes } from './syncTypes.ts'
|
|
5
9
|
import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
|
|
10
|
+
import { writeTypes } from './writeTypes.ts'
|
|
6
11
|
|
|
7
|
-
// 38.
|
|
12
|
+
// 38.11 — `abide lsp`: THE `.abide` LANGUAGE SERVER, OVER STDIO. It is `abide check` (38.3) asked
|
|
8
13
|
// about one open document at a time, so the squiggle an editor draws and the line CI fails on are
|
|
9
14
|
// one finding rather than two implementations that agree most of the time.
|
|
10
15
|
//
|
|
@@ -15,10 +20,14 @@ import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
|
|
|
15
20
|
// same text.
|
|
16
21
|
//
|
|
17
22
|
// THE APP IS THE DOCUMENT'S. An editor starts this at the root of whatever it has open, which in a
|
|
18
|
-
// monorepo is not an app at all, so each document is checked against the
|
|
19
|
-
//
|
|
23
|
+
// monorepo is not an app at all, so each document is checked against the root of the NEAREST
|
|
24
|
+
// package above it (`appRootOf`) — with one checker per app, kept for the session.
|
|
20
25
|
// `--root` pins every document to one.
|
|
21
26
|
//
|
|
27
|
+
// THE EDITOR'S SPELLING STOPS HERE. An app opened through a link (`/tmp` is one on macOS) is
|
|
28
|
+
// checked at its real path, the one bun resolves every import to (`openChecker`), so a path is
|
|
29
|
+
// made real on its way in (`placed`) and spelled under the link again on its way out (`spelled`).
|
|
30
|
+
//
|
|
22
31
|
// ONE CHECK AT A TIME. The checker holds one snapshot, and two checks interleaving would each
|
|
23
32
|
// register the other's text for a moment; a burst of keystrokes collapses onto the last one.
|
|
24
33
|
|
|
@@ -52,8 +61,33 @@ export async function serveLanguage(
|
|
|
52
61
|
const timers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
53
62
|
const checkers = new Map<string, AppChecker>()
|
|
54
63
|
const watchers = new Map<string, FSWatcher>()
|
|
64
|
+
// Each app's real root, to the root the editor reached it by.
|
|
65
|
+
const links = new Map<string, string>()
|
|
55
66
|
let queue: Promise<void> = Promise.resolve()
|
|
56
67
|
|
|
68
|
+
// The document at `uri`, in its app's real spelling; `undefined` where no app holds it.
|
|
69
|
+
const placed = (uri: string): { app: string; path: string } | undefined => {
|
|
70
|
+
const path = Bun.fileURLToPath(uri)
|
|
71
|
+
const given = root ?? appRootOf(path)
|
|
72
|
+
if (given === undefined) return undefined
|
|
73
|
+
const app = realOrAsGiven(given)
|
|
74
|
+
if (app !== given) links.set(app, given)
|
|
75
|
+
return {
|
|
76
|
+
app,
|
|
77
|
+
path: path.startsWith(`${given}/`)
|
|
78
|
+
? `${app}${path.slice(given.length)}`
|
|
79
|
+
: path,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// `path`, real, under the root the editor reached its app by.
|
|
84
|
+
const spelled = (path: string): string => {
|
|
85
|
+
for (const [app, given] of links)
|
|
86
|
+
if (path.startsWith(`${app}/`))
|
|
87
|
+
return `${given}${path.slice(app.length)}`
|
|
88
|
+
return path
|
|
89
|
+
}
|
|
90
|
+
|
|
57
91
|
const send = (message: object): void => {
|
|
58
92
|
const body = ENCODER.encode(
|
|
59
93
|
JSON.stringify({ jsonrpc: '2.0', ...message }),
|
|
@@ -118,8 +152,7 @@ export async function serveLanguage(
|
|
|
118
152
|
)
|
|
119
153
|
// What an open page says may be decided by the file that moved.
|
|
120
154
|
for (const uri of documents.keys())
|
|
121
|
-
if ((
|
|
122
|
-
check(uri)
|
|
155
|
+
if (placed(uri)?.app === app) check(uri)
|
|
123
156
|
}),
|
|
124
157
|
)
|
|
125
158
|
return checker
|
|
@@ -129,18 +162,18 @@ export async function serveLanguage(
|
|
|
129
162
|
const document = documents.get(uri)
|
|
130
163
|
if (document === undefined) return
|
|
131
164
|
const version = document.version
|
|
132
|
-
const path = Bun.fileURLToPath(uri)
|
|
133
|
-
const app = root ?? appRootOf(path)
|
|
134
165
|
// A `.abide` belonging to no app has nothing to be checked against.
|
|
135
|
-
|
|
166
|
+
const at = placed(uri)
|
|
167
|
+
if (at === undefined) return
|
|
168
|
+
const { app, path } = at
|
|
136
169
|
let found: Diagnosis[]
|
|
137
170
|
try {
|
|
138
171
|
found = await checkSource(
|
|
139
172
|
await checkerOf(app),
|
|
140
|
-
app,
|
|
141
173
|
path,
|
|
142
174
|
document.text,
|
|
143
|
-
(held) =>
|
|
175
|
+
(held) =>
|
|
176
|
+
documents.get(Bun.pathToFileURL(spelled(held)).href)?.text,
|
|
144
177
|
)
|
|
145
178
|
} catch (thrown) {
|
|
146
179
|
// The checker's own failure, not the page's: the next check opens another.
|
|
@@ -164,6 +197,27 @@ export async function serveLanguage(
|
|
|
164
197
|
// A newer text has arrived while this one was checked, and will be published.
|
|
165
198
|
if (documents.get(uri)?.version !== version) return
|
|
166
199
|
publish(uri, found, document.text)
|
|
200
|
+
await typesFor(app, path, document.text)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 34.24 — THE TYPES A PLAIN `.ts` READS AN OPEN FILE BY, as the editor holds it: the whole app's
|
|
204
|
+
// the first time an app is seen, and the one file as it is edited after that.
|
|
205
|
+
const synced = new Set<string>()
|
|
206
|
+
const typesFor = async (
|
|
207
|
+
app: string,
|
|
208
|
+
path: string,
|
|
209
|
+
text: string,
|
|
210
|
+
): Promise<void> => {
|
|
211
|
+
try {
|
|
212
|
+
if (synced.has(app))
|
|
213
|
+
await writeTypes(app, [{ path, file: parseAbide(text) }], false)
|
|
214
|
+
else {
|
|
215
|
+
synced.add(app)
|
|
216
|
+
await syncTypes(app, new Map([[path, text]]))
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// A file that does not parse keeps the types it last had; its check already says why.
|
|
220
|
+
}
|
|
167
221
|
}
|
|
168
222
|
|
|
169
223
|
const check = (uri: string): void => {
|
|
@@ -179,7 +233,20 @@ export async function serveLanguage(
|
|
|
179
233
|
|
|
180
234
|
// A question about a document is about the text the editor shows, so a check still settling
|
|
181
235
|
// runs now rather than after the answer; and it is answered in the queue, after that check.
|
|
182
|
-
const inspect = (params: unknown): Promise<Inspection | undefined> =>
|
|
236
|
+
const inspect = (params: unknown): Promise<Inspection | undefined> =>
|
|
237
|
+
ask(params, (session, path, text, offset) =>
|
|
238
|
+
session.inspect(path, text, offset),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const ask = <Answer>(
|
|
242
|
+
params: unknown,
|
|
243
|
+
question: (
|
|
244
|
+
session: AppChecker['session'],
|
|
245
|
+
path: string,
|
|
246
|
+
text: string,
|
|
247
|
+
offset: number,
|
|
248
|
+
) => Promise<Answer | undefined>,
|
|
249
|
+
): Promise<Answer | undefined> => {
|
|
183
250
|
const { textDocument, position } = params as {
|
|
184
251
|
textDocument: { uri: string }
|
|
185
252
|
position: { line: number; character: number }
|
|
@@ -192,12 +259,11 @@ export async function serveLanguage(
|
|
|
192
259
|
}
|
|
193
260
|
const answered = queue.then(async () => {
|
|
194
261
|
const document = documents.get(uri)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
path,
|
|
262
|
+
const at = placed(uri)
|
|
263
|
+
if (document === undefined || at === undefined) return undefined
|
|
264
|
+
return question(
|
|
265
|
+
(await checkerOf(at.app)).session,
|
|
266
|
+
at.path,
|
|
201
267
|
document.text,
|
|
202
268
|
offsetOf(document.text, position),
|
|
203
269
|
)
|
|
@@ -220,6 +286,7 @@ export async function serveLanguage(
|
|
|
220
286
|
textDocumentSync: 1,
|
|
221
287
|
hoverProvider: true,
|
|
222
288
|
definitionProvider: true,
|
|
289
|
+
completionProvider: { triggerCharacters: ['.'] },
|
|
223
290
|
},
|
|
224
291
|
serverInfo: { name: 'abide' },
|
|
225
292
|
},
|
|
@@ -279,11 +346,39 @@ export async function serveLanguage(
|
|
|
279
346
|
})
|
|
280
347
|
return true
|
|
281
348
|
}
|
|
349
|
+
// 38.98 — the protocol's `CompletionItemKind` is the checker's own numbering.
|
|
350
|
+
case 'textDocument/completion': {
|
|
351
|
+
const trigger = (
|
|
352
|
+
params?.context as { triggerCharacter?: string } | undefined
|
|
353
|
+
)?.triggerCharacter
|
|
354
|
+
const found = await ask(params, (session, path, text, offset) =>
|
|
355
|
+
session.complete(path, text, offset, trigger),
|
|
356
|
+
)
|
|
357
|
+
const items: object[] = []
|
|
358
|
+
for (const entry of found?.entries ?? [])
|
|
359
|
+
items.push({
|
|
360
|
+
label: entry.name,
|
|
361
|
+
kind: entry.kind,
|
|
362
|
+
sortText: entry.sortText,
|
|
363
|
+
filterText: entry.filterText,
|
|
364
|
+
insertText: entry.insertText,
|
|
365
|
+
detail: entry.detail,
|
|
366
|
+
labelDetails: entry.labelDetails,
|
|
367
|
+
})
|
|
368
|
+
send({
|
|
369
|
+
id: message.id,
|
|
370
|
+
result:
|
|
371
|
+
found === undefined
|
|
372
|
+
? null
|
|
373
|
+
: { isIncomplete: found.isIncomplete, items },
|
|
374
|
+
})
|
|
375
|
+
return true
|
|
376
|
+
}
|
|
282
377
|
case 'textDocument/definition': {
|
|
283
378
|
const found = await inspect(params)
|
|
284
379
|
const locations: object[] = []
|
|
285
380
|
for (const place of found?.definitions ?? [])
|
|
286
|
-
locations.push(locationOf(place))
|
|
381
|
+
locations.push(locationOf(spelled(place.path), place))
|
|
287
382
|
send({ id: message.id, result: locations })
|
|
288
383
|
return true
|
|
289
384
|
}
|
|
@@ -348,20 +443,16 @@ async function closeAll(
|
|
|
348
443
|
}
|
|
349
444
|
|
|
350
445
|
/**
|
|
351
|
-
* The app a file at `path` belongs to: the
|
|
446
|
+
* The app a file at `path` belongs to: the root 38.25 finds from the nearest package above it.
|
|
352
447
|
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
448
|
+
* That package's `src` where it has one, and the package itself where it does not — the same root
|
|
449
|
+
* `abide dev` run in that package would find. `undefined` where no package lies above the file.
|
|
355
450
|
*/
|
|
356
451
|
function appRootOf(path: string): string | undefined {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const up = dirname(at)
|
|
362
|
-
if (up === at) return found
|
|
363
|
-
at = up
|
|
364
|
-
}
|
|
452
|
+
const manifest = nearest(dirname(path), 'package.json')
|
|
453
|
+
if (manifest === undefined) return undefined
|
|
454
|
+
const at = dirname(manifest)
|
|
455
|
+
return existsSync(`${at}/src`) ? `${at}/src` : at
|
|
365
456
|
}
|
|
366
457
|
|
|
367
458
|
/**
|
|
@@ -390,10 +481,10 @@ function offsetOf(
|
|
|
390
481
|
return at + position.character
|
|
391
482
|
}
|
|
392
483
|
|
|
393
|
-
/** A `Place`, lines and columns from 1, as the protocol's `Location`, from 0. */
|
|
394
|
-
function locationOf(place: Place): object {
|
|
484
|
+
/** A `Place` at `path`, lines and columns from 1, as the protocol's `Location`, from 0. */
|
|
485
|
+
function locationOf(path: string, place: Place): object {
|
|
395
486
|
return {
|
|
396
|
-
uri: Bun.pathToFileURL(
|
|
487
|
+
uri: Bun.pathToFileURL(path).href,
|
|
397
488
|
range: {
|
|
398
489
|
start: {
|
|
399
490
|
line: place.start.line - 1,
|