@abide/abide 0.102.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 +10 -0
- package/bin/abide.ts +31 -28
- package/docs/REGISTRY.md +30 -26
- package/docs/RULEBOOK.md +98 -83
- package/docs/abide.md +771 -522
- package/package.json +2 -1
- package/scaffold/AGENTS.md +13 -6
- package/src/compiler/TypeSession.ts +110 -35
- package/src/compiler/appMap.ts +96 -0
- package/src/compiler/argsSchemaOf.ts +1 -1
- package/src/compiler/checkApp.ts +35 -18
- package/src/compiler/checkedShadow.ts +40 -7
- package/src/compiler/compileApp.ts +228 -70
- package/src/compiler/compiledEntry.ts +10 -6
- package/src/compiler/componentGraph.ts +6 -7
- package/src/compiler/declaredExports.ts +3 -3
- package/src/compiler/emitClient.ts +3 -3
- package/src/compiler/emitComponent.ts +9 -9
- package/src/compiler/emitSource.ts +5 -9
- package/src/compiler/handlersUnder.ts +114 -73
- package/src/compiler/hooksCalled.ts +35 -0
- package/src/compiler/mountsOf.ts +30 -18
- package/src/compiler/parseAbide.ts +12 -12
- package/src/compiler/projectShadow.ts +70 -45
- package/src/compiler/readsIn.ts +2 -2
- package/src/compiler/scaffold.ts +24 -21
- package/src/compiler/serveLanguage.ts +0 -24
- package/src/compiler/sharedModuleWrites.ts +2 -2
- package/src/compiler/splitImports.ts +4 -4
- package/src/compiler/writeDefaultProject.ts +28 -1
- package/src/server/MODULE_QUERIES.ts +12 -3
- package/src/server/abideModules.ts +117 -51
- package/src/server/answerRequest.ts +9 -4
- package/src/server/answerRpc.ts +18 -16
- package/src/server/appTree.ts +83 -24
- package/src/server/commands.ts +5 -4
- package/src/server/componentModules.d.ts +1 -2
- package/src/server/createApp.ts +23 -11
- package/src/server/devWorker.ts +5 -1
- package/src/server/fenceServer.ts +36 -13
- package/src/server/hooks.ts +17 -4
- package/src/server/index.ts +1 -0
- package/src/server/installSourceMaps.ts +6 -1
- package/src/server/launcherOptions.ts +12 -13
- package/src/server/launcherPage.ts +2 -2
- package/src/server/nativeMenu.ts +3 -3
- package/src/server/preload.ts +13 -0
- package/src/server/runDesktop.ts +11 -16
- package/src/server/serveDev.ts +8 -2
- package/src/server/surfaceOf.ts +2 -0
- package/src/server/withAbideSources.ts +1 -1
- package/src/shared/ADDRESSES.ts +3 -0
- package/src/shared/LAUNCHER_FILES.ts +9 -0
- package/src/shared/RESERVED_DIRECTORIES.ts +13 -4
- package/src/shared/index.ts +2 -1
- package/src/{ui → shared}/menuBridge.ts +1 -1
- package/src/shared/menuItem.ts +2 -2
- package/src/shared/onMenu.ts +58 -0
- package/src/{ui → shared}/reportMenu.ts +7 -9
- package/src/shared/routePattern.ts +58 -20
- package/src/shared/rpc.ts +1 -1
- package/src/ui/Router.ts +0 -6
- package/src/compiler/syncTypes.ts +0 -32
- package/src/compiler/writeTypes.ts +0 -82
- package/src/server/LAUNCHER_FILES.ts +0 -9
- /package/scaffold/src/{page.abide → index.abide} +0 -0
- /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
package/src/server/createApp.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { servedTable } from './abideModules.ts'
|
|
|
14
14
|
import { type AppSources, answerRequest } from './answerRequest.ts'
|
|
15
15
|
import { Mux, type MuxConnection } from './answerSocket.ts'
|
|
16
16
|
import { appDirectories } from './appDirectories.ts'
|
|
17
|
-
import {
|
|
17
|
+
import { holdsApp } from './appTree.ts'
|
|
18
18
|
import { buildDirectory } from './buildDirectory.ts'
|
|
19
19
|
import { APP_PACKAGE, type AppPackage, config, packageUnder } from './config.ts'
|
|
20
20
|
import { McpSurface } from './generatedSurfaces.ts'
|
|
@@ -65,21 +65,33 @@ export async function createApp(): Promise<App> {
|
|
|
65
65
|
throw new Error(
|
|
66
66
|
`abide: createApp found no app. Looked for a .abide under ${resolve('src')}, or under ${process.cwd()} where it has no src.`,
|
|
67
67
|
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// stale.
|
|
71
|
-
const out =
|
|
72
|
-
process.env.NODE_ENV === 'test'
|
|
73
|
-
? await testBuild(root)
|
|
74
|
-
: buildDirectory(root)
|
|
75
|
-
const { loadBuild, serveBuild } = await import('./serveBuild.ts')
|
|
76
|
-
if (!(await serveBuild(root, out)))
|
|
68
|
+
const out = await servedBuild(root)
|
|
69
|
+
if (out === undefined)
|
|
77
70
|
throw new Error(
|
|
78
71
|
`abide: createApp found nothing under ${root} to build.`,
|
|
79
72
|
)
|
|
73
|
+
const { loadBuild } = await import('./serveBuild.ts')
|
|
80
74
|
return bootApp(originUnder(root), () => loadBuild({ root, out }))
|
|
81
75
|
}
|
|
82
76
|
|
|
77
|
+
/**
|
|
78
|
+
* The build of the app under `root`, served to this process's imports — or `undefined` where
|
|
79
|
+
* nothing under it builds. `fresh` builds it for its sources as they stand.
|
|
80
|
+
*
|
|
81
|
+
* 42.16 — under `bun test` the app is built for its sources as they stand rather than read off
|
|
82
|
+
* whatever `abide build` last left, which a test run would otherwise need first and could find
|
|
83
|
+
* stale. One answer for `createApp` and the test preload (38.109), so a test that boots the app
|
|
84
|
+
* reuses the build its imports were served from.
|
|
85
|
+
*/
|
|
86
|
+
export async function servedBuild(
|
|
87
|
+
root: string,
|
|
88
|
+
fresh = process.env.NODE_ENV === 'test',
|
|
89
|
+
): Promise<string | undefined> {
|
|
90
|
+
const out = fresh ? await testBuild(root) : buildDirectory(root)
|
|
91
|
+
const { serveBuild } = await import('./serveBuild.ts')
|
|
92
|
+
return (await serveBuild(root, out)) ? out : undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
/**
|
|
84
96
|
* The app's root — `--root`, else `src` where the working directory holds one, else the working
|
|
85
97
|
* directory — or `undefined` where it is no app's.
|
|
@@ -99,7 +111,7 @@ export function appRoot(given: string | undefined): string | undefined {
|
|
|
99
111
|
let packaged = false
|
|
100
112
|
for (const directory of appDirectories(root))
|
|
101
113
|
packaged ||= existsSync(`${directory}/package.json`)
|
|
102
|
-
return packaged &&
|
|
114
|
+
return packaged && holdsApp(root) ? root : undefined
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
/**
|
package/src/server/devWorker.ts
CHANGED
|
@@ -378,7 +378,11 @@ async function open(
|
|
|
378
378
|
bundleInMemory(
|
|
379
379
|
root,
|
|
380
380
|
// 16.28, 16.29 — the browser's view of the server, as `abide build` gives it.
|
|
381
|
-
[
|
|
381
|
+
[
|
|
382
|
+
fenceServer(root, compilation.mounted),
|
|
383
|
+
abideModules(root, compilation),
|
|
384
|
+
...own,
|
|
385
|
+
],
|
|
382
386
|
paged,
|
|
383
387
|
publicAddresses,
|
|
384
388
|
compilation,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// 16.29 — WHAT THE BROWSER'S BUNDLE SEES OF THE SERVER: nothing under a `server` directory, at any
|
|
2
2
|
// depth, so a secret or a database client cannot ride into the browser by being imported from the
|
|
3
|
-
// wrong file. A handler crosses as the wrapper
|
|
4
|
-
// server block (
|
|
3
|
+
// wrong file. A handler crosses as the wrapper the compiler wrote in its place — a handler file as
|
|
4
|
+
// that file, a private handler in its server block's place (16.28); this is the fence around
|
|
5
|
+
// everything else.
|
|
5
6
|
//
|
|
6
7
|
// A PLUGIN ON THE BUNDLE, NOT A REWRITE OF THE PAGES. A rewrite reaches the specifiers the compiler
|
|
7
8
|
// emitted; the bundle reaches every edge — a helper the page imports, importing a server module,
|
|
@@ -9,11 +10,16 @@
|
|
|
9
10
|
//
|
|
10
11
|
// REGISTERED AHEAD OF `abideModules`, AND IN ITS NAMESPACES: a load is answered by the first plugin
|
|
11
12
|
// that answers it, and a component or a file's text is loaded there rather than as a `file`.
|
|
13
|
+
//
|
|
14
|
+
// BY POSITION, NOT BY PATH (23.19): a directory a file mounts places its files where the app says,
|
|
15
|
+
// so a `server` directory inside it is fenced as the app's own would be, wherever the package lies.
|
|
12
16
|
|
|
17
|
+
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
13
18
|
import { SERVER_DIRECTORY } from '#shared/RESERVED_DIRECTORIES.ts'
|
|
14
19
|
import { placeOf } from '#shared/routePattern.ts'
|
|
15
20
|
import {
|
|
16
21
|
COMPONENT_NAMESPACE,
|
|
22
|
+
type Compilation,
|
|
17
23
|
STYLE_NAMESPACE,
|
|
18
24
|
TEXT_NAMESPACE,
|
|
19
25
|
} from './abideModules.ts'
|
|
@@ -25,24 +31,41 @@ const FENCED = ['file', COMPONENT_NAMESPACE, TEXT_NAMESPACE, STYLE_NAMESPACE]
|
|
|
25
31
|
/**
|
|
26
32
|
* The bundler plugin refusing everything under a `server` directory to the browser's bundle.
|
|
27
33
|
*
|
|
28
|
-
* `root` is its real path (`appRoot`), which is how the bundler names a file
|
|
29
|
-
* `abideModules`.
|
|
34
|
+
* `root` is its real path (`appRoot`), which is how the bundler names a file; `mounted` is the
|
|
35
|
+
* compilation's. Listed before `abideModules`.
|
|
30
36
|
*/
|
|
31
|
-
export function fenceServer(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
export function fenceServer(
|
|
38
|
+
root: string,
|
|
39
|
+
mounted: Compilation['mounted'],
|
|
40
|
+
): Bun.BunPlugin {
|
|
41
|
+
// Mounted directories first: one under the root's `node_modules` is the app's at its position.
|
|
42
|
+
const directories: [string, string][] = []
|
|
43
|
+
for (const [directory, at] of Object.entries(mounted))
|
|
44
|
+
directories.push([`${resolve(root, directory)}/`, at])
|
|
45
|
+
const positionOf = (path: string): string | undefined => {
|
|
46
|
+
for (const [directory, at] of directories)
|
|
47
|
+
if (path.startsWith(directory))
|
|
48
|
+
return `${at}${path.slice(directory.length)}`
|
|
49
|
+
return path.startsWith(`${root}/`)
|
|
50
|
+
? path.slice(root.length + 1)
|
|
51
|
+
: undefined
|
|
52
|
+
}
|
|
53
|
+
// Anything with a `server` directory in its path, and `placeOf` decides by its position — a
|
|
54
|
+
// load it passes on falls through to the next plugin's, or the bundler's own.
|
|
55
|
+
const filter = new RegExp(`/${SERVER_DIRECTORY}/`)
|
|
37
56
|
return {
|
|
38
57
|
name: 'abide-fence',
|
|
39
58
|
setup(build) {
|
|
40
59
|
for (const namespace of FENCED)
|
|
41
60
|
build.onLoad({ filter, namespace }, (args) => {
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
61
|
+
const position = positionOf(args.path)
|
|
62
|
+
if (
|
|
63
|
+
position === undefined ||
|
|
64
|
+
placeOf(position) !== 'server'
|
|
65
|
+
)
|
|
66
|
+
return undefined
|
|
44
67
|
throw new Error(
|
|
45
|
-
`abide: ${
|
|
68
|
+
`abide: ${position.replace(/::\w+$/, '')} is under a server directory, and the browser's bundle imports it. RULEBOOK 16.29.`,
|
|
46
69
|
)
|
|
47
70
|
})
|
|
48
71
|
},
|
package/src/server/hooks.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// SECTION 37 — THE LIFECYCLE HOOKS, called from a
|
|
2
|
-
// than exported from it (37.1). Each call files its function here, at the ROUTE of the file that
|
|
1
|
+
// SECTION 37 — THE LIFECYCLE HOOKS, called from a `.abide`'s `<script server>` or a handler file
|
|
2
|
+
// rather than exported from it (37.1). Each call files its function here, at the ROUTE of the file that
|
|
3
3
|
// called it, and hands back what unfiles it; `createApp` reads what is filed once every module
|
|
4
4
|
// holding a server block has run.
|
|
5
5
|
//
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '#shared/routePattern.ts'
|
|
23
23
|
import type { Schema } from '#shared/Schema.ts'
|
|
24
24
|
import { config } from './config.ts'
|
|
25
|
+
import type { LauncherOptions } from './launcherOptions.ts'
|
|
25
26
|
import { currentRequestScope } from './requestScope.ts'
|
|
26
27
|
|
|
27
28
|
export type AppRung = Middleware<{ request: Request }, Response>
|
|
@@ -51,17 +52,20 @@ export const HOOKS = {
|
|
|
51
52
|
options: ConfigOptions | undefined
|
|
52
53
|
}>[],
|
|
53
54
|
principal: [] as Filed<(claims: unknown) => unknown | Promise<unknown>>[],
|
|
55
|
+
launcher: [] as Filed<() => LauncherOptions>[],
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
const ROOT = '/'
|
|
57
59
|
|
|
58
|
-
// The route a hook called now is filed at — the server block being evaluated, or
|
|
60
|
+
// The route a hook called now is filed at — the server block or handler file being evaluated, or
|
|
61
|
+
// the root.
|
|
59
62
|
let filing: string | undefined
|
|
60
63
|
|
|
61
64
|
/**
|
|
62
65
|
* Sets the route the hooks called next are filed at, or the root again for `undefined`.
|
|
63
66
|
*
|
|
64
|
-
* 37.1 — what a compiled server module calls around its `<script server
|
|
67
|
+
* 37.1 — what a compiled server module calls around its `<script server>`, and the plugin around a
|
|
68
|
+
* handler file; nothing an app writes.
|
|
65
69
|
*/
|
|
66
70
|
export function fileRoute(route: string | undefined): void {
|
|
67
71
|
filing = route
|
|
@@ -208,6 +212,15 @@ export function onPrincipal(
|
|
|
208
212
|
return filed(HOOKS.principal, resolve)
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Files what the desktop app's window and launcher are; returns what unfiles it.
|
|
217
|
+
*
|
|
218
|
+
* 38.80 — called in `src/launcher.abide` alone (38.107), and read as the app launches.
|
|
219
|
+
*/
|
|
220
|
+
export function onLauncher(options: () => LauncherOptions): () => void {
|
|
221
|
+
return filed(HOOKS.launcher, options)
|
|
222
|
+
}
|
|
223
|
+
|
|
211
224
|
/**
|
|
212
225
|
* Logs an unexpected error and hands it to every `onError` hook.
|
|
213
226
|
*
|
package/src/server/index.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// rebuild, and a test boots several apps in one process.
|
|
8
8
|
|
|
9
9
|
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
10
|
+
import { HANDLER_FACE } from './MODULE_QUERIES.ts'
|
|
10
11
|
import { decodeMappings, placeAt, type Segment } from './sourceMappings.ts'
|
|
11
12
|
|
|
12
13
|
/** Per `.abide` below the root, as its server module is served there: the file, and its `mappings`. */
|
|
@@ -63,7 +64,11 @@ function placeFrames(error: Error, frames: CallSite[]): string {
|
|
|
63
64
|
|
|
64
65
|
// A call site's column counts from 0 where the stack bun writes counts from 1.
|
|
65
66
|
function placed(frame: CallSite): string {
|
|
66
|
-
|
|
67
|
+
// A handler file's frames name the file the author wrote, not the face it is served as.
|
|
68
|
+
const file = (frame.getFileName() ?? '<anonymous>').replace(
|
|
69
|
+
HANDLER_FACE,
|
|
70
|
+
'',
|
|
71
|
+
)
|
|
67
72
|
const line = frame.getLineNumber() ?? 0
|
|
68
73
|
const column = frame.getColumnNumber() ?? 0
|
|
69
74
|
const registered = registeredFor(file)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
// 38.80 — THE `
|
|
2
|
-
//
|
|
3
|
-
// never by evaluating it during the bundle.
|
|
1
|
+
// 38.80 — WHAT THE `onLauncher` A `src/launcher.abide` CALLS ANSWERS: the window the desktop app opens
|
|
2
|
+
// and the forms its launcher offers, read as the app launches.
|
|
4
3
|
|
|
5
4
|
import { isRecord } from '#shared/isRecord.ts'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
|
-
* What
|
|
9
|
-
*
|
|
7
|
+
* What `onLauncher` may answer. 38.81 — turning off both forms is not one of them, so the checker
|
|
8
|
+
* refuses it where the answer is typed.
|
|
10
9
|
*/
|
|
11
10
|
export type LauncherOptions = {
|
|
12
11
|
width?: number
|
|
@@ -29,10 +28,10 @@ const SIZES = ['width', 'height', 'minWidth', 'minHeight'] as const
|
|
|
29
28
|
const FORMS = ['start', 'connect'] as const
|
|
30
29
|
|
|
31
30
|
/**
|
|
32
|
-
* The launcher `
|
|
33
|
-
* run time, where an untyped
|
|
31
|
+
* The launcher what `onLauncher` answered sets, or the refusal naming what is wrong with it — read
|
|
32
|
+
* at run time, where an untyped answer can still name a key it does not take.
|
|
34
33
|
*/
|
|
35
|
-
export function launcherOf(
|
|
34
|
+
export function launcherOf(answered: unknown): Launcher | string {
|
|
36
35
|
const launcher: Launcher = {
|
|
37
36
|
width: 1100,
|
|
38
37
|
height: 760,
|
|
@@ -41,9 +40,9 @@ export function launcherOf(exported: unknown): Launcher | string {
|
|
|
41
40
|
start: true,
|
|
42
41
|
connect: true,
|
|
43
42
|
}
|
|
44
|
-
if (
|
|
45
|
-
if (!isRecord(
|
|
46
|
-
for (const [key, value] of Object.entries(
|
|
43
|
+
if (answered === undefined) return launcher
|
|
44
|
+
if (!isRecord(answered)) return '`onLauncher` answers an object'
|
|
45
|
+
for (const [key, value] of Object.entries(answered)) {
|
|
47
46
|
if (
|
|
48
47
|
(SIZES as readonly string[]).includes(key) &&
|
|
49
48
|
typeof value === 'number' &&
|
|
@@ -56,10 +55,10 @@ export function launcherOf(exported: unknown): Launcher | string {
|
|
|
56
55
|
)
|
|
57
56
|
launcher[key as (typeof FORMS)[number]] = value
|
|
58
57
|
else
|
|
59
|
-
return
|
|
58
|
+
return `\`${key}\` is not a launcher option, or ${JSON.stringify(value)} is not a value it takes`
|
|
60
59
|
}
|
|
61
60
|
// 38.81.
|
|
62
61
|
if (!launcher.start && !launcher.connect)
|
|
63
|
-
return '`
|
|
62
|
+
return '`onLauncher` turns off both start and connect, which leaves the launcher nothing to do'
|
|
64
63
|
return launcher
|
|
65
64
|
}
|
|
@@ -21,9 +21,9 @@ const SYSTEM_ACCENT = '#0a84ff'
|
|
|
21
21
|
export type Launch = {
|
|
22
22
|
/** The app's name, as its package gives it. */
|
|
23
23
|
name: string
|
|
24
|
-
/** Where the start form posts, or `undefined` where
|
|
24
|
+
/** Where the start form posts, or `undefined` where what `onLauncher` answers turns starting off. */
|
|
25
25
|
start: string | undefined
|
|
26
|
-
/** Where the connect form posts, or `undefined` where
|
|
26
|
+
/** Where the connect form posts, or `undefined` where what `onLauncher` answers turns connecting off. */
|
|
27
27
|
connect: string | undefined
|
|
28
28
|
fields: ConfigField[]
|
|
29
29
|
/** The URL the last connect was given. */
|
package/src/server/nativeMenu.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// THE DESKTOP APP'S MENU BAR (38.88): the default, with
|
|
2
|
-
// built as an `NSMenu`.
|
|
1
|
+
// THE DESKTOP APP'S MENU BAR (38.88): the default, with the menus the standing layouts and page file
|
|
2
|
+
// with `onMenu`, built as an `NSMenu`.
|
|
3
3
|
|
|
4
4
|
import { FFIType } from 'bun:ffi'
|
|
5
5
|
import type { MenuRole } from '#shared/menuItem.ts'
|
|
@@ -77,7 +77,7 @@ const NAMED_KEYS: Record<string, string> = {
|
|
|
77
77
|
* The menu bar the window shows: the application, File, Edit, View and Window menus, and `added`.
|
|
78
78
|
*
|
|
79
79
|
* 38.88 — AN ADDED MENU NAMED AS A DEFAULT ONE GROWS IT, and any other stands before Window, where
|
|
80
|
-
* the platform puts an app's own. 38.83 — no Connect… where
|
|
80
|
+
* the platform puts an app's own. 38.83 — no Connect… where what `onLauncher` answers turns connecting off.
|
|
81
81
|
*/
|
|
82
82
|
export function menuBar(
|
|
83
83
|
appName: string,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// 38.109 — `@abide/abide/test`, PRELOADED BY `bun test`: the app's compilation served before any test
|
|
2
|
+
// file is loaded, so a test imports a `.abide` as any module — beside the code it tests, at the top
|
|
3
|
+
// of the file, with no `createApp` run first. Bun loads every import of a file before it evaluates
|
|
4
|
+
// any, so registering the compilation from inside a test is always too late; a preload is the
|
|
5
|
+
// one place that runs first. The build is `createApp`'s own under `bun test` (42.16), so a test that
|
|
6
|
+
// boots the app afterwards reuses it rather than building twice.
|
|
7
|
+
|
|
8
|
+
import { appRoot, servedBuild } from './createApp.ts'
|
|
9
|
+
|
|
10
|
+
const root = appRoot(undefined)
|
|
11
|
+
// A run from a directory that holds no app — a workspace's root — preloads nothing.
|
|
12
|
+
// Fresh whatever `NODE_ENV` says: `bun test` sets it only where nothing else has.
|
|
13
|
+
if (root !== undefined) await servedBuild(root, true)
|
package/src/server/runDesktop.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { APP_PACKAGE, configuredMount, dataDirectory } from './config.ts'
|
|
|
26
26
|
import { configFields } from './configFields.ts'
|
|
27
27
|
import { type App, type AppOrigin, bootApp } from './createApp.ts'
|
|
28
28
|
import { executableName } from './executableName.ts'
|
|
29
|
-
import { reportUnexpected } from './hooks.ts'
|
|
29
|
+
import { HOOKS, inOrder, reportUnexpected } from './hooks.ts'
|
|
30
30
|
import { installServer } from './installServer.ts'
|
|
31
31
|
import { launcherOf } from './launcherOptions.ts'
|
|
32
32
|
import { type Launch, launcherPage } from './launcherPage.ts'
|
|
@@ -53,15 +53,10 @@ type Failure = {
|
|
|
53
53
|
|
|
54
54
|
/** What `abide bundle` read of the launcher's files, carried in the executable. */
|
|
55
55
|
export type Desktop = {
|
|
56
|
-
/** 38.
|
|
56
|
+
/** 38.82 — `src/launcher.abide`'s page, where the app has one that paints. */
|
|
57
57
|
launch:
|
|
58
|
-
| (() => Promise<{
|
|
59
|
-
default: (props: Launch) => Component
|
|
60
|
-
launcher?: unknown
|
|
61
|
-
}>)
|
|
58
|
+
| (() => Promise<{ default: (props: Launch) => Component }>)
|
|
62
59
|
| undefined
|
|
63
|
-
/** 38.82 — whether that file has markup of its own, which then stands in for the built-in page. */
|
|
64
|
-
custom: boolean
|
|
65
60
|
/** The document the launch page renders into, its stylesheet already in its head (38.84). */
|
|
66
61
|
document: string
|
|
67
62
|
/** 38.101 — the app's icon as a data URL, and the accent taken from it. */
|
|
@@ -87,23 +82,23 @@ export async function runDesktop(
|
|
|
87
82
|
async function launch(
|
|
88
83
|
origin: AppOrigin,
|
|
89
84
|
load: () => ReturnType<typeof builtSources>,
|
|
90
|
-
{ launch: launchModule,
|
|
85
|
+
{ launch: launchModule, document, icon, accent }: Desktop,
|
|
91
86
|
): Promise<void> {
|
|
92
87
|
installServer()
|
|
93
88
|
APP_PACKAGE.current = origin.package
|
|
94
|
-
// 37.1 — imported for what it files, so the start form reads `onConfig
|
|
89
|
+
// 37.1 — imported for what it files, so the start form reads `onConfig` and the launcher's
|
|
90
|
+
// `onLauncher` is filed.
|
|
95
91
|
await origin.hooks?.()
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
const launcherModule = await launchModule?.().catch((thrown: unknown) => {
|
|
92
|
+
// 38.85 — a launcher page that fails to load leaves the built-in page, with its defaults.
|
|
93
|
+
const launchPage = await launchModule?.().catch((thrown: unknown) => {
|
|
99
94
|
reportUnexpected(thrown)
|
|
100
95
|
return undefined
|
|
101
96
|
})
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
// 38.80 — what its `onLauncher` answers.
|
|
98
|
+
const options = launcherOf(inOrder(HOOKS.launcher).at(-1)?.())
|
|
99
|
+
// 38.81 — an `onLauncher` answer the checker would have refused, written where nothing checked it.
|
|
104
100
|
if (typeof options === 'string')
|
|
105
101
|
throw new Error(`abide: ${options}. RULEBOOK 38.81.`)
|
|
106
|
-
const launchPage = custom ? launcherModule : undefined
|
|
107
102
|
const appName = executableName(origin.package.name)
|
|
108
103
|
const directory = dataDirectory(origin.package.name ?? 'app')
|
|
109
104
|
const stateFile = `${directory}/desktop.json`
|
package/src/server/serveDev.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { watch } from 'node:fs' // bun exposes no file watcher of its own
|
|
|
22
22
|
import { dirname, resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
23
23
|
import {
|
|
24
24
|
type AppChecker,
|
|
25
|
+
compilationOf,
|
|
25
26
|
compileApp,
|
|
26
27
|
openChecker,
|
|
27
28
|
} from '#compiler/compileApp.ts'
|
|
@@ -144,7 +145,7 @@ export async function serveDev(
|
|
|
144
145
|
console.warn(formatDiagnosis(warning, process.cwd()))
|
|
145
146
|
watchDependencies(compiled.files)
|
|
146
147
|
return {
|
|
147
|
-
compilation:
|
|
148
|
+
compilation: compilationOf(compiled),
|
|
148
149
|
published: compiled.published,
|
|
149
150
|
failure: '',
|
|
150
151
|
}
|
|
@@ -154,7 +155,12 @@ export async function serveDev(
|
|
|
154
155
|
thrown instanceof Error ? thrown.message : String(thrown)
|
|
155
156
|
console.error(`abide dev: ${failure}`)
|
|
156
157
|
return {
|
|
157
|
-
compilation: {
|
|
158
|
+
compilation: {
|
|
159
|
+
files: {},
|
|
160
|
+
handlers: {},
|
|
161
|
+
tables: {},
|
|
162
|
+
mounted: {},
|
|
163
|
+
},
|
|
158
164
|
published: new Map(),
|
|
159
165
|
failure,
|
|
160
166
|
}
|
package/src/server/surfaceOf.ts
CHANGED
|
@@ -31,6 +31,8 @@ export function surfaceOf(
|
|
|
31
31
|
mount: Mount | Mounted,
|
|
32
32
|
surface: Surface,
|
|
33
33
|
): Listed | undefined {
|
|
34
|
+
// 16.71 — a private handler is its file's alone.
|
|
35
|
+
if (mount.private) return undefined
|
|
34
36
|
const live = 'handler' in mount ? mount : undefined
|
|
35
37
|
// 16.37 — what its `clients` says, and the build's default where it says nothing.
|
|
36
38
|
const carried =
|
|
@@ -96,7 +96,7 @@ export async function withAbideSources(
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* 16.28, 34.
|
|
99
|
+
* 16.28, 34.19 — the `.abide`'s text with every `<script server>` body blanked: the map is served
|
|
100
100
|
* to a browser, which reads none of one. Each line is kept, so every line the map places is still
|
|
101
101
|
* the line it names.
|
|
102
102
|
*/
|
package/src/shared/ADDRESSES.ts
CHANGED
|
@@ -24,6 +24,9 @@ export const ABIDE_ADDRESS = '/__abide'
|
|
|
24
24
|
*/
|
|
25
25
|
export const CLIENT_ADDRESS = `${ABIDE_ADDRESS}/client`
|
|
26
26
|
|
|
27
|
+
/** Where a private handler is answered, below its file's path and then its name (16.70). */
|
|
28
|
+
export const PRIVATE_HANDLERS_ADDRESS = `${ABIDE_ADDRESS}/handlers`
|
|
29
|
+
|
|
27
30
|
// 20.5, 20.9, 36.7 — the generated surfaces, which the CLI reaches as well as the server answers.
|
|
28
31
|
|
|
29
32
|
/** The generated OpenAPI document. */
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// THE DESKTOP LAUNCHER'S FILES. Its page, which calls `onLauncher`, sits at the app's root beside the
|
|
2
|
+
// root layout, a name the framework finds rather than a page (23.21); the icon is a public file,
|
|
3
|
+
// so it is wherever the address it answers at says (36.11).
|
|
4
|
+
|
|
5
|
+
/** The `.abide` the desktop launcher renders, at the root alone. 38.80, 38.82. */
|
|
6
|
+
export const LAUNCHER_PAGE = 'launcher.abide'
|
|
7
|
+
|
|
8
|
+
/** The addresses of the desktop app's icon, the `.icns` first. 38.79. */
|
|
9
|
+
export const ICON_ADDRESSES = { icns: '/icon.icns', png: '/icon.png' } as const
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// THE
|
|
2
|
-
// `public` is answered as authored (36.11)
|
|
3
|
-
// address of the
|
|
1
|
+
// THE FENCES: the four directory names that are never an address (23.15). `server` fences a graph
|
|
2
|
+
// (16.29), `public` is answered as authored (36.11), and `lib` and `tests` hold what is neither an
|
|
3
|
+
// address nor either of those. Every other directory under the root is a segment of the routes of
|
|
4
|
+
// the files directly in it (23.14), and holds nothing else (23.22).
|
|
4
5
|
|
|
5
6
|
/** A module under one only the server loads. */
|
|
6
7
|
export const SERVER_DIRECTORY = 'server'
|
|
@@ -8,8 +9,16 @@ export const SERVER_DIRECTORY = 'server'
|
|
|
8
9
|
/** A file under one is answered as authored, at the path above it. */
|
|
9
10
|
export const PUBLIC_DIRECTORY = 'public'
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
+
/** The app's own code that is no address: components, modules, both sides. */
|
|
13
|
+
export const LIB_DIRECTORY = 'lib'
|
|
14
|
+
|
|
15
|
+
/** The app's tests. */
|
|
16
|
+
export const TESTS_DIRECTORY = 'tests'
|
|
17
|
+
|
|
18
|
+
/** All four, as the walk of the app tests a directory against them. */
|
|
12
19
|
export const RESERVED_DIRECTORIES: ReadonlySet<string> = new Set([
|
|
13
20
|
SERVER_DIRECTORY,
|
|
14
21
|
PUBLIC_DIRECTORY,
|
|
22
|
+
LIB_DIRECTORY,
|
|
23
|
+
TESTS_DIRECTORY,
|
|
15
24
|
])
|
package/src/shared/index.ts
CHANGED
|
@@ -36,13 +36,14 @@ export { listOf } from './listOf.ts'
|
|
|
36
36
|
export { type Logger, type LogRecord, log } from './log.ts'
|
|
37
37
|
export type { Middleware } from './Middleware.ts'
|
|
38
38
|
export type { Params } from './matchRoute.ts'
|
|
39
|
-
// 38.88 — a desktop app's menu items,
|
|
39
|
+
// 38.88 — a desktop app's menu items, and the hook a route's layouts and page file them with.
|
|
40
40
|
export {
|
|
41
41
|
type MenuEntry,
|
|
42
42
|
type MenuItem,
|
|
43
43
|
type MenuRole,
|
|
44
44
|
menuItem,
|
|
45
45
|
} from './menuItem.ts'
|
|
46
|
+
export { onMenu } from './onMenu.ts'
|
|
46
47
|
// `navigate` ALONE, of everything routing needs. This barrel is what an app resolves through the
|
|
47
48
|
// bare `@abide/abide` specifier, and the matcher, the pattern grammar and `routeOf` are reached by path
|
|
48
49
|
// from the two places that have them — the build and the client router. Putting them here would
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The channel a desktop app's window opens to the page for its menu, or `undefined` in a browser.
|
|
3
3
|
*
|
|
4
|
-
* ITS OWN MODULE, so
|
|
4
|
+
* ITS OWN MODULE, so `onMenu` asks without importing the reporter: `reportMenu` is a chunk only a
|
|
5
5
|
* page inside a desktop app loads. 38.88.
|
|
6
6
|
*/
|
|
7
7
|
export function menuBridge():
|
package/src/shared/menuItem.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// 38.88 — THE DESKTOP APP'S MENU BAR IS THE ROUTE'S: a layout or a page
|
|
2
|
-
// `<script
|
|
1
|
+
// 38.88 — THE DESKTOP APP'S MENU BAR IS THE ROUTE'S: a layout or a page calls `onMenu` in its
|
|
2
|
+
// `<script>`, and the window shows the menus filed while it stands. An item made by
|
|
3
3
|
// `menuItem` is one a page answers; the rest are an address or a role, which need no page.
|
|
4
4
|
|
|
5
5
|
/** What a menu item does natively, where an address or a page's own answer could not. 38.89. */
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// 38.88, 38.92 — THE MENUS A LAYOUT OR A PAGE ADDS TO A DESKTOP APP'S MENU BAR, filed while it
|
|
2
|
+
// stands. Called in its `<script>`, so the menus are a function of its state: what `read` reads moving
|
|
3
|
+
// tells the window again, and the component going takes them with it. Outside a desktop app's window
|
|
4
|
+
// there is no menu bar, and nothing is filed.
|
|
5
|
+
|
|
6
|
+
import { menuBridge } from './menuBridge.ts'
|
|
7
|
+
import type { MenuItem } from './menuItem.ts'
|
|
8
|
+
import { watch } from './reactive/index.ts'
|
|
9
|
+
|
|
10
|
+
/** One `onMenu`'s menus, as it last answered, and whether its component has gone. */
|
|
11
|
+
type Filed = { menus: MenuItem[]; gone: boolean }
|
|
12
|
+
|
|
13
|
+
// IN THE ORDER FILED, which is outermost first: a layout's setup runs before the page inside it.
|
|
14
|
+
let filed: Filed[] = []
|
|
15
|
+
let telling = false
|
|
16
|
+
|
|
17
|
+
/** Files the menus `read` answers for as long as the calling component stands; returns what unfiles them. */
|
|
18
|
+
export function onMenu(read: () => readonly MenuItem[]): () => void {
|
|
19
|
+
if (menuBridge() === undefined) return () => {}
|
|
20
|
+
const entry: Filed = { menus: [], gone: false }
|
|
21
|
+
filed.push(entry)
|
|
22
|
+
const stop = watch(() => {
|
|
23
|
+
entry.gone = false
|
|
24
|
+
entry.menus = [...read()]
|
|
25
|
+
tell()
|
|
26
|
+
// A rerun files again at once; a component that went stays gone.
|
|
27
|
+
return () => {
|
|
28
|
+
entry.gone = true
|
|
29
|
+
tell()
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
// 12.8 — a `read` that threw stopped the watch with no cleanup to run, so the unfiling is
|
|
33
|
+
// done here too.
|
|
34
|
+
return () => {
|
|
35
|
+
stop()
|
|
36
|
+
entry.gone = true
|
|
37
|
+
tell()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ONCE PER TURN, after every rerun and every component a navigation took has settled.
|
|
42
|
+
function tell(): void {
|
|
43
|
+
if (telling) return
|
|
44
|
+
telling = true
|
|
45
|
+
queueMicrotask(async () => {
|
|
46
|
+
telling = false
|
|
47
|
+
const standing: Filed[] = []
|
|
48
|
+
const menus: MenuItem[] = []
|
|
49
|
+
for (const entry of filed) {
|
|
50
|
+
if (entry.gone) continue
|
|
51
|
+
standing.push(entry)
|
|
52
|
+
menus.push(...entry.menus)
|
|
53
|
+
}
|
|
54
|
+
filed = standing
|
|
55
|
+
const { reportMenu } = await import('./reportMenu.ts')
|
|
56
|
+
reportMenu(menus)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// 38.88 — A ROUTE'S
|
|
2
|
-
// the window has opened `abideMenu` to the page; anywhere else
|
|
1
|
+
// 38.88 — A ROUTE'S MENUS, TOLD TO THE WINDOW IT IS SHOWN IN. Loaded only inside a desktop app, where
|
|
2
|
+
// the window has opened `abideMenu` to the page; anywhere else `onMenu` never imports it.
|
|
3
3
|
|
|
4
|
-
import { MenuEntry, type MenuItem } from '#shared/menuItem.ts'
|
|
5
|
-
import { navigate } from '#shared/navigate.ts'
|
|
6
4
|
import { menuBridge } from './menuBridge.ts'
|
|
5
|
+
import { MenuEntry, type MenuItem } from './menuItem.ts'
|
|
6
|
+
import { navigate } from './navigate.ts'
|
|
7
7
|
|
|
8
8
|
/** A menu item as the window builds it: an item the page answers carries the `id` it is told by. */
|
|
9
9
|
type Told =
|
|
@@ -20,15 +20,13 @@ type Told =
|
|
|
20
20
|
let numbered: Exclude<MenuItem, 'separator'>[] = []
|
|
21
21
|
let listening = false
|
|
22
22
|
|
|
23
|
-
/** Tells the window
|
|
24
|
-
export function reportMenu(
|
|
23
|
+
/** Tells the window `menus`, the layouts' outermost first and the page's last. */
|
|
24
|
+
export function reportMenu(menus: readonly MenuItem[]): void {
|
|
25
25
|
const bridge = menuBridge()
|
|
26
26
|
if (bridge === undefined) return
|
|
27
27
|
numbered = []
|
|
28
28
|
const told: Told[] = []
|
|
29
|
-
for (const
|
|
30
|
-
if (Array.isArray(level.menu))
|
|
31
|
-
for (const menu of level.menu as MenuItem[]) told.push(tell(menu))
|
|
29
|
+
for (const menu of menus) told.push(tell(menu))
|
|
32
30
|
if (!listening) {
|
|
33
31
|
listening = true
|
|
34
32
|
// 38.90, 38.91 — the window names the item picked; the page does what it means.
|