@abide/abide 0.101.0 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/abide.ts +223 -164
  3. package/docs/REGISTRY.md +726 -0
  4. package/docs/RULEBOOK.md +2753 -0
  5. package/docs/abide.md +12878 -0
  6. package/package.json +5 -3
  7. package/scaffold/AGENTS.md +31 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +262 -67
  13. package/src/compiler/argsSchemaOf.ts +1 -1
  14. package/src/compiler/checkApp.ts +43 -15
  15. package/src/compiler/checkedShadow.ts +39 -5
  16. package/src/compiler/classifyRead.ts +6 -4
  17. package/src/compiler/classifyTemplate.ts +1 -1
  18. package/src/compiler/compileApp.ts +433 -193
  19. package/src/compiler/compiledEntry.ts +43 -39
  20. package/src/compiler/componentGraph.ts +58 -71
  21. package/src/compiler/declaredExports.ts +24 -7
  22. package/src/compiler/emitClient.ts +141 -71
  23. package/src/compiler/emitComponent.ts +37 -8
  24. package/src/compiler/emitSource.ts +106 -13
  25. package/src/compiler/handlersUnder.ts +106 -59
  26. package/src/compiler/index.ts +7 -0
  27. package/src/compiler/mountsOf.ts +138 -0
  28. package/src/compiler/nearest.ts +14 -0
  29. package/src/compiler/parseAbide.ts +157 -20
  30. package/src/compiler/projectShadow.ts +213 -7
  31. package/src/compiler/reactiveTypes.ts +20 -7
  32. package/src/compiler/readSources.ts +15 -2
  33. package/src/compiler/readsIn.ts +61 -7
  34. package/src/compiler/renderMarkup.ts +3 -3
  35. package/src/compiler/scaffold.ts +30 -11
  36. package/src/compiler/serveLanguage.ts +125 -34
  37. package/src/compiler/sourceMapOf.ts +193 -0
  38. package/src/compiler/splitImports.ts +205 -83
  39. package/src/compiler/syncTypes.ts +32 -0
  40. package/src/compiler/tokenize.ts +14 -0
  41. package/src/compiler/writeDefaultProject.ts +1 -1
  42. package/src/compiler/writeTypes.ts +82 -0
  43. package/src/server/CLIENT_BUILD.ts +11 -13
  44. package/src/server/DEFAULT_PORT.ts +1 -1
  45. package/src/server/LAUNCHER_FILES.ts +9 -0
  46. package/src/server/MODULE_QUERIES.ts +18 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +391 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +235 -106
  52. package/src/server/answerRpc.ts +102 -30
  53. package/src/server/answerSocket.ts +65 -20
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +179 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +37 -37
  65. package/src/server/componentModules.d.ts +3 -1
  66. package/src/server/compressAnswer.ts +4 -4
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +59 -73
  70. package/src/server/devWorker.ts +114 -181
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +37 -35
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +124 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +11 -7
  77. package/src/server/installSourceMaps.ts +88 -0
  78. package/src/server/launcherOptions.ts +34 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +5 -5
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/principal.ts +10 -5
  85. package/src/server/realOrAsGiven.ts +10 -0
  86. package/src/server/refuseAddressCollision.ts +21 -14
  87. package/src/server/remote.ts +49 -52
  88. package/src/server/render.ts +65 -94
  89. package/src/server/renderComponent.ts +5 -0
  90. package/src/server/reportHealth.ts +2 -2
  91. package/src/server/requestScope.ts +12 -1
  92. package/src/server/runBuild.ts +24 -0
  93. package/src/server/runCompiled.ts +5 -5
  94. package/src/server/runDesktop.ts +52 -27
  95. package/src/server/serveBuild.ts +43 -52
  96. package/src/server/serveDev.ts +73 -37
  97. package/src/server/shell.ts +7 -22
  98. package/src/server/sourceMappings.ts +139 -0
  99. package/src/server/surfaceOf.ts +18 -3
  100. package/src/server/testBuild.ts +1 -19
  101. package/src/server/withAbideSources.ts +117 -0
  102. package/src/shared/ADDRESSES.ts +7 -18
  103. package/src/shared/APP_META.ts +7 -0
  104. package/src/shared/CopyOptions.ts +1 -1
  105. package/src/shared/DEBUG_META.ts +1 -1
  106. package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
  107. package/src/shared/cookies.ts +2 -2
  108. package/src/shared/csp.ts +1 -1
  109. package/src/shared/escapeText.ts +1 -1
  110. package/src/shared/filledPath.ts +85 -0
  111. package/src/shared/framing.ts +79 -6
  112. package/src/shared/headKeyOf.ts +2 -0
  113. package/src/shared/index.ts +1 -1
  114. package/src/shared/log.ts +27 -13
  115. package/src/shared/matchRoute.ts +23 -4
  116. package/src/shared/menuItem.ts +3 -3
  117. package/src/shared/reactive/Keyed.ts +2 -2
  118. package/src/shared/reactive/NotLanded.ts +1 -1
  119. package/src/shared/reactive/Reactive.ts +43 -43
  120. package/src/shared/reactive/argsContract.ts +4 -4
  121. package/src/shared/reactive/canonicalKey.ts +4 -4
  122. package/src/shared/reactive/chunksOfStream.ts +6 -0
  123. package/src/shared/reactive/index.ts +1 -1
  124. package/src/shared/reactive/recordWrites.ts +103 -5
  125. package/src/shared/reactive/structural.ts +1 -1
  126. package/src/shared/reactive/tracking.ts +1 -1
  127. package/src/shared/reactive/watch.ts +3 -3
  128. package/src/shared/refuse.ts +1 -1
  129. package/src/shared/routePattern.ts +82 -46
  130. package/src/shared/rpc.ts +37 -8
  131. package/src/shared/rpcRequest.ts +27 -3
  132. package/src/shared/url.ts +4 -44
  133. package/src/shared/writePath.ts +1 -1
  134. package/src/ui/LAYER_NAMES.ts +1 -1
  135. package/src/ui/List.ts +3 -3
  136. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  137. package/src/ui/Mismatch.ts +1 -1
  138. package/src/ui/RawRun.ts +1 -1
  139. package/src/ui/Router.ts +38 -31
  140. package/src/ui/Scope.ts +1 -1
  141. package/src/ui/TryRegion.ts +2 -2
  142. package/src/ui/applyHead.ts +33 -0
  143. package/src/ui/index.ts +1 -1
  144. package/src/ui/menuBridge.ts +1 -1
  145. package/src/ui/moveNode.ts +1 -1
  146. package/src/ui/reconcile.ts +2 -2
  147. package/src/ui/reportMenu.ts +2 -2
  148. package/src/ui/routing.ts +2 -2
  149. package/src/ui/setAttribute.ts +1 -1
  150. package/src/ui/setText.ts +1 -1
  151. package/src/ui/socketWire.ts +9 -7
  152. package/src/ui/wire.ts +1 -1
  153. package/scaffold/src/server/app.ts +0 -6
  154. package/scaffold/src/shared/.gitkeep +0 -0
  155. package/scaffold/src/ui/app.html +0 -8
  156. package/src/server/componentImports.ts +0 -56
  157. /package/scaffold/src/{ui/pages/page.abide → page.abide} +0 -0
