@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
@@ -1,4 +1,5 @@
1
1
  import { SyntaxKind } from 'typescript/unstable/ast'
2
+ import { COMPONENT_QUERY, TEXT_QUERY } from '#server/MODULE_QUERIES.ts'
2
3
  import { isContextualKeyword, type Token, tokenize } from './tokenize.ts'
3
4
 
4
5
  /**
@@ -11,13 +12,12 @@ import { isContextualKeyword, type Token, tokenize } from './tokenize.ts'
11
12
  * WHERE AN IMPORT BEGINS AND ENDS IS READ OFF the one tokenizer, which carries the template
12
13
  * re-scan a bare `` ` `` needs, so an import after a template literal is still found.
13
14
  *
14
- * With `landed`, every specifier the build moved is rewritten where it stands, a static import's
15
- * and an `import('…')`'s alike (`landedSpecifier`) — so the two cannot land in different places.
15
+ * Every specifier naming a face of a `.abide` is rewritten where it stands, a static import's and an
16
+ * `import('…')`'s alike (`facedSpecifier`) — so the two cannot ask for different faces.
16
17
  */
17
18
  export function splitImports(
18
19
  script: string,
19
- lane: '' | '.client' = '',
20
- landed?: ReadonlyMap<string, string>,
20
+ lane: Lane = 'component',
21
21
  ): {
22
22
  imports: string
23
23
  body: string
@@ -29,18 +29,13 @@ export function splitImports(
29
29
  let body = ''
30
30
  let at = 0
31
31
  for (const span of spans) {
32
+ const moved = facedSpecifier(span.specifier, lane)
32
33
  if (span.dynamic) {
33
- if (landed === undefined) continue
34
- const moved = landedSpecifier(span.specifier, lane, landed)
35
34
  if (moved === undefined) continue
36
35
  body += `${script.slice(at, span.literalStart + 1)}${moved}`
37
36
  at = span.end - 1
38
37
  continue
39
38
  }
40
- const moved =
41
- landed === undefined
42
- ? undefined
43
- : landedSpecifier(span.specifier, lane, landed)
44
39
  imports +=
45
40
  moved === undefined
46
41
  ? `${script.slice(span.start, span.end)}\n`
@@ -50,7 +45,9 @@ export function splitImports(
50
45
  // a `[` that automatic semicolon insertion does NOT break — it reads as `f()[2]`, a
51
46
  // different program with nothing about it saying so. The empty statement left in its
52
47
  // place is the separator the import was.
53
- body += `${script.slice(at, span.start)};`
48
+ // 38.97 — and as many lines as it spanned, so the body stays line for line with the
49
+ // script it came from and a source map can read the one off the other.
50
+ body += `${script.slice(at, span.start)};${'\n'.repeat(newlinesIn(script, span.start, span.end))}`
54
51
  at = span.end
55
52
  // The `;` an import may end with would otherwise be left behind beside the one above.
56
53
  if (script[at] === ';') at += 1
@@ -58,6 +55,18 @@ export function splitImports(
58
55
  return { imports, body: body + script.slice(at) }
59
56
  }
60
57
 
58
+ /** How many line breaks `text` holds between `start` and `end`. */
59
+ export function newlinesIn(text: string, start: number, end: number): number {
60
+ let count = 0
61
+ for (
62
+ let at = text.indexOf('\n', start);
63
+ at !== -1 && at < end;
64
+ at = text.indexOf('\n', at + 1)
65
+ )
66
+ count += 1
67
+ return count
68
+ }
69
+
61
70
  // What can stand between `import` and its specifier. A token that cannot is a malformed import,
62
71
  // and stopping there is what keeps one broken line from swallowing the statements after it.
63
72
  //
@@ -252,7 +261,14 @@ export function abideImports(
252
261
  const found: { specifier: string; closingQuote: number }[] = []
253
262
  if (!script.includes('.abide')) return found
254
263
  for (const span of importSpans(script))
255
- if (span.specifier.endsWith('.abide'))
264
+ if (
265
+ span.specifier.endsWith('.abide') &&
266
+ // 34.21 — a `.abide` imported as text is its text, and no component edge.
267
+ (span.dynamic ||
268
+ !IMPORT_ATTRIBUTES.text.test(
269
+ script.slice(span.literalEnd, span.end),
270
+ ))
271
+ )
256
272
  found.push({
257
273
  specifier: span.specifier,
258
274
  closingQuote: span.literalEnd - 1,
@@ -260,40 +276,167 @@ export function abideImports(
260
276
  return found
261
277
  }
262
278
 
279
+ /** One name an import statement binds. */
280
+ export type ImportBinding = {
281
+ /** `default`, `*`, or the name imported — a string literal's text where one is written. */
282
+ imported: string
283
+ local: string
284
+ /** Under a `type` modifier, on the binding or on the whole statement: no value of its own. */
285
+ typeOnly: boolean
286
+ /** The binding as a statement's clause spells it, its own modifier kept: `type a as b`. */
287
+ written: string
288
+ }
289
+
263
290
  /**
264
- * Every relative specifier a script imports, re-exports or `import('…')`s, as written, other than
265
- * a `.abide` one.
291
+ * Every static import statement a script makes: where it stands, what it names, and each binding,
292
+ * read off the one tokenizer — so a comment or a line break inside the clause is trivia, as it is
293
+ * to bun, and never part of a name.
266
294
  *
267
- * 34.14 — what the build rewrites to the path from the compiled module back to the source.
295
+ * `attributes` is the statement's text after its specifier, a `with { … }` or nothing. A statement of
296
+ * no shape this reads answers no bindings.
268
297
  */
269
- export function relativeSpecifiers(script: string): string[] {
270
- const specifiers: string[] = []
271
- if (!script.includes('./')) return specifiers
272
- for (const span of importSpans(script))
298
+ export function importStatements(script: string): {
299
+ start: number
300
+ end: number
301
+ specifier: string
302
+ typeOnly: boolean
303
+ attributes: string
304
+ bindings: ImportBinding[]
305
+ }[] {
306
+ const found: ReturnType<typeof importStatements> = []
307
+ if (!script.includes('import')) return found
308
+ for (const span of importSpans(script)) {
309
+ if (span.dynamic || !script.startsWith('import', span.start)) continue
310
+ const tokens = tokenize(script.slice(span.start, span.literalStart))
311
+ const bindings: ImportBinding[] = []
312
+ let at = 1
313
+ // `type` modifies the statement unless it is the default's own name: `import type from`.
314
+ const typeOnly =
315
+ tokens[at]?.text === 'type' &&
316
+ tokens[at + 1] !== undefined &&
317
+ tokens[at + 1]?.kind !== SyntaxKind.FromKeyword &&
318
+ tokens[at + 1]?.kind !== SyntaxKind.CommaToken
319
+ if (typeOnly) at += 1
320
+ const head = tokens[at]
273
321
  if (
274
- (span.specifier.startsWith('./') ||
275
- span.specifier.startsWith('../')) &&
276
- !span.specifier.endsWith('.abide')
277
- )
278
- specifiers.push(span.specifier)
279
- return specifiers
322
+ head !== undefined &&
323
+ head.kind !== SyntaxKind.OpenBraceToken &&
324
+ head.kind !== SyntaxKind.AsteriskToken &&
325
+ (tokens[at + 1]?.kind === SyntaxKind.FromKeyword ||
326
+ tokens[at + 1]?.kind === SyntaxKind.CommaToken)
327
+ ) {
328
+ bindings.push({
329
+ imported: 'default',
330
+ local: head.text,
331
+ typeOnly,
332
+ written: head.text,
333
+ })
334
+ at += tokens[at + 1]?.kind === SyntaxKind.CommaToken ? 2 : 1
335
+ }
336
+ if (tokens[at]?.kind === SyntaxKind.AsteriskToken) {
337
+ const local = tokens[at + 2]?.text ?? ''
338
+ bindings.push({
339
+ imported: '*',
340
+ local,
341
+ typeOnly,
342
+ written: `* as ${local}`,
343
+ })
344
+ } else if (tokens[at]?.kind === SyntaxKind.OpenBraceToken) {
345
+ // Each element between the braces: `[type] imported [as local]`.
346
+ let element: Token[] = []
347
+ for (at += 1; at < tokens.length; at += 1) {
348
+ const token = tokens[at] as Token
349
+ const closed = token.kind === SyntaxKind.CloseBraceToken
350
+ if (closed || token.kind === SyntaxKind.CommaToken) {
351
+ // `type as t` imports `type`; `type x` and `type x as y` modify.
352
+ const modified =
353
+ element[0]?.text === 'type' &&
354
+ (element.length === 2 || element.length === 4)
355
+ const name = element[modified ? 1 : 0]
356
+ if (name !== undefined) {
357
+ const imported =
358
+ name.kind === SyntaxKind.StringLiteral
359
+ ? name.text.slice(1, -1)
360
+ : name.text
361
+ let written = ''
362
+ for (const part of element)
363
+ written +=
364
+ written === '' ? part.text : ` ${part.text}`
365
+ bindings.push({
366
+ imported,
367
+ local: (element.at(-1) as Token).text,
368
+ typeOnly: typeOnly || modified,
369
+ written,
370
+ })
371
+ }
372
+ element = []
373
+ if (closed) break
374
+ } else element.push(token)
375
+ }
376
+ }
377
+ found.push({
378
+ start: span.start,
379
+ end: span.end,
380
+ specifier: span.specifier,
381
+ typeOnly,
382
+ attributes: script.slice(span.literalEnd, span.end).trim(),
383
+ bindings,
384
+ })
385
+ }
386
+ return found
280
387
  }
281
388
 
389
+ // The attribute a static import is read by: as text (34.21), or as a mount of routes (23.19).
390
+ export const IMPORT_ATTRIBUTES = {
391
+ text: /\btype\s*:\s*['"]text['"]/,
392
+ routes: /\btype\s*:\s*['"]routes['"]/,
393
+ } as const
394
+
282
395
  /**
283
- * Every import declaration in a script: its text and where it starts.
396
+ * Every static import a script makes `with { type }`: the specifier, where the statement and its
397
+ * literal begin, and where the attributes end.
284
398
  *
285
- * `import …` alone — an `export … from` binds nothing in the script it stands in.
399
+ * 34.21 — a text import's literal-to-end span is what the projection writes over with the specifier
400
+ * the checker types a `string` (`SOURCE_SUFFIX`), and the emitters ask for as text
401
+ * (`facedSpecifier`). 23.19 — a routes import's whole statement is what the projection blanks: a
402
+ * mount is read by the build off the file, and neither bun nor the checker has a module to give it.
286
403
  */
287
- function importDeclarations(script: string): { text: string; start: number }[] {
288
- const declarations: { text: string; start: number }[] = []
289
- if (!script.includes('import')) return declarations
404
+ export function attributedImports(
405
+ script: string,
406
+ type: keyof typeof IMPORT_ATTRIBUTES,
407
+ ): ImportSpan[] {
408
+ const found: ImportSpan[] = []
409
+ if (!script.includes(type)) return found
410
+ const attribute = IMPORT_ATTRIBUTES[type]
290
411
  for (const span of importSpans(script))
291
- if (!span.dynamic && script.startsWith('import', span.start))
292
- declarations.push({
293
- text: script.slice(span.start, span.end),
294
- start: span.start,
295
- })
296
- return declarations
412
+ if (
413
+ !span.dynamic &&
414
+ attribute.test(script.slice(span.literalEnd, span.end))
415
+ )
416
+ found.push(span)
417
+ return found
418
+ }
419
+
420
+ /** What a file imported as text is written as once projected: `./X.abide` → `./X.abide.source`. */
421
+ export const SOURCE_SUFFIX = '.source'
422
+
423
+ /**
424
+ * `source` with each module specifier — of a declaration, an `export … from` or an `import('…')` —
425
+ * written as `rewrite` answers for it, and left as it is where `rewrite` answers `undefined`.
426
+ */
427
+ export function respecified(
428
+ source: string,
429
+ rewrite: (specifier: string) => string | undefined,
430
+ ): string {
431
+ let out = ''
432
+ let at = 0
433
+ for (const span of importSpans(source)) {
434
+ const written = rewrite(span.specifier)
435
+ if (written === undefined) continue
436
+ out += `${source.slice(at, span.literalStart + 1)}${written}`
437
+ at = span.literalEnd - 1
438
+ }
439
+ return at === 0 ? source : out + source.slice(at)
297
440
  }
298
441
 
299
442
  /**
@@ -305,62 +448,41 @@ export function importedNames(
305
448
  script: string,
306
449
  ): { name: string; specifier: string }[] {
307
450
  const names: { name: string; specifier: string }[] = []
308
- for (const declaration of importDeclarations(script)) {
309
- const tokens = tokenize(declaration.text)
310
- const last = tokens.findLastIndex(
311
- (token) => token.kind === SyntaxKind.StringLiteral,
312
- )
313
- if (last === -1) continue
314
- const specifier = (tokens[last] as Token).text.slice(1, -1)
315
- for (let at = 1; at < last; at += 1) {
316
- const token = tokens[at] as Token
317
- if (token.kind === SyntaxKind.FromKeyword) break
318
- if (token.kind === SyntaxKind.AsKeyword) continue
319
- if (
320
- token.kind !== SyntaxKind.Identifier &&
321
- !isContextualKeyword(token.kind)
322
- )
323
- continue
324
- const next = tokens[at + 1] as Token
325
- // A name followed by `as` is renamed, and a modifier is followed by what it modifies.
326
- if (next.kind === SyntaxKind.AsKeyword) continue
327
- if (
328
- token.text === 'type' &&
329
- (next.kind === SyntaxKind.Identifier ||
330
- next.kind === SyntaxKind.OpenBraceToken ||
331
- next.kind === SyntaxKind.AsteriskToken ||
332
- isContextualKeyword(next.kind))
333
- )
334
- continue
335
- names.push({ name: token.text, specifier })
336
- }
337
- }
451
+ // `import …` alone — an `export … from` binds nothing in the script it stands in.
452
+ for (const declaration of importStatements(script))
453
+ for (const binding of declaration.bindings)
454
+ names.push({
455
+ name: binding.local,
456
+ specifier: declaration.specifier,
457
+ })
338
458
  return names
339
459
  }
340
460
 
341
461
  /**
342
- * Where the build moved `specifier`: a `.abide` one to the lane's compiled module for it, and a
343
- * relative one to its source — or `undefined` where it stays as written.
462
+ * The face of a `.abide` a specifier asks for, spelled on it (`MODULE_QUERIES`), or `undefined`
463
+ * where the specifier stays as written.
344
464
  *
345
- * 32.17 — A `.abide` IMPORT IS AN IMPORT OF THE MODULE THAT FILE COMPILES TO, and which of the two
346
- * it is is the LANE's business: the server module renders the child, the client module mounts it.
347
- * WHERE that module is, is the build's: `landed` maps a specifier to the path, relative and
348
- * without an extension, the build resolved it to (`componentGraph`) — and a `.abide` specifier it
349
- * does not map keeps the path as written.
465
+ * 34.14 — EVERY OTHER SPECIFIER STAYS AS WRITTEN: the compiled module is served at its `.abide`'s
466
+ * own path, so bun resolves a relative, a `#` or a bare specifier from the file that wrote it.
350
467
  *
351
- * 34.14 — `landed` maps any other relative specifier to the path from the module, in the output
352
- * tree, back to the file the author meant, which the output tree does not hold.
468
+ * 32.17 — a component block's `.abide` import is of the module that file compiles to; a server
469
+ * block's stays bare, for the face a module abide did not compile gets (34.16). 34.21 — a file the
470
+ * projection marked as text is asked for as its text.
353
471
  */
354
- function landedSpecifier(
355
- specifier: string,
356
- lane: '' | '.client',
357
- landed: ReadonlyMap<string, string>,
358
- ): string | undefined {
359
- if (specifier.endsWith('.abide'))
360
- return `${landed.get(specifier) ?? specifier.slice(0, -'.abide'.length)}${lane}.ts`
361
- return landed.get(specifier)
472
+ function facedSpecifier(specifier: string, lane: Lane): string | undefined {
473
+ if (specifier.endsWith(SOURCE_SUFFIX))
474
+ return `${specifier.slice(0, -SOURCE_SUFFIX.length)}${TEXT_QUERY}`
475
+ if (lane === 'component' && specifier.endsWith('.abide'))
476
+ return `${specifier}${COMPONENT_QUERY}`
477
+ return undefined
362
478
  }
363
479
 
480
+ /**
481
+ * Which face a block's imports are written for: a component's — its `<script>` and
482
+ * `<script module>`, in either lane — or a server block's, whose `.abide` imports stay bare (34.16).
483
+ */
484
+ export type Lane = 'component' | 'source'
485
+
364
486
  /**
365
487
  * `imports` with `props` dropped from the abide import that names it.
366
488
  *
@@ -31,6 +31,20 @@ export function tokenize(source: string): Token[] {
31
31
  return tokens
32
32
  }
33
33
 
34
+ /**
35
+ * Whether a token of the code scanned from `from` begins at `at` — false where `at` is inside a
36
+ * string, a template, a regex or a comment.
37
+ */
38
+ export function startsToken(source: string, from: number, at: number): boolean {
39
+ let found = false
40
+ scan(source, from, (_kind, scanner) => {
41
+ const start = scanner.getTokenStart()
42
+ found = start === at
43
+ return start < at
44
+ })
45
+ return found
46
+ }
47
+
34
48
  /**
35
49
  * How far `kind` moves the bracket depth a top-level separator is found at: a template's
36
50
  * substitutions nest as a call's arguments do, so `${a, b}` holds its comma.
@@ -1,4 +1,4 @@
1
- // 38.31 — AN APP WITH NO tsconfig.json IS TYPED AGAINST ONE OF ABIDE'S, not read untyped. The checker
1
+ // 38.30 — AN APP WITH NO tsconfig.json IS TYPED AGAINST ONE OF ABIDE'S, not read untyped. The checker
2
2
  // is what tells the compiler which names are `Reactive`s (31.3), and with no project to ask every
3
3
  // template position read as static: the server painted an unlanded read by throwing out of the
4
4
  // render (41.4), the client never bound a slot at all, and `props` went unseen — an app that built
@@ -10,6 +10,8 @@
10
10
 
11
11
  import { mkdtemp, rm } from 'node:fs/promises' // bun exposes no temporary-directory creation or recursive removal of its own
12
12
  import { tmpdir } from 'node:os' // bun exposes no temporary-directory path of its own
13
+ import { dirname, resolve } from 'node:path' // bun exposes no path helpers of its own
14
+ import { nearest } from './nearest.ts'
13
15
 
14
16
  /** What every abide app in the docs and the scaffold is written against. */
15
17
  export const COMPILER_OPTIONS = {
@@ -31,7 +33,7 @@ export async function writeDefaultProject(
31
33
  await Bun.write(
32
34
  tsconfig,
33
35
  JSON.stringify({
34
- compilerOptions: COMPILER_OPTIONS,
36
+ compilerOptions: { ...COMPILER_OPTIONS, paths: await aliasPaths(root) },
35
37
  include: [`${root}/**/*`],
36
38
  }),
37
39
  )
@@ -40,3 +42,28 @@ export async function writeDefaultProject(
40
42
  remove: () => rm(directory, { recursive: true, force: true }),
41
43
  }
42
44
  }
45
+
46
+ /**
47
+ * The app's `imports` aliases as `paths`, each target absolute.
48
+ *
49
+ * 34.21 — the checker resolves a text import's declaration beside the file a path names, and takes
50
+ * an `imports` target as the exact file, so `#src/x.abide` imported as text is found through these.
51
+ */
52
+ async function aliasPaths(root: string): Promise<Record<string, string[]>> {
53
+ const paths: Record<string, string[]> = {}
54
+ const manifest = nearest(root, 'package.json')
55
+ if (manifest === undefined) return paths
56
+ const imports = (
57
+ (await Bun.file(manifest).json()) as {
58
+ imports?: Record<string, unknown>
59
+ }
60
+ ).imports
61
+ const base = dirname(manifest)
62
+ for (const [key, targets] of Object.entries(imports ?? {})) {
63
+ const resolved: string[] = []
64
+ for (const target of [targets].flat())
65
+ if (typeof target === 'string') resolved.push(resolve(base, target))
66
+ if (resolved.length > 0) paths[key] = resolved
67
+ }
68
+ return paths
69
+ }
@@ -1,6 +1,6 @@
1
1
  // WHERE THE BROWSER BUNDLE LANDS, named once for the commands that have to agree about it:
2
2
  // `abide build` writes it, `abide start` answers it, and `abide dev` builds it into memory
3
- // (38.17, 38.21). A leaf, with no imports of its own, for the reason every constant crossing a
3
+ // (38.16, 38.20). A leaf, with no imports of its own, for the reason every constant crossing a
4
4
  // seam is one — the build is in `bin/` and the server is in `src/server/`, and a string spelled out
5
5
  // in both is a build that silently serves nothing. The addresses it is answered at are
6
6
  // `#shared/ADDRESSES.ts`.
@@ -24,10 +24,16 @@ export const CLIENT_ENTRY = 'entry'
24
24
  *
25
25
  * 38.5 — the manifest `manifestOf` reads off the bundle, below the build's own output directory
26
26
  * and BESIDE the client directory rather than in it: everything in there is answered under
27
- * `CLIENT_ADDRESS` (38.21), and the manifest is the server's, not the browser's.
27
+ * `CLIENT_ADDRESS` (38.20), and the manifest is the server's, not the browser's.
28
28
  */
29
29
  export const MANIFEST = 'manifest.json'
30
30
 
31
+ /**
32
+ * The compilation `abide build` keeps beside the client directory: every `.abide` compiled, and
33
+ * the build's own tables (`Compilation`), which `abide start` serves without the compiler.
34
+ */
35
+ export const COMPILATION = 'compilation.json'
36
+
31
37
  /**
32
38
  * The encodings a response can be compressed in, each with its precompressed file's suffix.
33
39
  *
@@ -39,18 +45,10 @@ export const MANIFEST = 'manifest.json'
39
45
  export const ENCODING_SUFFIX = { br: '.br', gzip: '.gz' } as const
40
46
 
41
47
  /**
42
- * Where `abide build` copies `src/ui/public`, below its own output directory.
48
+ * Where `abide build` copies every `public` directory, each file at its address, below its own
49
+ * output directory. The source's own name for one is `#shared/RESERVED_DIRECTORIES.ts`'s.
43
50
  *
44
- * 38.19 — a directory rather than the source tree: `abide start` boots what the build produced and
51
+ * 38.18 — a directory rather than the source tree: `abide start` boots what the build produced and
45
52
  * reaches for nothing an app might have moved since.
46
53
  */
47
54
  export const PUBLIC_DIRECTORY = 'public'
48
-
49
- /**
50
- * Where the build puts the module a browser gets in place of each handler module.
51
- *
52
- * 16.28 — below the build's own output: the source's path, with this in front. One lookup for the
53
- * client bundle to make (`fenceServer`), and a place a reader can open to see exactly what the
54
- * browser was handed.
55
- */
56
- export const WIRE_DIRECTORY = 'wire'
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The port an app listens on where neither `--port` nor `PORT` names one: `Bun.serve`'s own default,
3
- * so `abide start`'s, the first `abide dev` tries (38.64), and the one a command reaching the local
3
+ * so `abide start`'s, the first `abide dev` tries (38.63), and the one a command reaching the local
4
4
  * app calls.
5
5
  */
6
6
  export const DEFAULT_PORT = 3000
@@ -0,0 +1,27 @@
1
+ // WHAT AN IMPORT OF A `.abide` OR A HANDLER FILE ASKS FOR, spelled on the specifier: bun hands a
2
+ // plugin no import attributes, so the compiler writes the face it wants where the plugin can read it
3
+ // (`abideModules`). A `.abide` named with none is the face a module abide did not compile gets.
4
+
5
+ /** The module the file compiles to, as a compiled module imports it. 32.17. */
6
+ export const COMPONENT_QUERY = '?component'
7
+
8
+ /** The file's scoped stylesheet, which its browser module imports. 34.4. */
9
+ export const STYLE_QUERY = '?style'
10
+
11
+ /** The file's text. 34.21. */
12
+ export const TEXT_QUERY = '?text'
13
+
14
+ /**
15
+ * The file's browser module, asked for outside a browser bundle — a component's hydration tested
16
+ * under a DOM in `bun test`. Its own `.abide` imports are browser modules too.
17
+ */
18
+ export const CLIENT_QUERY = '?client'
19
+
20
+ /**
21
+ * What a handler file is imported as: its own path with this after it, so the plugin can serve it
22
+ * between the calls that file its hooks (37.1) and a browser its wrapper (16.74). A stack frame names
23
+ * the file without it (38.97).
24
+ */
25
+ export const HANDLER_SUFFIX = '.abide-handler'
26
+ /** A path that is a handler file's face. */
27
+ export const HANDLER_FACE = new RegExp(`${RegExp.escape(HANDLER_SUFFIX)}$`)
@@ -0,0 +1,7 @@
1
+ // The build's own modules below `__abide` (`abideModules`), beside the client entry (`CLIENT_ENTRY`).
2
+
3
+ /** The server's tables: routes, error pages, server blocks and handlers. */
4
+ export const SERVER_TABLE = 'server.ts'
5
+
6
+ /** The client router's table, and the `start` the entry calls. */
7
+ export const ROUTES_TABLE = 'routes.ts'
@@ -1,6 +1,5 @@
1
- // Where `abide dev`'s compiled modules pretend to live, below the app's root: the compile points a
2
- // relative import back at its source from here (34.14), and the worker serves the modules under it.
3
- // Under the app's own root so that `#ui/…` and a bare package name resolve from here exactly as they
4
- // resolve from the build tree — both sit inside the app's package — and under a `__abide` directory
5
- // for the reason the addresses are: no app writes one.
1
+ // Where the build's own modules — the server's tables, the router's and the client entry — are served
2
+ // from, below the app's root (`abideModules`). Under the root so a bare specifier in them resolves as
3
+ // one in the app does, and under a `__abide` directory for the reason the addresses are: no app writes
4
+ // one.
6
5
  export const VIRTUAL_DIRECTORY = '__abide'