@abide/abide 0.103.0 → 0.105.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/bin/abide.ts +80 -173
- package/docs/REGISTRY.md +27 -27
- package/docs/RULEBOOK.md +68 -24
- package/docs/abide.md +31 -46
- package/package.json +1 -1
- package/src/compiler/Lowering.ts +15 -5
- package/src/compiler/Refusals.ts +57 -0
- package/src/compiler/TypeSession.ts +46 -89
- package/src/compiler/appLayout.ts +447 -0
- package/src/compiler/appMap.ts +21 -25
- package/src/compiler/checkApp.ts +61 -97
- package/src/compiler/checkedShadow.ts +12 -10
- package/src/compiler/classifyRead.ts +3 -6
- package/src/compiler/compileApp.ts +183 -478
- package/src/compiler/componentGraph.ts +2 -71
- package/src/compiler/declaredExports.ts +79 -60
- package/src/compiler/emitClient.ts +11 -13
- package/src/compiler/emitComponent.ts +21 -6
- package/src/compiler/emitSource.ts +20 -20
- package/src/compiler/handlersUnder.ts +46 -36
- package/src/compiler/hookCalls.ts +152 -0
- package/src/compiler/mountsOf.ts +31 -54
- package/src/compiler/projectShadow.ts +1 -1
- package/src/compiler/readSources.ts +27 -42
- package/src/compiler/readsIn.ts +5 -16
- package/src/compiler/resolverIn.ts +77 -0
- package/src/server/abideModules.ts +152 -146
- package/src/server/answerRequest.ts +18 -4
- package/src/server/answerSocket.ts +136 -76
- package/src/server/answerValue.ts +28 -15
- package/src/server/appTree.ts +59 -73
- package/src/server/claudeApi.ts +2 -5
- package/src/server/clientBundle.ts +198 -0
- package/src/server/coerceArgs.ts +1 -1
- package/src/server/commands.ts +93 -57
- package/src/server/createApp.ts +90 -16
- package/src/server/deferredBuilds.ts +175 -0
- package/src/server/devWorker.ts +107 -319
- package/src/server/faceOf.ts +120 -0
- package/src/server/fenceServer.ts +10 -18
- package/src/server/generatedSurfaces.ts +12 -53
- package/src/server/index.ts +3 -0
- package/src/server/placement.ts +167 -0
- package/src/server/remote.ts +14 -42
- package/src/server/render.ts +25 -17
- package/src/server/renderComponent.ts +9 -5
- package/src/server/requestScope.ts +10 -4
- package/src/server/responseRelay.ts +389 -0
- package/src/server/runCompiled.ts +9 -12
- package/src/server/runDesktop.ts +6 -9
- package/src/server/serveBuild.ts +26 -73
- package/src/server/serveDev.ts +19 -176
- package/src/server/streamEvent.ts +29 -0
- package/src/server/versionOf.ts +1 -2
- package/src/server/withAbideSources.ts +2 -1
- package/src/shared/HandlerRoute.ts +68 -0
- package/src/shared/REFUSAL_KEY.ts +5 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +6 -0
- package/src/shared/answerOf.ts +105 -78
- package/src/shared/filledPath.ts +1 -9
- package/src/shared/framing.ts +127 -128
- package/src/shared/queryText.ts +15 -7
- package/src/shared/reactive/Cursor.ts +69 -124
- package/src/shared/reactive/Reactive.ts +296 -162
- package/src/shared/reactive/callLoad.ts +4 -18
- package/src/shared/reactive/index.ts +11 -8
- package/src/shared/reactive/pairReactive.ts +1 -1
- package/src/shared/reactive/scoped.ts +4 -3
- package/src/shared/reactive/share.ts +11 -1
- package/src/shared/routePattern.ts +0 -110
- package/src/shared/rpc.ts +17 -24
- package/src/shared/rpcRequest.ts +18 -47
- package/src/shared/takeEarlyCall.ts +12 -37
- package/src/shared/url.ts +1 -1
- package/src/ui/Level.ts +276 -0
- package/src/ui/List.ts +17 -17
- package/src/ui/Router.ts +20 -87
- package/src/ui/applyHead.ts +92 -79
- package/src/ui/index.ts +4 -2
- package/src/ui/reconcile.ts +4 -110
- package/src/ui/rows.ts +123 -2
- package/src/ui/wire.ts +12 -27
- package/src/compiler/hooksCalled.ts +0 -35
- package/src/server/clientBuildOptions.ts +0 -54
- package/src/server/refuseAddressCollision.ts +0 -37
- package/src/shared/reactive/chunksOfStream.ts +0 -57
- package/src/ui/MOUNTING_LEVEL.ts +0 -18
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
|
|
2
|
+
import { importStatements } from './splitImports.ts'
|
|
3
|
+
import { type Token, tokenize } from './tokenize.ts'
|
|
4
|
+
|
|
5
|
+
// WHICH OF ABIDE'S HOOKS A MODULE CALLS, read off its tokens and its imports — not run. What `abide
|
|
6
|
+
// routes` lists (38.108), what the build refuses where a call is never read (37.13, 38.107), and
|
|
7
|
+
// which files the checker types `Config` off (27.13) all ask this one question.
|
|
8
|
+
|
|
9
|
+
/** Every hook abide exports, by name. */
|
|
10
|
+
export const HOOK_NAMES: ReadonlySet<string> = new Set([
|
|
11
|
+
'middleware',
|
|
12
|
+
'onStart',
|
|
13
|
+
'onStop',
|
|
14
|
+
'onError',
|
|
15
|
+
'onConfig',
|
|
16
|
+
'onHealth',
|
|
17
|
+
'onPrincipal',
|
|
18
|
+
'onLauncher',
|
|
19
|
+
'onMenu',
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/** One call of one of abide's hooks. */
|
|
23
|
+
export type HookCall = {
|
|
24
|
+
hook: string
|
|
25
|
+
/** Where the callee starts — the hook's name, or the namespace it is a member of. */
|
|
26
|
+
at: number
|
|
27
|
+
/** Where the callee ends: the `(` follows. */
|
|
28
|
+
end: number
|
|
29
|
+
/** Whether the call stands as a statement at the module's top level. */
|
|
30
|
+
topLevel: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What `scripts`' imports from abide bind: each local name, a value's, to the name it imports — `*`
|
|
35
|
+
* for a namespace. Every script of one file, since a `<script module>`'s import reaches the blocks
|
|
36
|
+
* below it.
|
|
37
|
+
*/
|
|
38
|
+
export function abideBindings(
|
|
39
|
+
scripts: readonly string[],
|
|
40
|
+
): ReadonlyMap<string, string> {
|
|
41
|
+
const bound = new Map<string, string>()
|
|
42
|
+
for (const script of scripts) {
|
|
43
|
+
if (!script.includes(SHARED_SPECIFIER)) continue
|
|
44
|
+
for (const statement of importStatements(script))
|
|
45
|
+
if (isAbideSpecifier(statement.specifier))
|
|
46
|
+
for (const binding of statement.bindings)
|
|
47
|
+
if (!binding.typeOnly)
|
|
48
|
+
bound.set(binding.local, binding.imported)
|
|
49
|
+
}
|
|
50
|
+
return bound
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Each of abide's hooks `scripts` call — every script of one file — in the order written. Block by
|
|
55
|
+
* block: one block's last token would decide how the next one's first reads.
|
|
56
|
+
*/
|
|
57
|
+
export function hooksIn(scripts: readonly string[]): string[] {
|
|
58
|
+
const bindings = abideBindings(scripts)
|
|
59
|
+
const hooks: string[] = []
|
|
60
|
+
for (const script of scripts)
|
|
61
|
+
for (const call of hookCalls(script, bindings)) hooks.push(call.hook)
|
|
62
|
+
return hooks
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Each of abide's hooks `source` calls, in the order written.
|
|
67
|
+
*
|
|
68
|
+
* A HOOK IS WHAT ABIDE HANDED THE MODULE: a name `bindings` imports from abide, under whatever local
|
|
69
|
+
* name, or a member of a namespace it does — `server.onStart(…)` is the hook, and `emitter.onError(…)`
|
|
70
|
+
* in a module importing abide for `reactive` is not.
|
|
71
|
+
*/
|
|
72
|
+
export function hookCalls(
|
|
73
|
+
source: string,
|
|
74
|
+
bindings: ReadonlyMap<string, string> = abideBindings([source]),
|
|
75
|
+
): HookCall[] {
|
|
76
|
+
const calls: HookCall[] = []
|
|
77
|
+
// Untokenized where no name a hook is bound to — itself, or the namespace holding it — is in it.
|
|
78
|
+
let mentioned = false
|
|
79
|
+
for (const [local, imported] of bindings)
|
|
80
|
+
if (
|
|
81
|
+
(imported === '*' || HOOK_NAMES.has(imported)) &&
|
|
82
|
+
source.includes(local)
|
|
83
|
+
) {
|
|
84
|
+
mentioned = true
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
if (!mentioned) return calls
|
|
88
|
+
const tokens = tokenize(source)
|
|
89
|
+
let depth = 0
|
|
90
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
91
|
+
const token = tokens[index] as Token
|
|
92
|
+
const text = token.text
|
|
93
|
+
if (text === '(' || text === '[' || text === '{') depth += 1
|
|
94
|
+
else if (text === ')' || text === ']' || text === '}') depth -= 1
|
|
95
|
+
if (tokens[index + 1]?.text !== '(') continue
|
|
96
|
+
const previous = tokens[index - 1]
|
|
97
|
+
let hook: string | undefined
|
|
98
|
+
let first = index
|
|
99
|
+
if (previous?.text === '.') {
|
|
100
|
+
const owner = tokens[index - 2]
|
|
101
|
+
if (
|
|
102
|
+
HOOK_NAMES.has(text) &&
|
|
103
|
+
owner !== undefined &&
|
|
104
|
+
bindings.get(owner.text) === '*' &&
|
|
105
|
+
tokens[index - 3]?.text !== '.'
|
|
106
|
+
) {
|
|
107
|
+
hook = text
|
|
108
|
+
first = index - 2
|
|
109
|
+
}
|
|
110
|
+
} else if (previous?.text !== 'function') {
|
|
111
|
+
const imported = bindings.get(text)
|
|
112
|
+
if (imported !== undefined && HOOK_NAMES.has(imported))
|
|
113
|
+
hook = imported
|
|
114
|
+
}
|
|
115
|
+
if (hook === undefined) continue
|
|
116
|
+
const callee = tokens[first] as Token
|
|
117
|
+
calls.push({
|
|
118
|
+
hook,
|
|
119
|
+
at: callee.start,
|
|
120
|
+
end: token.end,
|
|
121
|
+
topLevel: depth === 0 && opensStatement(source, tokens, first),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
return calls
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Whether the token at `index` begins a statement: the first, after a `;` or a `}`, or on a line of
|
|
128
|
+
// its own after one that ended an expression — where a formatted module writes one.
|
|
129
|
+
function opensStatement(
|
|
130
|
+
source: string,
|
|
131
|
+
tokens: readonly Token[],
|
|
132
|
+
index: number,
|
|
133
|
+
): boolean {
|
|
134
|
+
const previous = tokens[index - 1]
|
|
135
|
+
if (previous === undefined) return true
|
|
136
|
+
if (previous.text === ';' || previous.text === '}') return true
|
|
137
|
+
return (
|
|
138
|
+
source
|
|
139
|
+
.slice(previous.end, (tokens[index] as Token).start)
|
|
140
|
+
.includes('\n') && ENDS_EXPRESSION.test(previous.text)
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const ENDS_EXPRESSION = /^(?:[\w$]+|[)\]]|['"`].*)$/s
|
|
145
|
+
|
|
146
|
+
/** `@abide/abide` or one of its subpaths. */
|
|
147
|
+
export function isAbideSpecifier(specifier: string): boolean {
|
|
148
|
+
return (
|
|
149
|
+
specifier === SHARED_SPECIFIER ||
|
|
150
|
+
specifier.startsWith(`${SHARED_SPECIFIER}/`)
|
|
151
|
+
)
|
|
152
|
+
}
|
package/src/compiler/mountsOf.ts
CHANGED
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
//
|
|
7
7
|
// A FILE KEEPS ITS PATH AND GAINS A POSITION. It is compiled, served and keyed by where it lies, as
|
|
8
8
|
// any dependency's is (34.23); its POSITION — the path it would have below the root — is what
|
|
9
|
-
// `
|
|
9
|
+
// `placement` reads, so a mounted page is a page by the same test as the app's.
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { realpathSync, statSync } from 'node:fs' // bun exposes no sync stat or realpath of its own
|
|
12
12
|
import { dirname, relative, resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
13
13
|
import { appFiles } from '#server/appTree.ts'
|
|
14
|
-
import { placeOf } from '#
|
|
14
|
+
import { placeOf } from '#server/placement.ts'
|
|
15
15
|
import { nearest } from './nearest.ts'
|
|
16
|
-
import { type AbideFile,
|
|
17
|
-
import {
|
|
16
|
+
import { type AbideFile, scriptBodies } from './parseAbide.ts'
|
|
17
|
+
import { Refusals } from './Refusals.ts'
|
|
18
|
+
import { attributedImports } from './splitImports.ts'
|
|
18
19
|
|
|
19
20
|
/** A `.abide` on disk, and the position it takes in the app. */
|
|
20
21
|
type Placed = { path: string; position: string }
|
|
@@ -33,8 +34,8 @@ type DirectoryMount = {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
* Every directory the file at `path` mounts, its position in the app being `position` — or
|
|
37
|
-
*
|
|
37
|
+
* Every directory the file at `path` mounts, its position in the app being `position` — or none,
|
|
38
|
+
* where one of them cannot be mounted and `refusals` is told what.
|
|
38
39
|
*
|
|
39
40
|
* `root` is the app's, real: a directory under it is the app's own already, with a position of its
|
|
40
41
|
* own, and mounting it would give its files a second (23.20).
|
|
@@ -44,7 +45,8 @@ export function mountsOf(
|
|
|
44
45
|
path: string,
|
|
45
46
|
position: string,
|
|
46
47
|
file: AbideFile,
|
|
47
|
-
|
|
48
|
+
refusals: Refusals,
|
|
49
|
+
): DirectoryMount[] {
|
|
48
50
|
const mounts: DirectoryMount[] = []
|
|
49
51
|
const at = position.includes('/')
|
|
50
52
|
? `${position.slice(0, position.lastIndexOf('/'))}/`
|
|
@@ -52,12 +54,27 @@ export function mountsOf(
|
|
|
52
54
|
for (const script of scriptBodies(file))
|
|
53
55
|
for (const { specifier } of attributedImports(script, 'routes')) {
|
|
54
56
|
const directory = mountedDirectory(specifier, dirname(path))
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// A file that mounts what it may not mounts nothing.
|
|
58
|
+
if (directory === undefined) {
|
|
59
|
+
refusals.at(
|
|
60
|
+
path,
|
|
61
|
+
new Error(
|
|
62
|
+
`abide: ${position} mounts ${specifier}, and no directory answers to it. RULEBOOK 23.19.`,
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
return []
|
|
66
|
+
}
|
|
67
|
+
if (ownDirectory(root, directory)) {
|
|
68
|
+
refusals.at(
|
|
69
|
+
path,
|
|
70
|
+
new Error(
|
|
71
|
+
`abide: ${position} mounts ${specifier}, which is the app's own directory already. RULEBOOK 23.20.`,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
return []
|
|
75
|
+
}
|
|
59
76
|
// The package's tree is the package's to refuse; a mount places what it finds.
|
|
60
|
-
const found = appFiles(directory, (
|
|
77
|
+
const found = appFiles(directory, new Refusals(directory, true))
|
|
61
78
|
const placed = (below: string): Placed => ({
|
|
62
79
|
path: `${directory}/${below}`,
|
|
63
80
|
position: `${at}${below}`,
|
|
@@ -73,51 +90,11 @@ export function mountsOf(
|
|
|
73
90
|
return mounts
|
|
74
91
|
}
|
|
75
92
|
|
|
76
|
-
/**
|
|
77
|
-
* Every file mounted under the app at `root` whose own `.abide` are `relatives`, and every one those
|
|
78
|
-
* mount in turn, at its position — read for nothing but its mounts, and refusing nothing: the
|
|
79
|
-
* build is what refuses a mount (`withDependencies`).
|
|
80
|
-
*
|
|
81
|
-
* THE CHECKER'S `Routes` (30.26) asks this on every check, so a file is parsed only where its text
|
|
82
|
-
* names a mount at all.
|
|
83
|
-
*/
|
|
84
|
-
export function mountedUnder(
|
|
85
|
-
root: string,
|
|
86
|
-
relatives: readonly string[],
|
|
87
|
-
): Placed[] {
|
|
88
|
-
const queue: Placed[] = []
|
|
89
|
-
for (const relative of relatives)
|
|
90
|
-
queue.push({ path: `${root}/${relative}`, position: relative })
|
|
91
|
-
const found: Placed[] = []
|
|
92
|
-
const seen = new Set<string>()
|
|
93
|
-
for (let at = 0; at < queue.length; at += 1) {
|
|
94
|
-
const { path, position } = queue[at] as Placed
|
|
95
|
-
const text = readFileSync(path, 'utf8')
|
|
96
|
-
if (!IMPORT_ATTRIBUTES.routes.test(text)) continue
|
|
97
|
-
let file: AbideFile
|
|
98
|
-
try {
|
|
99
|
-
file = parseAbide(text)
|
|
100
|
-
} catch {
|
|
101
|
-
continue
|
|
102
|
-
}
|
|
103
|
-
const mounts = mountsOf(root, path, position, file)
|
|
104
|
-
if (typeof mounts === 'string') continue
|
|
105
|
-
for (const mount of mounts)
|
|
106
|
-
for (const placed of mount.files) {
|
|
107
|
-
if (seen.has(placed.path)) continue
|
|
108
|
-
seen.add(placed.path)
|
|
109
|
-
found.push(placed)
|
|
110
|
-
queue.push(placed)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return found
|
|
114
|
-
}
|
|
115
|
-
|
|
116
93
|
// Under the root and not a dependency's there: one of the app's own, with positions of its own.
|
|
117
94
|
function ownDirectory(root: string, directory: string): boolean {
|
|
118
95
|
if (directory === root) return true
|
|
119
96
|
const below = relative(root, directory)
|
|
120
|
-
return !below.startsWith('..') && placeOf(`${below}
|
|
97
|
+
return !below.startsWith('..') && placeOf(`${below}/`) !== 'dependency'
|
|
121
98
|
}
|
|
122
99
|
|
|
123
100
|
/**
|
|
@@ -683,7 +683,7 @@ const ROWS_OF = '__rowsOf'
|
|
|
683
683
|
*
|
|
684
684
|
* The item is read off the subject as `listOf` iterates it: what a `Reactive` read by
|
|
685
685
|
* name hands back (31.1 lowers `rows` to `rows()`), or the subject itself. THE CALL IS THE FIRST
|
|
686
|
-
* OVERLOAD because a `Reactive` is ALSO an `AsyncIterable` of
|
|
686
|
+
* OVERLOAD because a `Reactive` is ALSO an `AsyncIterable` of what it produced (2.5, 2.6), and inferred
|
|
687
687
|
* over one union the item came out as the row OR the whole list. Declared AFTER everything, so no
|
|
688
688
|
* offset above it moves, and ambient, so there is nothing to emit. Where inference fails the item
|
|
689
689
|
* is `unknown`, and a member of that is unprovable — the safe side.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
|
|
2
|
-
import { diagnosisAt } from './lineOf.ts'
|
|
3
2
|
import { type AbideFile, parseAbide } from './parseAbide.ts'
|
|
3
|
+
import type { Refusals } from './Refusals.ts'
|
|
4
4
|
import type { Diagnosis, TypeSession } from './TypeSession.ts'
|
|
5
5
|
|
|
6
6
|
// EVERY COMPILE OF A `.abide` STARTS THE SAME WAY: the text parsed, then every file read by the
|
|
7
7
|
// checker in one snapshot. `abide build`, `abide check` and an editor's one file differ only in
|
|
8
|
-
// what a refusal does — the build stops at it, and a check reports it and goes on with
|
|
8
|
+
// what a refusal does (`Refusals`) — the build stops at it, and a check reports it and goes on with
|
|
9
|
+
// the rest.
|
|
9
10
|
|
|
10
11
|
/** One `.abide`, parsed. `quiet` is read for its props alone and reported on by nobody (33.4). */
|
|
11
12
|
export type ParsedSource = {
|
|
@@ -13,27 +14,21 @@ export type ParsedSource = {
|
|
|
13
14
|
text: string
|
|
14
15
|
file: AbideFile
|
|
15
16
|
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
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/** One `.abide` as the emitters and the checked lowering take it: parsed, and answered. */
|
|
21
|
-
export type ReadSource = ParsedSource & {
|
|
20
|
+
export type ReadSource<Source extends ParsedSource = ParsedSource> = Source & {
|
|
21
|
+
answer: CheckerAnswer
|
|
22
|
+
}
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
-
* Each source parsed.
|
|
25
|
-
*
|
|
26
|
-
* WITH `refused`, A SOURCE THE PARSER REFUSES IS A FINDING, at its first line, and the rest are
|
|
27
|
-
* parsed around it — a quiet one's is nobody's. Without it the refusal is thrown, as a build stops.
|
|
28
|
-
*/
|
|
24
|
+
/** Each source parsed, around a source the parser refuses — a quiet one's refusal is nobody's. */
|
|
29
25
|
export function parseSources(
|
|
30
26
|
sources: readonly {
|
|
31
27
|
path: string
|
|
32
28
|
text: string
|
|
33
29
|
quiet?: boolean
|
|
34
|
-
position?: string
|
|
35
30
|
}[],
|
|
36
|
-
|
|
31
|
+
refusals: Refusals,
|
|
37
32
|
): ParsedSource[] {
|
|
38
33
|
const parsed: ParsedSource[] = []
|
|
39
34
|
for (const source of sources) {
|
|
@@ -42,8 +37,7 @@ export function parseSources(
|
|
|
42
37
|
try {
|
|
43
38
|
file = parseAbide(source.text)
|
|
44
39
|
} catch (thrown) {
|
|
45
|
-
if (
|
|
46
|
-
if (!quiet) refused.push(refusal(source.path, thrown))
|
|
40
|
+
if (!quiet) refusals.at(source.path, thrown)
|
|
47
41
|
continue
|
|
48
42
|
}
|
|
49
43
|
parsed.push({
|
|
@@ -51,7 +45,6 @@ export function parseSources(
|
|
|
51
45
|
text: source.text,
|
|
52
46
|
file,
|
|
53
47
|
quiet,
|
|
54
|
-
position: source.position,
|
|
55
48
|
})
|
|
56
49
|
}
|
|
57
50
|
return parsed
|
|
@@ -60,22 +53,25 @@ export function parseSources(
|
|
|
60
53
|
/**
|
|
61
54
|
* Each parsed source with the checker's answer, and the compiler's warnings — in one snapshot.
|
|
62
55
|
*
|
|
63
|
-
* ONE SNAPSHOT FOR THE LOT, which is the whole of the compiler's build time (`readsFor`).
|
|
64
|
-
* `
|
|
65
|
-
* and the others still answer;
|
|
56
|
+
* ONE SNAPSHOT FOR THE LOT, which is the whole of the compiler's build time (`readsFor`). Where
|
|
57
|
+
* `refusals` collects, a batch that refuses is asked again a file at a time, so which file it was is
|
|
58
|
+
* known and the others still answer; where it throws, so does the batch.
|
|
66
59
|
*/
|
|
67
|
-
export async function readSources(
|
|
60
|
+
export async function readSources<Source extends ParsedSource>(
|
|
68
61
|
session: TypeSession,
|
|
69
|
-
parsed: readonly
|
|
70
|
-
|
|
71
|
-
): Promise<{
|
|
62
|
+
parsed: readonly Source[],
|
|
63
|
+
refusals: Refusals,
|
|
64
|
+
): Promise<{
|
|
65
|
+
read: ReadSource<Source>[]
|
|
66
|
+
warnings: Diagnosis[]
|
|
67
|
+
}> {
|
|
72
68
|
try {
|
|
73
69
|
const { answers, warnings } = await session.readsFor(parsed)
|
|
74
70
|
return { read: answered(parsed, answers), warnings }
|
|
75
71
|
} catch (thrown) {
|
|
76
|
-
if (
|
|
72
|
+
if (!refusals.collecting) throw thrown
|
|
77
73
|
}
|
|
78
|
-
const read: ReadSource[] = []
|
|
74
|
+
const read: ReadSource<Source>[] = []
|
|
79
75
|
const warnings: Diagnosis[] = []
|
|
80
76
|
for (const source of parsed)
|
|
81
77
|
try {
|
|
@@ -83,31 +79,20 @@ export async function readSources(
|
|
|
83
79
|
read.push(...answered([source], alone.answers))
|
|
84
80
|
warnings.push(...alone.warnings)
|
|
85
81
|
} catch (thrown) {
|
|
86
|
-
if (!source.quiet)
|
|
82
|
+
if (!source.quiet) refusals.at(source.path, thrown)
|
|
87
83
|
}
|
|
88
84
|
return { read, warnings }
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
function answered(
|
|
92
|
-
parsed: readonly
|
|
87
|
+
function answered<Source extends ParsedSource>(
|
|
88
|
+
parsed: readonly Source[],
|
|
93
89
|
answers: readonly CheckerAnswer[],
|
|
94
|
-
): ReadSource[] {
|
|
95
|
-
const read: ReadSource[] = []
|
|
90
|
+
): ReadSource<Source>[] {
|
|
91
|
+
const read: ReadSource<Source>[] = []
|
|
96
92
|
for (let at = 0; at < parsed.length; at += 1)
|
|
97
93
|
read.push({
|
|
98
|
-
...(parsed[at] as
|
|
94
|
+
...(parsed[at] as Source),
|
|
99
95
|
answer: answers[at] ?? UNCHECKED,
|
|
100
96
|
})
|
|
101
97
|
return read
|
|
102
98
|
}
|
|
103
|
-
|
|
104
|
-
/** A refusal as a finding at the first line of the file it was refused in. */
|
|
105
|
-
export function refusal(path: string, thrown: unknown): Diagnosis {
|
|
106
|
-
return diagnosisAt(
|
|
107
|
-
path,
|
|
108
|
-
undefined,
|
|
109
|
-
0,
|
|
110
|
-
'error',
|
|
111
|
-
thrown instanceof Error ? thrown.message : String(thrown),
|
|
112
|
-
)
|
|
113
|
-
}
|
package/src/compiler/readsIn.ts
CHANGED
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
type Type,
|
|
31
31
|
TypeFlags,
|
|
32
32
|
} from 'typescript/unstable/async'
|
|
33
|
-
import { fileKind, routeOf } from '#shared/routePattern.ts'
|
|
34
33
|
import { aliasedSymbol } from './aliasedSymbol.ts'
|
|
35
34
|
import { CheckerAnswer } from './CheckerAnswer.ts'
|
|
36
35
|
import {
|
|
@@ -125,8 +124,6 @@ export async function readsIn(
|
|
|
125
124
|
sourceFile: SourceFile,
|
|
126
125
|
shadowPath: string,
|
|
127
126
|
spans: ShadowSpans,
|
|
128
|
-
// The file below the app's root, which its route is read off (23.14).
|
|
129
|
-
routedPath: string,
|
|
130
127
|
file: AbideFile,
|
|
131
128
|
): Promise<{ answer: CheckerAnswer; warnings: Finding[] }> {
|
|
132
129
|
// A destructuring pattern binds names this does not see, and that is 31.12's case.
|
|
@@ -188,7 +185,7 @@ export async function readsIn(
|
|
|
188
185
|
// Awaited after the round trips below, so a 33.14 refusal would reject with nothing
|
|
189
186
|
// listening and be reported unhandled; `await props` still throws it.
|
|
190
187
|
props.catch(() => {})
|
|
191
|
-
const params = routeReads(checker, abide, sourceFile,
|
|
188
|
+
const params = routeReads(checker, abide, sourceFile, reads)
|
|
192
189
|
const configs = configReads(
|
|
193
190
|
checker,
|
|
194
191
|
abide,
|
|
@@ -693,21 +690,17 @@ const ROUTE_MEMBERS = new Set(['url', 'params', 'name', 'navigating'])
|
|
|
693
690
|
* READ AS A CALL RATHER THAN THROUGH THE CHECKER'S VALUES RECORD (`local`): the record is keyed by
|
|
694
691
|
* a name, and this read is spelled as a member.
|
|
695
692
|
*
|
|
696
|
-
* 23.9 — AND `route.params`
|
|
697
|
-
*
|
|
698
|
-
* `route
|
|
699
|
-
* Only the checker reads that — the running module reads the one `route` every page shares.
|
|
693
|
+
* 23.9 — AND `route.params` IS MARKED, for a checked lowering of a page or a layout to type by what
|
|
694
|
+
* the file's own route promises (`Lowering`): the file is the route, and the running module reads
|
|
695
|
+
* the one `route` every page shares.
|
|
700
696
|
*/
|
|
701
697
|
async function routeReads(
|
|
702
698
|
checker: Checker,
|
|
703
699
|
abide: Abide,
|
|
704
700
|
sourceFile: Node,
|
|
705
|
-
routedPath: string,
|
|
706
701
|
reads: Read[],
|
|
707
702
|
): Promise<void> {
|
|
708
703
|
if (abide.route === undefined) return
|
|
709
|
-
const routed = fileKind(routedPath)
|
|
710
|
-
const kind = routed === 'page' || routed === 'layout' ? routed : undefined
|
|
711
704
|
const accesses: Node[] = []
|
|
712
705
|
const owners: Node[] = []
|
|
713
706
|
const visit = (node: Node): void => {
|
|
@@ -725,7 +718,6 @@ async function routeReads(
|
|
|
725
718
|
sourceFile.forEachChild(visit)
|
|
726
719
|
if (owners.length === 0) return
|
|
727
720
|
const symbols = await checker.getSymbolAtLocation(owners)
|
|
728
|
-
const pattern = routeOf(routedPath)
|
|
729
721
|
for (let at = 0; at < accesses.length; at += 1) {
|
|
730
722
|
const symbol = symbols[at]
|
|
731
723
|
if (symbol === undefined) continue
|
|
@@ -735,11 +727,8 @@ async function routeReads(
|
|
|
735
727
|
const read = classifyRead(access)
|
|
736
728
|
if (read === undefined) continue
|
|
737
729
|
read.local = true
|
|
738
|
-
|
|
739
|
-
kind !== undefined &&
|
|
730
|
+
read.routeParams =
|
|
740
731
|
(access as { name?: { text: string } }).name?.text === 'params'
|
|
741
|
-
)
|
|
742
|
-
read.routeParams = [pattern, kind]
|
|
743
732
|
reads.push(read)
|
|
744
733
|
}
|
|
745
734
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs' // bun exposes no sync existence check or read of its own
|
|
2
|
+
import { dirname, posix } from 'node:path' // bun exposes no path helpers of its own
|
|
3
|
+
import { resolvedFile } from '#server/abideModules.ts'
|
|
4
|
+
import { nearest } from './nearest.ts'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a specifier from a directory as bun does, or `undefined` where it does not resolve.
|
|
8
|
+
*
|
|
9
|
+
* A COMPONENT IMPORT RESOLVES THE WAY THE APP'S OWN IMPORTS DO — its import map, from the file
|
|
10
|
+
* that wrote it. One that does not resolve is left as written, and the bundle names it. THE
|
|
11
|
+
* ANSWER IS A REAL PATH, as every root handed to the compiler is.
|
|
12
|
+
*/
|
|
13
|
+
export function resolverIn(): (
|
|
14
|
+
specifier: string,
|
|
15
|
+
from: string,
|
|
16
|
+
) => string | undefined {
|
|
17
|
+
// ONE ANSWER PER SPECIFIER AND DIRECTORY for as long as the resolver lives: a compile resolves
|
|
18
|
+
// every import once for its dependencies and again for its graph, and hands both one resolver.
|
|
19
|
+
const answered = new Map<string, string | undefined>()
|
|
20
|
+
return (specifier, from) => {
|
|
21
|
+
const key = `${from}\0${specifier}`
|
|
22
|
+
if (answered.has(key)) return answered.get(key)
|
|
23
|
+
const found = resolveFrom(specifier, from)
|
|
24
|
+
answered.set(key, found)
|
|
25
|
+
return found
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveFrom(specifier: string, from: string): string | undefined {
|
|
30
|
+
try {
|
|
31
|
+
return resolvedFile(specifier, from)
|
|
32
|
+
} catch {
|
|
33
|
+
// 38.65 — BUN KEEPS A DIRECTORY'S LISTING FOR THE LIFE OF THE PROCESS, and looks a `#` specifier up
|
|
34
|
+
// in it with no second look — where a relative one that misses reads the directory again.
|
|
35
|
+
// `abide dev` compiles in one process for its whole session, so a component created since
|
|
36
|
+
// its directory was listed never resolved, and was read as uninvoked. The `imports` map is
|
|
37
|
+
// read here instead, and the file it names looked for on disk.
|
|
38
|
+
return specifier.startsWith('#')
|
|
39
|
+
? throughImports(specifier, from)
|
|
40
|
+
: undefined
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A `#` specifier resolved through the nearest `package.json`'s `imports`, to a file that exists. */
|
|
45
|
+
function throughImports(specifier: string, from: string): string | undefined {
|
|
46
|
+
const manifest = nearest(from, 'package.json')
|
|
47
|
+
if (manifest === undefined) return undefined
|
|
48
|
+
const imports = (
|
|
49
|
+
JSON.parse(readFileSync(manifest, 'utf8')) as {
|
|
50
|
+
imports?: Record<string, unknown>
|
|
51
|
+
}
|
|
52
|
+
).imports
|
|
53
|
+
if (imports === undefined) return undefined
|
|
54
|
+
const base = dirname(manifest)
|
|
55
|
+
for (const [key, targets] of Object.entries(imports)) {
|
|
56
|
+
const star = key.indexOf('*')
|
|
57
|
+
let matched: string | undefined
|
|
58
|
+
if (star === -1) matched = key === specifier ? '' : undefined
|
|
59
|
+
else if (
|
|
60
|
+
specifier.startsWith(key.slice(0, star)) &&
|
|
61
|
+
specifier.endsWith(key.slice(star + 1)) &&
|
|
62
|
+
specifier.length >= key.length - 1
|
|
63
|
+
)
|
|
64
|
+
matched = specifier.slice(
|
|
65
|
+
star,
|
|
66
|
+
specifier.length - (key.length - star - 1),
|
|
67
|
+
)
|
|
68
|
+
if (matched === undefined) continue
|
|
69
|
+
// A conditions object names no path of its own, and is passed over.
|
|
70
|
+
for (const target of [targets].flat()) {
|
|
71
|
+
if (typeof target !== 'string') continue
|
|
72
|
+
const path = posix.join(base, target.replace('*', matched))
|
|
73
|
+
if (existsSync(path)) return path
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return undefined
|
|
77
|
+
}
|