@abide/abide 0.100.0 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/bin/abide.ts +227 -163
  3. package/docs/REGISTRY.md +726 -0
  4. package/docs/RULEBOOK.md +2753 -0
  5. package/docs/abide.md +12878 -0
  6. package/package.json +10 -3
  7. package/scaffold/AGENTS.md +31 -0
  8. package/scaffold/CLAUDE.md +1 -0
  9. package/scaffold/src/layout.abide +18 -0
  10. package/src/compiler/Lowering.ts +78 -9
  11. package/src/compiler/ShadowMap.ts +2 -2
  12. package/src/compiler/TypeSession.ts +262 -67
  13. package/src/compiler/argsSchemaOf.ts +1 -1
  14. package/src/compiler/checkApp.ts +43 -15
  15. package/src/compiler/checkedShadow.ts +39 -5
  16. package/src/compiler/classifyRead.ts +6 -4
  17. package/src/compiler/classifyTemplate.ts +1 -1
  18. package/src/compiler/compileApp.ts +433 -193
  19. package/src/compiler/compiledEntry.ts +43 -39
  20. package/src/compiler/componentGraph.ts +58 -71
  21. package/src/compiler/declaredExports.ts +24 -7
  22. package/src/compiler/emitClient.ts +141 -71
  23. package/src/compiler/emitComponent.ts +37 -8
  24. package/src/compiler/emitSource.ts +106 -13
  25. package/src/compiler/handlersUnder.ts +106 -56
  26. package/src/compiler/index.ts +7 -0
  27. package/src/compiler/mountsOf.ts +138 -0
  28. package/src/compiler/nearest.ts +14 -0
  29. package/src/compiler/parseAbide.ts +157 -20
  30. package/src/compiler/projectShadow.ts +213 -7
  31. package/src/compiler/reactiveTypes.ts +20 -7
  32. package/src/compiler/readSources.ts +15 -2
  33. package/src/compiler/readsIn.ts +61 -7
  34. package/src/compiler/renderMarkup.ts +3 -3
  35. package/src/compiler/scaffold.ts +30 -11
  36. package/src/compiler/serveLanguage.ts +125 -34
  37. package/src/compiler/sourceMapOf.ts +193 -0
  38. package/src/compiler/splitImports.ts +205 -83
  39. package/src/compiler/syncTypes.ts +32 -0
  40. package/src/compiler/tokenize.ts +14 -0
  41. package/src/compiler/writeDefaultProject.ts +1 -1
  42. package/src/compiler/writeTypes.ts +82 -0
  43. package/src/server/CLIENT_BUILD.ts +11 -13
  44. package/src/server/DEFAULT_PORT.ts +1 -1
  45. package/src/server/LAUNCHER_FILES.ts +9 -0
  46. package/src/server/MODULE_QUERIES.ts +18 -0
  47. package/src/server/TABLES.ts +7 -0
  48. package/src/server/VIRTUAL_DIRECTORY.ts +4 -5
  49. package/src/server/abideModules.ts +391 -0
  50. package/src/server/answerFailure.ts +16 -6
  51. package/src/server/answerRequest.ts +235 -106
  52. package/src/server/answerRpc.ts +124 -39
  53. package/src/server/answerSocket.ts +141 -48
  54. package/src/server/answerValue.ts +22 -9
  55. package/src/server/appDirectories.ts +1 -1
  56. package/src/server/appPlugins.ts +1 -1
  57. package/src/server/appTree.ts +179 -0
  58. package/src/server/argsFor.ts +2 -2
  59. package/src/server/assistant.ts +2 -2
  60. package/src/server/bridgeRelay.ts +6 -8
  61. package/src/server/claudeCode.ts +0 -6
  62. package/src/server/clientBuildOptions.ts +8 -3
  63. package/src/server/coerceArgs.ts +1 -1
  64. package/src/server/commands.ts +37 -37
  65. package/src/server/componentModules.d.ts +3 -1
  66. package/src/server/compressAnswer.ts +48 -14
  67. package/src/server/config.ts +25 -29
  68. package/src/server/configFields.ts +31 -25
  69. package/src/server/createApp.ts +59 -73
  70. package/src/server/devWorker.ts +144 -183
  71. package/src/server/executableName.ts +4 -0
  72. package/src/server/fenceServer.ts +37 -35
  73. package/src/server/generatedSurfaces.ts +99 -66
  74. package/src/server/hooks.ts +124 -52
  75. package/src/server/iconAccent.ts +191 -0
  76. package/src/server/index.ts +11 -7
  77. package/src/server/installSourceMaps.ts +88 -0
  78. package/src/server/launcherOptions.ts +34 -31
  79. package/src/server/launcherPage.ts +107 -64
  80. package/src/server/manifestOf.ts +16 -13
  81. package/src/server/namesUnder.ts +2 -2
  82. package/src/server/nativeMenu.ts +5 -5
  83. package/src/server/negotiateEncoding.ts +1 -1
  84. package/src/server/principal.ts +10 -5
  85. package/src/server/realOrAsGiven.ts +10 -0
  86. package/src/server/refuseAddressCollision.ts +21 -14
  87. package/src/server/remote.ts +49 -52
  88. package/src/server/render.ts +65 -94
  89. package/src/server/renderComponent.ts +5 -0
  90. package/src/server/reportHealth.ts +2 -2
  91. package/src/server/requestScope.ts +12 -1
  92. package/src/server/runBuild.ts +24 -0
  93. package/src/server/runCompiled.ts +5 -5
  94. package/src/server/runDesktop.ts +52 -27
  95. package/src/server/serveBuild.ts +43 -52
  96. package/src/server/serveDev.ts +81 -39
  97. package/src/server/shell.ts +7 -22
  98. package/src/server/sourceMappings.ts +139 -0
  99. package/src/server/surfaceOf.ts +18 -3
  100. package/src/server/testBuild.ts +1 -19
  101. package/src/server/withAbideSources.ts +117 -0
  102. package/src/shared/ADDRESSES.ts +7 -18
  103. package/src/shared/APP_META.ts +7 -0
  104. package/src/shared/CopyOptions.ts +1 -1
  105. package/src/shared/DEBUG_META.ts +1 -1
  106. package/src/shared/GLOBAL_GETS.ts +9 -3
  107. package/src/shared/RESERVED_DIRECTORIES.ts +15 -0
  108. package/src/shared/RPC_SERVER.ts +2 -2
  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 +1 -1
  116. package/src/shared/log.ts +27 -13
  117. package/src/shared/matchRoute.ts +23 -4
  118. package/src/shared/menuItem.ts +3 -3
  119. package/src/shared/reactive/Cursor.ts +3 -9
  120. package/src/shared/reactive/Keyed.ts +4 -3
  121. package/src/shared/reactive/NotLanded.ts +1 -1
  122. package/src/shared/reactive/Reactive.ts +81 -60
  123. package/src/shared/reactive/argsContract.ts +4 -4
  124. package/src/shared/reactive/canonicalKey.ts +4 -4
  125. package/src/shared/reactive/chunksOfStream.ts +6 -0
  126. package/src/shared/reactive/index.ts +1 -1
  127. package/src/shared/reactive/recordWrites.ts +103 -5
  128. package/src/shared/reactive/structural.ts +1 -1
  129. package/src/shared/reactive/tracking.ts +155 -21
  130. package/src/shared/reactive/watch.ts +10 -6
  131. package/src/shared/refuse.ts +1 -1
  132. package/src/shared/routePattern.ts +82 -46
  133. package/src/shared/rpc.ts +47 -13
  134. package/src/shared/rpcRequest.ts +27 -3
  135. package/src/shared/url.ts +4 -44
  136. package/src/shared/writePath.ts +1 -1
  137. package/src/ui/LAYER_NAMES.ts +1 -1
  138. package/src/ui/List.ts +3 -3
  139. package/src/ui/MOUNTING_LEVEL.ts +18 -0
  140. package/src/ui/Mismatch.ts +1 -1
  141. package/src/ui/RawRun.ts +1 -1
  142. package/src/ui/Router.ts +38 -31
  143. package/src/ui/Scope.ts +1 -1
  144. package/src/ui/TryRegion.ts +2 -2
  145. package/src/ui/applyHead.ts +33 -0
  146. package/src/ui/index.ts +1 -1
  147. package/src/ui/menuBridge.ts +1 -1
  148. package/src/ui/moveNode.ts +1 -1
  149. package/src/ui/reconcile.ts +2 -2
  150. package/src/ui/reportMenu.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 → page.abide} +0 -0
