@abide/abide 0.101.0 → 0.103.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 +26 -0
- package/bin/abide.ts +226 -164
- package/docs/REGISTRY.md +730 -0
- package/docs/RULEBOOK.md +2768 -0
- package/docs/abide.md +13127 -0
- package/package.json +6 -3
- package/scaffold/AGENTS.md +38 -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 +341 -71
- package/src/compiler/appMap.ts +96 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +53 -8
- package/src/compiler/checkedShadow.ts +73 -6
- package/src/compiler/classifyRead.ts +6 -4
- package/src/compiler/classifyTemplate.ts +1 -1
- package/src/compiler/compileApp.ts +598 -200
- package/src/compiler/compiledEntry.ts +48 -40
- package/src/compiler/componentGraph.ts +57 -71
- package/src/compiler/declaredExports.ts +26 -9
- package/src/compiler/emitClient.ts +142 -72
- package/src/compiler/emitComponent.ts +41 -12
- package/src/compiler/emitSource.ts +102 -13
- package/src/compiler/handlersUnder.ts +164 -76
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/index.ts +7 -0
- package/src/compiler/mountsOf.ts +150 -0
- package/src/compiler/nearest.ts +14 -0
- package/src/compiler/parseAbide.ts +157 -20
- package/src/compiler/projectShadow.ts +250 -19
- package/src/compiler/reactiveTypes.ts +20 -7
- package/src/compiler/readSources.ts +15 -2
- package/src/compiler/readsIn.ts +62 -8
- package/src/compiler/renderMarkup.ts +3 -3
- package/src/compiler/scaffold.ts +37 -15
- package/src/compiler/serveLanguage.ts +101 -34
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/sourceMapOf.ts +193 -0
- package/src/compiler/splitImports.ts +205 -83
- package/src/compiler/tokenize.ts +14 -0
- package/src/compiler/writeDefaultProject.ts +29 -2
- package/src/server/CLIENT_BUILD.ts +11 -13
- package/src/server/DEFAULT_PORT.ts +1 -1
- package/src/server/MODULE_QUERIES.ts +27 -0
- package/src/server/TABLES.ts +7 -0
- package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
- package/src/server/abideModules.ts +457 -0
- package/src/server/answerFailure.ts +16 -6
- package/src/server/answerRequest.ts +241 -107
- package/src/server/answerRpc.ts +107 -33
- package/src/server/answerSocket.ts +65 -20
- 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 +238 -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 +38 -37
- package/src/server/componentModules.d.ts +2 -1
- package/src/server/compressAnswer.ts +4 -4
- package/src/server/config.ts +25 -29
- package/src/server/configFields.ts +31 -25
- package/src/server/createApp.ts +77 -79
- package/src/server/devWorker.ts +118 -181
- package/src/server/executableName.ts +4 -0
- package/src/server/fenceServer.ts +57 -32
- package/src/server/generatedSurfaces.ts +99 -66
- package/src/server/hooks.ts +137 -52
- package/src/server/iconAccent.ts +191 -0
- package/src/server/index.ts +12 -7
- package/src/server/installSourceMaps.ts +93 -0
- package/src/server/launcherOptions.ts +33 -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 +6 -6
- package/src/server/negotiateEncoding.ts +1 -1
- package/src/server/preload.ts +13 -0
- 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 +48 -28
- package/src/server/serveBuild.ts +43 -52
- package/src/server/serveDev.ts +79 -37
- package/src/server/shell.ts +7 -22
- package/src/server/sourceMappings.ts +139 -0
- package/src/server/surfaceOf.ts +20 -3
- package/src/server/testBuild.ts +1 -19
- package/src/server/withAbideSources.ts +117 -0
- package/src/shared/ADDRESSES.ts +10 -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/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
- 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 +2 -1
- package/src/shared/log.ts +27 -13
- package/src/shared/matchRoute.ts +23 -4
- package/src/{ui → shared}/menuBridge.ts +2 -2
- package/src/shared/menuItem.ts +4 -4
- package/src/shared/onMenu.ts +58 -0
- package/src/shared/reactive/Keyed.ts +2 -2
- package/src/shared/reactive/NotLanded.ts +1 -1
- package/src/shared/reactive/Reactive.ts +43 -43
- 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 +1 -1
- package/src/shared/reactive/watch.ts +3 -3
- package/src/shared/refuse.ts +1 -1
- package/src/{ui → shared}/reportMenu.ts +8 -10
- package/src/shared/routePattern.ts +122 -48
- package/src/shared/rpc.ts +37 -8
- 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 +37 -36
- 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/moveNode.ts +1 -1
- package/src/ui/reconcile.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 → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
|
@@ -1,112 +1,200 @@
|
|
|
1
|
-
// 16.2 AND
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// 16.2 AND 16.70 — EVERY HANDLER AN APP DECLARES, AND WHERE IT IS ANSWERED. A handler file's default
|
|
2
|
+
// export answers at that file's route (23.14), and is the app's public surface; a handler a
|
|
3
|
+
// `<script server>` declares is its file's alone, answered below `/__abide/handlers/` at the file's
|
|
4
|
+
// path and its binding's name, and listed nowhere (16.71).
|
|
4
5
|
//
|
|
5
|
-
// READ, NOT RUN. The build finds handlers by what their
|
|
6
|
-
// so a build never runs server code:
|
|
7
|
-
// about building a browser bundle is a reason to. The checker the build already holds lists what
|
|
8
|
-
// module exports and which call declares each (
|
|
9
|
-
// not a handler a refusal here rather than an address that silently never answers.
|
|
6
|
+
// READ, NOT RUN. The build finds handlers by what their files SAY rather than by importing them,
|
|
7
|
+
// so a build never runs server code: a handler file may open a database at import, and nothing
|
|
8
|
+
// about building a browser bundle is a reason to. The checker the build already holds lists what
|
|
9
|
+
// each module exports and which call declares each (16.73) — which is also what makes an export
|
|
10
|
+
// that is not a handler a refusal here rather than an address that silently never answers.
|
|
10
11
|
|
|
11
12
|
import type { Mount } from '#server/answerRpc.ts'
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
13
|
+
import { refuseFrameworkRoute } from '#server/appTree.ts'
|
|
14
|
+
import { surfaceName } from '#server/surfaceOf.ts'
|
|
15
|
+
import { PRIVATE_HANDLERS_ADDRESS } from '#shared/ADDRESSES.ts'
|
|
16
|
+
import {
|
|
17
|
+
parseRoutePattern,
|
|
18
|
+
placeOf,
|
|
19
|
+
routeOf,
|
|
20
|
+
scopeOf,
|
|
21
|
+
} from '#shared/routePattern.ts'
|
|
14
22
|
import { declaredExports } from './declaredExports.ts'
|
|
23
|
+
import type { AbideFile } from './parseAbide.ts'
|
|
24
|
+
import { PRIVATE_EXPORT } from './projectShadow.ts'
|
|
15
25
|
import type { Diagnosis, TypeSession } from './TypeSession.ts'
|
|
16
26
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
function modulesUnder(root: string): HandlerModule[] {
|
|
24
|
-
const modules: HandlerModule[] = []
|
|
25
|
-
for (const name of namesUnder(
|
|
26
|
-
`${root}/${HANDLER_DIRECTORY}`,
|
|
27
|
-
'**/*.ts',
|
|
28
|
-
).sort())
|
|
29
|
-
modules.push({
|
|
30
|
-
source: `${HANDLER_DIRECTORY}/${name}`,
|
|
31
|
-
path: name.replace(/\.ts$/, ''),
|
|
32
|
-
})
|
|
33
|
-
return modules
|
|
27
|
+
/** A handler file, by its path and its path below the app's root. */
|
|
28
|
+
export type HandlerFile = {
|
|
29
|
+
path: string
|
|
30
|
+
relative: string
|
|
31
|
+
/** 23.19 — the path it answers by: its own, or the one its mount gives it. */
|
|
32
|
+
position: string
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
/** A `.abide` the build read. */
|
|
36
|
+
type Source = HandlerFile & { file: AbideFile }
|
|
37
|
+
|
|
36
38
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* Asked before a checker is opened to read it: an app of handlers alone has no `.abide` to say it
|
|
40
|
-
* is an app.
|
|
39
|
+
* Told of each thing the emit refuses, at the file below the root it names. The build's throws;
|
|
40
|
+
* `abide check` collects every one at its file (38.33).
|
|
41
41
|
*/
|
|
42
|
-
export
|
|
43
|
-
|
|
42
|
+
export type Refuse = (relative: string, thrown: unknown) => void
|
|
43
|
+
|
|
44
|
+
/** The build's answer to a refusal: the first one stops it. */
|
|
45
|
+
export const THROW: Refuse = (_relative, thrown) => {
|
|
46
|
+
throw thrown
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/**
|
|
47
50
|
* The app's handler mounts, with the warnings the build raised reading them.
|
|
48
51
|
*
|
|
49
|
-
* Off one read of the checker.
|
|
50
|
-
*
|
|
52
|
+
* Off one read of the checker. 18.15 — with what the build could not read of a copy's options.
|
|
53
|
+
* 19.1's one owner per route and method is `compileApp`'s, which holds the pages as well.
|
|
51
54
|
*/
|
|
52
55
|
export async function handlersUnder(
|
|
53
|
-
root: string,
|
|
54
56
|
session: TypeSession,
|
|
57
|
+
sources: readonly Source[],
|
|
58
|
+
files: readonly HandlerFile[],
|
|
59
|
+
refuse: Refuse = THROW,
|
|
55
60
|
): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
|
|
56
|
-
const
|
|
61
|
+
const serving: Source[] = []
|
|
62
|
+
for (const source of sources)
|
|
63
|
+
if (source.file.servers.length > 0) serving.push(source)
|
|
57
64
|
const paths: string[] = []
|
|
58
|
-
for (const
|
|
59
|
-
const project = await session.
|
|
65
|
+
for (const file of files) paths.push(file.path)
|
|
66
|
+
const project = await session.projectWith(paths)
|
|
67
|
+
for (const source of serving) paths.push(session.projectionOf(source.path))
|
|
60
68
|
const declared =
|
|
61
|
-
project === undefined
|
|
69
|
+
project === undefined || paths.length === 0
|
|
62
70
|
? []
|
|
63
71
|
: await declaredExports(project, session.tsconfig, paths)
|
|
64
72
|
const mounts: Mount[] = []
|
|
65
73
|
const warnings: Diagnosis[] = []
|
|
66
|
-
const
|
|
67
|
-
for (let at = 0; at <
|
|
68
|
-
const {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (method === undefined)
|
|
74
|
+
const names = new Map<string, string>()
|
|
75
|
+
for (let at = 0; at < files.length; at += 1) {
|
|
76
|
+
const { relative, position } = files[at] as HandlerFile
|
|
77
|
+
try {
|
|
78
|
+
const address = routeOf(position)
|
|
79
|
+
const exported = declared[at] ?? []
|
|
80
|
+
const handler = exported.find((each) => each.name === 'default')
|
|
81
|
+
// 16.73 — the default and nothing else, declared by one of the five methods.
|
|
82
|
+
for (const each of exported)
|
|
83
|
+
if (each.name !== 'default' || each.method === undefined)
|
|
84
|
+
throw new Error(
|
|
85
|
+
each.name === 'default'
|
|
86
|
+
? `abide: ${relative}'s default export is not declared as a handler. A handler file's default is declared by abide's \`GET(…)\` (or POST, PUT, PATCH, DELETE); anything else belongs under a lib directory. RULEBOOK 16.73.`
|
|
87
|
+
: `abide: ${relative} exports \`${each.name}\`, and a handler file exports its handler as its default and nothing else. Move \`${each.name}\` into a module under a lib or a server directory. RULEBOOK 16.73.`,
|
|
88
|
+
)
|
|
89
|
+
if (handler?.method === undefined)
|
|
82
90
|
throw new Error(
|
|
83
|
-
`abide: ${
|
|
91
|
+
`abide: ${relative} is a handler file — a .ts directly in an address directory — and exports no handler as its default. Export one (\`export default GET(…)\`), or move the file into a lib directory. RULEBOOK 16.73, 23.22.`,
|
|
84
92
|
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
93
|
+
refuseFrameworkRoute(address, relative)
|
|
94
|
+
refuseUndeclaredParameters(address, handler.args, relative)
|
|
95
|
+
// 20.13 — one name per handler on every generated surface.
|
|
96
|
+
const surface = surfaceName(handler.method, address)
|
|
97
|
+
const named = names.get(surface)
|
|
98
|
+
if (named !== undefined)
|
|
91
99
|
throw new Error(
|
|
92
|
-
`abide: ${
|
|
100
|
+
`abide: ${named} and ${relative} are both named ${surface} on the generated surfaces. RULEBOOK 20.13.`,
|
|
93
101
|
)
|
|
94
|
-
|
|
102
|
+
names.set(surface, relative)
|
|
95
103
|
mounts.push({
|
|
96
104
|
address,
|
|
97
|
-
method,
|
|
98
|
-
source,
|
|
99
|
-
name:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
method: handler.method,
|
|
106
|
+
source: relative,
|
|
107
|
+
name: address.slice(address.lastIndexOf('/') + 1),
|
|
108
|
+
export: 'default',
|
|
109
|
+
scope: scopeOf(position),
|
|
110
|
+
private: false,
|
|
111
|
+
ui: handler.ui,
|
|
112
|
+
cli: handler.cli,
|
|
113
|
+
mcp: handler.mcp,
|
|
114
|
+
description: handler.description,
|
|
115
|
+
copy: handler.copy,
|
|
116
|
+
defaults: handler.defaults,
|
|
117
|
+
args: handler.args,
|
|
107
118
|
})
|
|
108
|
-
warnings.push(...unread)
|
|
119
|
+
warnings.push(...handler.unread)
|
|
120
|
+
} catch (thrown) {
|
|
121
|
+
refuse(relative, thrown)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (let at = 0; at < serving.length; at += 1) {
|
|
125
|
+
const { relative, position } = serving[at] as Source
|
|
126
|
+
// 34.24 — a dependency's file answers where a mount places it, and its handlers below the
|
|
127
|
+
// path it has there.
|
|
128
|
+
const below = placeOf(position) === 'dependency' ? relative : position
|
|
129
|
+
for (const each of declared[files.length + at] ?? []) {
|
|
130
|
+
if (each.method === undefined) continue
|
|
131
|
+
if (!each.name.startsWith(PRIVATE_EXPORT)) continue
|
|
132
|
+
const name = each.name.slice(PRIVATE_EXPORT.length)
|
|
133
|
+
mounts.push({
|
|
134
|
+
address: privateAddress(below, name),
|
|
135
|
+
method: each.method,
|
|
136
|
+
source: relative,
|
|
137
|
+
name,
|
|
138
|
+
export: each.name,
|
|
139
|
+
scope: scopeOf(position),
|
|
140
|
+
private: true,
|
|
141
|
+
ui: each.ui,
|
|
142
|
+
cli: false,
|
|
143
|
+
mcp: false,
|
|
144
|
+
description: each.description,
|
|
145
|
+
copy: each.copy,
|
|
146
|
+
defaults: each.defaults,
|
|
147
|
+
args: each.args,
|
|
148
|
+
})
|
|
149
|
+
warnings.push(...each.unread)
|
|
109
150
|
}
|
|
110
151
|
}
|
|
111
152
|
return { mounts, warnings }
|
|
112
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The address of the private handler `name` the `.abide` at `path` declares: below
|
|
157
|
+
* `PRIVATE_HANDLERS_ADDRESS`, the file's path without its extension, each segment's brackets and
|
|
158
|
+
* dots dropped so nothing in it reads as a parameter, then its name (16.70).
|
|
159
|
+
*/
|
|
160
|
+
export function privateAddress(path: string, name: string): string {
|
|
161
|
+
const segments: string[] = []
|
|
162
|
+
for (const segment of path.replace(/\.abide$/, '').split('/'))
|
|
163
|
+
if (segment !== '' && segment !== '..')
|
|
164
|
+
segments.push(segment.replace(/[[\].]/g, ''))
|
|
165
|
+
return `${PRIVATE_HANDLERS_ADDRESS}/${segments.join('/')}/${name}`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Throws where a handler's `Args` does not declare a parameter its route carries.
|
|
170
|
+
*
|
|
171
|
+
* 16.67 — read off the JSON Schema its annotation derives, which is what the wire holds the args
|
|
172
|
+
* to: a parameter the schema does not name would reach the handler as a key it never typed.
|
|
173
|
+
*/
|
|
174
|
+
function refuseUndeclaredParameters(
|
|
175
|
+
address: string,
|
|
176
|
+
args: unknown,
|
|
177
|
+
source: string,
|
|
178
|
+
): void {
|
|
179
|
+
const schema = args as
|
|
180
|
+
| {
|
|
181
|
+
properties?: Record<string, unknown>
|
|
182
|
+
additionalProperties?: unknown
|
|
183
|
+
}
|
|
184
|
+
| undefined
|
|
185
|
+
// A record type — an index signature — declares every key.
|
|
186
|
+
const open =
|
|
187
|
+
schema?.additionalProperties !== undefined &&
|
|
188
|
+
schema.additionalProperties !== false
|
|
189
|
+
for (const segment of parseRoutePattern(address)) {
|
|
190
|
+
if (segment.kind === 'static' || open) continue
|
|
191
|
+
if (
|
|
192
|
+
schema?.properties !== undefined &&
|
|
193
|
+
segment.name in schema.properties
|
|
194
|
+
)
|
|
195
|
+
continue
|
|
196
|
+
throw new Error(
|
|
197
|
+
`abide: ${source} answers at ${address}, and its handler's args do not declare \`${segment.name}\`. RULEBOOK 16.67.`,
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { tokenize } from './tokenize.ts'
|
|
2
|
+
|
|
3
|
+
/** Every hook abide exports, by name. */
|
|
4
|
+
export const HOOK_NAMES: ReadonlySet<string> = new Set([
|
|
5
|
+
'middleware',
|
|
6
|
+
'onStart',
|
|
7
|
+
'onStop',
|
|
8
|
+
'onError',
|
|
9
|
+
'onConfig',
|
|
10
|
+
'onHealth',
|
|
11
|
+
'onPrincipal',
|
|
12
|
+
'onLauncher',
|
|
13
|
+
'onMenu',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Each of abide's hook names called in `source`, in the order written: the name, then `(`, and not
|
|
18
|
+
* a declaration — a member call included, since `server.onStart(…)` is abide's hook all the same.
|
|
19
|
+
* Read, not run — what `abide routes` lists and the build's refusals of a hook called where it is
|
|
20
|
+
* never read (38.107) go by.
|
|
21
|
+
*/
|
|
22
|
+
export function hooksCalled(source: string): string[] {
|
|
23
|
+
const called: string[] = []
|
|
24
|
+
let previous = ''
|
|
25
|
+
let named: string | undefined
|
|
26
|
+
for (const token of tokenize(source)) {
|
|
27
|
+
if (named !== undefined && token.text === '(') called.push(named)
|
|
28
|
+
named =
|
|
29
|
+
HOOK_NAMES.has(token.text) && previous !== 'function'
|
|
30
|
+
? token.text
|
|
31
|
+
: undefined
|
|
32
|
+
previous = token.text
|
|
33
|
+
}
|
|
34
|
+
return called
|
|
35
|
+
}
|
package/src/compiler/index.ts
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
// reached only by something already doing a build. Nothing here is in any client bundle, and
|
|
8
8
|
// `compiler.test.ts` holds the two apart by asserting what `@abide/abide/server` exports.
|
|
9
9
|
|
|
10
|
+
// 34.14 — what serves what this compiles, to a bundle or to the runtime (`abideModules`).
|
|
11
|
+
export {
|
|
12
|
+
abideModules,
|
|
13
|
+
type Compilation,
|
|
14
|
+
type CompiledFile,
|
|
15
|
+
serveCompilation,
|
|
16
|
+
} from '#server/abideModules.ts'
|
|
10
17
|
export { componentGraph } from './componentGraph.ts'
|
|
11
18
|
export { emitClient, type SelfFill } from './emitClient.ts'
|
|
12
19
|
export { emitComponent } from './emitComponent.ts'
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// 23.19 — A PACKAGE'S DIRECTORY, MOUNTED WHERE THE APP SAYS.
|
|
2
|
+
// `import 'kit/admin' with { type: 'routes' }` in a file of the app's places every `.abide` and
|
|
3
|
+
// handler file under the package's `admin` directory at the importing file's directory, so its
|
|
4
|
+
// pages, layouts, error pages and handlers answer below it — and the app, not the package, decides
|
|
5
|
+
// where: the mount is a line in a file the app writes, at the position the app gave it.
|
|
6
|
+
//
|
|
7
|
+
// A FILE KEEPS ITS PATH AND GAINS A POSITION. It is compiled, served and keyed by where it lies, as
|
|
8
|
+
// any dependency's is (34.23); its POSITION — the path it would have below the root — is what
|
|
9
|
+
// `placeOf`, `fileKind` and `routeOf` read, so a mounted page is a page by the same test as the app's.
|
|
10
|
+
|
|
11
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs' // bun exposes no sync read, stat or realpath of its own
|
|
12
|
+
import { dirname, relative, resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
13
|
+
import { appFiles } from '#server/appTree.ts'
|
|
14
|
+
import { placeOf } from '#shared/routePattern.ts'
|
|
15
|
+
import { nearest } from './nearest.ts'
|
|
16
|
+
import { type AbideFile, parseAbide, scriptBodies } from './parseAbide.ts'
|
|
17
|
+
import { attributedImports, IMPORT_ATTRIBUTES } from './splitImports.ts'
|
|
18
|
+
|
|
19
|
+
/** A `.abide` on disk, and the position it takes in the app. */
|
|
20
|
+
type Placed = { path: string; position: string }
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One directory a file mounts, and each `.abide` and each handler file under it at the position it
|
|
24
|
+
* takes in the app.
|
|
25
|
+
*/
|
|
26
|
+
type DirectoryMount = {
|
|
27
|
+
specifier: string
|
|
28
|
+
directory: string
|
|
29
|
+
/** The position its files take below the root: the mounting file's directory, `/`-ended. */
|
|
30
|
+
at: string
|
|
31
|
+
files: Placed[]
|
|
32
|
+
handlers: Placed[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Every directory the file at `path` mounts, its position in the app being `position` — or the
|
|
37
|
+
* refusal naming what one of them cannot be.
|
|
38
|
+
*
|
|
39
|
+
* `root` is the app's, real: a directory under it is the app's own already, with a position of its
|
|
40
|
+
* own, and mounting it would give its files a second (23.20).
|
|
41
|
+
*/
|
|
42
|
+
export function mountsOf(
|
|
43
|
+
root: string,
|
|
44
|
+
path: string,
|
|
45
|
+
position: string,
|
|
46
|
+
file: AbideFile,
|
|
47
|
+
): DirectoryMount[] | string {
|
|
48
|
+
const mounts: DirectoryMount[] = []
|
|
49
|
+
const at = position.includes('/')
|
|
50
|
+
? `${position.slice(0, position.lastIndexOf('/'))}/`
|
|
51
|
+
: ''
|
|
52
|
+
for (const script of scriptBodies(file))
|
|
53
|
+
for (const { specifier } of attributedImports(script, 'routes')) {
|
|
54
|
+
const directory = mountedDirectory(specifier, dirname(path))
|
|
55
|
+
if (directory === undefined)
|
|
56
|
+
return `abide: ${position} mounts ${specifier}, and no directory answers to it. RULEBOOK 23.19.`
|
|
57
|
+
if (ownDirectory(root, directory))
|
|
58
|
+
return `abide: ${position} mounts ${specifier}, which is the app's own directory already. RULEBOOK 23.20.`
|
|
59
|
+
// The package's tree is the package's to refuse; a mount places what it finds.
|
|
60
|
+
const found = appFiles(directory, () => {})
|
|
61
|
+
const placed = (below: string): Placed => ({
|
|
62
|
+
path: `${directory}/${below}`,
|
|
63
|
+
position: `${at}${below}`,
|
|
64
|
+
})
|
|
65
|
+
mounts.push({
|
|
66
|
+
specifier,
|
|
67
|
+
directory,
|
|
68
|
+
at,
|
|
69
|
+
files: found.abide.map(placed),
|
|
70
|
+
handlers: found.handlers.map(placed),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
return mounts
|
|
74
|
+
}
|
|
75
|
+
|
|
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
|
+
// Under the root and not a dependency's there: one of the app's own, with positions of its own.
|
|
117
|
+
function ownDirectory(root: string, directory: string): boolean {
|
|
118
|
+
if (directory === root) return true
|
|
119
|
+
const below = relative(root, directory)
|
|
120
|
+
return !below.startsWith('..') && placeOf(`${below}/-`) !== 'dependency'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The real directory `specifier` names from `from`: a relative path as written, and a bare one as a
|
|
125
|
+
* package and the path into it, found in the nearest `node_modules` above.
|
|
126
|
+
*
|
|
127
|
+
* A DIRECTORY, NOT A MODULE: a package's `exports` names its modules, and a mount asks for none of
|
|
128
|
+
* them, so its directory is found as bun would find the package and then walked.
|
|
129
|
+
*/
|
|
130
|
+
function mountedDirectory(specifier: string, from: string): string | undefined {
|
|
131
|
+
let candidate: string | undefined
|
|
132
|
+
if (specifier.startsWith('./') || specifier.startsWith('../'))
|
|
133
|
+
candidate = resolve(from, specifier)
|
|
134
|
+
else {
|
|
135
|
+
const parts = specifier.split('/')
|
|
136
|
+
const nameLength = specifier.startsWith('@') ? 2 : 1
|
|
137
|
+
const name = parts.slice(0, nameLength).join('/')
|
|
138
|
+
const inside = parts.slice(nameLength).join('/')
|
|
139
|
+
const found = nearest(from, `node_modules/${name}`)
|
|
140
|
+
if (found === undefined) return undefined
|
|
141
|
+
candidate = inside === '' ? found : `${found}/${inside}`
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
return statSync(candidate).isDirectory()
|
|
145
|
+
? realpathSync(candidate)
|
|
146
|
+
: undefined
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs' // bun exposes no sync existence check of its own
|
|
2
|
+
import { dirname } from 'node:path' // bun exposes no path helpers of its own
|
|
3
|
+
|
|
4
|
+
/** The nearest file or directory at `name` at or above `from`, or `undefined`. */
|
|
5
|
+
export function nearest(from: string, name: string): string | undefined {
|
|
6
|
+
let at = from
|
|
7
|
+
while (true) {
|
|
8
|
+
const candidate = `${at}/${name}`
|
|
9
|
+
if (existsSync(candidate)) return candidate
|
|
10
|
+
const up = dirname(at)
|
|
11
|
+
if (up === at) return undefined
|
|
12
|
+
at = up
|
|
13
|
+
}
|
|
14
|
+
}
|