package/bin/abide.ts CHANGED
@@ -5,19 +5,27 @@
5
5
  import { existsSync, realpathSync } from 'node:fs' // bun exposes no sync existence check or realpath of its own
6
6
  import { mkdir, rename, rm } from 'node:fs/promises' // bun exposes no directory creation, rename or recursive directory removal of its own
7
7
  import { dirname, resolve } from 'node:path' // bun exposes no path helpers of its own
8
- import { brotliCompress, constants } from 'node:zlib' // bun has gzip and zstd of its own, and no brotli
9
-
8
+ import { brotliCompress, constants } from 'node:zlib' // bun's brotli is `CompressionStream`, which compresses on the main thread — 1390 ms against this pool's 300 ms over the docs bundle
10
9
  import { checkApp } from '#compiler/checkApp.ts'
11
- import { compileApp } from '#compiler/compileApp.ts'
10
+ import { type CompiledApp, compileApp, paints } from '#compiler/compileApp.ts'
12
11
  import { compiledEntry } from '#compiler/compiledEntry.ts'
13
12
  import { formatDiagnosis } from '#compiler/formatDiagnosis.ts'
13
+ import { parseAbide } from '#compiler/parseAbide.ts'
14
14
  import { scaffold } from '#compiler/scaffold.ts'
15
15
  import { serveLanguage } from '#compiler/serveLanguage.ts'
16
+ import { syncTypes } from '#compiler/syncTypes.ts'
17
+ import {
18
+ abideModules,
19
+ type Compilation,
20
+ servedTable,
21
+ serverMapsOf,
22
+ } from '#server/abideModules.ts'
16
23
  import { appPlugins } from '#server/appPlugins.ts'
17
24
  import { banner } from '#server/banner.ts'
18
25
  import { buildDirectory } from '#server/buildDirectory.ts'