@@ -1,73 +1,83 @@
1
- // 16.2 AND 19.1 — EVERY HANDLER AN APP DECLARES, AND WHERE IT IS ANSWERED. A handler is an export of
2
- // a module under `server/rpc`, and its address is the module's path below that directory and then
3
- // the export's name — `default` answering at the path alone.
1
+ // 16.2 AND 19.15 — EVERY HANDLER AN APP DECLARES, AND WHERE IT IS ANSWERED. A handler is an export
2
+ // of a `<script server>` in a `.abide` of the app's own, and its address is that file's
3
+ // route — its directory's (23.14) — and then the export's name. A directory's handlers may sit in as
4
+ // many of its files as an author likes; two of them exporting one name is 19.1's refusal.
4
5
  //
5
- // READ, NOT RUN. The build finds handlers by what their modules SAY rather than by importing them,
6
- // so a build never runs server code: an app's module may open a database at import, and nothing
7
- // about building a browser bundle is a reason to. The checker the build already holds lists what a
8
- // module exports and which call declares each (19.15) — which is also what makes an export that is
9
- // not a handler a refusal here rather than an address that silently never answers.
6
+ // READ, NOT RUN. The build finds handlers by what their blocks SAY rather than by importing them,
7
+ // so a build never runs server code: a server block may open a database at import, and nothing
8
+ // about building a browser bundle is a reason to. The checker the build already holds lists what
9
+ // the file's projection exports and which call declares each (19.15) — which is also what makes an
10
+ // export that is not a handler a refusal here rather than an address that silently never answers.
10
11
 
