@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.
Files changed (161) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/abide.ts +226 -164
  3. package/docs/REGISTRY.md +730 -0
  4. package/docs/RULEBOOK.md +2768 -0
  5. package/docs/abide.md +13127 -0
  6. package/package.json +6 -3
  7. package/scaffold/AGENTS.md +38 -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 +341 -71
  13. package/src/compiler/appMap.ts +96 -0
  14. package/src/compiler/argsSchemaOf.ts +1 -1
  15. package/src/compiler/checkApp.ts +53 -8
  16. package/src/compiler/checkedShadow.ts +73 -6
  17. package/src/compiler/classifyRead.ts +6 -4
  18. package/src/compiler/classifyTemplate.ts +1 -1
  19. package/src/compiler/compileApp.ts +598 -200
  20. package/src/compiler/compiledEntry.ts +48 -40
  21. package/src/compiler/componentGraph.ts +57 -71
  22. package/src/compiler/declaredExports.ts +26 -9
  23. package/src/compiler/emitClient.ts +142 -72
  24. package/src/compiler/emitComponent.ts +41 -12
  25. package/src/compiler/emitSource.ts +102 -13
  26. package/src/compiler/handlersUnder.ts +164 -76
  27. package/src/compiler/hooksCalled.ts +35 -0
  28. package/src/compiler/index.ts +7 -0
  29. package/src/compiler/mountsOf.ts +150 -0
  30. package/src/compiler/nearest.ts +14 -0
  31. package/src/compiler/parseAbide.ts +157 -20
  32. package/src/compiler/projectShadow.ts +250 -19
  33. package/src/compiler/reactiveTypes.ts +20 -7
  34. package/src/compiler/readSources.ts +15 -2
  35. package/src/compiler/readsIn.ts +62 -8
  36. package/src/compiler/renderMarkup.ts +3 -3
  37. package/src/compiler/scaffold.ts +37 -15
  38. package/src/compiler/serveLanguage.ts +101 -34
  39. package/src/compiler/sharedModuleWrites.ts +2 -2
  40. package/src/compiler/sourceMapOf.ts +193 -0
  41. package/src/compiler/splitImports.ts +205 -83
  42. package/src/compiler/tokenize.ts +14 -0
  43. package/src/compiler/writeDefaultProject.ts +29 -2
  44. package/src/server/CLIENT_BUILD.ts +11 -13
  45. package/src/server/DEFAULT_PORT.ts +1 -1
  46. package/src/server/MODULE_QUERIES.ts +27 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +457 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +241 -107
  52. package/src/server/answerRpc.ts +107 -33
  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 +238 -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 +38 -37
  65. package/src/server/componentModules.d.ts +2 -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 +77 -79
  70. package/src/server/devWorker.ts +118 -181
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +57 -32
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +137 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +12 -7
  77. package/src/server/installSourceMaps.ts +93 -0
  78. package/src/server/launcherOptions.ts +33 -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 +6 -6
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/preload.ts +13 -0
  85. package/src/server/principal.ts +10 -5
  86. package/src/server/realOrAsGiven.ts +10 -0
  87. package/src/server/refuseAddressCollision.ts +21 -14
  88. package/src/server/remote.ts +49 -52
  89. package/src/server/render.ts +65 -94
  90. package/src/server/renderComponent.ts +5 -0
  91. package/src/server/reportHealth.ts +2 -2
  92. package/src/server/requestScope.ts +12 -1
  93. package/src/server/runBuild.ts +24 -0
  94. package/src/server/runCompiled.ts +5 -5
  95. package/src/server/runDesktop.ts +48 -28
  96. package/src/server/serveBuild.ts +43 -52
  97. package/src/server/serveDev.ts +79 -37
  98. package/src/server/shell.ts +7 -22
  99. package/src/server/sourceMappings.ts +139 -0
  100. package/src/server/surfaceOf.ts +20 -3
  101. package/src/server/testBuild.ts +1 -19
  102. package/src/server/withAbideSources.ts +117 -0
  103. package/src/shared/ADDRESSES.ts +10 -18
  104. package/src/shared/APP_META.ts +7 -0
  105. package/src/shared/CopyOptions.ts +1 -1
  106. package/src/shared/DEBUG_META.ts +1 -1
  107. package/src/shared/LAUNCHER_FILES.ts +9 -0
  108. package/src/shared/RESERVED_DIRECTORIES.ts +24 -0
  109. package/src/shared/cookies.ts +2 -2
  110. package/src/shared/csp.ts +1 -1
  111. package/src/shared/escapeText.ts +1 -1
  112. package/src/shared/filledPath.ts +85 -0
  113. package/src/shared/framing.ts +79 -6
  114. package/src/shared/headKeyOf.ts +2 -0
  115. package/src/shared/index.ts +2 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/{ui → shared}/menuBridge.ts +2 -2
  119. package/src/shared/menuItem.ts +4 -4
  120. package/src/shared/onMenu.ts +58 -0
  121. package/src/shared/reactive/Keyed.ts +2 -2
  122. package/src/shared/reactive/NotLanded.ts +1 -1
  123. package/src/shared/reactive/Reactive.ts +43 -43
  124. package/src/shared/reactive/argsContract.ts +4 -4
  125. package/src/shared/reactive/canonicalKey.ts +4 -4
  126. package/src/shared/reactive/chunksOfStream.ts +6 -0
  127. package/src/shared/reactive/index.ts +1 -1
  128. package/src/shared/reactive/recordWrites.ts +103 -5
  129. package/src/shared/reactive/structural.ts +1 -1
  130. package/src/shared/reactive/tracking.ts +1 -1
  131. package/src/shared/reactive/watch.ts +3 -3
  132. package/src/shared/refuse.ts +1 -1
  133. package/src/{ui → shared}/reportMenu.ts +8 -10
  134. package/src/shared/routePattern.ts +122 -48
  135. package/src/shared/rpc.ts +37 -8
  136. package/src/shared/rpcRequest.ts +27 -3
  137. package/src/shared/url.ts +4 -44
  138. package/src/shared/writePath.ts +1 -1
  139. package/src/ui/LAYER_NAMES.ts +1 -1
  140. package/src/ui/List.ts +3 -3
  141. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  142. package/src/ui/Mismatch.ts +1 -1
  143. package/src/ui/RawRun.ts +1 -1
  144. package/src/ui/Router.ts +37 -36
  145. package/src/ui/Scope.ts +1 -1
  146. package/src/ui/TryRegion.ts +2 -2
  147. package/src/ui/applyHead.ts +33 -0
  148. package/src/ui/index.ts +1 -1
  149. package/src/ui/moveNode.ts +1 -1
  150. package/src/ui/reconcile.ts +2 -2
  151. package/src/ui/routing.ts +2 -2
  152. package/src/ui/setAttribute.ts +1 -1
  153. package/src/ui/setText.ts +1 -1
  154. package/src/ui/socketWire.ts +9 -7
  155. package/src/ui/wire.ts +1 -1
  156. package/scaffold/src/server/app.ts +0 -6
  157. package/scaffold/src/shared/.gitkeep +0 -0
  158. package/scaffold/src/ui/app.html +0 -8
  159. package/src/server/componentImports.ts +0 -56
  160. /package/scaffold/src/{ui/pages/page.abide → index.abide} +0 -0
  161. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