19
26
  import {
20
27
  CLIENT_DIRECTORY,
28
+ COMPILATION,
21
29
  ENCODING_SUFFIX,
22
30
  MANIFEST,
23
31
  PUBLIC_DIRECTORY,
@@ -27,25 +35,21 @@ import {
27
35
  stylesheetUnderMount,
28
36
  } from '#server/clientBuildOptions.ts'
29
37
  import { type Given, type Host, runCommands } from '#server/commands.ts'
30
- import { componentImports } from '#server/componentImports.ts'
31
38
  import { type AppPackage, packageUnder } from '#server/config.ts'
32
- import {
33
- appRoot,
34
- createApp,
35
- hooksModuleUnder,
36
- originUnder,
37
- } from '#server/createApp.ts'
39
+ import { appRoot, createApp, originUnder } from '#server/createApp.ts'
40
+ import { executableName } from '#server/executableName.ts'
38
41
  import { fenceServer } from '#server/fenceServer.ts'
39
- import { launcherOptionsUnder } from '#server/launcherOptions.ts'
42
+ import { iconAccent } from '#server/iconAccent.ts'
43
+ import { ICON_ADDRESSES, LAUNCHER_PAGE } from '#server/LAUNCHER_FILES.ts'
40
44
  import { manifestOf, NO_MANIFEST } from '#server/manifestOf.ts'
41
45
  import { namesUnder } from '#server/namesUnder.ts'
42
46
  import { refuseAddressCollision } from '#server/refuseAddressCollision.ts'
43
- import { shellUnder } from '#server/render.ts'
47
+ import { DEFAULT_DOCUMENT, intoHead } from '#server/render.ts'
44
48
  import {
45
- handlersIn,
46
49
  loadBuild,
47
50
  pathsUnder,
48
51
  serveApp,
52
+ serveBuild,
49
53
  } from '#server/serveBuild.ts'
50
54
  import { serveDev } from '#server/serveDev.ts'
51
55
  import { escapeText } from '#shared/escapeText.ts'
@@ -54,94 +58,109 @@ import { escapeText } from '#shared/escapeText.ts'
54
58
  type AppDirectories = { root: string; out: string }
55
59
 
56
60
  /**
57
- * `abide`'s host: the app on disk, found by `--root` (38.26), and every command that needs the
61
+ * `abide`'s host: the app on disk, found by `--root` (38.25), and every command that needs the
58
62
  * source and the compiler.
59
63
  */
60
64
  const DISK: Host = {
61
65
  name: 'abide',
66
+ // A BUILD ALREADY THERE, NEVER ONE MADE FOR THE ASKING: `call` and the shell are aimed at a
67
+ // running app (38.7), which this tree may not even be.
62
68
  handlers: async ({ values }) => {
63
69
  const app = directoriesOf(values)
64
- return app !== undefined && existsSync(`${app.out}/rpc.ts`)
65
- ? await handlersIn(app.out)
66
- : []
70
+ if (
71
+ app === undefined ||
72
+ !(await serveBuild(app.root, app.out, async () => undefined))
73
+ )
74
+ return []
75
+ return (await servedTable(app.root)).handlers
67
76
  },
68
77
  appPackage: () => packageUnder(appRoot(undefined) ?? process.cwd()),
69
78
  boot: createApp,
70
- serve: inApp(({ root, out }, { values: { port } }) =>
71
- serveApp(
79
+ serve: inApp(async ({ root, out }, { values: { port } }) => {
80
+ if (!(await serveBuild(root, out, buildInProcess))) {
81
+ console.error(`abide start: nothing under ${root} to serve.`)
82
+ return 1
83
+ }
84
+ return serveApp(
72
85
  originUnder(root),
73
86
  () => loadBuild({ root, out }),
74
87
  port === undefined ? undefined : Number(port),
75
- ),
76
- ),
88
+ )
89
+ }),
77
90
  source: {
78
91
  scaffold: scaffoldCommand,
79
92
  dev: inApp(dev),
80
93
  build: inApp(build),
81
94
  check: inApp(check),
95
+ sync: inApp(sync),
82
96
  compile: inApp(compile),
83
97
  bundle: inApp(bundle),
84
98
  lsp,
85
99
  },
86
100
  }
87
101
 
102
+ // 38.100 — what `serveBuild` builds an app nobody built with, in this process rather than a child.
103
+ function buildInProcess(root: string, out: string): Promise<unknown> {
104
+ return writeBuild({ root, out })
105
+ }
106
+
88
107
  /** `abide build`: compiles the app, bundles its client and copies its public files into `out`. */
89
108
  async function build(directories: AppDirectories): Promise<number> {
90
109
  await writeBuild(directories)
91
110
  return 0
92
111
  }
93
112
 
94
- /** `build`'s work; answers whether there was anything under `root` to build. */
95
- async function writeBuild({ root, out }: AppDirectories): Promise<boolean> {
113
+ /** `build`'s work; answers the app it built, or `undefined` where there was nothing under `root`. */
114
+ async function writeBuild({
115
+ root,
116
+ out,
117
+ }: AppDirectories): Promise<CompiledApp | undefined> {
96
118
  process.env.NODE_ENV ??= 'production'
97
- // 38.67 — what this build may remove is only what was written before it began.
119
+ // 38.66 — what this build may remove is only what was written before it began.
98
120
  const began = Date.now()
121
+ // 38.100 — a build that fails leaves no build: the previous one's marker goes before anything
122
+ // can fail, so `abide start` builds again and says why rather than booting what is left.
123
+ const marker = `${out}/${COMPILATION}`
124
+ await rm(marker, { force: true })
99
125
  // `--out` is a real flag rather than a constant because two test files each building into
100
126
  // one `.abide/` is the contention CLAUDE.md's "checks" section already records once.
101
- const app = await compileApp(root, out)
127
+ const app = await compileApp(root)
102
128
  for (const warning of app.warnings)
103
129
  console.warn(formatDiagnosis(warning, process.cwd()))
104
- if (app.modules.size === 0) {
130
+ if (Object.keys(app.files).length === 0) {
105
131
  console.log(`abide build: no .abide sources or handlers under ${root}`)
106
- return false
132
+ return undefined
107
133
  }
108
- // WRITTEN TOGETHER. 175 modules for the docs app, each independent of the others, and one
109
- // `await` a time made the emit a queue — the same reason `publicFilesAt` and the dev
110
- // worker's own reads are one `Promise.all` each.
111
- const writes: Promise<void>[] = []
112
- for (const [name, text] of app.modules)
113
- writes.push(writeWhole(`${out}/${name}`, text))
114
- await Promise.all(writes)
115
-
116
- // A MODULE WHOSE SOURCE IS GONE IS REMOVED, not left standing, or the output is the union of
117
- // every build and a grep over it answers about code no build produces — the same class as the
118
- // orphan chunks `bundleClient` prunes, one level up.
119
- //
120
- // PRUNED AGAINST WHAT WAS JUST WRITTEN rather than by emptying `out`: the directory is named
121
- // by `--out` and a flag that deletes a tree is a different kind of thing from one that writes
122
- // into it. `client/` holds `.js` and is pruned where it is built.
123
- await pruneStale(out, '**/*.ts', app.modules, began)
124
- // 36.11 — the path below the directory IS the address. `favicon.ico` answers at
125
- // `/favicon.ico`, and nothing declares that anywhere.
126
- const publicNames = namesUnder(`${root}/ui/${PUBLIC_DIRECTORY}`)
127
- const publicAddresses = publicNames.map((name) => `/${name}`)
134
+ // 38.6 — THE COMPILATION, KEPT WHOLE: `abide start` serves every `.abide` out of it at the
135
+ // file's own path, with no compiler, and a module's imports resolve from the source they name.
136
+ const compilation: Compilation = { files: app.files, tables: app.tables }
137
+ // 36.11 — the directories above `public` and the path below it ARE the address.
138
+ // `src/public/favicon.ico` answers at `/favicon.ico`, and nothing declares that anywhere.
139
+ const publicAddresses = [...app.published.values()]
128
140
  // 36.13 IS CHECKED BEFORE A BYTE IS BUNDLED OR COPIED, through `refuseAddressCollision` — which
129
141
  // `abide dev` runs too. Checking it mid-copy left a partial output behind on the throw, and it
130
142
  // left the dev lane unchecked entirely.
131
- refuseAddressCollision(app.routes, publicAddresses, 'abide build')
143
+ refuseAddressCollision(app.routes, app.mounts, app.published, 'abide build')
132
144
  await bundleClient(
145
+ root,
133
146
  out,
134
- [fenceServer(root, app.modules), ...(await appPlugins(root))],
147
+ [
148
+ fenceServer(root),
149
+ abideModules(root, compilation),
150
+ ...(await appPlugins(root)),
151
+ ],
135
152
  publicAddresses,
136
153
  app.routes.length > 0,
137
154
  began,
138
155
  )
139
156
  // An app's static files are its own whether or not it has a page yet.
140
- const published = await copyPublic(root, out, publicNames)
157
+ const copied = await copyPublic(root, out, app.published, began)
158
+ // LAST, because its presence is what `serveBuild` reads as a finished build (38.100).
159
+ await writeWhole(marker, JSON.stringify(compilation))
141
160
  console.log(
142
- `abide build: ${app.sources} source(s), ${published} public file(s) -> ${out}`,
161
+ `abide build: ${app.sources} source(s), ${copied} public file(s) -> ${out}`,
143
162
  )
144
- return true
163
+ return app
145
164
  }
146
165
 
147
166
  /**
@@ -165,35 +184,35 @@ const SERVER_STYLESHEETS: Bun.BunPlugin = {
165
184
  /**
166
185
  * `abide compile`: builds the app, then one executable per target platform.
167
186
  *
168
- * 38.11 — THE BUILD, AND THEN ONE EXECUTABLE PER TARGET with all of it inside (`compiledEntry`).
187
+ * 38.10 — THE BUILD, AND THEN ONE EXECUTABLE PER TARGET with all of it inside (`compiledEntry`).
169
188
  * `--out` is the executable rather than the build, which goes where `abide build` puts it; with no
170
- * `--out` the executable is the app's own name, in the directory the command was run from (38.47).
189
+ * `--out` the executable is the app's own name, in the directory the command was run from (38.46).
171
190
  */
172
191
  async function compile(
173
192
  { root }: AppDirectories,
174
193
  { values }: Given,
175
194
  ): Promise<number> {
176
- // 38.49.
195
+ // 38.48.
177
196
  if (values.target !== undefined && values.platforms !== undefined) {
178
197
  console.error(
179
198
  'abide compile: --target is one platform and --platforms is several; give one of them.',
180
199
  )
181
200
  return 1
182
201
  }
183
- const entry = await writtenEntry(root, undefined)
184
- if (entry === undefined) {
202
+ const written = await writtenEntry(root, undefined)
203
+ if (written === undefined) {
185
204
  console.error(`abide compile: nothing under ${root} to compile.`)
186
205
  return 1
187
206
  }
188
- const named = resolve(values.out ?? executableName(root))
207
+ const named = resolve(values.out ?? executableName(packageUnder(root).name))
189
208
  const platforms =
190
209
  values.platforms?.split(',') ??
191
210
  (values.target === undefined ? [undefined] : [values.target])
192
211
  for (const platform of platforms) {
193
- // 38.48.
212
+ // 38.47.
194
213
  const outfile =
195
214
  values.platforms === undefined ? named : `${named}-${platform}`
196
- await compileExecutable(root, entry, platform, outfile)
215
+ await compileExecutable(root, written, platform, outfile)
197
216
  console.log(`abide compile: ${outfile}`)
198
217
  }
199
218
  return 0
@@ -202,39 +221,43 @@ async function compile(
202
221
  /**
203
222
  * `abide bundle`: builds the app, then a desktop app for this platform around its executable.
204
223
  *
205
- * 38.11, 38.71 — `compile`'s executable, started from `runDesktop`, inside a `.app` named after the
224
+ * 38.10, 38.70 — `compile`'s executable, started from `runDesktop`, inside a `.app` named after the
206
225
  * app where the command was run, or at `--out`.
207
226
  */
208
227
  async function bundle(
209
228
  { root }: AppDirectories,
210
229
  { values }: Given,
211
230
  ): Promise<number> {
212
- // 38.78.
231
+ // 38.77.
213
232
  if (process.platform !== 'darwin') {
214
233
  console.error(
215
234
  `abide bundle: a desktop app is built for macOS, and this is ${process.platform}. \`abide compile\` builds an executable for any platform.`,
216
235
  )
217
236
  return 1
218
237
  }
219
- const out = resolve(values.out ?? `${executableName(root)}.app`)
238
+ const out = resolve(
239
+ values.out ?? `${executableName(packageUnder(root).name)}.app`,
240
+ )
220
241
  if (!out.endsWith('.app')) {
221
242
  console.error(
222
243
  `abide bundle: --out names the .app to write, and ${out} is not one.`,
223
244
  )
224
245
  return 1
225
246
  }
226
- // 38.81 — REFUSED BEFORE ANYTHING IS BUILT, so a mistyped key costs no build.
227
- const options = await launcherOptionsUnder(root)
228
- if (typeof options === 'string') {
229
- console.error(`abide bundle: ${options}.`)
230
- return 1
231
- }
232
- const entry = await writtenEntry(root, async (out) => {
233
- const launch = `${root}/bundle/launch.abide`
247
+ const entry = await writtenEntry(root, async (app) => {
248
+ const launcherPath = `${root}/${LAUNCHER_PAGE}`
249
+ const hasLauncher = app.files[LAUNCHER_PAGE] !== undefined
250
+ const art = await launcherIcon(root)
234
251
  return {
235
- options,
236
- launch: (await Bun.file(launch).exists()) ? launch : undefined,
237
- shell: await launchShell(root, out),
252
+ // 34.32 — its `launcher` export is read when the app launches, off the module.
253
+ launch: hasLauncher ? launcherPath : undefined,
254
+ // 38.82 — a `launcher.abide` of its script blocks alone keeps the built-in page.
255
+ custom:
256
+ hasLauncher &&
257
+ paints(parseAbide(await Bun.file(launcherPath).text())),
258
+ document: launchDocument(app),
259
+ icon: art?.icon,
260
+ accent: art?.accent,
238
261
  }
239
262
  })
240
263
  if (entry === undefined) {
@@ -242,7 +265,7 @@ async function bundle(
242
265
  return 1
243
266
  }
244
267
  const identity = packageUnder(root)
245
- const name = executableName(root)
268
+ const name = executableName(packageUnder(root).name)
246
269
  await compileExecutable(
247
270
  root,
248
271
  entry,
@@ -252,7 +275,7 @@ async function bundle(
252
275
  const icon = await writeIcon(root, `${out}/Contents/Resources`)
253
276
  await Bun.write(
254
277
  `${out}/Contents/Info.plist`,
255
- infoPlist(name, options.name ?? name, identity, icon),
278
+ infoPlist(name, identity, icon),
256
279
  )
257
280
  // AD-HOC SIGNED, WHOLE: the executable bun wrote is signed on its own, and a bundle around a
258
281
  // signed executable that is not itself signed is one Gatekeeper reads as damaged.
@@ -262,11 +285,6 @@ async function bundle(
262
285
  return 0
263
286
  }
264
287
 
265
- // 38.47 — A SCOPED NAME IS NAMED BY ITS LAST PART: `@acme/ledger` is `ledger`, as `bunx` calls it.
266
- function executableName(root: string): string {
267
- return (packageUnder(root).name ?? 'app').replace(/^@[^/]+\//, '')
268
- }
269
-
270
288
  /**
271
289
  * Builds the app and writes the entry an executable starts from; `undefined` where there is no app.
272
290
  *
@@ -276,53 +294,46 @@ async function writtenEntry(
276
294
  root: string,
277
295
  desktop:
278
296
  | ((
279
- out: string,
297
+ app: CompiledApp,
280
298
  ) => Promise<Parameters<typeof compiledEntry>[0]['desktop']>)
281
299
  | undefined,
282
- ): Promise<string | undefined> {
300
+ ): Promise<{ entry: string; compilation: Compilation } | undefined> {
283
301
  const out = buildDirectory(root)
284
- if (!(await writeBuild({ root, out }))) return undefined
285
- const handlerModules = new Map<string, string>()
286
- for (const { source } of await handlersIn(out))
287
- handlerModules.set(source, `${root}/${source}`)
302
+ const app = await writeBuild({ root, out })
303
+ if (app === undefined) return undefined
288
304
  const entry = `${out}/compiled.ts`
289
305
  await Bun.write(
290
306
  entry,
291
307
  compiledEntry({
292
- desktop: await desktop?.(out),
293
- out,
294
- hooks: hooksModuleUnder(root),
308
+ desktop: await desktop?.(app),
309
+ root,
310
+ sourceMaps: serverMapsOf(app, root),
295
311
  package: packageUnder(root),
296
- shell: await shellUnder(root),
297
312
  manifest: await Bun.file(`${out}/${MANIFEST}`).json(),
298
- handlerModules,
299
313
  client: pathsUnder(`${out}/${CLIENT_DIRECTORY}`),
300
314
  published: pathsUnder(`${out}/${PUBLIC_DIRECTORY}`),
301
315
  }),
302
316
  )
303
- return entry
317
+ return { entry, compilation: app }
304
318
  }
305
319
 
306
- /** One executable from `entry`, for `platform` or this one, written to `outfile`. */
320
+ /** One executable from `written`'s entry, for `platform` or this one, written to `outfile`. */
307
321
  async function compileExecutable(
308
322
  root: string,
309
- entry: string,
323
+ written: { entry: string; compilation: Compilation },
310
324
  platform: string | undefined,
311
325
  outfile: string,
312
326
  ): Promise<void> {
313
327
  const compiled = await Bun.build({
314
- entrypoints: [entry],
328
+ entrypoints: [written.entry],
315
329
  target: 'bun',
316
- // 35.4 — a handler's `.abide` import, as `abide start` lands it.
317
- plugins: [
318
- SERVER_STYLESHEETS,
319
- componentImports(root, buildDirectory(root)),
320
- ],
330
+ // 34.16 — every `.abide` the app imports, as `abide start` serves it.
331
+ plugins: [SERVER_STYLESHEETS, abideModules(root, written.compilation)],
321
332
  // UNMINIFIED, AND MAPPED: the executable is a server, and an error it logs is read by
322
333
  // whoever operates it — a minified line prints as the whole page module it sits in. The
323
334
  // map is embedded in the executable, which reads it from there.
324
335
  sourcemap: 'inline',
325
- // 38.94 — parsed at build time rather than on every boot. ESM because the entry awaits at
336
+ // 38.93 — parsed at build time rather than on every boot. ESM because the entry awaits at
326
337
  // its top level, and bun's bytecode default is CommonJS, which refuses that. Nothing in the
327
338
  // executable or the build's outputs says bytecode is inside, so no test can see this.
328
339
  bytecode: true,
@@ -332,7 +343,7 @@ async function compileExecutable(
332
343
  ? {}
333
344
  : { target: `bun-${platform}` as Bun.Build.CompileTarget }),
334
345
  outfile,
335
- // 38.46 — THE EXECUTABLE IS THE BUILD'S, not the machine's: a `bunfig.toml`
346
+ // 38.45 — THE EXECUTABLE IS THE BUILD'S, not the machine's: a `bunfig.toml`
336
347
  // wherever it is started must not change what it runs. `.env` is still read, as
337
348
  // `abide start` reads it — configuration is the environment's (27.8).
338
349
  autoloadBunfig: false,
@@ -350,39 +361,67 @@ async function compileExecutable(
350
361
  }
351
362
 
352
363
  /**
353
- * The document `src/bundle/launch.abide` renders into: `src/bundle/launch.html`, or a bare one.
364
+ * The document `src/launcher.abide` renders into: abide's own, which the page writes its
365
+ * `<html>` onto (35.15).
354
366
  *
355
- * 38.85 — THE PAGE'S OWN `<style>` IS IN ITS HEAD. A component's stylesheet reaches a browser with
367
+ * 38.84 — THE PAGE'S OWN `<style>` IS IN ITS HEAD. A component's stylesheet reaches a browser with
356
368
  * its client module, and a launch page has none: it is rendered before the app it would load from
357
369
  * is served.
358
370
  */
359
- async function launchShell(root: string, out: string): Promise<string> {
360
- const given = Bun.file(`${root}/bundle/launch.html`)
361
- const document = (await given.exists())
362
- ? await given.text()
363
- : '<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<meta name="viewport" content="width=device-width, initial-scale=1">\n</head>\n<body></body>\n</html>\n'
364
- const sheet = Bun.file(`${out}/bundle/launch.style.css`)
365
- if (!(await sheet.exists())) return document
366
- const style = `<style>\n${await sheet.text()}</style>\n`
367
- const headEnd = document.search(/<\/head>/i)
368
- return headEnd === -1
369
- ? `${style}${document}`
370
- : `${document.slice(0, headEnd)}${style}${document.slice(headEnd)}`
371
+ function launchDocument(app: CompiledApp): string {
372
+ const sheet = app.files[LAUNCHER_PAGE]?.sheet
373
+ return sheet === undefined
374
+ ? DEFAULT_DOCUMENT
375
+ : intoHead(DEFAULT_DOCUMENT, `<style>\n${sheet}</style>`)
371
376
  }
372
377
 
373
378
  /**
374
- * Writes the bundle's icon from `src/bundle/`, and answers whether there was one.
379
+ * The icon the launcher page shows, as a data URL, and the accent it takes from it — or nothing
380
+ * where the app has no icon.
375
381
  *
376
- * 38.80 — `icon.icns` as it is, or `icon.png` drawn at every size an `.icns` holds.
382
+ * 38.101 — drawn small by `sips` first: the accent is a fact about the picture, not its pixels.
383
+ */
384
+ async function launcherIcon(
385
+ root: string,
386
+ ): Promise<{ icon: string; accent: string | undefined } | undefined> {
387
+ const source = await publishedIcon(root)
388
+ if (source === undefined) return undefined
389
+ const drawn = async (size: number): Promise<Uint8Array | undefined> => {
390
+ const path = `${buildDirectory(root)}/icon-${size}.png`
391
+ const code = await spawnedTool('sips', [
392
+ '-s',
393
+ 'format',
394
+ 'png',
395
+ '-z',
396
+ String(size),
397
+ String(size),
398
+ source,
399
+ '--out',
400
+ path,
401
+ ])
402
+ return code === 0 ? await Bun.file(path).bytes() : undefined
403
+ }
404
+ const [shown, sampled] = await Promise.all([drawn(256), drawn(64)])
405
+ if (shown === undefined) return undefined
406
+ return {
407
+ icon: `data:image/png;base64,${shown.toBase64()}`,
408
+ accent: sampled === undefined ? undefined : iconAccent(sampled),
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Writes the bundle's icon from the public files the build copied, and answers whether there was one.
414
+ *
415
+ * 38.79 — the file at `/icon.icns` as it is, or the one at `/icon.png` drawn at every size an
416
+ * `.icns` holds.
377
417
  */
378
418
  async function writeIcon(root: string, resources: string): Promise<boolean> {
379
- const icns = Bun.file(`${root}/bundle/icon.icns`)
380
- if (await icns.exists()) {
381
- await Bun.write(`${resources}/icon.icns`, icns)
419
+ const source = await publishedIcon(root)
420
+ if (source === undefined) return false
421
+ if (source.endsWith(ICON_ADDRESSES.icns)) {
422
+ await Bun.write(`${resources}/icon.icns`, Bun.file(source))
382
423
  return true
383
424
  }
384
- const png = `${root}/bundle/icon.png`
385
- if (!(await Bun.file(png).exists())) return false
386
425
  const iconset = `${buildDirectory(root)}/icon.iconset`
387
426
  await rm(iconset, { recursive: true, force: true })
388
427
  await mkdir(iconset, { recursive: true })
@@ -395,7 +434,7 @@ async function writeIcon(root: string, resources: string): Promise<boolean> {
395
434
  '-z',
396
435
  String(size * scale),
397
436
  String(size * scale),
398
- png,
437
+ source,
399
438
  '--out',
400
439
  `${iconset}/icon_${size}x${size}${scale === 2 ? '@2x' : ''}.png`,
401
440
  ]),
@@ -412,6 +451,15 @@ async function writeIcon(root: string, resources: string): Promise<boolean> {
412
451
  )
413
452
  }
414
453
 
454
+ /** 38.79 — the public file the app's icon is drawn from: `/icon.icns`, else `/icon.png`. */
455
+ async function publishedIcon(root: string): Promise<string | undefined> {
456
+ const published = `${buildDirectory(root)}/${PUBLIC_DIRECTORY}`
457
+ for (const address of [ICON_ADDRESSES.icns, ICON_ADDRESSES.png])
458
+ if (await Bun.file(`${published}${address}`).exists())
459
+ return `${published}${address}`
460
+ return undefined
461
+ }
462
+
415
463
  /** Runs one of macOS's own tools, warning with what it wrote where it fails; answers its exit code. */
416
464
  async function spawnedTool(tool: string, args: string[]): Promise<number> {
417
465
  const child = Bun.spawn([tool, ...args], {
@@ -427,12 +475,7 @@ async function spawnedTool(tool: string, args: string[]): Promise<number> {
427
475
  }
428
476
 
429
477
  /** The `Info.plist` a bundle names its executable, its identity and its icon in. */
430
- function infoPlist(
431
- name: string,
432
- displayName: string,
433
- identity: AppPackage,
434
- icon: boolean,
435
- ): string {
478
+ function infoPlist(name: string, identity: AppPackage, icon: boolean): string {
436
479
  const entries: [string, string][] = [
437
480
  ...(icon
438
481
  ? ([['CFBundleIconFile', '<string>icon</string>']] as [
@@ -445,8 +488,8 @@ function infoPlist(
445
488
  'CFBundleIdentifier',
446
489
  `<string>abide.app.${name.replace(/[^A-Za-z0-9.-]/g, '-')}</string>`,
447
490
  ],
448
- ['CFBundleName', `<string>${escapeText(displayName)}</string>`],
449
- ['CFBundleDisplayName', `<string>${escapeText(displayName)}</string>`],
491
+ ['CFBundleName', `<string>${escapeText(name)}</string>`],
492
+ ['CFBundleDisplayName', `<string>${escapeText(name)}</string>`],
450
493
  ['CFBundlePackageType', '<string>APPL</string>'],
451
494
  [
452
495
  'CFBundleShortVersionString',
@@ -456,7 +499,7 @@ function infoPlist(
456
499
  ['LSMinimumSystemVersion', '<string>13.0</string>'],
457
500
  ['NSHighResolutionCapable', '<true/>'],
458
501
  ['NSPrincipalClass', '<string>NSApplication</string>'],
459
- // 38.72 — the app served on loopback over http, and one connected to wherever it runs.
502
+ // 38.71 — the app served on loopback over http, and one connected to wherever it runs.
460
503
  [
461
504
  'NSAppTransportSecurity',
462
505
  '<dict><key>NSAllowsArbitraryLoads</key><true/></dict>',
@@ -475,30 +518,35 @@ ${body}</dict>
475
518
  }
476
519
 
477
520
  /**
478
- * Copies the app's public files into the build output, and returns how many.
521
+ * Copies the app's public files into the build output, each at its address, and returns how many.
479
522
  *
480
- * 38.19 — `src/ui/public`, copied into the built output. Copied rather than left where it is, so
481
- * `abide start` answers out of what the build produced and an app moving a file after a build
482
- * cannot change what is served.
523
+ * 38.18 — every `public` directory, copied into the built output at the address each file is
524
+ * answered at, so `abide start` reads the address off the output's own layout. Copied rather than
525
+ * left where it is, so `abide start` answers out of what the build produced and an app moving a
526
+ * file after a build cannot change what is served.
483
527
  */
484
528
  async function copyPublic(
485
529
  root: string,
486
530
  out: string,
487
- names: string[],
531
+ published: Map<string, string>,
532
+ began: number,
488
533
  ): Promise<number> {
489
- const from = `${root}/ui/${PUBLIC_DIRECTORY}`
490
-
491
534
  const copies: Promise<void>[] = []
535
+ const produced = new Set<string>()
492
536
  // 36.12 — the bytes as authored. `Bun.write` of a `BunFile` is a copy and not a re-encode.
493
- for (const name of names)
537
+ for (const [source, address] of published) {
538
+ produced.add(address.slice(1))
494
539
  copies.push(
495
540
  writeWhole(
496
- `${out}/${PUBLIC_DIRECTORY}/${name}`,
497
- Bun.file(`${from}/${name}`),
541
+ `${out}/${PUBLIC_DIRECTORY}${address}`,
542
+ Bun.file(`${root}/${source}`),
498
543
  ),
499
544
  )
545
+ }
500
546
  await Promise.all(copies)
501
- return names.length
547
+ // 38.66 — a public file the app no longer has is no longer answered.
548
+ await pruneStale(`${out}/${PUBLIC_DIRECTORY}`, produced, began)
549
+ return published.size
502
550
  }
503
551
 
504
552
  /**
@@ -511,10 +559,11 @@ async function copyPublic(
511
559
  * `--splitting`, so a destination's markup and bindings stay in the chunk its own route loads
512
560
  * (30.11) rather than landing in the entry every page pays for. 38.5 IS THIS FUNCTION: hashed and
513
561
  * minified by the bundler, the manifest off its metafile (the entry's name, and the chunks each
514
- * route module needs for 38.29), and the precompressed copies (`precompressOne`). What the manifest
515
- * does NOT carry yet is 38.19's list of public files.
562
+ * route module needs for 38.28), and the precompressed copies (`precompressOne`). What the manifest
563
+ * does NOT carry yet is 38.18's list of public files.
516
564
  */
517
565
  async function bundleClient(
566
+ root: string,
518
567
  out: string,
519
568
  plugins: Bun.BunPlugin[],
520
569
  publicAddresses: string[],
@@ -525,18 +574,18 @@ async function bundleClient(
525
574
  // AN APP OF HANDLERS ALONE has no page to boot, and is served with no client — the manifest
526
575
  // written anyway, so `abide start` reads one shape whatever the app holds.
527
576
  if (!paged) {
528
- await pruneStale(client, '**/*', new Set(), began)
577
+ await pruneStale(client, new Set(), began)
529
578
  await writeWhole(`${out}/${MANIFEST}`, JSON.stringify(NO_MANIFEST))
530
579
  return
531
580
  }
532
- // 38.67 — STAGED, THEN MOVED IN FILE BY FILE. `Bun.build` writes in place, so a second build
581
+ // 38.66 — STAGED, THEN MOVED IN FILE BY FILE. `Bun.build` writes in place, so a second build
533
582
  // into the same `out` — or `abide start` serving it — read a chunk half written, and emptying
534
583
  // `client/` first took the one it was serving away. Every name but the manifest's is a hash of
535
584
  // what it holds, so two builds of one tree move in the same bytes under the same names.
536
585
  const staging = `${out}/.staging-${crypto.randomUUID()}`
537
586
  try {
538
587
  const built = await Bun.build({
539
- ...clientBuildOptions(out, plugins, publicAddresses),
588
+ ...clientBuildOptions(root, plugins, publicAddresses),
540
589
  outdir: staging,
541
590
  // 38.5 — MINIFIED, and EVERY NAME HASHED, the entry's included: chunks and assets are
542
591
  // already `[name]-[hash]` by default and the entry is not, which left the one file every
@@ -575,16 +624,11 @@ async function bundleClient(
575
624
  )
576
625
  }
577
626
  await Promise.all(moves)
578
- await pruneStale(client, '**/*', produced, began)
579
- // THE REAL PATH, because the metafile names a file on disk with its symlinks resolved — and an
580
- // `--out` under `/tmp` is `/private/tmp` to the bundler, which would key nothing at all.
627
+ await pruneStale(client, produced, began)
581
628
  await writeWhole(
582
629
  `${out}/${MANIFEST}`,
583
630
  JSON.stringify(
584
- manifestOf(
585
- built.metafile as Bun.BuildMetafile,
586
- realpathSync(out),
587
- ),
631
+ manifestOf(built.metafile as Bun.BuildMetafile, root),
588
632
  ),
589
633
  )
590
634
  } finally {
@@ -597,18 +641,17 @@ async function bundleClient(
597
641
  *
598
642
  * A CHUNK WHOSE HASH CHANGED leaves the old one behind otherwise, and the directory is the union of
599
643
  * every build ever run into it — and the cost is not disk: a grep over the built output answers about
600
- * code no page loads. 38.67 — only what was written before this build began: anything newer is a
644
+ * code no page loads. 38.66 — only what was written before this build began: anything newer is a
601
645
  * concurrent build's, and its manifest may name it.
602
646
  */
603
647
  async function pruneStale(
604
648
  directory: string,
605
- pattern: string,
606
- produced: { has(name: string): boolean },
649
+ produced: ReadonlySet<string>,
607
650
  began: number,
608
651
  ): Promise<void> {
609
652
  const stale: Promise<void>[] = []
610
653
  // A file gone since the scan reads as last written in the far future, and is left alone.
611
- for (const name of namesUnder(directory, pattern))
654
+ for (const name of namesUnder(directory))
612
655
  if (
613
656
  !produced.has(name) &&
614
657
  Bun.file(`${directory}/${name}`).lastModified < began
@@ -620,7 +663,7 @@ async function pruneStale(
620
663
  /**
621
664
  * Writes `path` whole: to a name of its own beside it, then renamed over it.
622
665
  *
623
- * 38.67 — a reader, or a second build, finds the last complete file there and never a partial one.
666
+ * 38.66 — a reader, or a second build, finds the last complete file there and never a partial one.
624
667
  */
625
668
  async function writeWhole(path: string, content: string | Blob): Promise<void> {
626
669
  const partial = `${path}.${crypto.randomUUID()}.partial`
@@ -687,7 +730,7 @@ function inApp<Answer>(
687
730
  const app = directoriesOf(given.values)
688
731
  if (app === undefined) {
689
732
  console.error(
690
- `abide ${given.positionals[0]}: no app here. Looked for ui/ in ${process.cwd()} and in ${resolve('src')}. Point --root at the directory ui/ sits under.`,
733
+ `abide ${given.positionals[0]}: no app here. Looked for a .abide under ${resolve('src')}, or under ${process.cwd()} where it has no src. Point --root at the directory your pages sit under.`,
691
734
  )
692
735
  return 1
693
736
  }
@@ -775,10 +818,26 @@ async function check(
775
818
  return errors > 0 ? 1 : 0
776
819
  }
777
820
 
821
+ /**
822
+ * `abide sync`: writes the types a plain `.ts` reads the app's `.abide` files by.
823
+ *
824
+ * 34.24 — parse and projection alone, so it is fast enough to run on every install (the scaffold's
825
+ * `prepare`), where no checker and no build has run.
826
+ */
827
+ async function sync({ root }: AppDirectories): Promise<number> {
828
+ const { typed, refused } = await syncTypes(root)
829
+ for (const diagnosis of refused)
830
+ console.error(formatDiagnosis(diagnosis, process.cwd()))
831
+ console.log(
832
+ `abide sync: ${typed} .abide file${typed === 1 ? '' : 's'} typed`,
833
+ )
834
+ return refused.length === 0 ? 0 : 1
835
+ }
836
+
778
837
  /**
779
838
  * `abide lsp`: the language server over stdio.
780
839
  *
781
- * 38.12 — the language server, for as long as the editor holds stdin open. Nothing is printed: stdout
840
+ * 38.11 — the language server, for as long as the editor holds stdin open. Nothing is printed: stdout
782
841
  * is the protocol.
783
842
  *
784
843
  * Not `inApp`: an editor starts it wherever its workspace is, and each document finds its own app.