@pithy-sh/core 0.9.0 → 0.10.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 (162) hide show
  1. package/dist/asyncapi/document.d.ts +46 -0
  2. package/dist/asyncapi/document.d.ts.map +1 -0
  3. package/dist/asyncapi/document.js +115 -0
  4. package/dist/capability/capability.d.ts +7 -0
  5. package/dist/capability/capability.d.ts.map +1 -1
  6. package/dist/capability/composition.d.ts.map +1 -1
  7. package/dist/capability/composition.js +5 -1
  8. package/dist/capability/settings.d.ts +10 -2
  9. package/dist/capability/settings.d.ts.map +1 -1
  10. package/dist/capability/settings.js +5 -3
  11. package/dist/cli/command.d.ts +364 -0
  12. package/dist/cli/command.d.ts.map +1 -0
  13. package/dist/cli/command.js +418 -0
  14. package/dist/cli/commandCatalog.generated.d.ts +2275 -0
  15. package/dist/cli/commandCatalog.generated.d.ts.map +1 -0
  16. package/dist/cli/commandCatalog.generated.js +2390 -0
  17. package/dist/cli/commandFacts.d.ts +164 -0
  18. package/dist/cli/commandFacts.d.ts.map +1 -0
  19. package/dist/cli/commandFacts.js +215 -0
  20. package/dist/controlPlane/error/errors.d.ts.map +1 -1
  21. package/dist/controlPlane/error/errors.js +4 -3
  22. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  23. package/dist/controlPlane/http/guard.js +11 -1
  24. package/dist/controlPlane/http/routes.d.ts +1 -1
  25. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  26. package/dist/controlPlane/http/routes.js +34 -7
  27. package/dist/createBackend.d.ts +7 -0
  28. package/dist/createBackend.d.ts.map +1 -1
  29. package/dist/createBackend.js +34 -4
  30. package/dist/entitlement/require.js +1 -1
  31. package/dist/error/cause.d.ts +16 -116
  32. package/dist/error/cause.d.ts.map +1 -1
  33. package/dist/error/cause.js +19 -153
  34. package/dist/error/http.d.ts +40 -0
  35. package/dist/error/http.d.ts.map +1 -1
  36. package/dist/error/payload.d.ts +80 -0
  37. package/dist/error/payload.d.ts.map +1 -1
  38. package/dist/error/payload.js +26 -0
  39. package/dist/error/pithyError.d.ts +44 -0
  40. package/dist/error/pithyError.d.ts.map +1 -1
  41. package/dist/error/pithyError.js +56 -1
  42. package/dist/error/unresolved.d.ts +265 -0
  43. package/dist/error/unresolved.d.ts.map +1 -0
  44. package/dist/error/unresolved.js +599 -0
  45. package/dist/error/wire.d.ts +20 -0
  46. package/dist/error/wire.d.ts.map +1 -0
  47. package/dist/error/wire.js +19 -0
  48. package/dist/http/oidcWebhook.d.ts +3 -3
  49. package/dist/http/oidcWebhook.js +3 -3
  50. package/dist/http/routeContract.js +1 -1
  51. package/dist/http/routeSpec.d.ts +66 -0
  52. package/dist/http/routeSpec.d.ts.map +1 -0
  53. package/dist/http/routeSpec.js +40 -0
  54. package/dist/http/sameOrigin.d.ts +1 -1
  55. package/dist/http/sameOrigin.js +1 -1
  56. package/dist/http/signedWebhook.d.ts +3 -3
  57. package/dist/http/signedWebhook.js +3 -3
  58. package/dist/http/socket.d.ts +52 -0
  59. package/dist/http/socket.d.ts.map +1 -0
  60. package/dist/http/socket.js +2 -0
  61. package/dist/http/subprotocol.d.ts +29 -0
  62. package/dist/http/subprotocol.d.ts.map +1 -0
  63. package/dist/http/subprotocol.js +37 -0
  64. package/dist/http/validation.d.ts +40 -8
  65. package/dist/http/validation.d.ts.map +1 -1
  66. package/dist/http/validation.js +25 -1
  67. package/dist/index.js +2 -2
  68. package/dist/logger/record.d.ts +23 -0
  69. package/dist/logger/record.d.ts.map +1 -1
  70. package/dist/migrations/bookkeeping.d.ts +21 -0
  71. package/dist/migrations/bookkeeping.d.ts.map +1 -1
  72. package/dist/migrations/bookkeeping.js +32 -3
  73. package/dist/migrations/groups.d.ts +140 -0
  74. package/dist/migrations/groups.d.ts.map +1 -0
  75. package/dist/migrations/groups.js +191 -0
  76. package/dist/migrations/migrateStep.d.ts +13 -0
  77. package/dist/migrations/migrateStep.d.ts.map +1 -0
  78. package/dist/migrations/migrateStep.js +20 -0
  79. package/dist/migrations/owner.d.ts +7 -0
  80. package/dist/migrations/owner.d.ts.map +1 -1
  81. package/dist/migrations/owner.js +11 -7
  82. package/dist/migrations/retained.js +2 -2
  83. package/dist/migrations/runner.d.ts +76 -5
  84. package/dist/migrations/runner.d.ts.map +1 -1
  85. package/dist/migrations/runner.js +130 -19
  86. package/dist/naming/domains.d.ts +17 -0
  87. package/dist/naming/domains.d.ts.map +1 -1
  88. package/dist/naming/domains.js +62 -4
  89. package/dist/naming/environment.d.ts.map +1 -1
  90. package/dist/naming/environment.js +2 -1
  91. package/dist/naming/packageName.d.ts +17 -0
  92. package/dist/naming/packageName.d.ts.map +1 -0
  93. package/dist/naming/packageName.js +22 -0
  94. package/dist/naming/provisionScope.d.ts.map +1 -1
  95. package/dist/naming/provisionScope.js +2 -1
  96. package/dist/openapi/document.d.ts +106 -0
  97. package/dist/openapi/document.d.ts.map +1 -0
  98. package/dist/openapi/document.js +314 -0
  99. package/dist/openapi/routeGroups.d.ts +8 -0
  100. package/dist/openapi/routeGroups.d.ts.map +1 -0
  101. package/dist/openapi/routeGroups.js +23 -0
  102. package/dist/text/comments.js +66 -4
  103. package/dist/text/literals.d.ts +43 -0
  104. package/dist/text/literals.d.ts.map +1 -0
  105. package/dist/text/literals.js +293 -0
  106. package/dist/version.generated.d.ts +1 -1
  107. package/dist/version.generated.d.ts.map +1 -1
  108. package/dist/version.generated.js +1 -1
  109. package/dist/workflow/dispatchRoute.d.ts.map +1 -1
  110. package/dist/workflow/dispatchRoute.js +12 -6
  111. package/dist/workflow/faults.d.ts.map +1 -1
  112. package/dist/workflow/faults.js +2 -3
  113. package/dist/workflow/hostEnv.d.ts +1 -0
  114. package/dist/workflow/hostEnv.d.ts.map +1 -1
  115. package/dist/workflow/hostEnv.js +4 -2
  116. package/dist/workflow/loopback.d.ts.map +1 -1
  117. package/dist/workflow/loopback.js +2 -1
  118. package/package.json +3 -3
  119. package/src/asyncapi/document.ts +164 -0
  120. package/src/capability/capability.ts +7 -0
  121. package/src/capability/composition.ts +2 -1
  122. package/src/capability/settings.ts +19 -2
  123. package/src/cli/command.ts +865 -0
  124. package/src/cli/commandCatalog.generated.ts +2533 -0
  125. package/src/cli/commandFacts.ts +219 -0
  126. package/src/controlPlane/error/errors.ts +10 -3
  127. package/src/controlPlane/http/guard.ts +14 -2
  128. package/src/controlPlane/http/routes.ts +57 -9
  129. package/src/createBackend.ts +35 -2
  130. package/src/entitlement/require.ts +1 -1
  131. package/src/error/cause.ts +18 -181
  132. package/src/error/payload.ts +59 -0
  133. package/src/error/pithyError.ts +76 -0
  134. package/src/error/unresolved.ts +853 -0
  135. package/src/error/wire.ts +40 -0
  136. package/src/http/oidcWebhook.ts +3 -3
  137. package/src/http/routeContract.ts +1 -1
  138. package/src/http/routeSpec.ts +81 -0
  139. package/src/http/sameOrigin.ts +1 -1
  140. package/src/http/signedWebhook.ts +3 -3
  141. package/src/http/socket.ts +55 -0
  142. package/src/http/subprotocol.ts +47 -0
  143. package/src/http/validation.ts +50 -8
  144. package/src/migrations/bookkeeping.ts +39 -2
  145. package/src/migrations/groups.ts +295 -0
  146. package/src/migrations/migrateStep.ts +19 -0
  147. package/src/migrations/owner.ts +10 -6
  148. package/src/migrations/retained.ts +2 -2
  149. package/src/migrations/runner.ts +214 -22
  150. package/src/naming/domains.ts +75 -16
  151. package/src/naming/environment.ts +2 -1
  152. package/src/naming/packageName.ts +20 -0
  153. package/src/naming/provisionScope.ts +2 -1
  154. package/src/openapi/document.ts +412 -0
  155. package/src/openapi/routeGroups.ts +25 -0
  156. package/src/text/comments.ts +56 -4
  157. package/src/text/literals.ts +294 -0
  158. package/src/version.generated.ts +1 -1
  159. package/src/workflow/dispatchRoute.ts +13 -6
  160. package/src/workflow/faults.ts +5 -3
  161. package/src/workflow/hostEnv.ts +9 -1
  162. package/src/workflow/loopback.ts +2 -1