11
12
  import type { Mount } from '#server/answerRpc.ts'
12
- import { namesUnder } from '#server/namesUnder.ts'
13
- import { RPC_ADDRESS } from '#shared/ADDRESSES.ts'
13
+ import { refuseFrameworkRoute } from '#server/appTree.ts'
14
+ import { surfaceName } from '#server/surfaceOf.ts'
15
+ import { parseRoutePattern, placeOf, routeOf } from '#shared/routePattern.ts'
14
16
  import { declaredExports } from './declaredExports.ts'
17
+ import type { AbideFile } from './parseAbide.ts'
15
18
  import type { Diagnosis, TypeSession } from './TypeSession.ts'
16
19
 
17
- const HANDLER_DIRECTORY = 'server/rpc'
18
-
19
- // One module under the tree: where it is from the root, and its address path below the tree.
20
- type HandlerModule = { source: string; path: string }
21
-
22
- /** Every module under `server/rpc`. */
23
- function modulesUnder(root: string): HandlerModule[] {
24
- const modules: HandlerModule[] = []
25
- for (const name of namesUnder(
26
- `${root}/${HANDLER_DIRECTORY}`,
27
- '**/*.ts',
28
- ).sort())
29
- modules.push({
30
- source: `${HANDLER_DIRECTORY}/${name}`,
31
- path: name.replace(/\.ts$/, ''),
32
- })
33
- return modules
20
+ /** A `.abide` the build read, by its path and its path below the app's root. */
21
+ type Source = {
22
+ path: string
23
+ relative: string
24
+ /** 23.19 — the path it answers by: its own, or the one its mount gives it. */
25
+ position: string
26
+ file: AbideFile
34
27
  }
35
28
 
36
- /**
37
- * Whether the app has any module under `server/rpc`.
38
- *
39
- * Asked before a checker is opened to read it: an app of handlers alone has no `.abide` to say it
40
- * is an app.
41
- */
42
- export function declaresHandlers(root: string): boolean {
43
- return modulesUnder(root).length > 0
44
- }
29
+ const TRANSPILER = new Bun.Transpiler({ loader: 'ts' })
30
+
31
+ // 19.22 — as the transpiler prints it, one statement to a line; `export * as name` names its export.
32
+ const STAR_EXPORT = /^export \* from /m
45
33
 
46
34
  /**
47
35
  * The app's handler mounts, with the warnings the build raised reading them.
48
36
  *
49
- * Off one read of the checker. 19.1 — one owner per address, and the error names both files. 18.15 —
50
- * with what the build could not read of a copy's options.
37
+ * Off one read of the checker. 18.15 — with what the build could not read of a copy's options.
38
+ * 19.1's one owner per route and method is `compileApp`'s, which holds the pages as well.
51
39
  */
52
40
  export async function handlersUnder(
53
- root: string,
54
41
  session: TypeSession,
42
+ sources: readonly Source[],
55
43
  ): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
56
- const modules = modulesUnder(root)
57
- const paths: string[] = []
58
- for (const { source } of modules) paths.push(`${root}/${source}`)
44
+ // 34.31 — a dependency's server block has no address of its own: what it declares is mounted
45
+ // where a file of the app's re-exports it, and as that file's.
46
+ const serving: Source[] = []
47
+ for (const source of sources) {
48
+ if (source.file.servers.length === 0) continue
49
+ if (
50
+ STAR_EXPORT.test(
51
+ TRANSPILER.transformSync(source.file.servers.join('\n')),
52
+ )
53
+ )
54
+ throw new Error(
55
+ `abide: ${source.relative} re-exports with \`export *\` from its <script server>, whose names the build cannot read without running it. Name each handler it re-exports. RULEBOOK 19.22.`,
56
+ )
57
+ if (placeOf(source.position) !== 'dependency') serving.push(source)
58
+ }
59
59
  const project = await session.project()
60
60
  const declared =
61
- project === undefined
61
+ project === undefined || serving.length === 0
62
62
  ? []
63
- : await declaredExports(project, session.tsconfig, paths)
63
+ : await declaredExports(
64
+ project,
65
+ session.tsconfig,
66
+ serving.map((source) => session.projectionOf(source.path)),
67
+ )
64
68
  const mounts: Mount[] = []
65
69
  const warnings: Diagnosis[] = []
66
- const owners = new Map<string, string>()
67
- for (let at = 0; at < modules.length; at += 1) {
68
- const { source, path } = modules[at] as HandlerModule
70
+ const names = new Map<string, string>()
71
+ for (let at = 0; at < serving.length; at += 1) {
72
+ const { relative, position, file } = serving[at] as Source
73
+ const route = routeOf(position)
74
+ // 19.15 — what THE SERVER BLOCK exports, out of everything the file's module does: its
75
+ // component and its `<script module>`'s names are the module's own.
76
+ const exported = new Set(
77
+ TRANSPILER.scan(file.servers.join('\n')).exports,
78
+ )
69
79
  for (const {
70
- name: exported,
80
+ name,
71
81
  method,
72
82
  ui,
73
83
  cli,
@@ -78,22 +88,27 @@ export async function handlersUnder(
78
88
  args,
79
89
  unread,
80
90
  } of declared[at] ?? []) {
91
+ if (!exported.has(name)) continue
81
92
  if (method === undefined)
82
93
  throw new Error(
83
- `abide: ${source} exports \`${exported}\`, which is not declared as a handler. A module under ${HANDLER_DIRECTORY} exports handlers alone, each declared by abide's \`GET(…)\` (or POST, PUT, PATCH, DELETE); anything else belongs beside it. RULEBOOK 19.15.`,
94
+ `abide: ${relative} exports \`${name}\` from its <script server>, which is not declared as a handler. A server block exports handlers alone, each declared by abide's \`GET(…)\` (or POST, PUT, PATCH, DELETE); anything else belongs in a module of its own. RULEBOOK 19.15.`,
84
95
  )
85
- const address = `${RPC_ADDRESS}/${path}${exported === 'default' ? '' : `/${exported}`}`
86
- const owner = owners.get(address)
87
- if (owner !== undefined)
96
+ const address = `${route === '/' ? '' : route}/${name}`
97
+ refuseFrameworkRoute(address, relative)
98
+ refuseUndeclaredParameters(address, args, relative, name)
99
+ // 20.13 — one name per handler on every generated surface.
100
+ const surface = surfaceName(method, address)
101
+ const named = names.get(surface)
102
+ if (named !== undefined)
88
103
  throw new Error(
89
- `abide: ${owner} and ${source} both declare a handler at ${address}. RULEBOOK 19.1.`,
104
+ `abide: ${named} and ${relative} are both named ${surface} on the generated surfaces. RULEBOOK 20.13.`,
90
105
  )
91
- owners.set(address, source)
106
+ names.set(surface, relative)
92
107
  mounts.push({
93
108
  address,
94
109
  method,
95
- source,
96
- name: exported,
110
+ source: relative,
111
+ name,
97
112
  ui,
98
113
  cli,
99
114
  mcp,
@@ -107,3 +122,38 @@ export async function handlersUnder(
107
122
  }
108
123
  return { mounts, warnings }
109
124
  }
125
+
126
+ /**
127
+ * Throws where a handler's `Args` does not declare a parameter its route carries.
128
+ *
129
+ * 16.67 — read off the JSON Schema its annotation derives, which is what the wire holds the args
130
+ * to: a parameter the schema does not name would reach the handler as a key it never typed.
131
+ */
132
+ function refuseUndeclaredParameters(
133
+ address: string,
134
+ args: unknown,
135
+ source: string,
136
+ exported: string,
137
+ ): void {
138
+ const schema = args as
139
+ | {
140
+ properties?: Record<string, unknown>
141
+ additionalProperties?: unknown
142
+ }
143
+ | undefined
144
+ // A record type — an index signature — declares every key.
145
+ const open =
146
+ schema?.additionalProperties !== undefined &&
147
+ schema.additionalProperties !== false
148
+ for (const segment of parseRoutePattern(address)) {
149
+ if (segment.kind === 'static' || open) continue
150
+ if (
151
+ schema?.properties !== undefined &&
152
+ segment.name in schema.properties
153
+ )
154
+ continue
155
+ throw new Error(
156
+ `abide: ${source}'s \`${exported}\` answers at ${address}, and its args do not declare \`${segment.name}\`. RULEBOOK 16.67.`,
157
+ )
158
+ }
159
+ }
@@ -7,6 +7,13 @@
7
7
  // reached only by something already doing a build. Nothing here is in any client bundle, and
8
8
  // `compiler.test.ts` holds the two apart by asserting what `@abide/abide/server` exports.
9
9
 
10
+ // 34.14 — what serves what this compiles, to a bundle or to the runtime (`abideModules`).
11
+ export {
12
+ abideModules,
13
+ type Compilation,
14
+ type CompiledFile,
15
+ serveCompilation,
16
+ } from '#server/abideModules.ts'
10
17
  export { componentGraph } from './componentGraph.ts'
11
18
  export { emitClient, type SelfFill } from './emitClient.ts'
12
19
  export { emitComponent } from './emitComponent.ts'
@@ -0,0 +1,138 @@
1
+ // 23.19 — A PACKAGE'S DIRECTORY, MOUNTED WHERE THE APP SAYS. `import 'kit/admin' with { type: 'routes' }`
2
+ // in a file of the app's places every `.abide` under the package's `admin` directory at the importing
3
+ // file's directory, so its pages, layouts, error pages and handlers answer below it — and the app,
4
+ // not the package, decides where: the mount is a line in a file the app writes, at the position the
5
+ // app gave it.
6
+ //
7
+ // A FILE KEEPS ITS PATH AND GAINS A POSITION. It is compiled, served and keyed by where it lies, as
8
+ // any dependency's is (34.27); its POSITION — the path it would have below the root — is what
9
+ // `placeOf`, `fileKind` and `routeOf` read, so a mounted page is a page by the same test as the app's.
10
+
11
+ import { readFileSync, realpathSync, statSync } from 'node:fs' // bun exposes no sync read, stat or realpath of its own
12
+ import { dirname, relative, resolve } from 'node:path' // bun exposes no path helpers of its own
13
+ import { abideUnder } from '#server/appTree.ts'
14
+ import { placeOf } from '#shared/routePattern.ts'
15
+ import { nearest } from './nearest.ts'
16
+ import { type AbideFile, parseAbide, scriptBodies } from './parseAbide.ts'
17
+ import { attributedImports, IMPORT_ATTRIBUTES } from './splitImports.ts'
18
+
19
+ /** A `.abide` on disk, and the position it takes in the app. */
20
+ type Placed = { path: string; position: string }
21
+
22
+ /** One directory a file mounts, and each `.abide` under it at the position it takes in the app. */
23
+ export type Mounted = {
24
+ specifier: string
25
+ directory: string
26
+ files: Placed[]
27
+ }
28
+
29
+ /**
30
+ * Every directory the file at `path` mounts, its position in the app being `position` — or the
31
+ * refusal naming what one of them cannot be.
32
+ *
33
+ * `root` is the app's, real: a directory under it is the app's own already, with a position of its
34
+ * own, and mounting it would give its files a second (23.20).
35
+ */
36
+ export function mountsOf(
37
+ root: string,
38
+ path: string,
39
+ position: string,
40
+ file: AbideFile,
41
+ ): Mounted[] | string {
42
+ const mounts: Mounted[] = []
43
+ const at = position.includes('/')
44
+ ? `${position.slice(0, position.lastIndexOf('/'))}/`
45
+ : ''
46
+ for (const script of scriptBodies(file))
47
+ for (const { specifier } of attributedImports(script, 'routes')) {
48
+ const directory = mountedDirectory(specifier, dirname(path))
49
+ if (directory === undefined)
50
+ return `abide: ${position} mounts ${specifier}, and no directory answers to it. RULEBOOK 23.19.`
51
+ if (ownDirectory(root, directory))
52
+ return `abide: ${position} mounts ${specifier}, which is the app's own directory already. RULEBOOK 23.20.`
53
+ const files: Placed[] = []
54
+ for (const below of abideUnder(directory))
55
+ files.push({
56
+ path: `${directory}/${below}`,
57
+ position: `${at}${below}`,
58
+ })
59
+ mounts.push({ specifier, directory, files })
60
+ }
61
+ return mounts
62
+ }
63
+
64
+ /**
65
+ * Every file mounted under the app at `root` whose own `.abide` are `relatives`, and every one those
66
+ * mount in turn, at its position — read for nothing but its mounts, and refusing nothing: the
67
+ * build is what refuses a mount (`withDependencies`).
68
+ *
69
+ * THE CHECKER'S `Routes` (30.26) asks this on every check, so a file is parsed only where its text
70
+ * names a mount at all.
71
+ */
72
+ export function mountedUnder(
73
+ root: string,
74
+ relatives: readonly string[],
75
+ ): Placed[] {
76
+ const queue: Placed[] = []
77
+ for (const relative of relatives)
78
+ queue.push({ path: `${root}/${relative}`, position: relative })
79
+ const found: Placed[] = []
80
+ const seen = new Set<string>()
81
+ for (let at = 0; at < queue.length; at += 1) {
82
+ const { path, position } = queue[at] as Placed
83
+ const text = readFileSync(path, 'utf8')
84
+ if (!IMPORT_ATTRIBUTES.routes.test(text)) continue
85
+ let file: AbideFile
86
+ try {
87
+ file = parseAbide(text)
88
+ } catch {
89
+ continue
90
+ }
91
+ const mounts = mountsOf(root, path, position, file)
92
+ if (typeof mounts === 'string') continue
93
+ for (const mount of mounts)
94
+ for (const placed of mount.files) {
95
+ if (seen.has(placed.path)) continue
96
+ seen.add(placed.path)
97
+ found.push(placed)
98
+ queue.push(placed)
99
+ }
100
+ }
101
+ return found
102
+ }
103
+
104
+ // Under the root and not a dependency's there: one of the app's own, with positions of its own.
105
+ function ownDirectory(root: string, directory: string): boolean {
106
+ if (directory === root) return true
107
+ const below = relative(root, directory)
108
+ return !below.startsWith('..') && placeOf(`${below}/-`) !== 'dependency'
109
+ }
110
+
111
+ /**
112
+ * The real directory `specifier` names from `from`: a relative path as written, and a bare one as a
113
+ * package and the path into it, found in the nearest `node_modules` above.
114
+ *
115
+ * A DIRECTORY, NOT A MODULE: a package's `exports` names its modules, and a mount asks for none of
116
+ * them, so its directory is found as bun would find the package and then walked.
117
+ */
118
+ function mountedDirectory(specifier: string, from: string): string | undefined {
119
+ let candidate: string | undefined
120
+ if (specifier.startsWith('./') || specifier.startsWith('../'))
121
+ candidate = resolve(from, specifier)
122
+ else {
123
+ const parts = specifier.split('/')
124
+ const nameLength = specifier.startsWith('@') ? 2 : 1
125
+ const name = parts.slice(0, nameLength).join('/')
126
+ const inside = parts.slice(nameLength).join('/')
127
+ const found = nearest(from, `node_modules/${name}`)
128
+ if (found === undefined) return undefined
129
+ candidate = inside === '' ? found : `${found}/${inside}`
130
+ }
131
+ try {
132
+ return statSync(candidate).isDirectory()
133
+ ? realpathSync(candidate)
134
+ : undefined
135
+ } catch {
136
+ return undefined
137
+ }
138
+ }
@@ -0,0 +1,14 @@
1
+ import { existsSync } from 'node:fs' // bun exposes no sync existence check of its own
2
+ import { dirname } from 'node:path' // bun exposes no path helpers of its own
3
+
4
+ /** The nearest file or directory at `name` at or above `from`, or `undefined`. */
5
+ export function nearest(from: string, name: string): string | undefined {
6
+ let at = from
7
+ while (true) {
8
+ const candidate = `${at}/${name}`
9
+ if (existsSync(candidate)) return candidate
10
+ const up = dirname(at)
11
+ if (up === at) return undefined
12
+ at = up
13
+ }
14
+ }