@abide/abide 0.102.0 → 0.104.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 (73) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/bin/abide.ts +31 -28
  3. package/docs/REGISTRY.md +40 -36
  4. package/docs/RULEBOOK.md +113 -92
  5. package/docs/abide.md +788 -554
  6. package/package.json +2 -1
  7. package/scaffold/AGENTS.md +13 -6
  8. package/src/compiler/TypeSession.ts +108 -35
  9. package/src/compiler/appMap.ts +91 -0
  10. package/src/compiler/argsSchemaOf.ts +1 -1
  11. package/src/compiler/checkApp.ts +35 -18
  12. package/src/compiler/checkedShadow.ts +40 -7
  13. package/src/compiler/compileApp.ts +271 -76
  14. package/src/compiler/compiledEntry.ts +10 -6
  15. package/src/compiler/componentGraph.ts +6 -7
  16. package/src/compiler/declaredExports.ts +3 -3
  17. package/src/compiler/emitClient.ts +3 -3
  18. package/src/compiler/emitComponent.ts +9 -9
  19. package/src/compiler/emitSource.ts +5 -9
  20. package/src/compiler/handlersUnder.ts +114 -73
  21. package/src/compiler/hooksCalled.ts +35 -0
  22. package/src/compiler/mountsOf.ts +30 -18
  23. package/src/compiler/parseAbide.ts +12 -12
  24. package/src/compiler/projectShadow.ts +71 -46
  25. package/src/compiler/readsIn.ts +2 -2
  26. package/src/compiler/scaffold.ts +24 -21
  27. package/src/compiler/serveLanguage.ts +0 -24
  28. package/src/compiler/sharedModuleWrites.ts +2 -2
  29. package/src/compiler/splitImports.ts +4 -4
  30. package/src/compiler/writeDefaultProject.ts +28 -1
  31. package/src/server/MODULE_QUERIES.ts +12 -3
  32. package/src/server/abideModules.ts +156 -53
  33. package/src/server/answerRequest.ts +9 -4
  34. package/src/server/answerRpc.ts +18 -16
  35. package/src/server/appTree.ts +92 -25
  36. package/src/server/commands.ts +5 -4
  37. package/src/server/componentModules.d.ts +1 -2
  38. package/src/server/createApp.ts +23 -11
  39. package/src/server/devWorker.ts +5 -1
  40. package/src/server/fenceServer.ts +36 -13
  41. package/src/server/generatedSurfaces.ts +12 -53
  42. package/src/server/hooks.ts +17 -4
  43. package/src/server/index.ts +1 -0
  44. package/src/server/installSourceMaps.ts +6 -1
  45. package/src/server/launcherOptions.ts +12 -13
  46. package/src/server/launcherPage.ts +2 -2
  47. package/src/server/nativeMenu.ts +3 -3
  48. package/src/server/preload.ts +13 -0
  49. package/src/server/runDesktop.ts +11 -16
  50. package/src/server/serveDev.ts +8 -2
  51. package/src/server/surfaceOf.ts +2 -0
  52. package/src/server/versionOf.ts +1 -2
  53. package/src/server/withAbideSources.ts +1 -1
  54. package/src/shared/ADDRESSES.ts +3 -0
  55. package/src/shared/LAUNCHER_FILES.ts +9 -0
  56. package/src/shared/RESERVED_DIRECTORIES.ts +19 -4
  57. package/src/shared/index.ts +2 -1
  58. package/src/{ui → shared}/menuBridge.ts +1 -1
  59. package/src/shared/menuItem.ts +2 -2
  60. package/src/shared/onMenu.ts +58 -0
  61. package/src/shared/reactive/Cursor.ts +66 -123
  62. package/src/shared/reactive/Reactive.ts +74 -28
  63. package/src/shared/reactive/callLoad.ts +4 -18
  64. package/src/shared/reactive/chunksOfStream.ts +10 -34
  65. package/src/{ui → shared}/reportMenu.ts +7 -9
  66. package/src/shared/routePattern.ts +74 -23
  67. package/src/shared/rpc.ts +1 -1
  68. package/src/ui/Router.ts +0 -6
  69. package/src/compiler/syncTypes.ts +0 -32
  70. package/src/compiler/writeTypes.ts +0 -82
  71. package/src/server/LAUNCHER_FILES.ts +0 -9
  72. /package/scaffold/src/{page.abide → index.abide} +0 -0
  73. /package/src/{compiler → shared}/SPECIFIERS.ts +0 -0