@@ -0,0 +1,853 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { packageOf } from "../naming/packageName";
5
+ import { causeMessage, prop, rootCause } from "./cause";
6
+
7
+ /**
8
+ * **An import that would not resolve: which kind it was, whose it was, and the one thing to do about it —
9
+ * decided here and nowhere else (#709).**
10
+ *
11
+ * Three surfaces refuse a module that will not load: the CLI's `pithy.config.ts` loader, its capability
12
+ * loaders, and the vite plugin's Worker config. Each read core's facts and then kept its own rules for the
13
+ * rest — builtins, `#` imports, `file:` URLs, an unexported subpath, a package name. Three review rounds
14
+ * each found one of them giving advice the others had stopped giving: `bun install` where installing
15
+ * cannot help, an absolute path in a rendered field, a dependency's fault blamed on the capability. Fixing
16
+ * them site by site fixed today's three and left the class. So the rules moved here, the surfaces keep
17
+ * none, and `packages/cli/src/project/unresolvedSurfaces.test.ts` feeds every kind through all three.
18
+ *
19
+ * What a surface still owns is what is genuinely its own: a capability loader knows which package it was
20
+ * loading, and an absent *capability* is `pithy add`, not `bun install`. That is a fact about the target,
21
+ * not about the import, and it is the only override any surface makes.
22
+ *
23
+ * ## The two runtimes hand back different halves
24
+ *
25
+ * Measured on Bun 1.4.2 and Node 22.23.3 (Node 24.21.0 throws the same), importing a real tree:
26
+ *
27
+ * | the import | Bun 1.4.2 | Node 22 |
28
+ * |---|---|---|
29
+ * | absent package | `specifier`, `referrer`, `Cannot find package` | `Cannot find package 'pkg'` |
30
+ * | deep import, package absent **or** installed without it | the same sentence for both | `Cannot find package 'pkg'`, or the resolved `url` |
31
+ * | unexported subpath | the same sentence again | `ERR_PACKAGE_PATH_NOT_EXPORTED` |
32
+ * | `node:nope` | `ERR_UNKNOWN_BUILTIN_MODULE`, `specifier`, `referrer` | the code and sentence, nothing else |
33
+ * | `bun:nope` | `Cannot find package 'nope'` — the scheme is dropped | `ERR_UNSUPPORTED_ESM_URL_SCHEME` |
34
+ * | the adopter's file | the import as written, `referrer` | the resolved `url` |
35
+ * | `#internal` | `Cannot find package '#internal'` | `ERR_PACKAGE_IMPORT_NOT_DEFINED` |
36
+ * | installed package, `exports` with no `"."` | `Cannot find package 'pkg'` — the sentence for an absent one | `No "exports" main defined in …/pkg/package.json` |
37
+ * | installed package, `main` names a missing file | the same sentence again | `Cannot find package '…/node_modules/pkg/<main>'` |
38
+ * | installed package, `exports` `"."` names a missing file | the same sentence again | the resolved `url` of that file |
39
+ * | deep import, an `exports` pattern maps it to a missing file | the import as written, `referrer` | the resolved `url` of that file |
40
+ * | a `file:` URL | converted to a path | the resolved `url` |
41
+ * | a package's own relative import | the import as written, `referrer` in the package | the resolved `url` |
42
+ *
43
+ * Node gives the **already-resolved path**; Bun gives the **import as written** and the file it was
44
+ * written in. Both are read to one path before anything is decided, so a remedy does not depend on the
45
+ * runtime it was read on. Where Bun says less than Node — an unexported subpath reads as a missing one —
46
+ * the two share a remedy, which is the only way to keep them agreeing.
47
+ *
48
+ * **A bare name is asked of the probe too.** Bun 1.4.2 says `Cannot find package 'pkg'` for a package that
49
+ * is installed and has no entry to import by its bare name — every `@pithy-sh/*` package, whose `exports`
50
+ * map has no `"."` — exactly as it says it for one that is absent. Taken at face value, that told an
51
+ * adopter to install what was installed. So a bare name the probe finds is an installed package with no
52
+ * usable entry, named by the package, and Node's two sentences for the same tree read to the same remedy.
53
+ *
54
+ * **And so is its `package.json`.** Bun says the same sentence again for a package that *declares* an entry
55
+ * whose file is not there — an unbuilt workspace package, a broken install — and Node hands back that
56
+ * file's path, so a bare import read as a deep one the adopter never wrote. Neither is "no entry": it is
57
+ * that package to rebuild or reinstall. Only the manifest says which, so {@link packageEntryProbe} reads
58
+ * it, and every runtime's shape for the same tree reads to the same kind.
59
+ *
60
+ * **And a deep import is resolved through `exports` too.** Every `@pithy-sh/*` package exports `./src/*` as
61
+ * `./dist/*.js`, so in an unbuilt one every deep import matches the pattern and lands on a file that is not
62
+ * there. Bun and Vite hand back the import as written and Node the `dist` file it resolved to; read at
63
+ * face value, the first two told an adopter the package does not provide a path it declares, and Node
64
+ * quoted a path nobody wrote. So the import is resolved through the manifest — patterns and conditions,
65
+ * as the runtimes do — and Node's file is mapped back to the import that reaches it, which is what is
66
+ * shown. A target the manifest declares and the disk does not have is {@link exportedKind}'s to call.
67
+ *
68
+ * ## Vite's module runner says it a third way
69
+ *
70
+ * The vite plugin loads a config through `runnerImport` (Vite 8.3.0, measured), which resolves bare
71
+ * imports itself before any runtime does and throws its own shapes: `PLUGIN_ERROR` with
72
+ * *`"." is not exported under the conditions [...] from package <dir>`* for an `exports` map with no
73
+ * usable `"."` (or `"./sub"` for an unexported subpath), and `ERR_RESOLVE_PACKAGE_ENTRY_FAIL` —
74
+ * *Failed to resolve entry for package "pkg"* — for every other bare name with nothing on disk to load,
75
+ * declared or not. Neither names an importer, so the manifest is found from the project instead.
76
+ *
77
+ * **Bun 1.4.2 cannot be told from an absent package for `bun:nope`.** It drops the scheme and reports a
78
+ * package called `nope`, with nothing left in the cause to say otherwise. That one reads as an absent
79
+ * package, and is the one remedy here that a runtime takes out of this module's hands.
80
+ *
81
+ * ## Never a path, never a URL
82
+ *
83
+ * Every rendered field is built from what {@link UnresolvedImport.shown} allows: a bare specifier, a
84
+ * path relative to the project, or a path inside a package named from that package. An absolute path or
85
+ * a `file:` URL is never returned from here, so a surface cannot render one.
86
+ *
87
+ * ## A package that resolves and will not load
88
+ *
89
+ * One more failure belongs to an installed package and reads, unclassified, as the adopter's: a package
90
+ * whose code contradicts the `"type"` its `package.json` declares. Node throws it from inside that package
91
+ * — `module is not defined in ES module scope`, `Cannot use import statement outside a module` — and Vite's
92
+ * runner hands the import to Node, so it throws the same. Nothing resolves wrongly, so it is not an
93
+ * {@link unresolvedImport}; but a surface that falls through to its own rules says *the config threw* or
94
+ * *the config does not parse*. {@link packageLoadFailure} names the package instead, from the file the
95
+ * error was thrown in, and every surface asks it before deciding the failure was its own.
96
+ */
97
+
98
+ /** Which of the causes it was. Each has one remedy, and only three of them say to install. */
99
+ export type UnresolvedImportKind =
100
+ /** A package that is not installed. Installing is the remedy. */
101
+ | "package"
102
+ /**
103
+ * An installed package asked for a path it does not have, or does not export — or asked by its bare name
104
+ * when it has no entry there. Installing changes nothing.
105
+ */
106
+ | "package-subpath"
107
+ /**
108
+ * An installed package whose `package.json` names an entry — `exports` `"."` or `main` — that is not on
109
+ * disk: unbuilt, or a broken install. That package is rebuilt or reinstalled; the project's install
110
+ * does not touch a package it already has.
111
+ */
112
+ | "package-entry"
113
+ /** An installed package's own relative import of its own file. Its maintainer's, never an install. */
114
+ | "package-file"
115
+ /** A module the runtime provides, or does not: `node:nope`. Installing cannot add one. */
116
+ | "builtin"
117
+ /** A file in the adopter's own tree, a `file:` URL included. Theirs to create or correct. */
118
+ | "local-file"
119
+ /** A `#` import its importer's own `imports` map does not satisfy. Theirs, or the package's maintainer's. */
120
+ | "imports-map"
121
+ /** A deep import, and nothing to say whether its package is absent or only the path is. Both remedies. */
122
+ | "undetermined"
123
+ /** A resolution failure that names nothing. Both remedies. */
124
+ | "unnamed";
125
+
126
+ /** One unresolved import, classified. Every field may be rendered: none carries a path or a URL. */
127
+ export interface UnresolvedImport {
128
+ /** Which cause it was. */
129
+ kind: UnresolvedImportKind;
130
+ /** The import as it may be shown: a bare specifier, `./`-relative to the project, or `<package>/<path>`. */
131
+ shown?: string;
132
+ /** The package the import named, for the kinds that name one. */
133
+ package?: string;
134
+ /** The package whose file holds the import. Absent when that is the adopter's own code, or not said. */
135
+ owner?: string;
136
+ /** Whether installing the project's dependencies can fix it. */
137
+ installHelps: boolean;
138
+ /** The one sentence every surface puts in its `action`. */
139
+ remedy: string;
140
+ }
141
+
142
+ /**
143
+ * Whether `pkg` is installed where `referrer` would look for it. The caller's to answer: core touches no
144
+ * filesystem, and on Bun 1.4.2 nothing in the cause says. {@link nodeModulesProbe} builds the answer.
145
+ */
146
+ export type InstalledProbe = (pkg: string, referrer: string) => boolean;
147
+
148
+ /** What a surface tells the classifier about where it stands. */
149
+ export interface UnresolvedImportOptions {
150
+ /** Answers the one question the cause cannot. Without it, a deep import is `undetermined`. */
151
+ installed?: InstalledProbe;
152
+ /**
153
+ * Answers the next one: what an installed package declares, and what of it is on disk. Without it, Bun's
154
+ * bare name is taken to have no entry there, a loader that names the file or the package it could not
155
+ * load — Node reaching for a `main`, Vite's *Failed to resolve entry* — is taken to have reached for a
156
+ * declared one, and a deep import is a path the package does not provide.
157
+ */
158
+ entry?: EntryProbe;
159
+ /** The directory an adopter's paths are shown relative to. Without it, a resolved path is never shown. */
160
+ relativeTo?: string;
161
+ /** The package doing the importing — `@pithy-sh/cli`, `@pithy-sh/vite`. Never named as an owner. */
162
+ loader?: string;
163
+ }
164
+
165
+ /**
166
+ * An installed `pkg` found on the chain above `from`: its manifest, and whether a path inside it is on disk.
167
+ * `undefined` when there is no such package, or no manifest to read. {@link packageEntryProbe} builds the
168
+ * answer; what the manifest *means* — the entry it declares, where an `exports` pattern leads — is decided
169
+ * here, by {@link declaredEntry} and {@link exportedTarget}, so no surface reads a manifest its own way.
170
+ */
171
+ export type EntryProbe = (pkg: string, from: string) => InstalledPackage | undefined;
172
+
173
+ /** What {@link EntryProbe} answers: one installed package's manifest, and its directory to ask of. */
174
+ export interface InstalledPackage {
175
+ /** Its `package.json`, parsed. */
176
+ manifest: unknown;
177
+ /** Whether a path inside the package — a file or a directory, `""` for the package itself — is on disk. */
178
+ has(path: string): boolean;
179
+ }
180
+
181
+ /** The two filesystem calls {@link nodeModulesProbe} makes. `node:fs`'s `existsSync` and `realpathSync`. */
182
+ export interface ProbeFileSystem {
183
+ /** Whether a file is there. */
184
+ exists(path: string): boolean;
185
+ /** Where a path really is, through every symlink. May throw for a path that is not there. */
186
+ realpath(path: string): string;
187
+ }
188
+
189
+ /**
190
+ * The {@link InstalledProbe} a surface on Node or Bun passes: `pkg` on the `node_modules` chain above
191
+ * `referrer`, **walked from where `referrer` really is**.
192
+ *
193
+ * Both runtimes resolve from the real path of the importing file, and Bun's isolated linker puts every
194
+ * package behind a symlink into its store, with that package's own dependencies beside the real
195
+ * directory and not beside the link. Walked from the link, the chain is the root's — a different copy,
196
+ * or none — so a dependency the resolver could see was reported absent, and the adopter told to install
197
+ * what was installed. A `referrer` that will not resolve is walked as given.
198
+ *
199
+ * Core has no filesystem, so the two calls are the caller's. The walk is here, once.
200
+ */
201
+ export function nodeModulesProbe(fs: ProbeFileSystem): InstalledProbe {
202
+ return (pkg, referrer) => installedAt(fs, pkg, referrer) !== undefined;
203
+ }
204
+
205
+ /**
206
+ * The {@link EntryProbe} a surface on Node or Bun passes: the same walk as {@link nodeModulesProbe}, then
207
+ * the package's own `package.json`, and `exists` for the paths beside it.
208
+ */
209
+ export function packageEntryProbe(fs: ProbeFileSystem & { read(path: string): string }): EntryProbe {
210
+ return (pkg, from) => {
211
+ const dir = installedAt(fs, pkg, from);
212
+ if (dir === undefined) return undefined;
213
+ let manifest: unknown;
214
+ try {
215
+ manifest = JSON.parse(fs.read(`${dir}/package.json`));
216
+ } catch {
217
+ return undefined;
218
+ }
219
+ return { manifest, has: (path) => fs.exists(path === "" ? dir : `${dir}/${path}`) };
220
+ };
221
+ }
222
+
223
+ /** The entry an installed `pkg` declares, through the probe: `undefined` when there is no probe or no package. */
224
+ function declaredBy(options: UnresolvedImportOptions, pkg: string, from: string): string | null | undefined {
225
+ const installed = options.entry?.(pkg, from);
226
+ return installed === undefined ? undefined : declaredEntry(installed.manifest);
227
+ }
228
+
229
+ /** The directory `pkg` is installed in on the chain above `referrer`, walked from where it really is. */
230
+ function installedAt(fs: ProbeFileSystem, pkg: string, referrer: string): string | undefined {
231
+ let real = referrer;
232
+ try {
233
+ real = fs.realpath(referrer);
234
+ } catch {
235
+ // Not there to resolve. The chain above where it was said to be is the best there is.
236
+ }
237
+ let dir = parentOf(real);
238
+ for (;;) {
239
+ const at = `${dir.replace(/[\\/]+$/, "")}/node_modules/${pkg}`;
240
+ if (fs.exists(`${at}/package.json`)) return at;
241
+ const up = parentOf(dir);
242
+ if (up === dir) return undefined;
243
+ dir = up;
244
+ }
245
+ }
246
+
247
+ /** The conditions an ESM import of a package matches, on Node and on Vite's runner. */
248
+ const IMPORT_CONDITIONS = new Set(["import", "node", "module-sync", "default"]);
249
+
250
+ /**
251
+ * The entry a manifest declares for an `import` of the package's bare name, or `null` for none. `exports`
252
+ * wins when present — its `"."`, or the whole map when no key is a subpath — and a target is the first
253
+ * one whose condition an import matches. With no `exports`, it is `main`.
254
+ */
255
+ export function declaredEntry(manifest: unknown): string | null {
256
+ const exports = prop(manifest, "exports");
257
+ if (exports !== undefined && exports !== null) return importTarget(subpathMap(exports)["."]);
258
+ const main = prop(manifest, "main");
259
+ return typeof main === "string" && main.length > 0 ? inside(main) : null;
260
+ }
261
+
262
+ /** An `exports` field as its subpath map: a target or a map of conditions alone is the package's `"."`. */
263
+ function subpathMap(exports: unknown): Record<string, unknown> {
264
+ const subpaths =
265
+ typeof exports === "object" &&
266
+ exports !== null &&
267
+ !Array.isArray(exports) &&
268
+ Object.keys(exports).some((key) => key.startsWith("."));
269
+ return subpaths ? (exports as Record<string, unknown>) : { ".": exports };
270
+ }
271
+
272
+ /** Where one subpath leads through `exports`: the file, and for a pattern, the directory it expands under. */
273
+ export interface ExportedTarget {
274
+ /** The file an import takes, as a path inside the package. */
275
+ target: string;
276
+ /**
277
+ * For a target reached through a pattern (`./src/*` → `./dist/*.js`), the directory inside the package
278
+ * its `*` expands under (`dist`; `""` for the package itself). Absent for a subpath with a key of its own.
279
+ */
280
+ under?: string;
281
+ }
282
+
283
+ /**
284
+ * What an import of `subpath` (`"."`, `"./src/capability"`) takes from a manifest's `exports`, as Node and
285
+ * Vite resolve it: an exact key first, then the pattern with the longest prefix, each target followed
286
+ * through the conditions an import matches. `null` when the map does not export it — no key, no pattern,
287
+ * or a `null` target — and `undefined` when the manifest has no `exports`, so the subpath is a file path.
288
+ */
289
+ export function exportedTarget(manifest: unknown, subpath: string): ExportedTarget | null | undefined {
290
+ const exports = prop(manifest, "exports");
291
+ if (exports === undefined || exports === null) return undefined;
292
+ const map = subpathMap(exports);
293
+ if (Object.hasOwn(map, subpath) && !subpath.includes("*")) {
294
+ const target = importTarget(map[subpath]);
295
+ return target === null ? null : { target };
296
+ }
297
+ let best: { key: string; star: string } | undefined;
298
+ for (const key of Object.keys(map)) {
299
+ const at = key.indexOf("*");
300
+ if (at === -1 || key.indexOf("*", at + 1) !== -1) continue;
301
+ const prefix = key.slice(0, at);
302
+ const suffix = key.slice(at + 1);
303
+ if (!subpath.startsWith(prefix) || subpath === prefix) continue;
304
+ if (!subpath.endsWith(suffix) || subpath.length < key.length) continue;
305
+ // Node's PATTERN_KEY_COMPARE: the longer prefix wins, and then the longer key.
306
+ if (best !== undefined) {
307
+ const held = best.key.indexOf("*");
308
+ if (at < held || (at === held && key.length <= best.key.length)) continue;
309
+ }
310
+ best = { key, star: subpath.slice(prefix.length, subpath.length - suffix.length) };
311
+ }
312
+ if (best === undefined) return null;
313
+ const pattern = importTarget(map[best.key]);
314
+ if (pattern === null) return null;
315
+ return { target: pattern.replaceAll("*", best.star), under: patternBase(pattern) };
316
+ }
317
+
318
+ /**
319
+ * The import that reaches `file` — a path inside the package, the one Node reports — through `exports`:
320
+ * the key that maps to it, or the pattern whose `*` it fills. Only an answer that resolves back to `file`
321
+ * through {@link exportedTarget} counts, so a pattern a longer one shadows never names the import.
322
+ */
323
+ function exportedSubpath(manifest: unknown, file: string): (ExportedTarget & { subpath: string }) | undefined {
324
+ const exports = prop(manifest, "exports");
325
+ if (exports === undefined || exports === null) return undefined;
326
+ for (const [key, value] of Object.entries(subpathMap(exports))) {
327
+ const target = importTarget(value);
328
+ if (target === null) continue;
329
+ let subpath: string | undefined;
330
+ const at = target.indexOf("*");
331
+ if (!key.includes("*")) subpath = target === file ? key : undefined;
332
+ else if (at !== -1) {
333
+ const prefix = target.slice(0, at);
334
+ const suffix = target.slice(at + 1).split("*")[0] ?? "";
335
+ if (file.startsWith(prefix) && file.endsWith(suffix) && file.length > prefix.length + suffix.length) {
336
+ subpath = key.replace("*", file.slice(prefix.length, file.length - suffix.length));
337
+ }
338
+ }
339
+ const back = subpath === undefined ? undefined : exportedTarget(manifest, subpath);
340
+ if (subpath !== undefined && back?.target === file) return { ...back, subpath };
341
+ }
342
+ return undefined;
343
+ }
344
+
345
+ /** The directory inside the package a pattern target's `*` expands under: `dist/*.js` → `dist`. */
346
+ function patternBase(pattern: string): string {
347
+ const head = pattern.slice(0, pattern.indexOf("*"));
348
+ return head.slice(0, Math.max(head.lastIndexOf("/"), 0));
349
+ }
350
+
351
+ /**
352
+ * **A target the manifest declares and the disk does not have: the package unbuilt, or the import mistyped.**
353
+ *
354
+ * A key of its own is the package's word that the file exists, so its absence is the package's — unbuilt,
355
+ * or a broken install. A pattern says nothing of the sort: `./src/*` matches `./src/capabilty` as readily as
356
+ * `./src/capability`. What tells them apart is the directory the pattern expands under. With `dist` absent
357
+ * the package was never built, and every import of it fails the same way whatever was typed; with `dist`
358
+ * there, the build ran and this file is not in it, so the import names a path the package does not have.
359
+ * A file deleted from a built `dist` reads as the second, and its remedy — check the version you have —
360
+ * still leads to a reinstall.
361
+ */
362
+ function exportedKind(found: ExportedTarget, installed: InstalledPackage): "package-entry" | "package-subpath" {
363
+ if (found.under === undefined) return "package-entry";
364
+ return installed.has(found.under) ? "package-subpath" : "package-entry";
365
+ }
366
+
367
+ /** One `exports` target, followed through conditions and fallbacks to the path an import takes. */
368
+ function importTarget(target: unknown): string | null {
369
+ if (typeof target === "string") return inside(target);
370
+ if (Array.isArray(target)) {
371
+ for (const option of target) {
372
+ const found = importTarget(option);
373
+ if (found !== null) return found;
374
+ }
375
+ return null;
376
+ }
377
+ if (typeof target !== "object" || target === null) return null;
378
+ for (const [condition, value] of Object.entries(target)) {
379
+ if (!IMPORT_CONDITIONS.has(condition)) continue;
380
+ const found = importTarget(value);
381
+ if (found !== null) return found;
382
+ }
383
+ return null;
384
+ }
385
+
386
+ /** A manifest's path, as a path inside the package: no `./`, no trailing slash. */
387
+ function inside(path: string): string {
388
+ return path.replace(/^\.\//, "").replace(/\/+$/, "");
389
+ }
390
+
391
+ /** Every code a runtime throws for a module it could not find, or would not load as a module. */
392
+ const CODES = new Set([
393
+ "ERR_MODULE_NOT_FOUND",
394
+ "MODULE_NOT_FOUND",
395
+ "ERR_PACKAGE_PATH_NOT_EXPORTED",
396
+ "ERR_PACKAGE_IMPORT_NOT_DEFINED",
397
+ "ERR_UNSUPPORTED_DIR_IMPORT",
398
+ "ERR_UNKNOWN_BUILTIN_MODULE",
399
+ "ERR_UNSUPPORTED_ESM_URL_SCHEME",
400
+ "ERR_RESOLVE_PACKAGE_ENTRY_FAIL",
401
+ ]);
402
+
403
+ /** The same, said in prose by something that sets no code: Vite's module runner, and older loaders. */
404
+ const SENTENCE =
405
+ /Cannot find (?:package|module) |Failed to resolve (?:import|module)|Failed to load url |No such built-in module: |is not defined by "exports"|No "exports" main defined in |Package import specifier |is not exported under the conditions |Failed to resolve entry for package /;
406
+
407
+ /**
408
+ * Classify an import that would not resolve, or answer `undefined` when `cause` is not one.
409
+ *
410
+ * Read duck-typed, for `prop`'s reason in `cause.ts`, and through Bun's `AggregateError` wrapper, for
411
+ * `rootCause`'s.
412
+ */
413
+ export function unresolvedImport(
414
+ wrapped: unknown,
415
+ options: UnresolvedImportOptions = {},
416
+ ): UnresolvedImport | undefined {
417
+ const cause = rootCause(wrapped);
418
+ if (!isUnresolved(cause)) return undefined;
419
+ const referrer = importerPath(cause);
420
+ const owner = ownerOf(referrer, options.loader);
421
+ const found = locate(cause, referrer, options);
422
+ const installHelps = found.kind === "package" || found.kind === "undetermined" || found.kind === "unnamed";
423
+ return { ...found, ...(owner === undefined ? {} : { owner }), installHelps, remedy: remedy(found, owner) };
424
+ }
425
+
426
+ /**
427
+ * The package whose own source holds the import that did not resolve — **never a path**.
428
+ *
429
+ * #480 is why: `pithy add email` refused with *Nothing resolves "@pithy-sh/core/src/capability/capability"*
430
+ * while core resolved throughout, because the import was `@pithy-sh/email`'s and email was the package
431
+ * not linked. The importer is the half that points there. The **last** `node_modules` in the path wins,
432
+ * because every store nests (pnpm's `.pnpm/<id>/node_modules/<pkg>`, Bun's `.bun/<pkg>@<v>/node_modules/<pkg>`),
433
+ * and the name is held to what npm allows, so a store's bookkeeping directory answers nothing.
434
+ */
435
+ export function importingPackage(cause: unknown): string | undefined {
436
+ return ownerOf(importerPath(cause), undefined);
437
+ }
438
+
439
+ function isUnresolved(cause: unknown): boolean {
440
+ if (typeof cause !== "object" || cause === null) return false;
441
+ const code = prop(cause, "code");
442
+ if (typeof code === "string" && CODES.has(code)) return true;
443
+ if (prop(cause, "name") === "ResolveMessage") return true;
444
+ return SENTENCE.test(causeMessage(cause) ?? "");
445
+ }
446
+
447
+ /** What {@link locate} decides: everything but the owner and the sentence. */
448
+ interface Located {
449
+ kind: UnresolvedImportKind;
450
+ shown?: string;
451
+ package?: string;
452
+ }
453
+
454
+ /** A specifier that opens with a URL scheme: `node:`, `bun:`, `data:`. A Windows drive letter is one letter. */
455
+ const URL_SCHEME = /^[a-z][a-z0-9+.-]+:/i;
456
+
457
+ /** An absolute path: POSIX, a Windows drive, or a UNC share. */
458
+ const ABSOLUTE = /^(?:\/|\\\\|[A-Za-z]:[\\/])/;
459
+
460
+ function locate(cause: unknown, referrer: string | undefined, options: UnresolvedImportOptions): Located {
461
+ const code = prop(cause, "code");
462
+ const message = causeMessage(cause) ?? "";
463
+
464
+ // Node refuses a scheme it has no loader for before it resolves anything, and names only the scheme.
465
+ if (code === "ERR_UNSUPPORTED_ESM_URL_SCHEME") {
466
+ const scheme = /Received protocol '([a-z][a-z0-9+.-]*:)'/i.exec(message)?.[1];
467
+ return scheme === undefined ? { kind: "builtin" } : { kind: "builtin", shown: scheme };
468
+ }
469
+ if (
470
+ code === "ERR_PACKAGE_PATH_NOT_EXPORTED" ||
471
+ /is not defined by "exports"|No "exports" main defined in |is not exported under the conditions /.test(message)
472
+ ) {
473
+ return unexported(cause, message);
474
+ }
475
+ // Vite's runner, for a bare name it found installed and could load nothing for. It names the package and
476
+ // no importer, so the manifest is asked from the project; a package that declares nothing has no entry.
477
+ const unloadable = /Failed to resolve entry for package "([^"]+)"/.exec(message)?.[1];
478
+ if (unloadable !== undefined) {
479
+ const from = referrer ?? (options.relativeTo === undefined ? undefined : `${options.relativeTo}/package.json`);
480
+ const declared = from === undefined ? undefined : declaredBy(options, unloadable, from);
481
+ return { kind: declared === null ? "package-subpath" : "package-entry", package: unloadable, shown: unloadable };
482
+ }
483
+
484
+ const specifier = writtenSpecifier(cause, message);
485
+ if (specifier === undefined) return { kind: "unnamed" };
486
+ if (code === "ERR_UNKNOWN_BUILTIN_MODULE") return { kind: "builtin", shown: specifier };
487
+ if (specifier.startsWith("#")) return { kind: "imports-map", shown: specifier };
488
+ if (specifier.startsWith("file:")) return atPath(urlPath(specifier), referrer, options);
489
+ // `node:nope`: the runtime provides these, so no install will. Never a package by its name.
490
+ if (URL_SCHEME.test(specifier)) return { kind: "builtin", shown: specifier };
491
+
492
+ // Node states the path it resolved to. That is the fact, whatever the import was written as.
493
+ const url = prop(cause, "url");
494
+ if (typeof url === "string" && url.startsWith("file:")) return atPath(urlPath(url), referrer, options);
495
+
496
+ if (specifier.startsWith(".")) {
497
+ if (referrer === undefined) return { kind: "local-file", shown: specifier };
498
+ return atPath(joinPath(parentOf(referrer), specifier), referrer, options, specifier);
499
+ }
500
+ if (ABSOLUTE.test(specifier)) {
501
+ // Node's sentence for a bare import of a package with no `exports`: the *package* could not be found,
502
+ // at the path its `main` points to — or `index.js`, when it names none. What was imported was the
503
+ // package's bare name, and whether it declared that path is the manifest's to say.
504
+ const entry = /^Cannot find package '/.test(message) ? packageAt(specifier) : undefined;
505
+ if (entry !== undefined) {
506
+ const declared = declaredBy(options, entry.name, `${entry.dir}/package.json`);
507
+ return { kind: declared === null ? "package-subpath" : "package-entry", package: entry.name, shown: entry.name };
508
+ }
509
+ return atPath(specifier, referrer, options);
510
+ }
511
+
512
+ // A bare name is the package itself, and Bun says the same of one installed with no entry as of one
513
+ // absent. With no probe, it is taken as absent; with a path after it and no probe, nothing can say.
514
+ const pkg = packageOf(specifier);
515
+ const bare = pkg === specifier;
516
+ if (options.installed === undefined || referrer === undefined) {
517
+ return bare
518
+ ? { kind: "package", package: pkg, shown: pkg }
519
+ : { kind: "undetermined", package: pkg, shown: specifier };
520
+ }
521
+ if (!options.installed(pkg, referrer)) return { kind: "package", package: pkg, shown: pkg };
522
+ // Installed, and Bun and Vite say no more. The manifest says whether the import was declared: by its bare
523
+ // name, an entry; by a path, a key or a pattern in `exports` — and then whether its file was built.
524
+ const installed = options.entry?.(pkg, referrer);
525
+ if (bare) {
526
+ const declared = installed === undefined ? undefined : declaredEntry(installed.manifest);
527
+ return typeof declared === "string"
528
+ ? { kind: "package-entry", package: pkg, shown: pkg }
529
+ : { kind: "package-subpath", package: pkg, shown: specifier };
530
+ }
531
+ const exported =
532
+ installed === undefined ? undefined : exportedTarget(installed.manifest, `./${specifier.slice(pkg.length + 1)}`);
533
+ return installed !== undefined && exported !== undefined && exported !== null
534
+ ? { kind: exportedKind(exported, installed), package: pkg, shown: specifier }
535
+ : { kind: "package-subpath", package: pkg, shown: specifier };
536
+ }
537
+
538
+ /**
539
+ * An installed package that does not export the path. The loader's field is the import as written where
540
+ * there is one; Node's sentence names the subpath and the package's `package.json`, from which the
541
+ * package — never the path — is taken. An `exports` map with no `"."` is the bare name unexported, and
542
+ * Node's sentence for it names only the `package.json`.
543
+ */
544
+ function unexported(cause: unknown, message: string): Located {
545
+ const field = prop(cause, "specifier");
546
+ if (typeof field === "string" && field.length > 0 && !ABSOLUTE.test(field) && !field.startsWith(".")) {
547
+ return { kind: "package-subpath", package: packageOf(field), shown: field };
548
+ }
549
+ const main = /No "exports" main defined in (.+?)[\\/]package\.json/.exec(message)?.[1];
550
+ const entry = main === undefined ? undefined : packageAt(`${main}/package.json`);
551
+ if (entry !== undefined) return { kind: "package-subpath", package: entry.name, shown: entry.name };
552
+ const match =
553
+ /Package subpath '([^']+)' is not defined by "exports" in (.+?)[\\/]package\.json/.exec(message) ??
554
+ /"([^"]+)" is not exported under the conditions \[[^\]]*\] from package (.+?) \(see exports field/.exec(message);
555
+ const at = match?.[2] === undefined ? undefined : packageAt(`${match[2]}/package.json`);
556
+ if (match?.[1] === undefined || at === undefined) return { kind: "package-subpath" };
557
+ const subpath = match[1].replace(/^\.\/?/, "");
558
+ return { kind: "package-subpath", package: at.name, shown: subpath === "" ? at.name : `${at.name}/${subpath}` };
559
+ }
560
+
561
+ /**
562
+ * A path that did not resolve, from either runtime: inside an installed package, or in the adopter's tree.
563
+ *
564
+ * Inside a package, it is that package's own missing file when the import was written in the same package,
565
+ * and a path the package does not have when anything else asked for it. In the adopter's tree, it is shown
566
+ * relative to the project, or as the relative import it was written as, or not at all.
567
+ */
568
+ function atPath(
569
+ path: string,
570
+ referrer: string | undefined,
571
+ options: UnresolvedImportOptions,
572
+ written?: string,
573
+ ): Located {
574
+ const inPackage = packageAt(path);
575
+ if (inPackage !== undefined) {
576
+ const shown = inPackage.rest === "" ? inPackage.name : `${inPackage.name}/${inPackage.rest}`;
577
+ const own = referrer !== undefined && packageAt(referrer)?.name === inPackage.name;
578
+ if (own) return { kind: "package-file", package: inPackage.name, shown };
579
+ // Node resolves an import through `exports` and reports the file it landed on, so the path is the
580
+ // package's declared entry when the manifest names it — and the import was the bare name, not this.
581
+ const installed = options.entry?.(inPackage.name, `${inPackage.dir}/package.json`);
582
+ const declared = installed === undefined ? undefined : declaredEntry(installed.manifest);
583
+ if (typeof declared === "string" && isEntry(inPackage.rest, declared)) {
584
+ return { kind: "package-entry", package: inPackage.name, shown: inPackage.name };
585
+ }
586
+ // Or a deep import an `exports` key or pattern led there: shown as it was written, never as the file.
587
+ const written = installed === undefined ? undefined : exportedSubpath(installed.manifest, inPackage.rest);
588
+ if (installed !== undefined && written !== undefined) {
589
+ const subpath = written.subpath.replace(/^\.\/?/, "");
590
+ const as = subpath === "" ? inPackage.name : `${inPackage.name}/${subpath}`;
591
+ return { kind: exportedKind(written, installed), package: inPackage.name, shown: as };
592
+ }
593
+ return { kind: "package-subpath", package: inPackage.name, shown };
594
+ }
595
+ const { relativeTo } = options;
596
+ const shown = (relativeTo === undefined ? undefined : within(path, relativeTo)) ?? written;
597
+ return shown === undefined ? { kind: "local-file" } : { kind: "local-file", shown };
598
+ }
599
+
600
+ /** A package name as npm allows one: an optional `@scope/`, and never a store's dot-directory. */
601
+ const PACKAGE_NAME = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i;
602
+
603
+ /**
604
+ * The package a path sits in, by the last `node_modules` in it: its name, its directory, and the path
605
+ * inside it. The directory is for reading its manifest and never leaves this module.
606
+ */
607
+ function packageAt(path: string): { name: string; dir: string; rest: string } | undefined {
608
+ const segments = path.split(/[\\/]/);
609
+ const at = segments.lastIndexOf("node_modules");
610
+ if (at === -1) return undefined;
611
+ const tail = segments.slice(at + 1);
612
+ const name = packageOf(tail.join("/"));
613
+ if (!PACKAGE_NAME.test(name)) return undefined;
614
+ const depth = name.split("/").length;
615
+ return {
616
+ name,
617
+ dir: segments.slice(0, at + 1 + depth).join("/"),
618
+ rest: tail.slice(depth).join("/"),
619
+ };
620
+ }
621
+
622
+ /**
623
+ * Whether a path inside a package is the entry it declares, or that entry with the `.js` or `/index.js` a
624
+ * `main` is tried with. Exact, so a deep import beneath a directory `main` is never read as the entry.
625
+ */
626
+ function isEntry(rest: string, declared: string): boolean {
627
+ return rest === declared || rest === `${declared}.js` || rest === `${declared}/index.js`;
628
+ }
629
+
630
+ /** The importing package, unless it is the loader's own frame. */
631
+ function ownerOf(referrer: string | undefined, loader: string | undefined): string | undefined {
632
+ const name = referrer === undefined ? undefined : packageAt(referrer)?.name;
633
+ return name === loader ? undefined : name;
634
+ }
635
+
636
+ /** `path` as `./…` beneath `root`, or `undefined` when it is not beneath it. Separators are shown as `/`. */
637
+ function within(path: string, root: string): string | undefined {
638
+ const target = path.replaceAll("\\", "/");
639
+ const base = root.replaceAll("\\", "/").replace(/\/+$/, "");
640
+ if (target === base) return ".";
641
+ return target.startsWith(`${base}/`) ? `./${target.slice(base.length + 1)}` : undefined;
642
+ }
643
+
644
+ /** The directory a path is in. A root is its own parent, which is what ends a walk. */
645
+ function parentOf(path: string): string {
646
+ const trimmed = path.replace(/[\\/]+$/, "");
647
+ const at = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
648
+ if (at < 0) return path;
649
+ if (at === 0) return path.slice(0, 1);
650
+ const head = trimmed.slice(0, at);
651
+ return /^[A-Za-z]:$/.test(head) ? `${head}${trimmed[at]}` : head;
652
+ }
653
+
654
+ /** A relative import, followed from the directory it was written in. `..` past the root stays at the root. */
655
+ function joinPath(base: string, relative: string): string {
656
+ const separator = base.includes("/") ? "/" : "\\";
657
+ const parts = base.replace(/[\\/]+$/, "").split(/[\\/]/);
658
+ for (const part of relative.split(/[\\/]/)) {
659
+ if (part === "" || part === ".") continue;
660
+ if (part === "..") {
661
+ if (parts.length > 1) parts.pop();
662
+ continue;
663
+ }
664
+ parts.push(part);
665
+ }
666
+ return parts.join(separator) || separator;
667
+ }
668
+
669
+ /** The path a `file:` URL names, or the URL with its scheme taken off when it will not parse as one. */
670
+ function urlPath(specifier: string): string {
671
+ try {
672
+ const path = decodeURIComponent(new URL(specifier).pathname);
673
+ return /^\/[A-Za-z]:\//.test(path) ? path.slice(1) : path;
674
+ } catch {
675
+ return specifier.replace(/^file:\/*/, "/");
676
+ }
677
+ }
678
+
679
+ /**
680
+ * The import that did not resolve, as the runtime named it. Bun's field where there is one; otherwise the
681
+ * one sentence each loader states it in. The prose around it names the referrer's absolute path, which
682
+ * is why only the quoted part is taken.
683
+ */
684
+ function writtenSpecifier(cause: unknown, message: string): string | undefined {
685
+ const field = prop(cause, "specifier");
686
+ if (typeof field === "string" && field.length > 0) return field;
687
+ const match =
688
+ /Cannot find (?:package|module) ['"]([^'"]+)['"]/.exec(message) ??
689
+ /^Failed to load url (\S+) \(resolved id: /.exec(message) ??
690
+ /^No such built-in module: (\S+)$/.exec(message) ??
691
+ /^Package import specifier "([^"]+)" is not defined/.exec(message) ??
692
+ /^Directory import '([^']+)' is not supported/.exec(message);
693
+ return match?.[1];
694
+ }
695
+
696
+ /**
697
+ * The file the failing import was written in, as the runtime named it. **Module-private on purpose**: it
698
+ * is an absolute path, and only its package half ever leaves here.
699
+ *
700
+ * Node ends its sentence with the path, so it is taken to the end — a directory may contain a space.
701
+ * Vite's module runner quotes it, and its own `ERR_LOAD_URL` sentence names it before `. Does the file
702
+ * exist?` (Vite 8.3.0, measured).
703
+ */
704
+ function importerPath(cause: unknown): string | undefined {
705
+ const field = prop(cause, "referrer");
706
+ if (typeof field === "string" && field.length > 0) return field;
707
+ const message = causeMessage(cause) ?? "";
708
+ const match =
709
+ / imported from '(.+)'$/.exec(message) ??
710
+ / imported from (.+)$/.exec(message) ??
711
+ /^Failed to load url \S+ \(resolved id: [^)]*\) in (.+)\. Does the file exist\?$/.exec(message);
712
+ return match?.[1];
713
+ }
714
+
715
+ /** Every remedy that cannot be helped by an install says so, in these words. */
716
+ const NO_INSTALL = "installing dependencies will not help.";
717
+
718
+ /** The one sentence for a classified import. Built only from fields that cannot carry a path. */
719
+ function remedy(found: Located, owner: string | undefined): string {
720
+ const { kind, shown, package: pkg } = found;
721
+ switch (kind) {
722
+ case "package": {
723
+ const name = pkg ?? shown ?? "a package";
724
+ return owner === undefined
725
+ ? `"${name}" is not installed. Install the project's dependencies (bun install), or correct that import.`
726
+ : `${owner} imports "${name}", which is not installed. Install the project's dependencies (bun install). If it is still missing, ${owner} does not declare it — report that to its maintainer.`;
727
+ }
728
+ case "package-subpath":
729
+ if (shown === undefined || pkg === undefined) {
730
+ return `An import asks an installed package for a path it does not provide. Check the version you have, or correct that import — ${NO_INSTALL}`;
731
+ }
732
+ if (shown === pkg && (owner === undefined || owner === pkg)) {
733
+ return `The installed ${pkg} has no entry to import by its bare name. Import one of its paths, or check the version of ${pkg} you have — ${NO_INSTALL}`;
734
+ }
735
+ return owner === undefined || owner === pkg
736
+ ? `"${shown}" is not something the installed ${pkg} provides. Check the version of ${pkg} you have, or correct that import — ${NO_INSTALL}`
737
+ : `${owner} imports "${shown}", which the installed ${pkg} does not provide. Bring ${owner} and ${pkg} to matching versions, or report this to its maintainer — ${NO_INSTALL}`;
738
+ case "package-entry": {
739
+ const fix = `so it is unbuilt or its install is broken. Rebuild ${pkg}, or remove it and install it again.`;
740
+ // A deep import is named as it was written: the path its `exports` maps it to is never shown.
741
+ const deep = shown !== undefined && shown !== pkg;
742
+ const missing = deep
743
+ ? `exports "${shown}" from a file that is not there, ${fix}`
744
+ : `names an entry that is not there, ${fix}`;
745
+ if (owner === undefined || owner === pkg) return `The installed ${pkg} ${missing}`;
746
+ return deep
747
+ ? `${owner} imports "${shown}", and the installed ${pkg} exports it from a file that is not there, ${fix}`
748
+ : `${owner} imports "${pkg}", and the installed ${pkg} ${missing}`;
749
+ }
750
+ case "package-file":
751
+ return `${pkg} is missing its own file "${shown}". Report this to its maintainer — ${NO_INSTALL}`;
752
+ case "builtin":
753
+ if (shown === undefined)
754
+ return `An import names a module this runtime does not provide. Correct that import — ${NO_INSTALL}`;
755
+ return owner === undefined
756
+ ? `"${shown}" is not a module this runtime provides. Correct that import — ${NO_INSTALL}`
757
+ : `${owner} imports "${shown}", which this runtime does not provide. Run it on a runtime that has that module, or report this to its maintainer — ${NO_INSTALL}`;
758
+ case "local-file":
759
+ return shown === undefined
760
+ ? `An import names a file that is not there. Create that file or correct the import — ${NO_INSTALL}`
761
+ : `Nothing at "${shown}". Create that file or correct the import — ${NO_INSTALL}`;
762
+ case "imports-map":
763
+ return owner === undefined
764
+ ? `"${shown}" is not in the project's imports map. Add it to the imports field of package.json, or correct the import — ${NO_INSTALL}`
765
+ : `${owner} imports "${shown}", which its own imports map does not provide. Report this to its maintainer — ${NO_INSTALL}`;
766
+ case "undetermined":
767
+ return owner === undefined
768
+ ? `Nothing resolves "${shown}". Install the project's dependencies (bun install), or correct that import.`
769
+ : `${owner} imports "${shown}", and nothing resolves it. Install the project's dependencies (bun install), or bring ${owner} and ${pkg} to matching versions.`;
770
+ case "unnamed":
771
+ return "An import does not resolve. Check the imports, then install the project's dependencies (bun install).";
772
+ }
773
+ }
774
+
775
+ /** A package's file that would not load: the package it is in, and the one sentence for it. */
776
+ export interface PackageLoadFailure {
777
+ /** The installed package whose own file threw. Never the loader. */
778
+ owner: string;
779
+ /** The one sentence every surface puts in its `action`. Names the package, never a path. */
780
+ remedy: string;
781
+ }
782
+
783
+ /** What Node says of code whose syntax contradicts the module type its package declares. */
784
+ const MODULE_TYPE =
785
+ / is not defined in ES module scope|Cannot use import statement outside a module|Unexpected token 'export'|Cannot use 'import\.meta' outside a module/;
786
+
787
+ /**
788
+ * **A file inside an installed package would not load, so the package is named and never the config.**
789
+ *
790
+ * A `"type"` its code contradicts — `module` over `module.exports`, `commonjs` over `import` — throws from
791
+ * inside that package, on Node and through Vite's runner alike, as a `ReferenceError` or a `SyntaxError`.
792
+ * Unclassified, the first read as *the config threw while loading* and the second as *the config does not
793
+ * parse*: the adopter told to fix a file that is fine. A package's file that does not parse at all is the
794
+ * same fault in the same place, and is named the same way.
795
+ *
796
+ * **The file is the one that failed to load, and only that one.** Node heads a compile error with the file
797
+ * and line it could not compile, and Bun 1.4.2 hands over a `BuildMessage` with no stack at all, the file
798
+ * in its `position`. Otherwise the error's first stack frame must be a module's own top level, run as it
799
+ * is imported — `at file:///…:L:C`, or Vite's `at eval (…)`, with no function of its own. A first frame
800
+ * inside a function is the config calling into a package, and a later frame is a caller: so a package's
801
+ * deliberate throw, which the config provoked — json5's `parse` throws a `SyntaxError` on bad input — is
802
+ * never laid at the package's door. Only errors a module throws while it is loaded are read: a
803
+ * `SyntaxError` or a `BuildMessage`, and a `ReferenceError` for a CommonJS global in a module's scope.
804
+ *
805
+ * `undefined` when the cause is not one, or was thrown in the adopter's own file — that stays theirs.
806
+ */
807
+ export function packageLoadFailure(
808
+ wrapped: unknown,
809
+ options: Pick<UnresolvedImportOptions, "loader"> = {},
810
+ ): PackageLoadFailure | undefined {
811
+ const cause = rootCause(wrapped);
812
+ const typeMismatch = MODULE_TYPE.test(causeMessage(cause) ?? "");
813
+ const name = prop(cause, "name");
814
+ if (!typeMismatch && name !== "SyntaxError" && name !== "BuildMessage") return undefined;
815
+ const owner = ownerOf(failedToLoad(cause), options.loader);
816
+ if (owner === undefined) return undefined;
817
+ const check = `Check the version of ${owner} you have, or report this to its maintainer — ${NO_INSTALL}`;
818
+ return {
819
+ owner,
820
+ remedy: typeMismatch
821
+ ? `The installed ${owner} does not load as the module type its package.json declares. ${check}`
822
+ : `The installed ${owner} does not parse. ${check}`,
823
+ };
824
+ }
825
+
826
+ /**
827
+ * The file that failed as it loaded, or none: the file and line Node heads a compile error with, the file a
828
+ * stackless `BuildMessage` names in its `position` (Bun 1.4.2), or a first frame at a module's top level.
829
+ * Never a frame inside a function, which is code running after its module loaded. **Module-private**, like
830
+ * {@link importerPath}: it is an absolute path.
831
+ */
832
+ function failedToLoad(cause: unknown): string | undefined {
833
+ const stack = prop(cause, "stack");
834
+ if (typeof stack !== "string") {
835
+ const file = prop(prop(cause, "position"), "file");
836
+ return typeof file === "string" ? locationPath(file) : undefined;
837
+ }
838
+ const lines = stack.split("\n");
839
+ const head = /^(.+):\d+$/.exec(lines[0] ?? "")?.[1];
840
+ if (head !== undefined && (head.startsWith("file:") || ABSOLUTE.test(head))) return locationPath(head);
841
+ const frame = lines.find((line) => /^\s+at /.test(line));
842
+ if (frame === undefined) return undefined;
843
+ // A module's top level: no function at all, or the `eval` Vite's runner evaluates a module in.
844
+ const at =
845
+ /^\s+at eval \((.+?):\d+(?::\d+)?\)$/.exec(frame)?.[1] ?? /^\s+at ([^()]+?):\d+(?::\d+)?$/.exec(frame)?.[1];
846
+ return at === undefined ? undefined : locationPath(at);
847
+ }
848
+
849
+ /** A stack location as a path: a `file:` URL converted, an absolute path kept, anything else — `node:` — none. */
850
+ function locationPath(location: string): string | undefined {
851
+ if (location.startsWith("file:")) return urlPath(location);
852
+ return ABSOLUTE.test(location) ? location : undefined;
853
+ }