package/bin/abide.ts CHANGED
@@ -5,19 +5,32 @@
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
9
+ import { appMap, mapText } from '#compiler/appMap.ts'
10
10
  import { checkApp } from '#compiler/checkApp.ts'
11
- import { compileApp } from '#compiler/compileApp.ts'
11
+ import {
12
+ type CompiledApp,
13
+ compilationOf,
14
+ compileApp,
15
+ paints,
16
+ } from '#compiler/compileApp.ts'
12
17
  import { compiledEntry } from '#compiler/compiledEntry.ts'
13
18
  import { formatDiagnosis } from '#compiler/formatDiagnosis.ts'
19
+ import { parseAbide } from '#compiler/parseAbide.ts'
14
20
  import { scaffold } from '#compiler/scaffold.ts'
15
21
  import { serveLanguage } from '#compiler/serveLanguage.ts'
22
+ import {
23
+ abideModules,
24
+ type Compilation,
25
+ servedTable,
26
+ serverMapsOf,
27
+ } from '#server/abideModules.ts'
16
28
  import { appPlugins } from '#server/appPlugins.ts'
17
29
  import { banner } from '#server/banner.ts'
18
30
  import { buildDirectory } from '#server/buildDirectory.ts'
19
31
  import {
20
32
  CLIENT_DIRECTORY,
33
+ COMPILATION,
21
34
  ENCODING_SUFFIX,
22
35
  MANIFEST,
23
36
  PUBLIC_DIRECTORY,
@@ -27,121 +40,135 @@ import {
27
40
  stylesheetUnderMount,
28
41
  } from '#server/clientBuildOptions.ts'
29
42
  import { type Given, type Host, runCommands } from '#server/commands.ts'
30
- import { componentImports } from '#server/componentImports.ts'
31
43
  import { type AppPackage, packageUnder } from '#server/config.ts'
32
- import {
33
- appRoot,
34
- createApp,
35
- hooksModuleUnder,
36
- originUnder,
37
- } from '#server/createApp.ts'
44
+ import { appRoot, createApp, originUnder } from '#server/createApp.ts'
45
+ import { executableName } from '#server/executableName.ts'
38
46
  import { fenceServer } from '#server/fenceServer.ts'
39
- import { launcherOptionsUnder } from '#server/launcherOptions.ts'
47
+ import { iconAccent } from '#server/iconAccent.ts'
40
48
  import { manifestOf, NO_MANIFEST } from '#server/manifestOf.ts'
41
49
  import { namesUnder } from '#server/namesUnder.ts'
42
50
  import { refuseAddressCollision } from '#server/refuseAddressCollision.ts'
43
- import { shellUnder } from '#server/render.ts'
51
+ import { DEFAULT_DOCUMENT, intoHead } from '#server/render.ts'
44
52
  import {
45
- handlersIn,
46
53
  loadBuild,
47
54
  pathsUnder,
48
55
  serveApp,
56
+ serveBuild,
49
57
  } from '#server/serveBuild.ts'
50
58
  import { serveDev } from '#server/serveDev.ts'
51
59
  import { escapeText } from '#shared/escapeText.ts'
60
+ import { ICON_ADDRESSES, LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
52
61
 
53
62
  /** Where the app a command builds or serves is, and where its build goes — both absolute. */
54
63
  type AppDirectories = { root: string; out: string }
55
64
 
56
65
  /**
57
- * `abide`'s host: the app on disk, found by `--root` (38.26), and every command that needs the
66
+ * `abide`'s host: the app on disk, found by `--root` (38.25), and every command that needs the
58
67
  * source and the compiler.
59
68
  */
60
69
  const DISK: Host = {
61
70
  name: 'abide',
71
+ // A BUILD ALREADY THERE, NEVER ONE MADE FOR THE ASKING: `call` and the shell are aimed at a
72
+ // running app (38.7), which this tree may not even be.
62
73
  handlers: async ({ values }) => {
63
74
  const app = directoriesOf(values)
64
- return app !== undefined && existsSync(`${app.out}/rpc.ts`)
65
- ? await handlersIn(app.out)
66
- : []
75
+ if (
76
+ app === undefined ||
77
+ !(await serveBuild(app.root, app.out, async () => undefined))
78
+ )
79
+ return []
80
+ return (await servedTable(app.root)).handlers
67
81
  },
68
82
  appPackage: () => packageUnder(appRoot(undefined) ?? process.cwd()),
69
83
  boot: createApp,
70
- serve: inApp(({ root, out }, { values: { port } }) =>
71
- serveApp(
84
+ serve: inApp(async ({ root, out }, { values: { port } }) => {
85
+ if (!(await serveBuild(root, out, buildInProcess))) {
86
+ console.error(`abide start: nothing under ${root} to serve.`)
87
+ return 1
88
+ }
89
+ return serveApp(
72
90
  originUnder(root),
73
91
  () => loadBuild({ root, out }),
74
92
  port === undefined ? undefined : Number(port),
75
- ),
76
- ),
93
+ )
94
+ }),
77
95
  source: {
78
96
  scaffold: scaffoldCommand,
79
97
  dev: inApp(dev),
80
98
  build: inApp(build),
81
99
  check: inApp(check),
100
+ routes: inApp(routes),
82
101
  compile: inApp(compile),
83
102
  bundle: inApp(bundle),
84
103
  lsp,
85
104
  },
86
105
  }
87
106
 
107
+ // 38.100 — what `serveBuild` builds an app nobody built with, in this process rather than a child.
108
+ function buildInProcess(root: string, out: string): Promise<unknown> {
109
+ return writeBuild({ root, out })
110
+ }
111
+
88
112
  /** `abide build`: compiles the app, bundles its client and copies its public files into `out`. */
89
113
  async function build(directories: AppDirectories): Promise<number> {
90
114
  await writeBuild(directories)
91
115
  return 0
92
116
  }
93
117
 
94
- /** `build`'s work; answers whether there was anything under `root` to build. */
95
- async function writeBuild({ root, out }: AppDirectories): Promise<boolean> {
118
+ /** `build`'s work; answers the app it built, or `undefined` where there was nothing under `root`. */
119
+ async function writeBuild({
120
+ root,
121
+ out,
122
+ }: AppDirectories): Promise<CompiledApp | undefined> {
96
123
  process.env.NODE_ENV ??= 'production'
97
- // 38.67 — what this build may remove is only what was written before it began.
124
+ // 38.66 — what this build may remove is only what was written before it began.
98
125
  const began = Date.now()
126
+ // 38.100 — a build that fails leaves no build: the previous one's marker goes before anything
127
+ // can fail, so `abide start` builds again and says why rather than booting what is left.
128
+ const marker = `${out}/${COMPILATION}`
129
+ await rm(marker, { force: true })
99
130
  // `--out` is a real flag rather than a constant because two test files each building into
100
131
  // one `.abide/` is the contention CLAUDE.md's "checks" section already records once.
101
- const app = await compileApp(root, out)
132
+ const app = await compileApp(root)
102
133
  for (const warning of app.warnings)
103
134
  console.warn(formatDiagnosis(warning, process.cwd()))
104
- if (app.modules.size === 0) {
135
+ if (
136
+ Object.keys(app.files).length === 0 &&
137
+ Object.keys(app.handlers).length === 0
138
+ ) {
105
139
  console.log(`abide build: no .abide sources or handlers under ${root}`)
106
- return false
140
+ return undefined
107
141
  }
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}`)
142
+ // 38.6 — THE COMPILATION, KEPT WHOLE: `abide start` serves every `.abide` out of it at the
143
+ // file's own path, with no compiler, and a module's imports resolve from the source they name.
144
+ const compilation = compilationOf(app)
145
+ // 36.11 — the directories above `public` and the path below it ARE the address.
146
+ // `src/public/favicon.ico` answers at `/favicon.ico`, and nothing declares that anywhere.
147
+ const publicAddresses = [...app.published.values()]
128
148
  // 36.13 IS CHECKED BEFORE A BYTE IS BUNDLED OR COPIED, through `refuseAddressCollision` — which
129
149
  // `abide dev` runs too. Checking it mid-copy left a partial output behind on the throw, and it
130
150
  // left the dev lane unchecked entirely.
131
- refuseAddressCollision(app.routes, publicAddresses, 'abide build')
151
+ refuseAddressCollision(app.routes, app.mounts, app.published, 'abide build')
132
152
  await bundleClient(
153
+ root,
133
154
  out,
134
- [fenceServer(root, app.modules), ...(await appPlugins(root))],
155
+ [
156
+ fenceServer(root, compilation.mounted),
157
+ abideModules(root, compilation),
158
+ ...(await appPlugins(root)),
159
+ ],
135
160
  publicAddresses,
136
161
  app.routes.length > 0,
137
162
  began,
138
163
  )
139
164
  // An app's static files are its own whether or not it has a page yet.
140
- const published = await copyPublic(root, out, publicNames)
165
+ const copied = await copyPublic(root, out, app.published, began)
166
+ // LAST, because its presence is what `serveBuild` reads as a finished build (38.100).
167
+ await writeWhole(marker, JSON.stringify(compilation))
141
168
  console.log(
142
- `abide build: ${app.sources} source(s), ${published} public file(s) -> ${out}`,
169
+ `abide build: ${app.sources} source(s), ${copied} public file(s) -> ${out}`,
143
170
  )
144
- return true
171
+ return app
145
172
  }
146
173
 
147
174
  /**
@@ -165,35 +192,35 @@ const SERVER_STYLESHEETS: Bun.BunPlugin = {
165
192
  /**
166
193
  * `abide compile`: builds the app, then one executable per target platform.
167
194
  *
168
- * 38.11 — THE BUILD, AND THEN ONE EXECUTABLE PER TARGET with all of it inside (`compiledEntry`).
195
+ * 38.10 — THE BUILD, AND THEN ONE EXECUTABLE PER TARGET with all of it inside (`compiledEntry`).
169
196
  * `--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).
197
+ * `--out` the executable is the app's own name, in the directory the command was run from (38.46).
171
198
  */
172
199
  async function compile(
173
200
  { root }: AppDirectories,
174
201
  { values }: Given,
175
202
  ): Promise<number> {
176
- // 38.49.
203
+ // 38.48.
177
204
  if (values.target !== undefined && values.platforms !== undefined) {
178
205
  console.error(
179
206
  'abide compile: --target is one platform and --platforms is several; give one of them.',
180
207
  )
181
208
  return 1
182
209
  }
183
- const entry = await writtenEntry(root, undefined)
184
- if (entry === undefined) {
210
+ const written = await writtenEntry(root, undefined)
211
+ if (written === undefined) {
185
212
  console.error(`abide compile: nothing under ${root} to compile.`)
186
213
  return 1
187
214
  }
188
- const named = resolve(values.out ?? executableName(root))
215
+ const named = resolve(values.out ?? executableName(packageUnder(root).name))
189
216
  const platforms =
190
217
  values.platforms?.split(',') ??
191
218
  (values.target === undefined ? [undefined] : [values.target])
192
219
  for (const platform of platforms) {
193
- // 38.48.
220
+ // 38.47.
194
221
  const outfile =
195
222
  values.platforms === undefined ? named : `${named}-${platform}`
196
- await compileExecutable(root, entry, platform, outfile)
223
+ await compileExecutable(root, written, platform, outfile)
197
224
  console.log(`abide compile: ${outfile}`)
198
225
  }
199
226
  return 0
@@ -202,39 +229,44 @@ async function compile(
202
229
  /**
203
230
  * `abide bundle`: builds the app, then a desktop app for this platform around its executable.
204
231
  *
205
- * 38.11, 38.71 — `compile`'s executable, started from `runDesktop`, inside a `.app` named after the
232
+ * 38.10, 38.70 — `compile`'s executable, started from `runDesktop`, inside a `.app` named after the
206
233
  * app where the command was run, or at `--out`.
207
234
  */
208
235
  async function bundle(
209
236
  { root }: AppDirectories,
210
237
  { values }: Given,
211
238
  ): Promise<number> {
212
- // 38.78.
239
+ // 38.77.
213
240
  if (process.platform !== 'darwin') {
214
241
  console.error(
215
242
  `abide bundle: a desktop app is built for macOS, and this is ${process.platform}. \`abide compile\` builds an executable for any platform.`,
216
243
  )
217
244
  return 1
218
245
  }
219
- const out = resolve(values.out ?? `${executableName(root)}.app`)
246
+ const out = resolve(
247
+ values.out ?? `${executableName(packageUnder(root).name)}.app`,
248
+ )
220
249
  if (!out.endsWith('.app')) {
221
250
  console.error(
222
251
  `abide bundle: --out names the .app to write, and ${out} is not one.`,
223
252
  )
224
253
  return 1
225
254
  }
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`
255
+ const entry = await writtenEntry(root, async (app) => {
256
+ const launcherPath = `${root}/${LAUNCHER_PAGE}`
257
+ const hasLauncher = app.files[LAUNCHER_PAGE] !== undefined
258
+ const art = await launcherIcon(root)
234
259
  return {
235
- options,
236
- launch: (await Bun.file(launch).exists()) ? launch : undefined,
237
- shell: await launchShell(root, out),
260
+ // 38.82 — the page, where it paints: a `launcher.abide` of its script blocks alone keeps
261
+ // the built-in page, and what its `onLauncher` answers is filed by the boot (37.1).
262
+ launch:
263
+ hasLauncher &&
264
+ paints(parseAbide(await Bun.file(launcherPath).text()))
265
+ ? launcherPath
266
+ : undefined,
267
+ document: launchDocument(app),
268
+ icon: art?.icon,
269
+ accent: art?.accent,
238
270
  }
239
271
  })
240
272
  if (entry === undefined) {
@@ -242,7 +274,7 @@ async function bundle(
242
274
  return 1
243
275
  }
244
276
  const identity = packageUnder(root)
245
- const name = executableName(root)
277
+ const name = executableName(packageUnder(root).name)
246
278
  await compileExecutable(
247
279
  root,
248
280
  entry,
@@ -252,7 +284,7 @@ async function bundle(
252
284
  const icon = await writeIcon(root, `${out}/Contents/Resources`)
253
285
  await Bun.write(
254
286
  `${out}/Contents/Info.plist`,
255
- infoPlist(name, options.name ?? name, identity, icon),
287
+ infoPlist(name, identity, icon),
256
288
  )
257
289
  // AD-HOC SIGNED, WHOLE: the executable bun wrote is signed on its own, and a bundle around a
258
290
  // signed executable that is not itself signed is one Gatekeeper reads as damaged.
@@ -262,11 +294,6 @@ async function bundle(
262
294
  return 0
263
295
  }
264
296
 
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
297
  /**
271
298
  * Builds the app and writes the entry an executable starts from; `undefined` where there is no app.
272
299
  *
@@ -276,53 +303,46 @@ async function writtenEntry(
276
303
  root: string,
277
304
  desktop:
278
305
  | ((
279
- out: string,
306
+ app: CompiledApp,
280
307
  ) => Promise<Parameters<typeof compiledEntry>[0]['desktop']>)
281
308
  | undefined,
282
- ): Promise<string | undefined> {
309
+ ): Promise<{ entry: string; compilation: Compilation } | undefined> {
283
310
  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}`)
311
+ const app = await writeBuild({ root, out })
312
+ if (app === undefined) return undefined
288
313
  const entry = `${out}/compiled.ts`
289
314
  await Bun.write(
290
315
  entry,
291
316
  compiledEntry({
292
- desktop: await desktop?.(out),
293
- out,
294
- hooks: hooksModuleUnder(root),
317
+ desktop: await desktop?.(app),
318
+ root,
319
+ sourceMaps: serverMapsOf(app, root),
295
320
  package: packageUnder(root),
296
- shell: await shellUnder(root),
297
321
  manifest: await Bun.file(`${out}/${MANIFEST}`).json(),
298
- handlerModules,
299
322
  client: pathsUnder(`${out}/${CLIENT_DIRECTORY}`),
300
323
  published: pathsUnder(`${out}/${PUBLIC_DIRECTORY}`),
301
324
  }),
302
325
  )
303
- return entry
326
+ return { entry, compilation: app }
304
327
  }
305
328
 
306
- /** One executable from `entry`, for `platform` or this one, written to `outfile`. */
329
+ /** One executable from `written`'s entry, for `platform` or this one, written to `outfile`. */
307
330
  async function compileExecutable(
308
331
  root: string,
309
- entry: string,
332
+ written: { entry: string; compilation: Compilation },
310
333
  platform: string | undefined,
311
334
  outfile: string,
312
335
  ): Promise<void> {
313
336
  const compiled = await Bun.build({
314
- entrypoints: [entry],
337
+ entrypoints: [written.entry],
315
338
  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
- ],
339
+ // 34.16 — every `.abide` the app imports, as `abide start` serves it.
340
+ plugins: [SERVER_STYLESHEETS, abideModules(root, written.compilation)],
321
341
  // UNMINIFIED, AND MAPPED: the executable is a server, and an error it logs is read by
322
342
  // whoever operates it — a minified line prints as the whole page module it sits in. The
323
343
  // map is embedded in the executable, which reads it from there.
324
344
  sourcemap: 'inline',
325
- // 38.94 — parsed at build time rather than on every boot. ESM because the entry awaits at
345
+ // 38.93 — parsed at build time rather than on every boot. ESM because the entry awaits at
326
346
  // its top level, and bun's bytecode default is CommonJS, which refuses that. Nothing in the
327
347
  // executable or the build's outputs says bytecode is inside, so no test can see this.
328
348
  bytecode: true,
@@ -332,7 +352,7 @@ async function compileExecutable(
332
352
  ? {}
333
353
  : { target: `bun-${platform}` as Bun.Build.CompileTarget }),
334
354
  outfile,
335
- // 38.46 — THE EXECUTABLE IS THE BUILD'S, not the machine's: a `bunfig.toml`
355
+ // 38.45 — THE EXECUTABLE IS THE BUILD'S, not the machine's: a `bunfig.toml`
336
356
  // wherever it is started must not change what it runs. `.env` is still read, as
337
357
  // `abide start` reads it — configuration is the environment's (27.8).
338
358
  autoloadBunfig: false,
@@ -350,39 +370,67 @@ async function compileExecutable(
350
370
  }
351
371
 
352
372
  /**
353
- * The document `src/bundle/launch.abide` renders into: `src/bundle/launch.html`, or a bare one.
373
+ * The document `src/launcher.abide` renders into: abide's own, which the page writes its
374
+ * `<html>` onto (35.15).
354
375
  *
355
- * 38.85 — THE PAGE'S OWN `<style>` IS IN ITS HEAD. A component's stylesheet reaches a browser with
376
+ * 38.84 — THE PAGE'S OWN `<style>` IS IN ITS HEAD. A component's stylesheet reaches a browser with
356
377
  * its client module, and a launch page has none: it is rendered before the app it would load from
357
378
  * is served.
358
379
  */
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)}`
380
+ function launchDocument(app: CompiledApp): string {
381
+ const sheet = app.files[LAUNCHER_PAGE]?.sheet
382
+ return sheet === undefined
383
+ ? DEFAULT_DOCUMENT
384
+ : intoHead(DEFAULT_DOCUMENT, `<style>\n${sheet}</style>`)
371
385
  }
372
386
 
373
387
  /**
374
- * Writes the bundle's icon from `src/bundle/`, and answers whether there was one.
388
+ * The icon the launcher page shows, as a data URL, and the accent it takes from it — or nothing
389
+ * where the app has no icon.
375
390
  *
376
- * 38.80 — `icon.icns` as it is, or `icon.png` drawn at every size an `.icns` holds.
391
+ * 38.101 — drawn small by `sips` first: the accent is a fact about the picture, not its pixels.
392
+ */
393
+ async function launcherIcon(
394
+ root: string,
395
+ ): Promise<{ icon: string; accent: string | undefined } | undefined> {
396
+ const source = await publishedIcon(root)
397
+ if (source === undefined) return undefined
398
+ const drawn = async (size: number): Promise<Uint8Array | undefined> => {
399
+ const path = `${buildDirectory(root)}/icon-${size}.png`
400
+ const code = await spawnedTool('sips', [
401
+ '-s',
402
+ 'format',
403
+ 'png',
404
+ '-z',
405
+ String(size),
406
+ String(size),
407
+ source,
408
+ '--out',
409
+ path,
410
+ ])
411
+ return code === 0 ? await Bun.file(path).bytes() : undefined
412
+ }
413
+ const [shown, sampled] = await Promise.all([drawn(256), drawn(64)])
414
+ if (shown === undefined) return undefined
415
+ return {
416
+ icon: `data:image/png;base64,${shown.toBase64()}`,
417
+ accent: sampled === undefined ? undefined : iconAccent(sampled),
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Writes the bundle's icon from the public files the build copied, and answers whether there was one.
423
+ *
424
+ * 38.79 — the file at `/icon.icns` as it is, or the one at `/icon.png` drawn at every size an
425
+ * `.icns` holds.
377
426
  */
378
427
  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)
428
+ const source = await publishedIcon(root)
429
+ if (source === undefined) return false
430
+ if (source.endsWith(ICON_ADDRESSES.icns)) {
431
+ await Bun.write(`${resources}/icon.icns`, Bun.file(source))
382
432
  return true
383
433
  }
384
- const png = `${root}/bundle/icon.png`
385
- if (!(await Bun.file(png).exists())) return false
386
434
  const iconset = `${buildDirectory(root)}/icon.iconset`
387
435
  await rm(iconset, { recursive: true, force: true })
388
436
  await mkdir(iconset, { recursive: true })
@@ -395,7 +443,7 @@ async function writeIcon(root: string, resources: string): Promise<boolean> {
395
443
  '-z',
396
444
  String(size * scale),
397
445
  String(size * scale),
398
- png,
446
+ source,
399
447
  '--out',
400
448
  `${iconset}/icon_${size}x${size}${scale === 2 ? '@2x' : ''}.png`,
401
449
  ]),
@@ -412,6 +460,15 @@ async function writeIcon(root: string, resources: string): Promise<boolean> {
412
460
  )
413
461
  }
414
462
 
463
+ /** 38.79 — the public file the app's icon is drawn from: `/icon.icns`, else `/icon.png`. */
464
+ async function publishedIcon(root: string): Promise<string | undefined> {
465
+ const published = `${buildDirectory(root)}/${PUBLIC_DIRECTORY}`
466
+ for (const address of [ICON_ADDRESSES.icns, ICON_ADDRESSES.png])
467
+ if (await Bun.file(`${published}${address}`).exists())
468
+ return `${published}${address}`
469
+ return undefined
470
+ }
471
+
415
472
  /** Runs one of macOS's own tools, warning with what it wrote where it fails; answers its exit code. */
416
473
  async function spawnedTool(tool: string, args: string[]): Promise<number> {
417
474
  const child = Bun.spawn([tool, ...args], {
@@ -427,12 +484,7 @@ async function spawnedTool(tool: string, args: string[]): Promise<number> {
427
484
  }
428
485
 
429
486
  /** 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 {
487
+ function infoPlist(name: string, identity: AppPackage, icon: boolean): string {
436
488
  const entries: [string, string][] = [
437
489
  ...(icon
438
490
  ? ([['CFBundleIconFile', '<string>icon</string>']] as [
@@ -445,8 +497,8 @@ function infoPlist(
445
497
  'CFBundleIdentifier',
446
498
  `<string>abide.app.${name.replace(/[^A-Za-z0-9.-]/g, '-')}</string>`,
447
499
  ],
448
- ['CFBundleName', `<string>${escapeText(displayName)}</string>`],
449
- ['CFBundleDisplayName', `<string>${escapeText(displayName)}</string>`],
500
+ ['CFBundleName', `<string>${escapeText(name)}</string>`],
501
+ ['CFBundleDisplayName', `<string>${escapeText(name)}</string>`],
450
502
  ['CFBundlePackageType', '<string>APPL</string>'],
451
503
  [
452
504
  'CFBundleShortVersionString',
@@ -456,7 +508,7 @@ function infoPlist(
456
508
  ['LSMinimumSystemVersion', '<string>13.0</string>'],
457
509
  ['NSHighResolutionCapable', '<true/>'],
458
510
  ['NSPrincipalClass', '<string>NSApplication</string>'],
459
- // 38.72 — the app served on loopback over http, and one connected to wherever it runs.
511
+ // 38.71 — the app served on loopback over http, and one connected to wherever it runs.
460
512
  [
461
513
  'NSAppTransportSecurity',
462
514
  '<dict><key>NSAllowsArbitraryLoads</key><true/></dict>',
@@ -475,30 +527,35 @@ ${body}</dict>
475
527
  }
476
528
 
477
529
  /**
478
- * Copies the app's public files into the build output, and returns how many.
530
+ * Copies the app's public files into the build output, each at its address, and returns how many.
479
531
  *
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.
532
+ * 38.18 — every `public` directory, copied into the built output at the address each file is
533
+ * answered at, so `abide start` reads the address off the output's own layout. Copied rather than
534
+ * left where it is, so `abide start` answers out of what the build produced and an app moving a
535
+ * file after a build cannot change what is served.
483
536
  */
484
537
  async function copyPublic(
485
538
  root: string,
486
539
  out: string,
487
- names: string[],
540
+ published: Map<string, string>,
541
+ began: number,
488
542
  ): Promise<number> {
489
- const from = `${root}/ui/${PUBLIC_DIRECTORY}`
490
-
491
543
  const copies: Promise<void>[] = []
544
+ const produced = new Set<string>()
492
545
  // 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)
546
+ for (const [source, address] of published) {
547
+ produced.add(address.slice(1))
494
548
  copies.push(
495
549
  writeWhole(
496
- `${out}/${PUBLIC_DIRECTORY}/${name}`,
497
- Bun.file(`${from}/${name}`),
550
+ `${out}/${PUBLIC_DIRECTORY}${address}`,
551
+ Bun.file(`${root}/${source}`),
498
552
  ),
499
553
  )
554
+ }
500
555
  await Promise.all(copies)
501
- return names.length
556
+ // 38.66 — a public file the app no longer has is no longer answered.
557
+ await pruneStale(`${out}/${PUBLIC_DIRECTORY}`, produced, began)
558
+ return published.size
502
559
  }
503
560
 
504
561
  /**
@@ -511,10 +568,11 @@ async function copyPublic(
511
568
  * `--splitting`, so a destination's markup and bindings stay in the chunk its own route loads
512
569
  * (30.11) rather than landing in the entry every page pays for. 38.5 IS THIS FUNCTION: hashed and
513
570
  * 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.
571
+ * route module needs for 38.28), and the precompressed copies (`precompressOne`). What the manifest
572
+ * does NOT carry yet is 38.18's list of public files.
516
573
  */
517
574
  async function bundleClient(
575
+ root: string,
518
576
  out: string,
519
577
  plugins: Bun.BunPlugin[],
520
578
  publicAddresses: string[],
@@ -525,18 +583,18 @@ async function bundleClient(
525
583
  // AN APP OF HANDLERS ALONE has no page to boot, and is served with no client — the manifest
526
584
  // written anyway, so `abide start` reads one shape whatever the app holds.
527
585
  if (!paged) {
528
- await pruneStale(client, '**/*', new Set(), began)
586
+ await pruneStale(client, new Set(), began)
529
587
  await writeWhole(`${out}/${MANIFEST}`, JSON.stringify(NO_MANIFEST))
530
588
  return
531
589
  }
532
- // 38.67 — STAGED, THEN MOVED IN FILE BY FILE. `Bun.build` writes in place, so a second build
590
+ // 38.66 — STAGED, THEN MOVED IN FILE BY FILE. `Bun.build` writes in place, so a second build
533
591
  // into the same `out` — or `abide start` serving it — read a chunk half written, and emptying
534
592
  // `client/` first took the one it was serving away. Every name but the manifest's is a hash of
535
593
  // what it holds, so two builds of one tree move in the same bytes under the same names.
536
594
  const staging = `${out}/.staging-${crypto.randomUUID()}`
537
595
  try {
538
596
  const built = await Bun.build({
539
- ...clientBuildOptions(out, plugins, publicAddresses),
597
+ ...clientBuildOptions(root, plugins, publicAddresses),
540
598
  outdir: staging,
541
599
  // 38.5 — MINIFIED, and EVERY NAME HASHED, the entry's included: chunks and assets are
542
600
  // already `[name]-[hash]` by default and the entry is not, which left the one file every
@@ -575,16 +633,11 @@ async function bundleClient(
575
633
  )
576
634
  }
577
635
  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.
636
+ await pruneStale(client, produced, began)
581
637
  await writeWhole(
582
638
  `${out}/${MANIFEST}`,
583
639
  JSON.stringify(
584
- manifestOf(
585
- built.metafile as Bun.BuildMetafile,
586
- realpathSync(out),
587
- ),
640
+ manifestOf(built.metafile as Bun.BuildMetafile, root),
588
641
  ),
589
642
  )
590
643
  } finally {
@@ -597,18 +650,17 @@ async function bundleClient(
597
650
  *
598
651
  * A CHUNK WHOSE HASH CHANGED leaves the old one behind otherwise, and the directory is the union of
599
652
  * 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
653
+ * code no page loads. 38.66 — only what was written before this build began: anything newer is a
601
654
  * concurrent build's, and its manifest may name it.
602
655
  */
603
656
  async function pruneStale(
604
657
  directory: string,
605
- pattern: string,
606
- produced: { has(name: string): boolean },
658
+ produced: ReadonlySet<string>,
607
659
  began: number,
608
660
  ): Promise<void> {
609
661
  const stale: Promise<void>[] = []
610
662
  // 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))
663
+ for (const name of namesUnder(directory))
612
664
  if (
613
665
  !produced.has(name) &&
614
666
  Bun.file(`${directory}/${name}`).lastModified < began
@@ -620,7 +672,7 @@ async function pruneStale(
620
672
  /**
621
673
  * Writes `path` whole: to a name of its own beside it, then renamed over it.
622
674
  *
623
- * 38.67 — a reader, or a second build, finds the last complete file there and never a partial one.
675
+ * 38.66 — a reader, or a second build, finds the last complete file there and never a partial one.
624
676
  */
625
677
  async function writeWhole(path: string, content: string | Blob): Promise<void> {
626
678
  const partial = `${path}.${crypto.randomUUID()}.partial`
@@ -687,7 +739,7 @@ function inApp<Answer>(
687
739
  const app = directoriesOf(given.values)
688
740
  if (app === undefined) {
689
741
  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.`,
742
+ `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
743
  )
692
744
  return 1
693
745
  }
@@ -734,6 +786,16 @@ async function scaffoldCommand({
734
786
  return 0
735
787
  }
736
788
 
789
+ /** `abide routes`: the app's map, as text or with `--json` as one document. 38.108. */
790
+ async function routes(
791
+ { root }: AppDirectories,
792
+ { values }: Given,
793
+ ): Promise<number> {
794
+ const map = await appMap(root)
795
+ console.log(values.json ? JSON.stringify(map, null, 4) : mapText(map))
796
+ return 0
797
+ }
798
+
737
799
  /**
738
800
  * `abide check`: prints every finding, and fails on any error.
739
801
  *
@@ -778,7 +840,7 @@ async function check(
778
840
  /**
779
841
  * `abide lsp`: the language server over stdio.
780
842
  *
781
- * 38.12 — the language server, for as long as the editor holds stdin open. Nothing is printed: stdout
843
+ * 38.11 — the language server, for as long as the editor holds stdin open. Nothing is printed: stdout
782
844
  * is the protocol.
783
845
  *
784
846
  * Not `inApp`: an editor starts it wherever its workspace is, and each document finds its own app.