@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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// 38.97 — WHERE A COMPILED MODULE'S LINES CAME FROM IN ITS `.abide`. The emitters build a module
|
|
2
|
+
// as text around what `Lowering` hands them, so a chunk is placed by FINDING it in what they wrote
|
|
3
|
+
// rather than by threading offsets through every template string that assembles one:
|
|
4
|
+
//
|
|
5
|
+
// - a script block is many lines and lands whole, its imports each left as a `;` on as many lines
|
|
6
|
+
// (`splitImports`), so it is anchored by its longest line found once in the module and then read
|
|
7
|
+
// line for line;
|
|
8
|
+
// - a template expression is one line and may land more than once — a client module writes a slot
|
|
9
|
+
// in its markup and again in the effect that updates it — and every landing is the same source.
|
|
10
|
+
//
|
|
11
|
+
// What no chunk covers is the framework's own scaffolding, and maps to nothing.
|
|
12
|
+
|
|
13
|
+
import { encodeMappings, type Segment } from '#server/sourceMappings.ts'
|
|
14
|
+
import type { LoweredChunk } from './Lowering.ts'
|
|
15
|
+
import type { Origin } from './projectShadow.ts'
|
|
16
|
+
import { mapBack } from './ShadowMap.ts'
|
|
17
|
+
|
|
18
|
+
/** The module's `mappings`, into the `.abide` whose text is `source`. */
|
|
19
|
+
export function sourceMapOf(
|
|
20
|
+
output: string,
|
|
21
|
+
chunks: readonly LoweredChunk[],
|
|
22
|
+
projected: readonly Origin[],
|
|
23
|
+
source: string,
|
|
24
|
+
): string {
|
|
25
|
+
const sourceStarts = lineStarts(source)
|
|
26
|
+
const outputLines = output.split('\n')
|
|
27
|
+
const outputStarts = lineStarts(output)
|
|
28
|
+
const lines: Segment[][] = []
|
|
29
|
+
for (let index = 0; index < outputLines.length; index += 1) lines.push([])
|
|
30
|
+
// Where a projection offset is in the `.abide`, as a line and a column.
|
|
31
|
+
const place = (line: number, column: number, offset: number): void => {
|
|
32
|
+
const at = mapBack(projected, offset)
|
|
33
|
+
const sourceLine = lineAt(sourceStarts, at)
|
|
34
|
+
;(lines[line] as Segment[]).push([
|
|
35
|
+
column,
|
|
36
|
+
0,
|
|
37
|
+
sourceLine,
|
|
38
|
+
at - (sourceStarts[sourceLine] as number),
|
|
39
|
+
])
|
|
40
|
+
}
|
|
41
|
+
const claimed = new Set<number>()
|
|
42
|
+
|
|
43
|
+
const multiline: LoweredChunk[] = []
|
|
44
|
+
const single: LoweredChunk[] = []
|
|
45
|
+
for (const chunk of chunks)
|
|
46
|
+
(chunk.text.includes('\n') ? multiline : single).push(chunk)
|
|
47
|
+
multiline.sort((one, two) => two.text.length - one.text.length)
|
|
48
|
+
|
|
49
|
+
for (const chunk of multiline) {
|
|
50
|
+
const chunkLines = chunk.text.split('\n')
|
|
51
|
+
const first = anchor(chunkLines, outputLines, outputStarts, output)
|
|
52
|
+
if (first === undefined) continue
|
|
53
|
+
const chunkStarts = lineStarts(chunk.text)
|
|
54
|
+
for (let index = 0; index < chunkLines.length; index += 1) {
|
|
55
|
+
const line = first + index
|
|
56
|
+
const written = outputLines[line]
|
|
57
|
+
const lowered = chunkLines[index] as string
|
|
58
|
+
if (written === undefined || claimed.has(line)) continue
|
|
59
|
+
if (written.trim() !== lowered.trim() || lowered.trim() === '')
|
|
60
|
+
continue
|
|
61
|
+
claimed.add(line)
|
|
62
|
+
const start = chunkStarts[index] as number
|
|
63
|
+
const end = start + lowered.length
|
|
64
|
+
// Lines the emitter re-indented keep their text, and move by the margin it added.
|
|
65
|
+
const shift = written.length - lowered.length
|
|
66
|
+
for (const origin of chunk.origins) {
|
|
67
|
+
const runEnd = origin.at + Math.max(origin.length, 1)
|
|
68
|
+
if (origin.at >= end || runEnd <= start) continue
|
|
69
|
+
const from = Math.max(origin.at, start)
|
|
70
|
+
place(
|
|
71
|
+
line,
|
|
72
|
+
from - start + shift,
|
|
73
|
+
origin.source + (from - origin.at),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// LONGEST FIRST, each landing claimed: `count() + 1` is found again inside the handler
|
|
80
|
+
// `() => { count.set(count() + 1) }` was lowered from, and is that handler's there.
|
|
81
|
+
single.sort((one, two) => two.text.trim().length - one.text.trim().length)
|
|
82
|
+
const taken: [number, number][] = []
|
|
83
|
+
const seen = new Set<string>()
|
|
84
|
+
for (const chunk of single) {
|
|
85
|
+
const text = chunk.text.trim()
|
|
86
|
+
if (text.length < 2) continue
|
|
87
|
+
// An expression an emitter asked for twice is one chunk.
|
|
88
|
+
const key = `${chunk.origins[0]?.source}:${text}`
|
|
89
|
+
if (seen.has(key)) continue
|
|
90
|
+
seen.add(key)
|
|
91
|
+
const lead = chunk.text.indexOf(text)
|
|
92
|
+
for (
|
|
93
|
+
let found = output.indexOf(text);
|
|
94
|
+
found !== -1;
|
|
95
|
+
found = output.indexOf(text, found + text.length)
|
|
96
|
+
) {
|
|
97
|
+
const end = found + text.length
|
|
98
|
+
if (!standsAlone(output, found, end)) continue
|
|
99
|
+
const line = lineAt(outputStarts, found)
|
|
100
|
+
if (claimed.has(line)) continue
|
|
101
|
+
if (taken.some(([from, to]) => found < to && end > from)) continue
|
|
102
|
+
taken.push([found, end])
|
|
103
|
+
const column = found - (outputStarts[line] as number)
|
|
104
|
+
for (const origin of chunk.origins) {
|
|
105
|
+
const at = origin.at - lead
|
|
106
|
+
if (at < 0 || at > text.length) continue
|
|
107
|
+
place(line, column + at, origin.source)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Two runs placed at one column — a pin and the copy after it — keep the later, which is the
|
|
113
|
+
// author's own text.
|
|
114
|
+
for (const segments of lines) {
|
|
115
|
+
segments.sort((one, two) => one[0] - two[0])
|
|
116
|
+
for (let at = segments.length - 1; at > 0; at -= 1)
|
|
117
|
+
if (
|
|
118
|
+
(segments[at - 1] as Segment)[0] ===
|
|
119
|
+
(segments[at] as Segment)[0]
|
|
120
|
+
)
|
|
121
|
+
segments.splice(at - 1, 1)
|
|
122
|
+
}
|
|
123
|
+
return encodeMappings(lines)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The module line `chunkLines` starts on: its longest line found exactly once, standing where the
|
|
128
|
+
* rest of the chunk agrees — or `undefined` where no line places it.
|
|
129
|
+
*/
|
|
130
|
+
function anchor(
|
|
131
|
+
chunkLines: readonly string[],
|
|
132
|
+
outputLines: readonly string[],
|
|
133
|
+
outputStarts: readonly number[],
|
|
134
|
+
output: string,
|
|
135
|
+
): number | undefined {
|
|
136
|
+
const order: number[] = []
|
|
137
|
+
for (let index = 0; index < chunkLines.length; index += 1) order.push(index)
|
|
138
|
+
order.sort(
|
|
139
|
+
(one, two) =>
|
|
140
|
+
(chunkLines[two] as string).trim().length -
|
|
141
|
+
(chunkLines[one] as string).trim().length,
|
|
142
|
+
)
|
|
143
|
+
for (const index of order) {
|
|
144
|
+
const text = (chunkLines[index] as string).trim()
|
|
145
|
+
if (text.length < 8) return undefined
|
|
146
|
+
const found = output.indexOf(text)
|
|
147
|
+
if (found === -1 || output.indexOf(text, found + 1) !== -1) continue
|
|
148
|
+
const first = lineAt(outputStarts, found) - index
|
|
149
|
+
if (first < 0) continue
|
|
150
|
+
let agreeing = 0
|
|
151
|
+
let written = 0
|
|
152
|
+
for (let at = 0; at < chunkLines.length; at += 1) {
|
|
153
|
+
const lowered = (chunkLines[at] as string).trim()
|
|
154
|
+
if (lowered === '') continue
|
|
155
|
+
written += 1
|
|
156
|
+
if (outputLines[first + at]?.trim() === lowered) agreeing += 1
|
|
157
|
+
}
|
|
158
|
+
if (agreeing * 2 >= written) return first
|
|
159
|
+
}
|
|
160
|
+
return undefined
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const NAME = /[\w$.]/
|
|
164
|
+
|
|
165
|
+
// Not the middle of a longer name, or a member of one: `count()` inside `__count()` is not it.
|
|
166
|
+
function standsAlone(output: string, start: number, end: number): boolean {
|
|
167
|
+
return (
|
|
168
|
+
!NAME.test(output[start - 1] ?? ' ') &&
|
|
169
|
+
!/[\w$]/.test(output[end] ?? ' ')
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function lineStarts(text: string): number[] {
|
|
174
|
+
const starts = [0]
|
|
175
|
+
for (
|
|
176
|
+
let at = text.indexOf('\n');
|
|
177
|
+
at !== -1;
|
|
178
|
+
at = text.indexOf('\n', at + 1)
|
|
179
|
+
)
|
|
180
|
+
starts.push(at + 1)
|
|
181
|
+
return starts
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function lineAt(starts: readonly number[], offset: number): number {
|
|
185
|
+
let low = 0
|
|
186
|
+
let high = starts.length - 1
|
|
187
|
+
while (low < high) {
|
|
188
|
+
const middle = (low + high + 1) >>> 1
|
|
189
|
+
if ((starts[middle] as number) <= offset) low = middle
|
|
190
|
+
else high = middle - 1
|
|
191
|
+
}
|
|
192
|
+
return low
|
|
193
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SyntaxKind } from 'typescript/unstable/ast'
|
|
2
|
+
import { COMPONENT_QUERY, TEXT_QUERY } from '#server/MODULE_QUERIES.ts'
|
|
2
3
|
import { isContextualKeyword, type Token, tokenize } from './tokenize.ts'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -11,13 +12,12 @@ import { isContextualKeyword, type Token, tokenize } from './tokenize.ts'
|
|
|
11
12
|
* WHERE AN IMPORT BEGINS AND ENDS IS READ OFF the one tokenizer, which carries the template
|
|
12
13
|
* re-scan a bare `` ` `` needs, so an import after a template literal is still found.
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
15
|
+
* Every specifier naming a face of a `.abide` is rewritten where it stands, a static import's and an
|
|
16
|
+
* `import('…')`'s alike (`facedSpecifier`) — so the two cannot ask for different faces.
|
|
16
17
|
*/
|
|
17
18
|
export function splitImports(
|
|
18
19
|
script: string,
|
|
19
|
-
lane:
|
|
20
|
-
landed?: ReadonlyMap<string, string>,
|
|
20
|
+
lane: Lane = 'component',
|
|
21
21
|
): {
|
|
22
22
|
imports: string
|
|
23
23
|
body: string
|
|
@@ -29,18 +29,13 @@ export function splitImports(
|
|
|
29
29
|
let body = ''
|
|
30
30
|
let at = 0
|
|
31
31
|
for (const span of spans) {
|
|
32
|
+
const moved = facedSpecifier(span.specifier, lane)
|
|
32
33
|
if (span.dynamic) {
|
|
33
|
-
if (landed === undefined) continue
|
|
34
|
-
const moved = landedSpecifier(span.specifier, lane, landed)
|
|
35
34
|
if (moved === undefined) continue
|
|
36
35
|
body += `${script.slice(at, span.literalStart + 1)}${moved}`
|
|
37
36
|
at = span.end - 1
|
|
38
37
|
continue
|
|
39
38
|
}
|
|
40
|
-
const moved =
|
|
41
|
-
landed === undefined
|
|
42
|
-
? undefined
|
|
43
|
-
: landedSpecifier(span.specifier, lane, landed)
|
|
44
39
|
imports +=
|
|
45
40
|
moved === undefined
|
|
46
41
|
? `${script.slice(span.start, span.end)}\n`
|
|
@@ -50,7 +45,9 @@ export function splitImports(
|
|
|
50
45
|
// a `[` that automatic semicolon insertion does NOT break — it reads as `f()[2]`, a
|
|
51
46
|
// different program with nothing about it saying so. The empty statement left in its
|
|
52
47
|
// place is the separator the import was.
|
|
53
|
-
|
|
48
|
+
// 38.97 — and as many lines as it spanned, so the body stays line for line with the
|
|
49
|
+
// script it came from and a source map can read the one off the other.
|
|
50
|
+
body += `${script.slice(at, span.start)};${'\n'.repeat(newlinesIn(script, span.start, span.end))}`
|
|
54
51
|
at = span.end
|
|
55
52
|
// The `;` an import may end with would otherwise be left behind beside the one above.
|
|
56
53
|
if (script[at] === ';') at += 1
|
|
@@ -58,6 +55,18 @@ export function splitImports(
|
|
|
58
55
|
return { imports, body: body + script.slice(at) }
|
|
59
56
|
}
|
|
60
57
|
|
|
58
|
+
/** How many line breaks `text` holds between `start` and `end`. */
|
|
59
|
+
export function newlinesIn(text: string, start: number, end: number): number {
|
|
60
|
+
let count = 0
|
|
61
|
+
for (
|
|
62
|
+
let at = text.indexOf('\n', start);
|
|
63
|
+
at !== -1 && at < end;
|
|
64
|
+
at = text.indexOf('\n', at + 1)
|
|
65
|
+
)
|
|
66
|
+
count += 1
|
|
67
|
+
return count
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
// What can stand between `import` and its specifier. A token that cannot is a malformed import,
|
|
62
71
|
// and stopping there is what keeps one broken line from swallowing the statements after it.
|
|
63
72
|
//
|
|
@@ -252,7 +261,14 @@ export function abideImports(
|
|
|
252
261
|
const found: { specifier: string; closingQuote: number }[] = []
|
|
253
262
|
if (!script.includes('.abide')) return found
|
|
254
263
|
for (const span of importSpans(script))
|
|
255
|
-
if (
|
|
264
|
+
if (
|
|
265
|
+
span.specifier.endsWith('.abide') &&
|
|
266
|
+
// 34.25 — a `.abide` imported as text is its text, and no component edge.
|
|
267
|
+
(span.dynamic ||
|
|
268
|
+
!IMPORT_ATTRIBUTES.text.test(
|
|
269
|
+
script.slice(span.literalEnd, span.end),
|
|
270
|
+
))
|
|
271
|
+
)
|
|
256
272
|
found.push({
|
|
257
273
|
specifier: span.specifier,
|
|
258
274
|
closingQuote: span.literalEnd - 1,
|
|
@@ -260,40 +276,167 @@ export function abideImports(
|
|
|
260
276
|
return found
|
|
261
277
|
}
|
|
262
278
|
|
|
279
|
+
/** One name an import statement binds. */
|
|
280
|
+
export type ImportBinding = {
|
|
281
|
+
/** `default`, `*`, or the name imported — a string literal's text where one is written. */
|
|
282
|
+
imported: string
|
|
283
|
+
local: string
|
|
284
|
+
/** Under a `type` modifier, on the binding or on the whole statement: no value of its own. */
|
|
285
|
+
typeOnly: boolean
|
|
286
|
+
/** The binding as a statement's clause spells it, its own modifier kept: `type a as b`. */
|
|
287
|
+
written: string
|
|
288
|
+
}
|
|
289
|
+
|
|
263
290
|
/**
|
|
264
|
-
* Every
|
|
265
|
-
* a
|
|
291
|
+
* Every static import statement a script makes: where it stands, what it names, and each binding,
|
|
292
|
+
* read off the one tokenizer — so a comment or a line break inside the clause is trivia, as it is
|
|
293
|
+
* to bun, and never part of a name.
|
|
266
294
|
*
|
|
267
|
-
*
|
|
295
|
+
* `attributes` is the statement's text after its specifier, a `with { … }` or nothing. A statement of
|
|
296
|
+
* no shape this reads answers no bindings.
|
|
268
297
|
*/
|
|
269
|
-
export function
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
298
|
+
export function importStatements(script: string): {
|
|
299
|
+
start: number
|
|
300
|
+
end: number
|
|
301
|
+
specifier: string
|
|
302
|
+
typeOnly: boolean
|
|
303
|
+
attributes: string
|
|
304
|
+
bindings: ImportBinding[]
|
|
305
|
+
}[] {
|
|
306
|
+
const found: ReturnType<typeof importStatements> = []
|
|
307
|
+
if (!script.includes('import')) return found
|
|
308
|
+
for (const span of importSpans(script)) {
|
|
309
|
+
if (span.dynamic || !script.startsWith('import', span.start)) continue
|
|
310
|
+
const tokens = tokenize(script.slice(span.start, span.literalStart))
|
|
311
|
+
const bindings: ImportBinding[] = []
|
|
312
|
+
let at = 1
|
|
313
|
+
// `type` modifies the statement unless it is the default's own name: `import type from`.
|
|
314
|
+
const typeOnly =
|
|
315
|
+
tokens[at]?.text === 'type' &&
|
|
316
|
+
tokens[at + 1] !== undefined &&
|
|
317
|
+
tokens[at + 1]?.kind !== SyntaxKind.FromKeyword &&
|
|
318
|
+
tokens[at + 1]?.kind !== SyntaxKind.CommaToken
|
|
319
|
+
if (typeOnly) at += 1
|
|
320
|
+
const head = tokens[at]
|
|
273
321
|
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
322
|
+
head !== undefined &&
|
|
323
|
+
head.kind !== SyntaxKind.OpenBraceToken &&
|
|
324
|
+
head.kind !== SyntaxKind.AsteriskToken &&
|
|
325
|
+
(tokens[at + 1]?.kind === SyntaxKind.FromKeyword ||
|
|
326
|
+
tokens[at + 1]?.kind === SyntaxKind.CommaToken)
|
|
327
|
+
) {
|
|
328
|
+
bindings.push({
|
|
329
|
+
imported: 'default',
|
|
330
|
+
local: head.text,
|
|
331
|
+
typeOnly,
|
|
332
|
+
written: head.text,
|
|
333
|
+
})
|
|
334
|
+
at += tokens[at + 1]?.kind === SyntaxKind.CommaToken ? 2 : 1
|
|
335
|
+
}
|
|
336
|
+
if (tokens[at]?.kind === SyntaxKind.AsteriskToken) {
|
|
337
|
+
const local = tokens[at + 2]?.text ?? ''
|
|
338
|
+
bindings.push({
|
|
339
|
+
imported: '*',
|
|
340
|
+
local,
|
|
341
|
+
typeOnly,
|
|
342
|
+
written: `* as ${local}`,
|
|
343
|
+
})
|
|
344
|
+
} else if (tokens[at]?.kind === SyntaxKind.OpenBraceToken) {
|
|
345
|
+
// Each element between the braces: `[type] imported [as local]`.
|
|
346
|
+
let element: Token[] = []
|
|
347
|
+
for (at += 1; at < tokens.length; at += 1) {
|
|
348
|
+
const token = tokens[at] as Token
|
|
349
|
+
const closed = token.kind === SyntaxKind.CloseBraceToken
|
|
350
|
+
if (closed || token.kind === SyntaxKind.CommaToken) {
|
|
351
|
+
// `type as t` imports `type`; `type x` and `type x as y` modify.
|
|
352
|
+
const modified =
|
|
353
|
+
element[0]?.text === 'type' &&
|
|
354
|
+
(element.length === 2 || element.length === 4)
|
|
355
|
+
const name = element[modified ? 1 : 0]
|
|
356
|
+
if (name !== undefined) {
|
|
357
|
+
const imported =
|
|
358
|
+
name.kind === SyntaxKind.StringLiteral
|
|
359
|
+
? name.text.slice(1, -1)
|
|
360
|
+
: name.text
|
|
361
|
+
let written = ''
|
|
362
|
+
for (const part of element)
|
|
363
|
+
written +=
|
|
364
|
+
written === '' ? part.text : ` ${part.text}`
|
|
365
|
+
bindings.push({
|
|
366
|
+
imported,
|
|
367
|
+
local: (element.at(-1) as Token).text,
|
|
368
|
+
typeOnly: typeOnly || modified,
|
|
369
|
+
written,
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
element = []
|
|
373
|
+
if (closed) break
|
|
374
|
+
} else element.push(token)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
found.push({
|
|
378
|
+
start: span.start,
|
|
379
|
+
end: span.end,
|
|
380
|
+
specifier: span.specifier,
|
|
381
|
+
typeOnly,
|
|
382
|
+
attributes: script.slice(span.literalEnd, span.end).trim(),
|
|
383
|
+
bindings,
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
return found
|
|
280
387
|
}
|
|
281
388
|
|
|
389
|
+
// The attribute a static import is read by: as text (34.25), or as a mount of routes (23.19).
|
|
390
|
+
export const IMPORT_ATTRIBUTES = {
|
|
391
|
+
text: /\btype\s*:\s*['"]text['"]/,
|
|
392
|
+
routes: /\btype\s*:\s*['"]routes['"]/,
|
|
393
|
+
} as const
|
|
394
|
+
|
|
282
395
|
/**
|
|
283
|
-
* Every import
|
|
396
|
+
* Every static import a script makes `with { type }`: the specifier, where the statement and its
|
|
397
|
+
* literal begin, and where the attributes end.
|
|
284
398
|
*
|
|
285
|
-
*
|
|
399
|
+
* 34.25 — a text import's literal-to-end span is what the projection writes over with the specifier
|
|
400
|
+
* the checker types a `string` (`SOURCE_SUFFIX`), and the emitters ask for as text
|
|
401
|
+
* (`facedSpecifier`). 23.19 — a routes import's whole statement is what the projection blanks: a
|
|
402
|
+
* mount is read by the build off the file, and neither bun nor the checker has a module to give it.
|
|
286
403
|
*/
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
404
|
+
export function attributedImports(
|
|
405
|
+
script: string,
|
|
406
|
+
type: keyof typeof IMPORT_ATTRIBUTES,
|
|
407
|
+
): ImportSpan[] {
|
|
408
|
+
const found: ImportSpan[] = []
|
|
409
|
+
if (!script.includes(type)) return found
|
|
410
|
+
const attribute = IMPORT_ATTRIBUTES[type]
|
|
290
411
|
for (const span of importSpans(script))
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return
|
|
412
|
+
if (
|
|
413
|
+
!span.dynamic &&
|
|
414
|
+
attribute.test(script.slice(span.literalEnd, span.end))
|
|
415
|
+
)
|
|
416
|
+
found.push(span)
|
|
417
|
+
return found
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** What a file imported as text is written as once projected: `./X.abide` → `./X.abide.source`. */
|
|
421
|
+
export const SOURCE_SUFFIX = '.source'
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* `source` with each module specifier — of a declaration, an `export … from` or an `import('…')` —
|
|
425
|
+
* written as `rewrite` answers for it, and left as it is where `rewrite` answers `undefined`.
|
|
426
|
+
*/
|
|
427
|
+
export function respecified(
|
|
428
|
+
source: string,
|
|
429
|
+
rewrite: (specifier: string) => string | undefined,
|
|
430
|
+
): string {
|
|
431
|
+
let out = ''
|
|
432
|
+
let at = 0
|
|
433
|
+
for (const span of importSpans(source)) {
|
|
434
|
+
const written = rewrite(span.specifier)
|
|
435
|
+
if (written === undefined) continue
|
|
436
|
+
out += `${source.slice(at, span.literalStart + 1)}${written}`
|
|
437
|
+
at = span.literalEnd - 1
|
|
438
|
+
}
|
|
439
|
+
return at === 0 ? source : out + source.slice(at)
|
|
297
440
|
}
|
|
298
441
|
|
|
299
442
|
/**
|
|
@@ -305,62 +448,41 @@ export function importedNames(
|
|
|
305
448
|
script: string,
|
|
306
449
|
): { name: string; specifier: string }[] {
|
|
307
450
|
const names: { name: string; specifier: string }[] = []
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
for (let at = 1; at < last; at += 1) {
|
|
316
|
-
const token = tokens[at] as Token
|
|
317
|
-
if (token.kind === SyntaxKind.FromKeyword) break
|
|
318
|
-
if (token.kind === SyntaxKind.AsKeyword) continue
|
|
319
|
-
if (
|
|
320
|
-
token.kind !== SyntaxKind.Identifier &&
|
|
321
|
-
!isContextualKeyword(token.kind)
|
|
322
|
-
)
|
|
323
|
-
continue
|
|
324
|
-
const next = tokens[at + 1] as Token
|
|
325
|
-
// A name followed by `as` is renamed, and a modifier is followed by what it modifies.
|
|
326
|
-
if (next.kind === SyntaxKind.AsKeyword) continue
|
|
327
|
-
if (
|
|
328
|
-
token.text === 'type' &&
|
|
329
|
-
(next.kind === SyntaxKind.Identifier ||
|
|
330
|
-
next.kind === SyntaxKind.OpenBraceToken ||
|
|
331
|
-
next.kind === SyntaxKind.AsteriskToken ||
|
|
332
|
-
isContextualKeyword(next.kind))
|
|
333
|
-
)
|
|
334
|
-
continue
|
|
335
|
-
names.push({ name: token.text, specifier })
|
|
336
|
-
}
|
|
337
|
-
}
|
|
451
|
+
// `import …` alone — an `export … from` binds nothing in the script it stands in.
|
|
452
|
+
for (const declaration of importStatements(script))
|
|
453
|
+
for (const binding of declaration.bindings)
|
|
454
|
+
names.push({
|
|
455
|
+
name: binding.local,
|
|
456
|
+
specifier: declaration.specifier,
|
|
457
|
+
})
|
|
338
458
|
return names
|
|
339
459
|
}
|
|
340
460
|
|
|
341
461
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
462
|
+
* The face of a `.abide` a specifier asks for, spelled on it (`MODULE_QUERIES`), or `undefined`
|
|
463
|
+
* where the specifier stays as written.
|
|
344
464
|
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* WHERE that module is, is the build's: `landed` maps a specifier to the path, relative and
|
|
348
|
-
* without an extension, the build resolved it to (`componentGraph`) — and a `.abide` specifier it
|
|
349
|
-
* does not map keeps the path as written.
|
|
465
|
+
* 34.14 — EVERY OTHER SPECIFIER STAYS AS WRITTEN: the compiled module is served at its `.abide`'s
|
|
466
|
+
* own path, so bun resolves a relative, a `#` or a bare specifier from the file that wrote it.
|
|
350
467
|
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
468
|
+
* 32.17 — a component block's `.abide` import is of the module that file compiles to; a server
|
|
469
|
+
* block's stays bare, for the face a module abide did not compile gets (34.16). 34.25 — a file the
|
|
470
|
+
* projection marked as text is asked for as its text.
|
|
353
471
|
*/
|
|
354
|
-
function
|
|
355
|
-
specifier
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return `${landed.get(specifier) ?? specifier.slice(0, -'.abide'.length)}${lane}.ts`
|
|
361
|
-
return landed.get(specifier)
|
|
472
|
+
function facedSpecifier(specifier: string, lane: Lane): string | undefined {
|
|
473
|
+
if (specifier.endsWith(SOURCE_SUFFIX))
|
|
474
|
+
return `${specifier.slice(0, -SOURCE_SUFFIX.length)}${TEXT_QUERY}`
|
|
475
|
+
if (lane === 'component' && specifier.endsWith('.abide'))
|
|
476
|
+
return `${specifier}${COMPONENT_QUERY}`
|
|
477
|
+
return undefined
|
|
362
478
|
}
|
|
363
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Which face a block's imports are written for: a component's — its `<script>` and
|
|
482
|
+
* `<script module>`, in either lane — or a server block's, whose `.abide` imports stay bare (34.16).
|
|
483
|
+
*/
|
|
484
|
+
export type Lane = 'component' | 'source'
|
|
485
|
+
|
|
364
486
|
/**
|
|
365
487
|
* `imports` with `props` dropped from the abide import that names it.
|
|
366
488
|
*
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// 34.24 — THE TYPES A PLAIN `.ts` READS A `.abide` BY, written with no checker and no build: the
|
|
2
|
+
// files parsed and projected, and nothing else. What `abide sync` runs — and the scaffold's `prepare`
|
|
3
|
+
// with it, so a project is typed as soon as it is installed — and what the language server runs as
|
|
4
|
+
// a file is edited.
|
|
5
|
+
|
|
6
|
+
import { sourcesUnder } from './compileApp.ts'
|
|
7
|
+
import { parseSources } from './readSources.ts'
|
|
8
|
+
import type { Diagnosis } from './TypeSession.ts'
|
|
9
|
+
import { writeTypes } from './writeTypes.ts'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Writes the types of every `.abide` under `appRoot`, `held` standing in for the text on disk of
|
|
13
|
+
* the files an editor has open; answers how many files it typed and what it refused. A file that
|
|
14
|
+
* does not parse, or whose projection is refused, is left as it last was and the rest are typed.
|
|
15
|
+
*/
|
|
16
|
+
export async function syncTypes(
|
|
17
|
+
appRoot: string,
|
|
18
|
+
held: ReadonlyMap<string, string> = new Map(),
|
|
19
|
+
): Promise<{ typed: number; refused: Diagnosis[] }> {
|
|
20
|
+
const { root, sources } = await sourcesUnder(appRoot)
|
|
21
|
+
const read: { path: string; text: string }[] = []
|
|
22
|
+
for (const source of sources)
|
|
23
|
+
read.push({
|
|
24
|
+
path: source.path,
|
|
25
|
+
text: held.get(source.path) ?? source.text,
|
|
26
|
+
})
|
|
27
|
+
const refused: Diagnosis[] = []
|
|
28
|
+
const parsed = parseSources(read, refused)
|
|
29
|
+
const unparsed = refused.length
|
|
30
|
+
await writeTypes(root, parsed, true, refused)
|
|
31
|
+
return { typed: parsed.length - (refused.length - unparsed), refused }
|
|
32
|
+
}
|
package/src/compiler/tokenize.ts
CHANGED
|
@@ -31,6 +31,20 @@ export function tokenize(source: string): Token[] {
|
|
|
31
31
|
return tokens
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Whether a token of the code scanned from `from` begins at `at` — false where `at` is inside a
|
|
36
|
+
* string, a template, a regex or a comment.
|
|
37
|
+
*/
|
|
38
|
+
export function startsToken(source: string, from: number, at: number): boolean {
|
|
39
|
+
let found = false
|
|
40
|
+
scan(source, from, (_kind, scanner) => {
|
|
41
|
+
const start = scanner.getTokenStart()
|
|
42
|
+
found = start === at
|
|
43
|
+
return start < at
|
|
44
|
+
})
|
|
45
|
+
return found
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
/**
|
|
35
49
|
* How far `kind` moves the bracket depth a top-level separator is found at: a template's
|
|
36
50
|
* substitutions nest as a call's arguments do, so `${a, b}` holds its comma.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 38.
|
|
1
|
+
// 38.30 — AN APP WITH NO tsconfig.json IS TYPED AGAINST ONE OF ABIDE'S, not read untyped. The checker
|
|
2
2
|
// is what tells the compiler which names are `Reactive`s (31.3), and with no project to ask every
|
|
3
3
|
// template position read as static: the server painted an unlanded read by throwing out of the
|
|
4
4
|
// render (41.4), the client never bound a slot at all, and `props` went unseen — an app that built
|