@@ -24,7 +24,7 @@ import type {
24
24
  Setup,
25
25
  TemplateNode,
26
26
  } from './TemplateNode.ts'
27
- import { tokenize } from './tokenize.ts'
27
+ import { nesting, tokenize } from './tokenize.ts'
28
28
 
29
29
  export type Span = { start: number; end: number }
30
30
 
@@ -45,7 +45,7 @@ export type Span = { start: number; end: number }
45
45
  export type Projection = {
46
46
  text: string
47
47
  modules: Span
48
- /** 34.20 — the `<script server>` bodies, module scope like `modules` and the server's alone. */
48
+ /** 34.19 — the `<script server>` bodies, module scope like `modules` and the server's alone. */
49
49
  servers: Span
50
50
  scripts: Span
51
51
  /** Every template position, ascending, each with its kind — what the checker's tree is asked. */
@@ -95,7 +95,7 @@ export function projectShadow(file: AbideFile): Projection {
95
95
 
96
96
  type ImportStatement = ReturnType<typeof importStatements>[number]
97
97
 
98
- /** 34.26 — a binding as two blocks importing the same thing agree on it. */
98
+ /** 34.22 — a binding as two blocks importing the same thing agree on it. */
99
99
  function bindingKey(
100
100
  statement: ImportStatement,
101
101
  binding: ImportBinding,
@@ -104,7 +104,7 @@ function bindingKey(
104
104
  }
105
105
 
106
106
  /**
107
- * 34.26 — what is left of an import once the bindings `drops` answers for are taken out:
107
+ * 34.22 — what is left of an import once the bindings `drops` answers for are taken out:
108
108
  * `undefined` where none is, `''` where every one is, and the statement over the rest otherwise,
109
109
  * spelled from its tokens.
110
110
  */
@@ -163,10 +163,10 @@ function project(file: AbideFile): Projection {
163
163
  }
164
164
  return { start, end: text.length }
165
165
  }
166
- // 34.25 — a block copied as written, save that a `.abide` imported as text names the module
166
+ // 34.21 — a block copied as written, save that a `.abide` imported as text names the module
167
167
  // holding its text, and loses the attribute bun would otherwise read that module as text by.
168
168
  //
169
- // 34.26 — and a binding a `<script server>` and the file's `<script module>` or `<script>` both
169
+ // 34.22 — and a binding a `<script server>` and the file's `<script module>` or `<script>` both
170
170
  // import is written once: the server's module holds the other blocks' imports too, and written
171
171
  // twice it was a duplicate binding. `drops` says which copy goes. A blank line per line it
172
172
  // spanned keeps the block line for line with the file.
@@ -215,7 +215,7 @@ function project(file: AbideFile): Projection {
215
215
  origin === undefined ? undefined : origin + from,
216
216
  )
217
217
  }
218
- // 34.26 — per binding the blocks both sides run import, whether one of them imports a value;
218
+ // 34.22 — per binding the blocks both sides run import, whether one of them imports a value;
219
219
  // and the bindings the server block imports as values.
220
220
  const shared = new Map<string, boolean>()
221
221
  for (const body of [...file.modules, ...file.scripts])
@@ -568,11 +568,19 @@ function project(file: AbideFile): Projection {
568
568
  // 34.3 — BOTH BLOCKS. A `<script>` is a function body on both sides, where an `await` at its top
569
569
  // does not parse; a `<script module>` is module scope, where it would, and holds the import of
570
570
  // every file that reaches it until it settles — the render waiting on it in all but name.
571
- refuseTopLevelAwait(file.modules.join('\n'), '<script module>')
572
- refuseTopLevelAwait(file.servers.join('\n'), '<script server>')
571
+ const modulesText = file.modules.join('\n')
572
+ const serversText = file.servers.join('\n')
573
+ refuseTopLevelAwait(modulesText, '<script module>')
574
+ refuseTopLevelAwait(serversText, '<script server>')
573
575
  refuseTopLevelAwait(scriptsText, '<script>')
574
- refuseInstanceExport(scriptsText)
575
- refuseServerDefault(file.servers.join('\n'))
576
+ // A kind at a time: a token at the end of one kind's text would decide how the next one's first
577
+ // reads.
578
+ refuseExport(modulesText)
579
+ refuseExport(serversText)
580
+ refuseExport(scriptsText)
581
+ // 16.70 — what the server block binds, re-exported under a name no author writes, so the
582
+ // checker reads each one's declaring call as it reads a handler file's default.
583
+ text += privateExports(serversText)
576
584
  return {
577
585
  text,
578
586
  modules,
@@ -675,7 +683,7 @@ const ROWS_OF = '__rowsOf'
675
683
  *
676
684
  * The item is read off the subject as `listOf` iterates it: what a `Reactive` read by
677
685
  * name hands back (31.1 lowers `rows` to `rows()`), or the subject itself. THE CALL IS THE FIRST
678
- * OVERLOAD because a `Reactive` is ALSO an `AsyncIterable` of its productions (2.5), and inferred
686
+ * OVERLOAD because a `Reactive` is ALSO an `AsyncIterable` of what it produced (2.5, 2.6), and inferred
679
687
  * over one union the item came out as the row OR the whole list. Declared AFTER everything, so no
680
688
  * offset above it moves, and ambient, so there is nothing to emit. Where inference fails the item
681
689
  * is `unknown`, and a member of that is unprovable — the safe side.
@@ -699,6 +707,7 @@ declare function ${ROWS_OF}<T>(subject: Iterable<T> | AsyncIterable<T> | null |
699
707
  */
700
708
  function refuseTopLevelAwait(source: string, block: string): void {
701
709
  if (!source.includes(AWAIT)) return
710
+ // Brackets alone, not `nesting`: a template's substitution is still the top level here.
702
711
  let depth = 0
703
712
  for (const token of tokenize(source)) {
704
713
  const text = token.text
@@ -712,35 +721,72 @@ function refuseTopLevelAwait(source: string, block: string): void {
712
721
  }
713
722
 
714
723
  /**
715
- * Throws where a `<script>` exports.
724
+ * Throws where a script block exports.
716
725
  *
717
- * 34.18 — AN EXPORT IS THE MODULE'S, and a `<script>` runs once per instance: there is no one
718
- * instance an importer could be reading from. Refused rather than emitted, as 34.3's `await` is —
719
- * the block is a function body on both sides, and the bundler's own error names neither the page
720
- * nor the fix.
726
+ * 34.17 — A `.abide`'S ONE EXPORT IS ITS COMPONENT. A handler another file calls is a handler file
727
+ * (16.2), a value two files share is a module under a lib directory, and what the framework reads
728
+ * off a file is a hook it calls (37.1). Refused rather than emitted, as 34.3's `await` is: the
729
+ * bundler's own error names neither the file nor the fix.
721
730
  */
722
- function refuseInstanceExport(source: string): void {
731
+ function refuseExport(source: string): void {
723
732
  if (!source.includes(EXPORT)) return
724
733
  let depth = 0
725
734
  let previous = ''
726
735
  for (const token of tokenize(source)) {
727
736
  const text = token.text
728
- if (text === '{' || text === '(' || text === '[') depth += 1
729
- else if (text === '}' || text === ')' || text === ']') depth -= 1
737
+ const moved = nesting(token.kind)
738
+ if (moved !== 0) depth += moved
730
739
  else if (depth === 0 && text === EXPORT && previous !== '.')
731
740
  throw new Error(
732
- "abide: a <script> cannot export — it runs once per instance, and an export is the module's. Move the declaration to <script module>. RULEBOOK 34.18.",
741
+ 'abide: a .abide cannot export — its component is its one export. A handler another file calls is a handler file (a .ts beside the page, `export default GET(…)`); a value files share is a module under a lib directory. RULEBOOK 34.17.',
733
742
  )
734
743
  previous = text
735
744
  }
736
745
  }
737
746
 
747
+ /** The prefix a server block's bindings are re-exported under, for the build alone (16.70). */
748
+ export const PRIVATE_EXPORT = '__handler_'
749
+
750
+ /**
751
+ * 16.70 — the `export { … }` re-exporting each name the server blocks bind under `PRIVATE_EXPORT`,
752
+ * or `''` where they bind none. One spelling for the checker's view and the emitted module, which
753
+ * the build's handler table reads a private handler out of by the same name.
754
+ */
755
+ export function privateExports(servers: string): string {
756
+ const listed: string[] = []
757
+ for (const name of serverBindings(servers))
758
+ listed.push(`${name} as ${PRIVATE_EXPORT}${name}`)
759
+ return listed.length === 0 ? '' : `export { ${listed.join(', ')} }\n`
760
+ }
761
+
738
762
  /**
739
- * 34.22 — a `<script server>` exporting `default`, refused: the module's default is its component,
740
- * and a handler is addressed by its name (16.2).
763
+ * The names a `<script server>` binds with `const`, `let` or `var` at its top level, a
764
+ * destructuring's aside: a handler is a call bound to one name.
741
765
  */
766
+ function serverBindings(source: string): string[] {
767
+ const names: string[] = []
768
+ if (source === '') return names
769
+ let depth = 0
770
+ let declaring = false
771
+ for (const token of tokenize(source)) {
772
+ const text = token.text
773
+ const moved = nesting(token.kind)
774
+ if (moved !== 0) {
775
+ depth += moved
776
+ declaring = false
777
+ } else if (depth !== 0) continue
778
+ else if (text === 'const' || text === 'let' || text === 'var')
779
+ declaring = true
780
+ else if (declaring && token.kind === SyntaxKind.Identifier) {
781
+ names.push(text)
782
+ declaring = false
783
+ } else declaring = false
784
+ }
785
+ return names
786
+ }
787
+
742
788
  /**
743
- * 34.26 — throws where the binding both blocks import is a `.abide`'s default or namespace: the
789
+ * 34.22 — throws where the binding both blocks import is a `.abide`'s default or namespace: the
744
790
  * server block's is the function binding its props (34.16) and the other block's is the module it
745
791
  * compiles to (32.17), and one name in one module scope cannot hold both.
746
792
  */
@@ -754,29 +800,8 @@ function refuseTwoFaces(
754
800
  (binding.imported === 'default' || binding.imported === '*')
755
801
  )
756
802
  throw new Error(
757
- `abide: <script server> imports ${binding.local} from '${statement.specifier}' as another script block of its file does — a server block's .abide is the function binding its props (34.16), the other block's is its module (32.17), and one name cannot be both. Import it under another name in the <script server>. RULEBOOK 34.26.`,
758
- )
759
- }
760
-
761
- function refuseServerDefault(source: string): void {
762
- if (!source.includes(EXPORT)) return
763
- let depth = 0
764
- let previous = ''
765
- for (const token of tokenize(source)) {
766
- const text = token.text
767
- if (text === '{' || text === '(' || text === '[') depth += 1
768
- else if (text === '}' || text === ')' || text === ']') depth -= 1
769
- // `export { x as default }` names it too.
770
- else if (
771
- text === 'default' &&
772
- ((depth === 0 && previous === EXPORT) ||
773
- (depth === 1 && previous === 'as'))
803
+ `abide: <script server> imports ${binding.local} from '${statement.specifier}' as another script block of its file does — a server block's .abide is the function binding its props (34.16), the other block's is its module (32.17), and one name cannot be both. Import it under another name in the <script server>. RULEBOOK 34.22.`,
774
804
  )
775
- throw new Error(
776
- "abide: a <script server> cannot export `default` — the module's default is its component. Export the handler under a name; the name is its address's last segment. RULEBOOK 34.22.",
777
- )
778
- previous = text
779
- }
780
805
  }
781
806
 
782
807
  /**
@@ -282,7 +282,7 @@ export async function readsIn(
282
282
  await proveSlots(checker, expressions, reads, statics)
283
283
  reads.sort(byPosition)
284
284
  const warnings = await warningsFor(checker, located, spans.positions)
285
- // 34.19.
285
+ // 34.18.
286
286
  for (const warning of await sharedModuleWrites(
287
287
  checker,
288
288
  shadowPath,
@@ -694,7 +694,7 @@ const ROUTE_MEMBERS = new Set(['url', 'params', 'name', 'navigating'])
694
694
  * a name, and this read is spelled as a member.
695
695
  *
696
696
  * 23.9 — AND `route.params` IN A PAGE OR A LAYOUT IS WHAT THAT FILE'S OWN ROUTE PROMISES. The file
697
- * is the route: `media/[id]/page.abide` only ever renders where `id` matched, so its
697
+ * is the route: `media/[id].abide` only ever renders where `id` matched, so its
698
698
  * `route.params` has an `id`, and a layout's has its own segments and whatever a page below adds.
699
699
  * Only the checker reads that — the running module reads the one `route` every page shares.
700
700
  */
@@ -1,6 +1,6 @@
1
1
  // 38.1 — `abide scaffold <name>`: THE LAYOUT EVERY ABIDE APP HAS, written into `<name>`. There is
2
- // nothing to choose, because what is scaffolded is a route, the shell and the hooks — the files the
3
- // commands read.
2
+ // nothing to choose, because what is scaffolded is a root layout and a page — the files the commands
3
+ // read — and the test preload.
4
4
  // A file that is already there is left as it is and reported skipped, so a scaffold run over a
5
5
  // half-made project finishes it rather than overwriting it.
6
6
 
@@ -8,8 +8,8 @@ import { withoutTrailingSlash } from '#server/withoutTrailingSlash.ts'
8
8
  import abidePackage from '../../package.json' with { type: 'json' }
9
9
  import { COMPILER_OPTIONS } from './writeDefaultProject.ts'
10
10
 
11
- // THE STARTER'S OWN FILES, as files: a real document, a real page and a real module, copied as they
12
- // stand. What depends on the name — the manifests — is written here.
11
+ // THE STARTER'S OWN FILES, as files: a real document, a real page and the agent notes, copied as
12
+ // they stand. What depends on the name, or what a packer drops (`filesFor`), is written here.
13
13
  const STARTER = Bun.fileURLToPath(new URL('../../scaffold/', import.meta.url))
14
14
 
15
15
  // THE ABIDE THAT WROTE THE PROJECT is the one it depends on. Installed from a registry, that is its
@@ -22,23 +22,35 @@ const ABIDE_DEPENDENCY = PACKAGE_ROOT.includes('/node_modules/')
22
22
  ? `^${abidePackage.version}`
23
23
  : `file:${PACKAGE_ROOT}`
24
24
 
25
+ // The `#src` alias, written as the package's `imports` and again as the checker's `paths`.
26
+ const SOURCE_ALIAS = { '#src/*': ['./src/*', './src/*.ts'] }
27
+
28
+ /**
29
+ * 38.109 — the starter's `bunfig.toml`: a test imports the app's `.abide` files as modules, with
30
+ * nothing booted first.
31
+ */
32
+ export const TEST_PRELOAD =
33
+ '# 38.109 — a test imports the app\'s `.abide` files as modules, with nothing booted first.\n[test]\npreload = ["@abide/abide/test"]\n'
34
+
35
+ // WRITTEN, NOT COPIED, beside the manifests: a packer drops a `bunfig.toml` or a `.gitignore` from
36
+ // the tarball (`bun pm pack` both, `npm pack` the second), so neither survives as a starter file.
25
37
  function filesFor(name: string): Record<string, string> {
26
38
  return {
39
+ 'bunfig.toml': TEST_PRELOAD,
40
+ '.gitignore': 'node_modules\n.abide\n',
27
41
  'package.json': `${JSON.stringify(
28
42
  {
29
43
  name,
30
44
  private: true,
31
45
  type: 'module',
32
- // 34.24 — `prepare` runs on `bun install`, so a `.ts` importing a `.abide` is typed
33
- // before any other abide command has run; and never fails it, as it would where
34
- // `package.json` is installed before `src` is there to sync (38.26).
35
46
  scripts: {
36
- prepare: 'abide sync || true',
37
47
  // 38.103 — the checker over `.abide`, which `tsc` does not read.
38
48
  check: 'abide check',
39
49
  dev: 'abide dev',
40
50
  build: 'abide build',
41
51
  start: 'abide start',
52
+ // 38.108 — every address, and the file answering it.
53
+ routes: 'abide routes',
42
54
  },
43
55
  // ONE ALIAS FOR THE SOURCE, and two targets for it, because an extensionless
44
56
  // specifier is how an import is written and only bun probes for the extension:
@@ -47,9 +59,7 @@ function filesFor(name: string): Record<string, string> {
47
59
  // them (31.3). The first target is what bun resolves, and the one a `.ts` or
48
60
  // `.abide` specifier needs. Which side a module is on is its directory's to say
49
61
  // (36.10), not its alias's.
50
- imports: {
51
- '#src/*': ['./src/*', './src/*.ts'],
52
- },
62
+ imports: SOURCE_ALIAS,
53
63
  dependencies: { '@abide/abide': ABIDE_DEPENDENCY },
54
64
  // `Bun`, `bun:sqlite` and the rest of the runtime a server module reaches for are
55
65
  // typed by these, and `abide check` reports every one untyped without them.
@@ -60,20 +70,13 @@ function filesFor(name: string): Record<string, string> {
60
70
  )}\n`,
61
71
  'tsconfig.json': `${JSON.stringify(
62
72
  // TypeScript 7 includes no `@types` package it is not named, so the runtime's is named.
63
- // 34.24 — `rootDirs` and the `#src/*` path reach what abide writes to `.abide/types`,
64
- // where a `.ts` importing a `.abide` finds the names it exports.
73
+ // 34.21 — the alias as a path too: the checker resolves a text import's declaration
74
+ // beside the file a path names, and takes an `imports` target as the exact file.
65
75
  {
66
76
  compilerOptions: {
67
77
  ...COMPILER_OPTIONS,
68
78
  types: ['bun'],
69
- rootDirs: ['.', '.abide/types'],
70
- paths: {
71
- '#src/*': [
72
- './src/*',
73
- './src/*.ts',
74
- './.abide/types/src/*',
75
- ],
76
- },
79
+ paths: SOURCE_ALIAS,
77
80
  },
78
81
  include: ['src'],
79
82
  },
@@ -4,10 +4,7 @@ import { realOrAsGiven } from '#server/realOrAsGiven.ts'
4
4
  import { checkSource } from './checkApp.ts'
5
5
  import { type AppChecker, openChecker } from './compileApp.ts'
6
6
  import { nearest } from './nearest.ts'
7
- import { parseAbide } from './parseAbide.ts'
8
- import { syncTypes } from './syncTypes.ts'
9
7
  import type { Diagnosis, Inspection, Place } from './TypeSession.ts'
10
- import { writeTypes } from './writeTypes.ts'
11
8
 
12
9
  // 38.11 — `abide lsp`: THE `.abide` LANGUAGE SERVER, OVER STDIO. It is `abide check` (38.3) asked
13
10
  // about one open document at a time, so the squiggle an editor draws and the line CI fails on are
@@ -197,27 +194,6 @@ export async function serveLanguage(
197
194
  // A newer text has arrived while this one was checked, and will be published.
198
195
  if (documents.get(uri)?.version !== version) return
199
196
  publish(uri, found, document.text)
200
- await typesFor(app, path, document.text)
201
- }
202
-
203
- // 34.24 — THE TYPES A PLAIN `.ts` READS AN OPEN FILE BY, as the editor holds it: the whole app's
204
- // the first time an app is seen, and the one file as it is edited after that.
205
- const synced = new Set<string>()
206
- const typesFor = async (
207
- app: string,
208
- path: string,
209
- text: string,
210
- ): Promise<void> => {
211
- try {
212
- if (synced.has(app))
213
- await writeTypes(app, [{ path, file: parseAbide(text) }], false)
214
- else {
215
- synced.add(app)
216
- await syncTypes(app, new Map([[path, text]]))
217
- }
218
- } catch {
219
- // A file that does not parse keeps the types it last had; its check already says why.
220
- }
221
197
  }
222
198
 
223
199
  const check = (uri: string): void => {
@@ -11,7 +11,7 @@ import { classifyRead, FUNCTIONS } from './classifyRead.ts'
11
11
  import type { Finding } from './Finding.ts'
12
12
  import type { Span } from './projectShadow.ts'
13
13
 
14
- // 34.19 — A PLAIN `let` IN A `<script module>` THAT A `<script>` WRITES AS IT SETS UP. The block is
14
+ // 34.18 — A PLAIN `let` IN A `<script module>` THAT A `<script>` WRITES AS IT SETS UP. The block is
15
15
  // module scope (34.2), which a server shares across every request, and setup runs inside a render —
16
16
  // so the value one request leaves is the one the next request starts from, and its markup drifts
17
17
  // from what the browser builds. A `reactive` is per request wherever it is declared (11.32), which
@@ -74,7 +74,7 @@ export async function sharedModuleWrites(
74
74
  if (insideFunction(node)) continue
75
75
  warnings.push({
76
76
  at,
77
- message: `abide: \`${name}\` is a plain \`let\` in <script module>, which a server shares across every request, and this <script> writes it while the page renders — so one request's value is where the next one starts. Declare it with \`reactive\`, which is per request wherever it stands, or move it into <script>. RULEBOOK 34.19.`,
77
+ message: `abide: \`${name}\` is a plain \`let\` in <script module>, which a server shares across every request, and this <script> writes it while the page renders — so one request's value is where the next one starts. Declare it with \`reactive\`, which is per request wherever it stands, or move it into <script>. RULEBOOK 34.18.`,
78
78
  })
79
79
  }
80
80
  }
@@ -263,7 +263,7 @@ export function abideImports(
263
263
  for (const span of importSpans(script))
264
264
  if (
265
265
  span.specifier.endsWith('.abide') &&
266
- // 34.25 — a `.abide` imported as text is its text, and no component edge.
266
+ // 34.21 — a `.abide` imported as text is its text, and no component edge.
267
267
  (span.dynamic ||
268
268
  !IMPORT_ATTRIBUTES.text.test(
269
269
  script.slice(span.literalEnd, span.end),
@@ -386,7 +386,7 @@ export function importStatements(script: string): {
386
386
  return found
387
387
  }
388
388
 
389
- // The attribute a static import is read by: as text (34.25), or as a mount of routes (23.19).
389
+ // The attribute a static import is read by: as text (34.21), or as a mount of routes (23.19).
390
390
  export const IMPORT_ATTRIBUTES = {
391
391
  text: /\btype\s*:\s*['"]text['"]/,
392
392
  routes: /\btype\s*:\s*['"]routes['"]/,
@@ -396,7 +396,7 @@ export const IMPORT_ATTRIBUTES = {
396
396
  * Every static import a script makes `with { type }`: the specifier, where the statement and its
397
397
  * literal begin, and where the attributes end.
398
398
  *
399
- * 34.25 — a text import's literal-to-end span is what the projection writes over with the specifier
399
+ * 34.21 — a text import's literal-to-end span is what the projection writes over with the specifier
400
400
  * the checker types a `string` (`SOURCE_SUFFIX`), and the emitters ask for as text
401
401
  * (`facedSpecifier`). 23.19 — a routes import's whole statement is what the projection blanks: a
402
402
  * mount is read by the build off the file, and neither bun nor the checker has a module to give it.
@@ -466,7 +466,7 @@ export function importedNames(
466
466
  * own path, so bun resolves a relative, a `#` or a bare specifier from the file that wrote it.
467
467
  *
468
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.25 — a file the
469
+ * block's stays bare, for the face a module abide did not compile gets (34.16). 34.21 — a file the
470
470
  * projection marked as text is asked for as its text.
471
471
  */
472
472
  function facedSpecifier(specifier: string, lane: Lane): string | undefined {
@@ -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,5 +1,5 @@
1
- // WHAT AN IMPORT OF A `.abide` ASKS FOR, spelled on the specifier: bun hands a plugin no import
2
- // attributes, so the compiler writes the face it wants where the plugin can read it
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
3
  // (`abideModules`). A `.abide` named with none is the face a module abide did not compile gets.
4
4
 
5
5
  /** The module the file compiles to, as a compiled module imports it. 32.17. */
@@ -8,7 +8,7 @@ export const COMPONENT_QUERY = '?component'
8
8
  /** The file's scoped stylesheet, which its browser module imports. 34.4. */
9
9
  export const STYLE_QUERY = '?style'
10
10
 
11
- /** The file's text. 34.25. */
11
+ /** The file's text. 34.21. */
12
12
  export const TEXT_QUERY = '?text'
13
13
 
14
14
  /**
@@ -16,3 +16,12 @@ export const TEXT_QUERY = '?text'
16
16
  * under a DOM in `bun test`. Its own `.abide` imports are browser modules too.
17
17
  */
18
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)}$`)