@objectstack/types 17.1.0 → 17.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # @objectstack/types
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 46d34ab: `createHostImporter`: resolve the undeclared fallback from the CALLER, not from `@objectstack/types`
8
+
9
+ The helper's documented contract said the undeclared case "falls back to the importing
10
+ package's own resolution". It did not. The fallback was a bare `import()` written inside
11
+ `@objectstack/types`, and Node ESM resolves a bare specifier against the module that
12
+ CONTAINS the call — so it resolved from `@objectstack/types`, which under a pnpm-isolated
13
+ layout can see only its own single dependency, `@objectstack/spec`. Measured from an app
14
+ declaring nothing: `@objectstack/plugin-auth`, `@objectstack/plugin-audit` and `chalk` all
15
+ resolve from `packages/cli` and all failed through the helper. Under a hoisted npm/yarn
16
+ layout the same fallback usually does find the caller's dependencies, so the claim was
17
+ green in some installs and absent in others.
18
+
19
+ `createHostImporter(hostRoot, options)` now takes the caller's resolution base as
20
+ `options.fallbackImport` — the caller's own `import()`, written in the calling module:
21
+
22
+ ```ts
23
+ createHostImporter(hostRoot, { fallbackImport: (s) => import(s) })
24
+ ```
25
+
26
+ **minor, not patch, and not major.** New exported API (`HostImporterOptions`,
27
+ `FallbackImport`, a second parameter) makes it additive rather than a fix-only patch. It
28
+ is not a breaking change because the parameter is optional and omitting it keeps the
29
+ previous resolution base exactly — an existing caller compiles and behaves as before. The
30
+ `undeclared` failure text now names that retained default when a caller has not passed a
31
+ base, so the gap reports itself instead of being rediscovered by measurement.
32
+
33
+ `@objectstack/verify` (patch) passes its own base from `bootStack`. Measured: this changes
34
+ nothing for `@objectstack/organizations`, the only specifier it routes through the helper —
35
+ that package is cloud-private and resolves from nowhere in the framework workspace. It is
36
+ what stops the next app-supplied package added to that path from silently missing
37
+ `packages/verify`'s own dependencies.
38
+
39
+ A string `parentURL` / `import.meta.url` base was measured on Node v22.22.2 and rejected in
40
+ both spellings: `import.meta.resolve`'s parent argument is silently ignored without
41
+ `--experimental-import-meta-resolve` (a change that would have compiled, run, and pinned
42
+ green while ignoring the base), and `createRequire(parentURL)` is CJS resolution, which
43
+ honours `NODE_PATH` — the hole the declaration gate exists to close, re-opened on the
44
+ fallback path.
45
+
46
+ ### Patch Changes
47
+
48
+ - Updated dependencies [6936d07]
49
+ - Updated dependencies [59eb04d]
50
+ - Updated dependencies [9f05b7d]
51
+ - Updated dependencies [7d2d112]
52
+ - Updated dependencies [5fa0d72]
53
+ - Updated dependencies [02b3b07]
54
+ - Updated dependencies [914c413]
55
+ - Updated dependencies [55809a0]
56
+ - Updated dependencies [52db1d1]
57
+ - Updated dependencies [5649efb]
58
+ - Updated dependencies [2306a76]
59
+ - Updated dependencies [e5ea701]
60
+ - Updated dependencies [a40dcc1]
61
+ - Updated dependencies [def0d3e]
62
+ - Updated dependencies [8d0bb79]
63
+ - Updated dependencies [5acb58d]
64
+ - Updated dependencies [2e3cf95]
65
+ - Updated dependencies [4c93387]
66
+ - Updated dependencies [a037f7c]
67
+ - Updated dependencies [3ee8ddf]
68
+ - Updated dependencies [16cef97]
69
+ - Updated dependencies [a79bd35]
70
+ - Updated dependencies [6ceaa4b]
71
+ - Updated dependencies [15ea214]
72
+ - Updated dependencies [de19489]
73
+ - Updated dependencies [c684d00]
74
+ - Updated dependencies [923c424]
75
+ - Updated dependencies [1ec36b7]
76
+ - Updated dependencies [5f2e54c]
77
+ - Updated dependencies [189373b]
78
+ - Updated dependencies [35ad101]
79
+ - Updated dependencies [ceb33a9]
80
+ - Updated dependencies [73d9795]
81
+ - Updated dependencies [8012960]
82
+ - Updated dependencies [f34f56b]
83
+ - Updated dependencies [f399618]
84
+ - Updated dependencies [75e9301]
85
+ - Updated dependencies [2810695]
86
+ - @objectstack/spec@17.2.0
87
+
3
88
  ## 17.1.0
4
89
 
5
90
  ### Minor Changes
package/dist/node.d.mts CHANGED
@@ -7,6 +7,32 @@
7
7
  * did.
8
8
  */
9
9
  type HostImporter = (pkg: string) => Promise<any>;
10
+ /**
11
+ * The importing package's OWN dynamic import — write it literally, in the
12
+ * calling module:
13
+ *
14
+ * createHostImporter(hostRoot, { fallbackImport: (s) => import(s) })
15
+ *
16
+ * `any` for the same reason {@link HostImporter} uses it: the module namespace
17
+ * belongs to a package this repo does not compile against.
18
+ */
19
+ type FallbackImport = (specifier: string) => Promise<any>;
20
+ /** Options for {@link createHostImporter}. */
21
+ interface HostImporterOptions {
22
+ /**
23
+ * The resolution base for everything the host app does NOT declare — supplied
24
+ * as the caller's own `import()` rather than as a URL string, because a
25
+ * string base was MEASURED to be unimplementable without a regression. See
26
+ * {@link createHostImporter}'s "why a function" note for both measurements.
27
+ *
28
+ * Omitted ⇒ the fallback resolves from `@objectstack/types`, which sees only
29
+ * `@objectstack/types`'s own dependencies. That default is retained so an
30
+ * out-of-tree caller cannot be broken by this parameter's arrival, and the
31
+ * `undeclared` failure text names it explicitly so the gap reports itself
32
+ * instead of being rediscovered.
33
+ */
34
+ fallbackImport?: FallbackImport;
35
+ }
10
36
  /**
11
37
  * A `require` anchored at the **host app's** `package.json` — i.e. the project
12
38
  * `objectstack serve` was invoked in, or the app `bootStack` is verifying, whose
@@ -123,20 +149,64 @@ declare function hostImportFailureKind(err: unknown): HostImportFailureKind | un
123
149
  * the install is broken. It is not retried bare: falling back there would
124
150
  * reintroduce exactly the "some other package happens to supply it" accident
125
151
  * this gate closes, and would report an install problem as an absence.
126
- * 3. Undeclared falls back to the importing package's own resolution, which is
127
- * what keeps every framework-owned load working (`serve`'s plugin-auth /
128
- * service-i18n path, `bootStack`'s service plugins). Bare `import()` is ESM,
129
- * and ESM does not honour `NODE_PATH`, so the fallback cannot re-open the
130
- * hole either. Only when that fails as module-not-found does the undeclared
131
- * error surface; a package that RESOLVES and then throws while evaluating is
132
- * a genuine crash and propagates untouched, as before.
152
+ * 3. Undeclared falls back to the CALLER's own resolution, which is what keeps
153
+ * every framework-owned load working (`serve`'s plugin-auth / service-i18n
154
+ * path, `bootStack`'s service plugins). Bare `import()` is ESM, and ESM does
155
+ * not honour `NODE_PATH`, so the fallback cannot re-open the hole either.
156
+ * Only when that fails as module-not-found does the undeclared error
157
+ * surface; a package that RESOLVES and then throws while evaluating is a
158
+ * genuine crash and propagates untouched, as before.
159
+ *
160
+ * ── The caller supplies that base, and why it is a FUNCTION (#10943) ─────────
161
+ *
162
+ * Step 3 said "the importing package's own resolution" long before anything
163
+ * made it true. The fallback was a bare `import()` written HERE, and ESM
164
+ * resolves a bare specifier against the module containing the call — so it
165
+ * resolved from `@objectstack/types`, which under a pnpm-isolated layout can
166
+ * see only `@objectstack/types`'s own dependencies. Measured on `main` from an
167
+ * app declaring nothing, `@objectstack/plugin-auth`, `@objectstack/plugin-audit`
168
+ * and `chalk` all resolve from `packages/cli` and all failed through this
169
+ * helper; `@objectstack/spec` — the one dependency this package declares — was
170
+ * the only name that came back OK, which is the whole pattern. Under a hoisted
171
+ * npm/yarn layout the same fallback usually DOES find the caller's
172
+ * dependencies, so the claim was green in some installs and absent in others:
173
+ * the layout-dependence class cloud#1013 and #10645 exist to close, one level
174
+ * up. A declared contract the implementation does not keep is the thing this
175
+ * repo fixes at the producer (Prime Directive #12), so the mechanism moved
176
+ * rather than the sentence.
177
+ *
178
+ * The base arrives as the caller's own `import()` and NOT as a `parentURL` /
179
+ * `import.meta.url` string. Both string spellings were measured on Node
180
+ * v22.22.2 and both are wrong:
181
+ *
182
+ * - `import.meta.resolve(specifier, parentURL)` — the parent argument is
183
+ * SILENTLY IGNORED without `--experimental-import-meta-resolve`. Measured:
184
+ * resolving `@objectstack/plugin-auth` against a `packages/types` parent
185
+ * returned `packages/cli/node_modules/...`, i.e. the caller's own answer,
186
+ * byte-identical to passing no parent at all. It would have compiled, run,
187
+ * and pinned green while ignoring the base — a phantom fix of exactly the
188
+ * kind this card is about.
189
+ * - `createRequire(parentURL).resolve(specifier)` — CJS resolution, which
190
+ * honours `NODE_PATH`. Measured against a store reachable only through
191
+ * `NODE_PATH`: the CJS resolve found it (with and without the `paths`
192
+ * option, since GLOBAL_FOLDERS are always appended) while the ESM bare
193
+ * `import()` did not. That is #4719's hole re-opened on the fallback path,
194
+ * and it would have falsified the "ESM does not honour NODE_PATH" sentence
195
+ * three lines above.
196
+ *
197
+ * A function written in the calling module is the only spelling that uses
198
+ * Node's real ESM resolver anchored where the caller actually lives: no flag,
199
+ * no `NODE_PATH`, no second resolution algorithm to drift from the first.
133
200
  *
134
201
  * @param hostRoot Directory holding the host app's `package.json` (default: the
135
202
  * process CWD, which is where the CLI reads `objectstack.config.ts` from too).
136
203
  * Note this used to take a pre-built `NodeRequire`; it needs the ROOT now,
137
204
  * because a `NodeRequire` cannot be asked where it was anchored and the manifest
138
205
  * has to be read from there.
206
+ * @param options {@link HostImporterOptions.fallbackImport} carries the caller's
207
+ * resolution base. Omitting it keeps the pre-#10943 behaviour (this package's
208
+ * own resolution) so no out-of-tree caller changes under its feet.
139
209
  */
140
- declare function createHostImporter(hostRoot?: string): HostImporter;
210
+ declare function createHostImporter(hostRoot?: string, options?: HostImporterOptions): HostImporter;
141
211
 
142
- export { HOST_DECLARATION_FIELDS, HOST_IMPORT_FAILURE_KIND, type HostDeclaration, type HostDeclarationField, type HostImportFailureKind, type HostImporter, createHostImporter, createHostRequire, hostImportFailureKind, isDeclaredByHost, packageNameFromSpecifier, readHostDeclaration };
212
+ export { type FallbackImport, HOST_DECLARATION_FIELDS, HOST_IMPORT_FAILURE_KIND, type HostDeclaration, type HostDeclarationField, type HostImportFailureKind, type HostImporter, type HostImporterOptions, createHostImporter, createHostRequire, hostImportFailureKind, isDeclaredByHost, packageNameFromSpecifier, readHostDeclaration };
package/dist/node.d.ts CHANGED
@@ -7,6 +7,32 @@
7
7
  * did.
8
8
  */
9
9
  type HostImporter = (pkg: string) => Promise<any>;
10
+ /**
11
+ * The importing package's OWN dynamic import — write it literally, in the
12
+ * calling module:
13
+ *
14
+ * createHostImporter(hostRoot, { fallbackImport: (s) => import(s) })
15
+ *
16
+ * `any` for the same reason {@link HostImporter} uses it: the module namespace
17
+ * belongs to a package this repo does not compile against.
18
+ */
19
+ type FallbackImport = (specifier: string) => Promise<any>;
20
+ /** Options for {@link createHostImporter}. */
21
+ interface HostImporterOptions {
22
+ /**
23
+ * The resolution base for everything the host app does NOT declare — supplied
24
+ * as the caller's own `import()` rather than as a URL string, because a
25
+ * string base was MEASURED to be unimplementable without a regression. See
26
+ * {@link createHostImporter}'s "why a function" note for both measurements.
27
+ *
28
+ * Omitted ⇒ the fallback resolves from `@objectstack/types`, which sees only
29
+ * `@objectstack/types`'s own dependencies. That default is retained so an
30
+ * out-of-tree caller cannot be broken by this parameter's arrival, and the
31
+ * `undeclared` failure text names it explicitly so the gap reports itself
32
+ * instead of being rediscovered.
33
+ */
34
+ fallbackImport?: FallbackImport;
35
+ }
10
36
  /**
11
37
  * A `require` anchored at the **host app's** `package.json` — i.e. the project
12
38
  * `objectstack serve` was invoked in, or the app `bootStack` is verifying, whose
@@ -123,20 +149,64 @@ declare function hostImportFailureKind(err: unknown): HostImportFailureKind | un
123
149
  * the install is broken. It is not retried bare: falling back there would
124
150
  * reintroduce exactly the "some other package happens to supply it" accident
125
151
  * this gate closes, and would report an install problem as an absence.
126
- * 3. Undeclared falls back to the importing package's own resolution, which is
127
- * what keeps every framework-owned load working (`serve`'s plugin-auth /
128
- * service-i18n path, `bootStack`'s service plugins). Bare `import()` is ESM,
129
- * and ESM does not honour `NODE_PATH`, so the fallback cannot re-open the
130
- * hole either. Only when that fails as module-not-found does the undeclared
131
- * error surface; a package that RESOLVES and then throws while evaluating is
132
- * a genuine crash and propagates untouched, as before.
152
+ * 3. Undeclared falls back to the CALLER's own resolution, which is what keeps
153
+ * every framework-owned load working (`serve`'s plugin-auth / service-i18n
154
+ * path, `bootStack`'s service plugins). Bare `import()` is ESM, and ESM does
155
+ * not honour `NODE_PATH`, so the fallback cannot re-open the hole either.
156
+ * Only when that fails as module-not-found does the undeclared error
157
+ * surface; a package that RESOLVES and then throws while evaluating is a
158
+ * genuine crash and propagates untouched, as before.
159
+ *
160
+ * ── The caller supplies that base, and why it is a FUNCTION (#10943) ─────────
161
+ *
162
+ * Step 3 said "the importing package's own resolution" long before anything
163
+ * made it true. The fallback was a bare `import()` written HERE, and ESM
164
+ * resolves a bare specifier against the module containing the call — so it
165
+ * resolved from `@objectstack/types`, which under a pnpm-isolated layout can
166
+ * see only `@objectstack/types`'s own dependencies. Measured on `main` from an
167
+ * app declaring nothing, `@objectstack/plugin-auth`, `@objectstack/plugin-audit`
168
+ * and `chalk` all resolve from `packages/cli` and all failed through this
169
+ * helper; `@objectstack/spec` — the one dependency this package declares — was
170
+ * the only name that came back OK, which is the whole pattern. Under a hoisted
171
+ * npm/yarn layout the same fallback usually DOES find the caller's
172
+ * dependencies, so the claim was green in some installs and absent in others:
173
+ * the layout-dependence class cloud#1013 and #10645 exist to close, one level
174
+ * up. A declared contract the implementation does not keep is the thing this
175
+ * repo fixes at the producer (Prime Directive #12), so the mechanism moved
176
+ * rather than the sentence.
177
+ *
178
+ * The base arrives as the caller's own `import()` and NOT as a `parentURL` /
179
+ * `import.meta.url` string. Both string spellings were measured on Node
180
+ * v22.22.2 and both are wrong:
181
+ *
182
+ * - `import.meta.resolve(specifier, parentURL)` — the parent argument is
183
+ * SILENTLY IGNORED without `--experimental-import-meta-resolve`. Measured:
184
+ * resolving `@objectstack/plugin-auth` against a `packages/types` parent
185
+ * returned `packages/cli/node_modules/...`, i.e. the caller's own answer,
186
+ * byte-identical to passing no parent at all. It would have compiled, run,
187
+ * and pinned green while ignoring the base — a phantom fix of exactly the
188
+ * kind this card is about.
189
+ * - `createRequire(parentURL).resolve(specifier)` — CJS resolution, which
190
+ * honours `NODE_PATH`. Measured against a store reachable only through
191
+ * `NODE_PATH`: the CJS resolve found it (with and without the `paths`
192
+ * option, since GLOBAL_FOLDERS are always appended) while the ESM bare
193
+ * `import()` did not. That is #4719's hole re-opened on the fallback path,
194
+ * and it would have falsified the "ESM does not honour NODE_PATH" sentence
195
+ * three lines above.
196
+ *
197
+ * A function written in the calling module is the only spelling that uses
198
+ * Node's real ESM resolver anchored where the caller actually lives: no flag,
199
+ * no `NODE_PATH`, no second resolution algorithm to drift from the first.
133
200
  *
134
201
  * @param hostRoot Directory holding the host app's `package.json` (default: the
135
202
  * process CWD, which is where the CLI reads `objectstack.config.ts` from too).
136
203
  * Note this used to take a pre-built `NodeRequire`; it needs the ROOT now,
137
204
  * because a `NodeRequire` cannot be asked where it was anchored and the manifest
138
205
  * has to be read from there.
206
+ * @param options {@link HostImporterOptions.fallbackImport} carries the caller's
207
+ * resolution base. Omitting it keeps the pre-#10943 behaviour (this package's
208
+ * own resolution) so no out-of-tree caller changes under its feet.
139
209
  */
140
- declare function createHostImporter(hostRoot?: string): HostImporter;
210
+ declare function createHostImporter(hostRoot?: string, options?: HostImporterOptions): HostImporter;
141
211
 
142
- export { HOST_DECLARATION_FIELDS, HOST_IMPORT_FAILURE_KIND, type HostDeclaration, type HostDeclarationField, type HostImportFailureKind, type HostImporter, createHostImporter, createHostRequire, hostImportFailureKind, isDeclaredByHost, packageNameFromSpecifier, readHostDeclaration };
212
+ export { type FallbackImport, HOST_DECLARATION_FIELDS, HOST_IMPORT_FAILURE_KIND, type HostDeclaration, type HostDeclarationField, type HostImportFailureKind, type HostImporter, type HostImporterOptions, createHostImporter, createHostRequire, hostImportFailureKind, isDeclaredByHost, packageNameFromSpecifier, readHostDeclaration };
package/dist/node.js CHANGED
@@ -97,9 +97,10 @@ function hostImportError(kind, message, cause) {
97
97
  [HOST_IMPORT_FAILURE_KIND]: kind
98
98
  });
99
99
  }
100
- function undeclaredMessage(declaration, cause) {
100
+ function undeclaredMessage(declaration, cause, callerBaseSupplied) {
101
101
  const { packageName, hostRoot, manifestMissing } = declaration;
102
102
  const detail = cause instanceof Error ? cause.message : String(cause);
103
+ const baseNote = callerBaseSupplied ? "" : "\n (the caller did not pass `fallbackImport`, so that fallback resolved from\n @objectstack/types, which can see only its own dependencies \u2014 a caller that\n needs its own resolution passes `{ fallbackImport: (s) => import(s) }`, #10943)";
103
104
  return `Cannot find package '${packageName}': the host app does not declare it.
104
105
  host app: ${hostRoot}
105
106
  ` + (manifestMissing ? " no readable package.json was found there \u2014 nothing can be declared\n" : ` checked: ${HOST_DECLARATION_FIELDS.join(", ")}
@@ -112,7 +113,7 @@ function undeclaredMessage(declaration, cause) {
112
113
  at in every pnpm bin shim \u2014 used to resolve here regardless of the app's
113
114
  package.json, so the same app booted or refused depending on how the
114
115
  process was launched. The declaration is the contract.
115
- (fallback resolution also failed: ${detail})`;
116
+ (fallback resolution also failed: ${detail})${baseNote}`;
116
117
  }
117
118
  function unresolvableMessage(declaration, cause) {
118
119
  const { packageName, hostRoot, field, specifier } = declaration;
@@ -128,8 +129,13 @@ function unresolvableMessage(declaration, cause) {
128
129
  \u2022 it IS installed but its "main"/"exports" points at a dist that was never built
129
130
  (resolver: ${detail})`;
130
131
  }
131
- function createHostImporter(hostRoot = process.cwd()) {
132
+ function createHostImporter(hostRoot = process.cwd(), options = {}) {
132
133
  const hostRequire = createHostRequire(hostRoot);
134
+ const { fallbackImport } = options;
135
+ const importAsCaller = fallbackImport ?? ((specifier) => import(
136
+ /* webpackIgnore: true */
137
+ specifier
138
+ ));
133
139
  return async (pkg) => {
134
140
  if (packageNameFromSpecifier(pkg) === void 0) {
135
141
  return import(
@@ -152,13 +158,14 @@ function createHostImporter(hostRoot = process.cwd()) {
152
158
  return import((0, import_node_url.pathToFileURL)(resolved).href);
153
159
  }
154
160
  try {
155
- return await import(
156
- /* webpackIgnore: true */
157
- pkg
158
- );
161
+ return await importAsCaller(pkg);
159
162
  } catch (cause) {
160
163
  if (!isModuleNotFoundError(cause)) throw cause;
161
- throw hostImportError("undeclared", undeclaredMessage(declaration, cause), cause);
164
+ throw hostImportError(
165
+ "undeclared",
166
+ undeclaredMessage(declaration, cause, fallbackImport !== void 0),
167
+ cause
168
+ );
162
169
  }
163
170
  };
164
171
  }
package/dist/node.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/node.ts","../src/module-not-found.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/types/node` — the **node-only** slice of the shared utilities.\n *\n * WHY A SUBPATH AND NOT THE ROOT EXPORT. `@objectstack/types` is a dependency of\n * `@objectstack/hono`, whose whole reason to exist is \"edge-compatible REST API\n * server for Cloudflare Workers, Deno, Bun, and Node\" — and of the plugin/service\n * layer a `LiteKernel` boots on Workers. The root entry (`src/index.ts`) reaches\n * **zero** `node:` builtins today, and that is a property those consumers depend\n * on: a Workers bundle that pulls in `node:module` fails to build (or dies at\n * first call) even when nothing ever invokes it. Everything here needs\n * `node:module` / `node:url` by definition — it exists to drive Node's own\n * resolver — so it lives behind its own entry point instead.\n *\n * The isolation is structural, not conventional: `tsup` builds `src/index.ts` and\n * `src/node.ts` as separate entries with `splitting: false`, so the root bundle\n * contains no reference to this file, and `node-isolation.test.ts` fails the\n * build if anything reachable from the root ever imports a `node:` builtin. Same\n * arrangement `@objectstack/metadata` already ships for `./node`.\n *\n * ── What lives here ──────────────────────────────────────────────────────────\n *\n * Resolving optional packages from the **host app**, not from the framework\n * package doing the importing.\n *\n * Node ESM resolves a bare `import('pkg')` against the **importer's own\n * realpath**. Framework packages (the CLI, `@objectstack/verify`,\n * `@objectstack/dogfood`) are reached through `link:`/workspace dependencies, so\n * their realpath is inside the *framework* workspace — a bare import from any of\n * them can only ever see packages installed in the framework's own\n * `node_modules`. Every package that lives OUTSIDE that workspace and is supplied\n * by the app being served, verified or tested — a cloud-private package such as\n * `@objectstack/organizations` or `@objectstack/service-ai-studio`, or anything a\n * customer installs into their own project — is therefore invisible to a bare\n * import, no matter what the host app declares in its `package.json`\n * (cloud#1013: `objectstack serve` could never load the enterprise multi-org\n * runtime, so every self-hosted walled-posture deployment hit the ADR-0093 D5\n * fail-fast and exited 1; framework#4700: `bootStack({ multiTenant: true })` told\n * apps to install a package they had already installed, and the dogfood\n * multi-org probes were constant-false).\n *\n * The fix is to resolve from the host app's root and import the resolved\n * absolute path. The importing package's own resolution stays as the fallback,\n * for the framework-owned packages it depends on and the host does not declare.\n *\n * Resolution failure is the ONLY thing that falls back. A package the host\n * resolves but that throws while it evaluates is a genuine crash and propagates\n * unchanged: re-importing it bare would replace the real cause with a\n * `MODULE_NOT_FOUND`, which every caller here classifies as \"not installed\" —\n * turning a broken package into a silent skip (or, on the organizations path,\n * into a fatal message telling the operator to install what is already there).\n *\n * ── #4719: the host's DECLARATION gates the lookup, not its resolvability ────\n *\n * \"Resolve from the host app\" was implemented as a CJS `createRequire` anchored\n * at the host's `package.json`, and **CJS resolution honours `NODE_PATH`**\n * (`Module.globalPaths`). The first thing a pnpm-generated bin shim does is\n *\n * export NODE_PATH=\"<workspace>/node_modules/.pnpm/node_modules\"\n *\n * and every `serve` / `dev` child process inherits it. Everything any package in\n * the workspace transitively depends on lives in that hoisted store, so\n * `hostRequire.resolve(pkg)` succeeded for packages the host app had never\n * declared — the answer depended on HOW THE PROCESS WAS LAUNCHED, not on the\n * app. Measured on cloud's `apps/objectos-ee`, which did not declare\n * `@objectstack/organizations`: `pnpm start` (through the shim) booted with the\n * organizations plugin mounted and ADR-0093 D5 silent, while\n * `node node_modules/@objectstack/cli/bin/run.js serve` (no shim, no NODE_PATH)\n * hit the D5 fail-fast and exited 1. Same app, same `package.json`, same\n * posture. D5's own message told operators to \"declare it in the app's\n * package.json\" — the one thing the CLI never checked.\n *\n * So the host lookup is now gated on the host's **declaration**: a package name\n * is looked up in the host's `node_modules` only when it appears in the host\n * `package.json` (see {@link HOST_DECLARATION_FIELDS}). Reachability through a\n * hoisted store or `NODE_PATH` is deliberately not accepted — it is precisely\n * the accident that made the contract unenforced. This is the \"declared =\n * enforced\" shape the rest of the repo uses (Prime Directive #10): the\n * declaration is a deliberate authoring act, machine-checkable at the moment of\n * boot, and independent of launcher, package manager and hoist layout.\n *\n * The two failures it separates were, until now, one indistinguishable\n * `MODULE_NOT_FOUND`, with opposite remedies:\n *\n * - **undeclared** — the app never asked for this package. Remedy: declare it\n * in the app's `package.json` and install.\n * - **declared but unresolvable** — the app asked for it and the install is\n * broken/pruned/unbuilt. Remedy: fix the install. Re-reading the\n * `package.json` is wasted effort; the declaration is right there.\n *\n * {@link hostImportFailureKind} exposes that classification to callers so their\n * fail-fast text can say which one it is (`packages/cli` ADR-0093 D5,\n * `packages/verify` `bootStack`, `packages/qa/dogfood`'s enterprise probe).\n */\n\nimport { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { isModuleNotFoundError } from './module-not-found.js';\n\n/**\n * Imports a package as the host app would see it.\n *\n * `any` is the module namespace of a package this repo does not compile against\n * (it is not a dependency of the importing package at all) — every call site\n * reads an export off it dynamically, exactly as the bare `import()` it replaces\n * did.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type HostImporter = (pkg: string) => Promise<any>;\n\n/**\n * A `require` anchored at the **host app's** `package.json` — i.e. the project\n * `objectstack serve` was invoked in, or the app `bootStack` is verifying, whose\n * `node_modules` carries the packages it declares.\n *\n * @param hostRoot Directory holding the host app's `package.json` (default: the\n * process CWD, which is where the CLI reads `objectstack.config.ts` from too).\n */\nexport function createHostRequire(hostRoot: string = process.cwd()): NodeRequire {\n return createRequire(join(hostRoot, 'package.json'));\n}\n\n/**\n * The `package.json` fields whose KEYS count as a host-app declaration (#4719).\n *\n * All four are deliberate authoring acts in the app's own manifest that name the\n * package, which is the signal this gate is built on — not \"is it reachable\".\n * Why each is in:\n *\n * - `dependencies` — the obvious one: the app runs with it.\n * - `devDependencies` — the app being served / verified / dogfooded IS the\n * project, not a library someone else consumes, so its dev deps are installed\n * in exactly the environment this resolver runs in.\n * - `optionalDependencies` — npm/pnpm install them and tolerate an install\n * failure. \"Installed ⇒ declared\" holds; and if it did NOT install, the\n * declared-but-unresolvable branch says so precisely instead of pretending the\n * app never asked.\n * - `peerDependencies` — an app is nobody's peer, so this is an unusual place to\n * put an enterprise add-on; but it still NAMES the package on purpose, and\n * `packages/cli`'s own edition gate (`serve`'s AI-service opt-in, #1597) has\n * read all four since it was written. Accepting three here and four there\n * would fork \"declared\" into two dialects for one question — the shape Prime\n * Directive #12 exists to prevent. That gate now delegates to this list, so\n * there is one owner and one answer.\n *\n * `bundleDependencies` is absent on purpose: it is an array of names that must\n * ALSO appear in `dependencies`, so it can never be the only declaration.\n */\nexport const HOST_DECLARATION_FIELDS = [\n 'dependencies',\n 'devDependencies',\n 'optionalDependencies',\n 'peerDependencies',\n] as const;\n\nexport type HostDeclarationField = (typeof HOST_DECLARATION_FIELDS)[number];\n\n/** What the host app's `package.json` says about one package name. */\nexport interface HostDeclaration {\n /** Bare package name the specifier belongs to (subpath stripped). */\n packageName: string;\n /** Directory whose `package.json` was consulted. */\n hostRoot: string;\n /** True when {@link packageName} is a key of one of {@link HOST_DECLARATION_FIELDS}. */\n declared: boolean;\n /** Which field carried it (first match, in {@link HOST_DECLARATION_FIELDS} order). */\n field?: HostDeclarationField;\n /** The version range AS WRITTEN — `^1.2.3`, `workspace:*`, `npm:@acme/x@1`, `link:../x`. */\n specifier?: string;\n /** True when `hostRoot` has no readable / parseable `package.json` at all. */\n manifestMissing?: boolean;\n}\n\n/**\n * The package a bare specifier belongs to, or `undefined` when the specifier is\n * not a bare package name at all (a relative/absolute path, a `file:`/`data:`\n * URL, or a `node:`-prefixed builtin). Those bypass the declaration gate: they\n * are not things a `package.json` can declare.\n *\n * Subpaths are stripped, so `@objectstack/platform-objects/plugin` is declared\n * by `\"@objectstack/platform-objects\"`, which is the only key that can exist.\n * Scoped names keep both segments.\n *\n * Alias dependencies need no special case, and that is the point: with\n * `\"foo\": \"npm:bar@1\"` the importable specifier is `foo` and the manifest key is\n * `foo`, so keying on the KEY (never the value) is exactly right — `import('bar')`\n * correctly reads as undeclared unless `bar` is itself a key. Same for\n * `workspace:` / `link:` / `file:` specifiers: the key is the name, the value is\n * the package manager's business.\n */\nexport function packageNameFromSpecifier(specifier: string): string | undefined {\n if (!specifier || specifier.startsWith('.') || specifier.startsWith('/')) return undefined;\n // A URL-ish or protocol-prefixed specifier (`node:fs`, `file:///…`, `data:…`).\n if (/^[a-z][a-z0-9+.-]*:/i.test(specifier)) return undefined;\n const segments = specifier.split('/');\n if (specifier.startsWith('@')) {\n if (segments.length < 2 || !segments[0] || !segments[1]) return undefined;\n return `${segments[0]}/${segments[1]}`;\n }\n return segments[0] || undefined;\n}\n\n/**\n * Read what the host app's `package.json` declares about `specifier`.\n *\n * Deliberately a plain manifest READ, never a resolution attempt: resolvability\n * is the property #4719 proved unreliable (it moved with `NODE_PATH` and the\n * hoist layout), while the manifest is the same fact in every launcher.\n */\nexport function readHostDeclaration(\n specifier: string,\n hostRoot: string = process.cwd(),\n): HostDeclaration {\n const packageName = packageNameFromSpecifier(specifier) ?? specifier;\n const base: HostDeclaration = { packageName, hostRoot, declared: false };\n\n let manifest: Record<string, unknown>;\n try {\n manifest = JSON.parse(readFileSync(join(hostRoot, 'package.json'), 'utf8')) as Record<\n string,\n unknown\n >;\n } catch {\n // No manifest ⇒ nothing is declared. Recorded rather than swallowed so the\n // failure text can say \"there is no package.json here\" instead of the\n // misleading \"you did not declare it\".\n return { ...base, manifestMissing: true };\n }\n\n for (const field of HOST_DECLARATION_FIELDS) {\n const entries = manifest[field];\n if (!entries || typeof entries !== 'object') continue;\n const specifierValue = (entries as Record<string, unknown>)[packageName];\n if (specifierValue === undefined) continue;\n return { ...base, declared: true, field, specifier: String(specifierValue) };\n }\n return base;\n}\n\n/** Convenience predicate over {@link readHostDeclaration}. */\nexport function isDeclaredByHost(specifier: string, hostRoot?: string): boolean {\n return readHostDeclaration(specifier, hostRoot).declared;\n}\n\n/**\n * Why a {@link HostImporter} could not produce a module.\n *\n * - `undeclared` — the host app's `package.json` never names the package, and\n * the importing framework package cannot supply it either. Remedy: DECLARE it\n * in the app and install.\n * - `declared-unresolvable` — the app declares it and it still would not\n * resolve. Remedy: fix the INSTALL. Re-reading the manifest is wasted effort.\n *\n * An evaluation crash is neither: it propagates untouched and carries no kind.\n */\nexport type HostImportFailureKind = 'undeclared' | 'declared-unresolvable';\n\n/**\n * Property carrying {@link HostImportFailureKind} on a thrown error.\n *\n * A string property, read by {@link hostImportFailureKind} — never `instanceof`.\n * `serve` loads plugins through this importer, so CLI and package can hold\n * different module instances of anything class-shaped; the #4818 comment in\n * `serve.ts` names that trap explicitly.\n */\nexport const HOST_IMPORT_FAILURE_KIND = 'objectstackHostImportFailureKind';\n\n/** The classification on an error thrown by a {@link HostImporter}, if any. */\nexport function hostImportFailureKind(err: unknown): HostImportFailureKind | undefined {\n const kind = (err as Record<string, unknown> | null | undefined)?.[HOST_IMPORT_FAILURE_KIND];\n return kind === 'undeclared' || kind === 'declared-unresolvable' ? kind : undefined;\n}\n\nfunction hostImportError(\n kind: HostImportFailureKind,\n message: string,\n cause: unknown,\n): Error {\n // `cause` is assigned rather than passed to the constructor: this package\n // compiles against a lib without the ES2022 `ErrorOptions` overload.\n const err = new Error(message);\n // Every caller classifies \"missing vs crashed\" through\n // `isModuleNotFoundError`; both of these ARE the missing case, just with\n // different remedies, so they must keep answering true to it.\n return Object.assign(err, {\n cause,\n code: 'MODULE_NOT_FOUND',\n [HOST_IMPORT_FAILURE_KIND]: kind,\n });\n}\n\nfunction undeclaredMessage(declaration: HostDeclaration, cause: unknown): string {\n const { packageName, hostRoot, manifestMissing } = declaration;\n const detail = cause instanceof Error ? cause.message : String(cause);\n return (\n `Cannot find package '${packageName}': the host app does not declare it.\\n` +\n ` host app: ${hostRoot}\\n` +\n (manifestMissing\n ? ' no readable package.json was found there — nothing can be declared\\n'\n : ` checked: ${HOST_DECLARATION_FIELDS.join(', ')}\\n`) +\n `\\n Declare it in that app's package.json and install it, e.g.\\n` +\n ` cd ${hostRoot} && pnpm add ${packageName}\\n` +\n '\\n Being merely REACHABLE is not enough and is rejected on purpose (#4719):\\n' +\n ' a package hoisted into a workspace store — which is what NODE_PATH points\\n' +\n \" at in every pnpm bin shim — used to resolve here regardless of the app's\\n\" +\n ' package.json, so the same app booted or refused depending on how the\\n' +\n ' process was launched. The declaration is the contract.\\n' +\n ` (fallback resolution also failed: ${detail})`\n );\n}\n\nfunction unresolvableMessage(declaration: HostDeclaration, cause: unknown): string {\n const { packageName, hostRoot, field, specifier } = declaration;\n const detail = cause instanceof Error ? cause.message : String(cause);\n return (\n `Cannot find module '${packageName}': the host app DECLARES it ` +\n `(${field}: ${JSON.stringify(specifier)}) but it could not be resolved.\\n` +\n ` host app: ${hostRoot}\\n` +\n '\\n This is an INSTALL problem, not a declaration problem — the declaration is\\n' +\n ' already there, so re-reading the package.json will not help. Check:\\n' +\n ` • dependencies never installed, or installed before the declaration was added → run \\`pnpm install\\` in ${hostRoot}\\n` +\n ' • a production prune / filtered deploy dropped it (devDependencies and\\n' +\n ' optionalDependencies go first)\\n' +\n ' • it IS installed but its \"main\"/\"exports\" points at a dist that was never built\\n' +\n ` (resolver: ${detail})`\n );\n}\n\n/**\n * Build an importer that loads a package **as the host app declares it**, and\n * otherwise falls back to the importing package's own resolution.\n *\n * Order of operations, and why (#4719):\n *\n * 1. The host `package.json` is READ. Only a declared name is looked up in the\n * host's `node_modules`. An undeclared name never reaches the host resolver,\n * so no amount of `NODE_PATH` / hoisting can make it appear to be the app's.\n * 2. Declared but unresolvable is reported AS SUCH — the app asked for it and\n * the install is broken. It is not retried bare: falling back there would\n * reintroduce exactly the \"some other package happens to supply it\" accident\n * this gate closes, and would report an install problem as an absence.\n * 3. Undeclared falls back to the importing package's own resolution, which is\n * what keeps every framework-owned load working (`serve`'s plugin-auth /\n * service-i18n path, `bootStack`'s service plugins). Bare `import()` is ESM,\n * and ESM does not honour `NODE_PATH`, so the fallback cannot re-open the\n * hole either. Only when that fails as module-not-found does the undeclared\n * error surface; a package that RESOLVES and then throws while evaluating is\n * a genuine crash and propagates untouched, as before.\n *\n * @param hostRoot Directory holding the host app's `package.json` (default: the\n * process CWD, which is where the CLI reads `objectstack.config.ts` from too).\n * Note this used to take a pre-built `NodeRequire`; it needs the ROOT now,\n * because a `NodeRequire` cannot be asked where it was anchored and the manifest\n * has to be read from there.\n */\nexport function createHostImporter(hostRoot: string = process.cwd()): HostImporter {\n const hostRequire = createHostRequire(hostRoot);\n return async (pkg: string): Promise<any> => {\n // Not a bare package name (a path, a URL, a `node:` builtin) — nothing a\n // manifest could declare. Hand it to the normal resolver untouched.\n if (packageNameFromSpecifier(pkg) === undefined) {\n return import(/* webpackIgnore: true */ pkg);\n }\n\n const declaration = readHostDeclaration(pkg, hostRoot);\n\n if (declaration.declared) {\n let resolved: string;\n try {\n resolved = hostRequire.resolve(pkg);\n } catch (cause) {\n throw hostImportError(\n 'declared-unresolvable',\n unresolvableMessage(declaration, cause),\n cause,\n );\n }\n return import(pathToFileURL(resolved).href);\n }\n\n try {\n return await import(/* webpackIgnore: true */ pkg);\n } catch (cause) {\n // A package that resolved and then exploded is a crash, not an absence.\n if (!isModuleNotFoundError(cause)) throw cause;\n throw hostImportError('undeclared', undeclaredMessage(declaration, cause), cause);\n }\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * True when a dynamic `import()` / `require.resolve()` failed because the\n * module is simply NOT INSTALLED — as opposed to the module being present but\n * throwing while it loads (a real crash). Checking `err.code` FIRST matters:\n * ESM reports a missing package as `err.code === 'ERR_MODULE_NOT_FOUND'` with\n * the human message `Cannot find package '...'`; matching only the older\n * `Cannot find module` string mis-classifies that as a crash (framework#1595).\n *\n * Single shared owner for this classification (framework#3265): the CLI's\n * optional-plugin guards and `requires` capability resolver delegate here, and\n * cloud's `objectos-runtime` capability loader is expected to adopt it at its\n * next framework pin bump — so the parallel loaders cannot drift apart and\n * re-introduce the #1595 false-alarm class.\n */\nexport function isModuleNotFoundError(err: unknown): boolean {\n const code = (err as { code?: string } | null | undefined)?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;\n const msg = err instanceof Error ? err.message : String(err);\n return msg.includes('Cannot find module') || msg.includes('Cannot find package');\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/node.ts","../src/module-not-found.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/types/node` — the **node-only** slice of the shared utilities.\n *\n * WHY A SUBPATH AND NOT THE ROOT EXPORT. `@objectstack/types` is a dependency of\n * `@objectstack/hono`, whose whole reason to exist is \"edge-compatible REST API\n * server for Cloudflare Workers, Deno, Bun, and Node\" — and of the plugin/service\n * layer a `LiteKernel` boots on Workers. The root entry (`src/index.ts`) reaches\n * **zero** `node:` builtins today, and that is a property those consumers depend\n * on: a Workers bundle that pulls in `node:module` fails to build (or dies at\n * first call) even when nothing ever invokes it. Everything here needs\n * `node:module` / `node:url` by definition — it exists to drive Node's own\n * resolver — so it lives behind its own entry point instead.\n *\n * The isolation is structural, not conventional: `tsup` builds `src/index.ts` and\n * `src/node.ts` as separate entries with `splitting: false`, so the root bundle\n * contains no reference to this file, and `node-isolation.test.ts` fails the\n * build if anything reachable from the root ever imports a `node:` builtin. Same\n * arrangement `@objectstack/metadata` already ships for `./node`.\n *\n * ── What lives here ──────────────────────────────────────────────────────────\n *\n * Resolving optional packages from the **host app**, not from the framework\n * package doing the importing.\n *\n * Node ESM resolves a bare `import('pkg')` against the **importer's own\n * realpath**. Framework packages (the CLI, `@objectstack/verify`,\n * `@objectstack/dogfood`) are reached through `link:`/workspace dependencies, so\n * their realpath is inside the *framework* workspace — a bare import from any of\n * them can only ever see packages installed in the framework's own\n * `node_modules`. Every package that lives OUTSIDE that workspace and is supplied\n * by the app being served, verified or tested — a cloud-private package such as\n * `@objectstack/organizations` or `@objectstack/service-ai-studio`, or anything a\n * customer installs into their own project — is therefore invisible to a bare\n * import, no matter what the host app declares in its `package.json`\n * (cloud#1013: `objectstack serve` could never load the enterprise multi-org\n * runtime, so every self-hosted walled-posture deployment hit the ADR-0093 D5\n * fail-fast and exited 1; framework#4700: `bootStack({ multiTenant: true })` told\n * apps to install a package they had already installed, and the dogfood\n * multi-org probes were constant-false).\n *\n * The fix is to resolve from the host app's root and import the resolved\n * absolute path. The importing package's own resolution stays as the fallback,\n * for the framework-owned packages it depends on and the host does not declare\n * — and since #10943 that fallback is the base the CALLER hands in\n * ({@link HostImporterOptions.fallbackImport}), because a fallback written here\n * resolved from `@objectstack/types` and could only ever see\n * `@objectstack/spec`. Same defect class as the paragraph above, one level up:\n * a bare import resolves against the module that CONTAINS it, and this module\n * is not the one doing the asking.\n *\n * Resolution failure is the ONLY thing that falls back. A package the host\n * resolves but that throws while it evaluates is a genuine crash and propagates\n * unchanged: re-importing it bare would replace the real cause with a\n * `MODULE_NOT_FOUND`, which every caller here classifies as \"not installed\" —\n * turning a broken package into a silent skip (or, on the organizations path,\n * into a fatal message telling the operator to install what is already there).\n *\n * ── #4719: the host's DECLARATION gates the lookup, not its resolvability ────\n *\n * \"Resolve from the host app\" was implemented as a CJS `createRequire` anchored\n * at the host's `package.json`, and **CJS resolution honours `NODE_PATH`**\n * (`Module.globalPaths`). The first thing a pnpm-generated bin shim does is\n *\n * export NODE_PATH=\"<workspace>/node_modules/.pnpm/node_modules\"\n *\n * and every `serve` / `dev` child process inherits it. Everything any package in\n * the workspace transitively depends on lives in that hoisted store, so\n * `hostRequire.resolve(pkg)` succeeded for packages the host app had never\n * declared — the answer depended on HOW THE PROCESS WAS LAUNCHED, not on the\n * app. Measured on cloud's `apps/objectos-ee`, which did not declare\n * `@objectstack/organizations`: `pnpm start` (through the shim) booted with the\n * organizations plugin mounted and ADR-0093 D5 silent, while\n * `node node_modules/@objectstack/cli/bin/run.js serve` (no shim, no NODE_PATH)\n * hit the D5 fail-fast and exited 1. Same app, same `package.json`, same\n * posture. D5's own message told operators to \"declare it in the app's\n * package.json\" — the one thing the CLI never checked.\n *\n * So the host lookup is now gated on the host's **declaration**: a package name\n * is looked up in the host's `node_modules` only when it appears in the host\n * `package.json` (see {@link HOST_DECLARATION_FIELDS}). Reachability through a\n * hoisted store or `NODE_PATH` is deliberately not accepted — it is precisely\n * the accident that made the contract unenforced. This is the \"declared =\n * enforced\" shape the rest of the repo uses (Prime Directive #10): the\n * declaration is a deliberate authoring act, machine-checkable at the moment of\n * boot, and independent of launcher, package manager and hoist layout.\n *\n * The two failures it separates were, until now, one indistinguishable\n * `MODULE_NOT_FOUND`, with opposite remedies:\n *\n * - **undeclared** — the app never asked for this package. Remedy: declare it\n * in the app's `package.json` and install.\n * - **declared but unresolvable** — the app asked for it and the install is\n * broken/pruned/unbuilt. Remedy: fix the install. Re-reading the\n * `package.json` is wasted effort; the declaration is right there.\n *\n * {@link hostImportFailureKind} exposes that classification to callers so their\n * fail-fast text can say which one it is (`packages/cli` ADR-0093 D5,\n * `packages/verify` `bootStack`, `packages/qa/dogfood`'s enterprise probe).\n */\n\nimport { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { isModuleNotFoundError } from './module-not-found.js';\n\n/**\n * Imports a package as the host app would see it.\n *\n * `any` is the module namespace of a package this repo does not compile against\n * (it is not a dependency of the importing package at all) — every call site\n * reads an export off it dynamically, exactly as the bare `import()` it replaces\n * did.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type HostImporter = (pkg: string) => Promise<any>;\n\n/**\n * The importing package's OWN dynamic import — write it literally, in the\n * calling module:\n *\n * createHostImporter(hostRoot, { fallbackImport: (s) => import(s) })\n *\n * `any` for the same reason {@link HostImporter} uses it: the module namespace\n * belongs to a package this repo does not compile against.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type FallbackImport = (specifier: string) => Promise<any>;\n\n/** Options for {@link createHostImporter}. */\nexport interface HostImporterOptions {\n /**\n * The resolution base for everything the host app does NOT declare — supplied\n * as the caller's own `import()` rather than as a URL string, because a\n * string base was MEASURED to be unimplementable without a regression. See\n * {@link createHostImporter}'s \"why a function\" note for both measurements.\n *\n * Omitted ⇒ the fallback resolves from `@objectstack/types`, which sees only\n * `@objectstack/types`'s own dependencies. That default is retained so an\n * out-of-tree caller cannot be broken by this parameter's arrival, and the\n * `undeclared` failure text names it explicitly so the gap reports itself\n * instead of being rediscovered.\n */\n fallbackImport?: FallbackImport;\n}\n\n/**\n * A `require` anchored at the **host app's** `package.json` — i.e. the project\n * `objectstack serve` was invoked in, or the app `bootStack` is verifying, whose\n * `node_modules` carries the packages it declares.\n *\n * @param hostRoot Directory holding the host app's `package.json` (default: the\n * process CWD, which is where the CLI reads `objectstack.config.ts` from too).\n */\nexport function createHostRequire(hostRoot: string = process.cwd()): NodeRequire {\n return createRequire(join(hostRoot, 'package.json'));\n}\n\n/**\n * The `package.json` fields whose KEYS count as a host-app declaration (#4719).\n *\n * All four are deliberate authoring acts in the app's own manifest that name the\n * package, which is the signal this gate is built on — not \"is it reachable\".\n * Why each is in:\n *\n * - `dependencies` — the obvious one: the app runs with it.\n * - `devDependencies` — the app being served / verified / dogfooded IS the\n * project, not a library someone else consumes, so its dev deps are installed\n * in exactly the environment this resolver runs in.\n * - `optionalDependencies` — npm/pnpm install them and tolerate an install\n * failure. \"Installed ⇒ declared\" holds; and if it did NOT install, the\n * declared-but-unresolvable branch says so precisely instead of pretending the\n * app never asked.\n * - `peerDependencies` — an app is nobody's peer, so this is an unusual place to\n * put an enterprise add-on; but it still NAMES the package on purpose, and\n * `packages/cli`'s own edition gate (`serve`'s AI-service opt-in, #1597) has\n * read all four since it was written. Accepting three here and four there\n * would fork \"declared\" into two dialects for one question — the shape Prime\n * Directive #12 exists to prevent. That gate now delegates to this list, so\n * there is one owner and one answer.\n *\n * `bundleDependencies` is absent on purpose: it is an array of names that must\n * ALSO appear in `dependencies`, so it can never be the only declaration.\n */\nexport const HOST_DECLARATION_FIELDS = [\n 'dependencies',\n 'devDependencies',\n 'optionalDependencies',\n 'peerDependencies',\n] as const;\n\nexport type HostDeclarationField = (typeof HOST_DECLARATION_FIELDS)[number];\n\n/** What the host app's `package.json` says about one package name. */\nexport interface HostDeclaration {\n /** Bare package name the specifier belongs to (subpath stripped). */\n packageName: string;\n /** Directory whose `package.json` was consulted. */\n hostRoot: string;\n /** True when {@link packageName} is a key of one of {@link HOST_DECLARATION_FIELDS}. */\n declared: boolean;\n /** Which field carried it (first match, in {@link HOST_DECLARATION_FIELDS} order). */\n field?: HostDeclarationField;\n /** The version range AS WRITTEN — `^1.2.3`, `workspace:*`, `npm:@acme/x@1`, `link:../x`. */\n specifier?: string;\n /** True when `hostRoot` has no readable / parseable `package.json` at all. */\n manifestMissing?: boolean;\n}\n\n/**\n * The package a bare specifier belongs to, or `undefined` when the specifier is\n * not a bare package name at all (a relative/absolute path, a `file:`/`data:`\n * URL, or a `node:`-prefixed builtin). Those bypass the declaration gate: they\n * are not things a `package.json` can declare.\n *\n * Subpaths are stripped, so `@objectstack/platform-objects/plugin` is declared\n * by `\"@objectstack/platform-objects\"`, which is the only key that can exist.\n * Scoped names keep both segments.\n *\n * Alias dependencies need no special case, and that is the point: with\n * `\"foo\": \"npm:bar@1\"` the importable specifier is `foo` and the manifest key is\n * `foo`, so keying on the KEY (never the value) is exactly right — `import('bar')`\n * correctly reads as undeclared unless `bar` is itself a key. Same for\n * `workspace:` / `link:` / `file:` specifiers: the key is the name, the value is\n * the package manager's business.\n */\nexport function packageNameFromSpecifier(specifier: string): string | undefined {\n if (!specifier || specifier.startsWith('.') || specifier.startsWith('/')) return undefined;\n // A URL-ish or protocol-prefixed specifier (`node:fs`, `file:///…`, `data:…`).\n if (/^[a-z][a-z0-9+.-]*:/i.test(specifier)) return undefined;\n const segments = specifier.split('/');\n if (specifier.startsWith('@')) {\n if (segments.length < 2 || !segments[0] || !segments[1]) return undefined;\n return `${segments[0]}/${segments[1]}`;\n }\n return segments[0] || undefined;\n}\n\n/**\n * Read what the host app's `package.json` declares about `specifier`.\n *\n * Deliberately a plain manifest READ, never a resolution attempt: resolvability\n * is the property #4719 proved unreliable (it moved with `NODE_PATH` and the\n * hoist layout), while the manifest is the same fact in every launcher.\n */\nexport function readHostDeclaration(\n specifier: string,\n hostRoot: string = process.cwd(),\n): HostDeclaration {\n const packageName = packageNameFromSpecifier(specifier) ?? specifier;\n const base: HostDeclaration = { packageName, hostRoot, declared: false };\n\n let manifest: Record<string, unknown>;\n try {\n manifest = JSON.parse(readFileSync(join(hostRoot, 'package.json'), 'utf8')) as Record<\n string,\n unknown\n >;\n } catch {\n // No manifest ⇒ nothing is declared. Recorded rather than swallowed so the\n // failure text can say \"there is no package.json here\" instead of the\n // misleading \"you did not declare it\".\n return { ...base, manifestMissing: true };\n }\n\n for (const field of HOST_DECLARATION_FIELDS) {\n const entries = manifest[field];\n if (!entries || typeof entries !== 'object') continue;\n const specifierValue = (entries as Record<string, unknown>)[packageName];\n if (specifierValue === undefined) continue;\n return { ...base, declared: true, field, specifier: String(specifierValue) };\n }\n return base;\n}\n\n/** Convenience predicate over {@link readHostDeclaration}. */\nexport function isDeclaredByHost(specifier: string, hostRoot?: string): boolean {\n return readHostDeclaration(specifier, hostRoot).declared;\n}\n\n/**\n * Why a {@link HostImporter} could not produce a module.\n *\n * - `undeclared` — the host app's `package.json` never names the package, and\n * the importing framework package cannot supply it either. Remedy: DECLARE it\n * in the app and install.\n * - `declared-unresolvable` — the app declares it and it still would not\n * resolve. Remedy: fix the INSTALL. Re-reading the manifest is wasted effort.\n *\n * An evaluation crash is neither: it propagates untouched and carries no kind.\n */\nexport type HostImportFailureKind = 'undeclared' | 'declared-unresolvable';\n\n/**\n * Property carrying {@link HostImportFailureKind} on a thrown error.\n *\n * A string property, read by {@link hostImportFailureKind} — never `instanceof`.\n * `serve` loads plugins through this importer, so CLI and package can hold\n * different module instances of anything class-shaped; the #4818 comment in\n * `serve.ts` names that trap explicitly.\n */\nexport const HOST_IMPORT_FAILURE_KIND = 'objectstackHostImportFailureKind';\n\n/** The classification on an error thrown by a {@link HostImporter}, if any. */\nexport function hostImportFailureKind(err: unknown): HostImportFailureKind | undefined {\n const kind = (err as Record<string, unknown> | null | undefined)?.[HOST_IMPORT_FAILURE_KIND];\n return kind === 'undeclared' || kind === 'declared-unresolvable' ? kind : undefined;\n}\n\nfunction hostImportError(\n kind: HostImportFailureKind,\n message: string,\n cause: unknown,\n): Error {\n // `cause` is assigned rather than passed to the constructor: this package\n // compiles against a lib without the ES2022 `ErrorOptions` overload.\n const err = new Error(message);\n // Every caller classifies \"missing vs crashed\" through\n // `isModuleNotFoundError`; both of these ARE the missing case, just with\n // different remedies, so they must keep answering true to it.\n return Object.assign(err, {\n cause,\n code: 'MODULE_NOT_FOUND',\n [HOST_IMPORT_FAILURE_KIND]: kind,\n });\n}\n\n/**\n * @param callerBaseSupplied Did the caller state its own resolution base\n * ({@link HostImporterOptions.fallbackImport})? When it did not, the fallback\n * ran from `@objectstack/types`, which sees only `@objectstack/spec` — so the\n * absence being reported may be an artefact of the missing base rather than a\n * real one. #10943 kept that default for out-of-tree callers; saying so here is\n * what stops it being silent, because the alternative is a reader re-deriving\n * the whole measurement from a `MODULE_NOT_FOUND` that names nothing.\n */\nfunction undeclaredMessage(\n declaration: HostDeclaration,\n cause: unknown,\n callerBaseSupplied: boolean,\n): string {\n const { packageName, hostRoot, manifestMissing } = declaration;\n const detail = cause instanceof Error ? cause.message : String(cause);\n const baseNote = callerBaseSupplied\n ? ''\n : '\\n (the caller did not pass `fallbackImport`, so that fallback resolved from\\n' +\n \" @objectstack/types, which can see only its own dependencies — a caller that\\n\" +\n ' needs its own resolution passes `{ fallbackImport: (s) => import(s) }`, #10943)';\n return (\n `Cannot find package '${packageName}': the host app does not declare it.\\n` +\n ` host app: ${hostRoot}\\n` +\n (manifestMissing\n ? ' no readable package.json was found there — nothing can be declared\\n'\n : ` checked: ${HOST_DECLARATION_FIELDS.join(', ')}\\n`) +\n `\\n Declare it in that app's package.json and install it, e.g.\\n` +\n ` cd ${hostRoot} && pnpm add ${packageName}\\n` +\n '\\n Being merely REACHABLE is not enough and is rejected on purpose (#4719):\\n' +\n ' a package hoisted into a workspace store — which is what NODE_PATH points\\n' +\n \" at in every pnpm bin shim — used to resolve here regardless of the app's\\n\" +\n ' package.json, so the same app booted or refused depending on how the\\n' +\n ' process was launched. The declaration is the contract.\\n' +\n ` (fallback resolution also failed: ${detail})${baseNote}`\n );\n}\n\nfunction unresolvableMessage(declaration: HostDeclaration, cause: unknown): string {\n const { packageName, hostRoot, field, specifier } = declaration;\n const detail = cause instanceof Error ? cause.message : String(cause);\n return (\n `Cannot find module '${packageName}': the host app DECLARES it ` +\n `(${field}: ${JSON.stringify(specifier)}) but it could not be resolved.\\n` +\n ` host app: ${hostRoot}\\n` +\n '\\n This is an INSTALL problem, not a declaration problem — the declaration is\\n' +\n ' already there, so re-reading the package.json will not help. Check:\\n' +\n ` • dependencies never installed, or installed before the declaration was added → run \\`pnpm install\\` in ${hostRoot}\\n` +\n ' • a production prune / filtered deploy dropped it (devDependencies and\\n' +\n ' optionalDependencies go first)\\n' +\n ' • it IS installed but its \"main\"/\"exports\" points at a dist that was never built\\n' +\n ` (resolver: ${detail})`\n );\n}\n\n/**\n * Build an importer that loads a package **as the host app declares it**, and\n * otherwise falls back to the importing package's own resolution.\n *\n * Order of operations, and why (#4719):\n *\n * 1. The host `package.json` is READ. Only a declared name is looked up in the\n * host's `node_modules`. An undeclared name never reaches the host resolver,\n * so no amount of `NODE_PATH` / hoisting can make it appear to be the app's.\n * 2. Declared but unresolvable is reported AS SUCH — the app asked for it and\n * the install is broken. It is not retried bare: falling back there would\n * reintroduce exactly the \"some other package happens to supply it\" accident\n * this gate closes, and would report an install problem as an absence.\n * 3. Undeclared falls back to the CALLER's own resolution, which is what keeps\n * every framework-owned load working (`serve`'s plugin-auth / service-i18n\n * path, `bootStack`'s service plugins). Bare `import()` is ESM, and ESM does\n * not honour `NODE_PATH`, so the fallback cannot re-open the hole either.\n * Only when that fails as module-not-found does the undeclared error\n * surface; a package that RESOLVES and then throws while evaluating is a\n * genuine crash and propagates untouched, as before.\n *\n * ── The caller supplies that base, and why it is a FUNCTION (#10943) ─────────\n *\n * Step 3 said \"the importing package's own resolution\" long before anything\n * made it true. The fallback was a bare `import()` written HERE, and ESM\n * resolves a bare specifier against the module containing the call — so it\n * resolved from `@objectstack/types`, which under a pnpm-isolated layout can\n * see only `@objectstack/types`'s own dependencies. Measured on `main` from an\n * app declaring nothing, `@objectstack/plugin-auth`, `@objectstack/plugin-audit`\n * and `chalk` all resolve from `packages/cli` and all failed through this\n * helper; `@objectstack/spec` — the one dependency this package declares — was\n * the only name that came back OK, which is the whole pattern. Under a hoisted\n * npm/yarn layout the same fallback usually DOES find the caller's\n * dependencies, so the claim was green in some installs and absent in others:\n * the layout-dependence class cloud#1013 and #10645 exist to close, one level\n * up. A declared contract the implementation does not keep is the thing this\n * repo fixes at the producer (Prime Directive #12), so the mechanism moved\n * rather than the sentence.\n *\n * The base arrives as the caller's own `import()` and NOT as a `parentURL` /\n * `import.meta.url` string. Both string spellings were measured on Node\n * v22.22.2 and both are wrong:\n *\n * - `import.meta.resolve(specifier, parentURL)` — the parent argument is\n * SILENTLY IGNORED without `--experimental-import-meta-resolve`. Measured:\n * resolving `@objectstack/plugin-auth` against a `packages/types` parent\n * returned `packages/cli/node_modules/...`, i.e. the caller's own answer,\n * byte-identical to passing no parent at all. It would have compiled, run,\n * and pinned green while ignoring the base — a phantom fix of exactly the\n * kind this card is about.\n * - `createRequire(parentURL).resolve(specifier)` — CJS resolution, which\n * honours `NODE_PATH`. Measured against a store reachable only through\n * `NODE_PATH`: the CJS resolve found it (with and without the `paths`\n * option, since GLOBAL_FOLDERS are always appended) while the ESM bare\n * `import()` did not. That is #4719's hole re-opened on the fallback path,\n * and it would have falsified the \"ESM does not honour NODE_PATH\" sentence\n * three lines above.\n *\n * A function written in the calling module is the only spelling that uses\n * Node's real ESM resolver anchored where the caller actually lives: no flag,\n * no `NODE_PATH`, no second resolution algorithm to drift from the first.\n *\n * @param hostRoot Directory holding the host app's `package.json` (default: the\n * process CWD, which is where the CLI reads `objectstack.config.ts` from too).\n * Note this used to take a pre-built `NodeRequire`; it needs the ROOT now,\n * because a `NodeRequire` cannot be asked where it was anchored and the manifest\n * has to be read from there.\n * @param options {@link HostImporterOptions.fallbackImport} carries the caller's\n * resolution base. Omitting it keeps the pre-#10943 behaviour (this package's\n * own resolution) so no out-of-tree caller changes under its feet.\n */\nexport function createHostImporter(\n hostRoot: string = process.cwd(),\n options: HostImporterOptions = {},\n): HostImporter {\n const hostRequire = createHostRequire(hostRoot);\n const { fallbackImport } = options;\n const importAsCaller: FallbackImport =\n fallbackImport ?? ((specifier) => import(/* webpackIgnore: true */ specifier));\n return async (pkg: string): Promise<any> => {\n // Not a bare package name (a path, a URL, a `node:` builtin) — nothing a\n // manifest could declare. Hand it to the normal resolver untouched.\n //\n // ⚠️ Deliberately NOT re-based onto `fallbackImport` (#10943). Every\n // base-INDEPENDENT spelling here — `file://`, `node:`, `data:`, an absolute\n // path — means the same module whoever imports it, so the base is not a\n // question they can even ask. The one spelling it WOULD move is a RELATIVE\n // one, and where that should resolve from is an open policy question owned\n // by #10944 (`serve` refuses a relative `plugins: [...]` entry rather than\n // silently re-basing it) — with a measured consumer count of zero here:\n // `serve` handles non-package specifiers before this helper is reached, and\n // `bootStack` / the dogfood probe pass package names only. Answering half\n // of another card's undecided question, for nobody, is not a repair.\n if (packageNameFromSpecifier(pkg) === undefined) {\n return import(/* webpackIgnore: true */ pkg);\n }\n\n const declaration = readHostDeclaration(pkg, hostRoot);\n\n if (declaration.declared) {\n let resolved: string;\n try {\n resolved = hostRequire.resolve(pkg);\n } catch (cause) {\n throw hostImportError(\n 'declared-unresolvable',\n unresolvableMessage(declaration, cause),\n cause,\n );\n }\n return import(pathToFileURL(resolved).href);\n }\n\n try {\n return await importAsCaller(pkg);\n } catch (cause) {\n // A package that resolved and then exploded is a crash, not an absence.\n if (!isModuleNotFoundError(cause)) throw cause;\n throw hostImportError(\n 'undeclared',\n undeclaredMessage(declaration, cause, fallbackImport !== undefined),\n cause,\n );\n }\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * True when a dynamic `import()` / `require.resolve()` failed because the\n * module is simply NOT INSTALLED — as opposed to the module being present but\n * throwing while it loads (a real crash). Checking `err.code` FIRST matters:\n * ESM reports a missing package as `err.code === 'ERR_MODULE_NOT_FOUND'` with\n * the human message `Cannot find package '...'`; matching only the older\n * `Cannot find module` string mis-classifies that as a crash (framework#1595).\n *\n * Single shared owner for this classification (framework#3265): the CLI's\n * optional-plugin guards and `requires` capability resolver delegate here, and\n * cloud's `objectos-runtime` capability loader is expected to adopt it at its\n * next framework pin bump — so the parallel loaders cannot drift apart and\n * re-introduce the #1595 false-alarm class.\n */\nexport function isModuleNotFoundError(err: unknown): boolean {\n const code = (err as { code?: string } | null | undefined)?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;\n const msg = err instanceof Error ? err.message : String(err);\n return msg.includes('Cannot find module') || msg.includes('Cannot find package');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsGA,qBAA6B;AAC7B,yBAA8B;AAC9B,uBAAqB;AACrB,sBAA8B;;;ACzFvB,SAAS,sBAAsB,KAAuB;AAC3D,QAAM,OAAQ,KAA8C;AAC5D,MAAI,SAAS,0BAA0B,SAAS,mBAAoB,QAAO;AAC3E,QAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,SAAO,IAAI,SAAS,oBAAoB,KAAK,IAAI,SAAS,qBAAqB;AACjF;;;ADuIO,SAAS,kBAAkB,WAAmB,QAAQ,IAAI,GAAgB;AAC/E,aAAO,sCAAc,uBAAK,UAAU,cAAc,CAAC;AACrD;AA4BO,IAAM,0BAA0B;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAqCO,SAAS,yBAAyB,WAAuC;AAC9E,MAAI,CAAC,aAAa,UAAU,WAAW,GAAG,KAAK,UAAU,WAAW,GAAG,EAAG,QAAO;AAEjF,MAAI,uBAAuB,KAAK,SAAS,EAAG,QAAO;AACnD,QAAM,WAAW,UAAU,MAAM,GAAG;AACpC,MAAI,UAAU,WAAW,GAAG,GAAG;AAC7B,QAAI,SAAS,SAAS,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,EAAG,QAAO;AAChE,WAAO,GAAG,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC;AAAA,EACtC;AACA,SAAO,SAAS,CAAC,KAAK;AACxB;AASO,SAAS,oBACd,WACA,WAAmB,QAAQ,IAAI,GACd;AACjB,QAAM,cAAc,yBAAyB,SAAS,KAAK;AAC3D,QAAM,OAAwB,EAAE,aAAa,UAAU,UAAU,MAAM;AAEvE,MAAI;AACJ,MAAI;AACF,eAAW,KAAK,UAAM,iCAAa,uBAAK,UAAU,cAAc,GAAG,MAAM,CAAC;AAAA,EAI5E,QAAQ;AAIN,WAAO,EAAE,GAAG,MAAM,iBAAiB,KAAK;AAAA,EAC1C;AAEA,aAAW,SAAS,yBAAyB;AAC3C,UAAM,UAAU,SAAS,KAAK;AAC9B,QAAI,CAAC,WAAW,OAAO,YAAY,SAAU;AAC7C,UAAM,iBAAkB,QAAoC,WAAW;AACvE,QAAI,mBAAmB,OAAW;AAClC,WAAO,EAAE,GAAG,MAAM,UAAU,MAAM,OAAO,WAAW,OAAO,cAAc,EAAE;AAAA,EAC7E;AACA,SAAO;AACT;AAGO,SAAS,iBAAiB,WAAmB,UAA4B;AAC9E,SAAO,oBAAoB,WAAW,QAAQ,EAAE;AAClD;AAuBO,IAAM,2BAA2B;AAGjC,SAAS,sBAAsB,KAAiD;AACrF,QAAM,OAAQ,MAAqD,wBAAwB;AAC3F,SAAO,SAAS,gBAAgB,SAAS,0BAA0B,OAAO;AAC5E;AAEA,SAAS,gBACP,MACA,SACA,OACO;AAGP,QAAM,MAAM,IAAI,MAAM,OAAO;AAI7B,SAAO,OAAO,OAAO,KAAK;AAAA,IACxB;AAAA,IACA,MAAM;AAAA,IACN,CAAC,wBAAwB,GAAG;AAAA,EAC9B,CAAC;AACH;AAWA,SAAS,kBACP,aACA,OACA,oBACQ;AACR,QAAM,EAAE,aAAa,UAAU,gBAAgB,IAAI;AACnD,QAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACpE,QAAM,WAAW,qBACb,KACA;AAGJ,SACE,wBAAwB,WAAW;AAAA,cACpB,QAAQ;AAAA,KACtB,kBACG,gFACA,cAAc,wBAAwB,KAAK,IAAI,CAAC;AAAA,KACpD;AAAA;AAAA,WACY,QAAQ,gBAAgB,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sCAMR,MAAM,IAAI,QAAQ;AAE7D;AAEA,SAAS,oBAAoB,aAA8B,OAAwB;AACjF,QAAM,EAAE,aAAa,UAAU,OAAO,UAAU,IAAI;AACpD,QAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACpE,SACE,uBAAuB,WAAW,gCAC9B,KAAK,KAAK,KAAK,UAAU,SAAS,CAAC;AAAA,cACxB,QAAQ;AAAA;AAAA;AAAA;AAAA,wHAGwF,QAAQ;AAAA;AAAA;AAAA;AAAA,eAIvG,MAAM;AAE1B;AAyEO,SAAS,mBACd,WAAmB,QAAQ,IAAI,GAC/B,UAA+B,CAAC,GAClB;AACd,QAAM,cAAc,kBAAkB,QAAQ;AAC9C,QAAM,EAAE,eAAe,IAAI;AAC3B,QAAM,iBACJ,mBAAmB,CAAC,cAAc;AAAA;AAAA,IAAiC;AAAA;AACrE,SAAO,OAAO,QAA8B;AAc1C,QAAI,yBAAyB,GAAG,MAAM,QAAW;AAC/C,aAAO;AAAA;AAAA,QAAiC;AAAA;AAAA,IAC1C;AAEA,UAAM,cAAc,oBAAoB,KAAK,QAAQ;AAErD,QAAI,YAAY,UAAU;AACxB,UAAI;AACJ,UAAI;AACF,mBAAW,YAAY,QAAQ,GAAG;AAAA,MACpC,SAAS,OAAO;AACd,cAAM;AAAA,UACJ;AAAA,UACA,oBAAoB,aAAa,KAAK;AAAA,UACtC;AAAA,QACF;AAAA,MACF;AACA,aAAO,WAAO,+BAAc,QAAQ,EAAE;AAAA,IACxC;AAEA,QAAI;AACF,aAAO,MAAM,eAAe,GAAG;AAAA,IACjC,SAAS,OAAO;AAEd,UAAI,CAAC,sBAAsB,KAAK,EAAG,OAAM;AACzC,YAAM;AAAA,QACJ;AAAA,QACA,kBAAkB,aAAa,OAAO,mBAAmB,MAAS;AAAA,QAClE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/dist/node.mjs CHANGED
@@ -66,9 +66,10 @@ function hostImportError(kind, message, cause) {
66
66
  [HOST_IMPORT_FAILURE_KIND]: kind
67
67
  });
68
68
  }
69
- function undeclaredMessage(declaration, cause) {
69
+ function undeclaredMessage(declaration, cause, callerBaseSupplied) {
70
70
  const { packageName, hostRoot, manifestMissing } = declaration;
71
71
  const detail = cause instanceof Error ? cause.message : String(cause);
72
+ const baseNote = callerBaseSupplied ? "" : "\n (the caller did not pass `fallbackImport`, so that fallback resolved from\n @objectstack/types, which can see only its own dependencies \u2014 a caller that\n needs its own resolution passes `{ fallbackImport: (s) => import(s) }`, #10943)";
72
73
  return `Cannot find package '${packageName}': the host app does not declare it.
73
74
  host app: ${hostRoot}
74
75
  ` + (manifestMissing ? " no readable package.json was found there \u2014 nothing can be declared\n" : ` checked: ${HOST_DECLARATION_FIELDS.join(", ")}
@@ -81,7 +82,7 @@ function undeclaredMessage(declaration, cause) {
81
82
  at in every pnpm bin shim \u2014 used to resolve here regardless of the app's
82
83
  package.json, so the same app booted or refused depending on how the
83
84
  process was launched. The declaration is the contract.
84
- (fallback resolution also failed: ${detail})`;
85
+ (fallback resolution also failed: ${detail})${baseNote}`;
85
86
  }
86
87
  function unresolvableMessage(declaration, cause) {
87
88
  const { packageName, hostRoot, field, specifier } = declaration;
@@ -97,8 +98,13 @@ function unresolvableMessage(declaration, cause) {
97
98
  \u2022 it IS installed but its "main"/"exports" points at a dist that was never built
98
99
  (resolver: ${detail})`;
99
100
  }
100
- function createHostImporter(hostRoot = process.cwd()) {
101
+ function createHostImporter(hostRoot = process.cwd(), options = {}) {
101
102
  const hostRequire = createHostRequire(hostRoot);
103
+ const { fallbackImport } = options;
104
+ const importAsCaller = fallbackImport ?? ((specifier) => import(
105
+ /* webpackIgnore: true */
106
+ specifier
107
+ ));
102
108
  return async (pkg) => {
103
109
  if (packageNameFromSpecifier(pkg) === void 0) {
104
110
  return import(
@@ -121,13 +127,14 @@ function createHostImporter(hostRoot = process.cwd()) {
121
127
  return import(pathToFileURL(resolved).href);
122
128
  }
123
129
  try {
124
- return await import(
125
- /* webpackIgnore: true */
126
- pkg
127
- );
130
+ return await importAsCaller(pkg);
128
131
  } catch (cause) {
129
132
  if (!isModuleNotFoundError(cause)) throw cause;
130
- throw hostImportError("undeclared", undeclaredMessage(declaration, cause), cause);
133
+ throw hostImportError(
134
+ "undeclared",
135
+ undeclaredMessage(declaration, cause, fallbackImport !== void 0),
136
+ cause
137
+ );
131
138
  }
132
139
  };
133
140
  }
package/dist/node.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/node.ts","../src/module-not-found.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/types/node` — the **node-only** slice of the shared utilities.\n *\n * WHY A SUBPATH AND NOT THE ROOT EXPORT. `@objectstack/types` is a dependency of\n * `@objectstack/hono`, whose whole reason to exist is \"edge-compatible REST API\n * server for Cloudflare Workers, Deno, Bun, and Node\" — and of the plugin/service\n * layer a `LiteKernel` boots on Workers. The root entry (`src/index.ts`) reaches\n * **zero** `node:` builtins today, and that is a property those consumers depend\n * on: a Workers bundle that pulls in `node:module` fails to build (or dies at\n * first call) even when nothing ever invokes it. Everything here needs\n * `node:module` / `node:url` by definition — it exists to drive Node's own\n * resolver — so it lives behind its own entry point instead.\n *\n * The isolation is structural, not conventional: `tsup` builds `src/index.ts` and\n * `src/node.ts` as separate entries with `splitting: false`, so the root bundle\n * contains no reference to this file, and `node-isolation.test.ts` fails the\n * build if anything reachable from the root ever imports a `node:` builtin. Same\n * arrangement `@objectstack/metadata` already ships for `./node`.\n *\n * ── What lives here ──────────────────────────────────────────────────────────\n *\n * Resolving optional packages from the **host app**, not from the framework\n * package doing the importing.\n *\n * Node ESM resolves a bare `import('pkg')` against the **importer's own\n * realpath**. Framework packages (the CLI, `@objectstack/verify`,\n * `@objectstack/dogfood`) are reached through `link:`/workspace dependencies, so\n * their realpath is inside the *framework* workspace — a bare import from any of\n * them can only ever see packages installed in the framework's own\n * `node_modules`. Every package that lives OUTSIDE that workspace and is supplied\n * by the app being served, verified or tested — a cloud-private package such as\n * `@objectstack/organizations` or `@objectstack/service-ai-studio`, or anything a\n * customer installs into their own project — is therefore invisible to a bare\n * import, no matter what the host app declares in its `package.json`\n * (cloud#1013: `objectstack serve` could never load the enterprise multi-org\n * runtime, so every self-hosted walled-posture deployment hit the ADR-0093 D5\n * fail-fast and exited 1; framework#4700: `bootStack({ multiTenant: true })` told\n * apps to install a package they had already installed, and the dogfood\n * multi-org probes were constant-false).\n *\n * The fix is to resolve from the host app's root and import the resolved\n * absolute path. The importing package's own resolution stays as the fallback,\n * for the framework-owned packages it depends on and the host does not declare.\n *\n * Resolution failure is the ONLY thing that falls back. A package the host\n * resolves but that throws while it evaluates is a genuine crash and propagates\n * unchanged: re-importing it bare would replace the real cause with a\n * `MODULE_NOT_FOUND`, which every caller here classifies as \"not installed\" —\n * turning a broken package into a silent skip (or, on the organizations path,\n * into a fatal message telling the operator to install what is already there).\n *\n * ── #4719: the host's DECLARATION gates the lookup, not its resolvability ────\n *\n * \"Resolve from the host app\" was implemented as a CJS `createRequire` anchored\n * at the host's `package.json`, and **CJS resolution honours `NODE_PATH`**\n * (`Module.globalPaths`). The first thing a pnpm-generated bin shim does is\n *\n * export NODE_PATH=\"<workspace>/node_modules/.pnpm/node_modules\"\n *\n * and every `serve` / `dev` child process inherits it. Everything any package in\n * the workspace transitively depends on lives in that hoisted store, so\n * `hostRequire.resolve(pkg)` succeeded for packages the host app had never\n * declared — the answer depended on HOW THE PROCESS WAS LAUNCHED, not on the\n * app. Measured on cloud's `apps/objectos-ee`, which did not declare\n * `@objectstack/organizations`: `pnpm start` (through the shim) booted with the\n * organizations plugin mounted and ADR-0093 D5 silent, while\n * `node node_modules/@objectstack/cli/bin/run.js serve` (no shim, no NODE_PATH)\n * hit the D5 fail-fast and exited 1. Same app, same `package.json`, same\n * posture. D5's own message told operators to \"declare it in the app's\n * package.json\" — the one thing the CLI never checked.\n *\n * So the host lookup is now gated on the host's **declaration**: a package name\n * is looked up in the host's `node_modules` only when it appears in the host\n * `package.json` (see {@link HOST_DECLARATION_FIELDS}). Reachability through a\n * hoisted store or `NODE_PATH` is deliberately not accepted — it is precisely\n * the accident that made the contract unenforced. This is the \"declared =\n * enforced\" shape the rest of the repo uses (Prime Directive #10): the\n * declaration is a deliberate authoring act, machine-checkable at the moment of\n * boot, and independent of launcher, package manager and hoist layout.\n *\n * The two failures it separates were, until now, one indistinguishable\n * `MODULE_NOT_FOUND`, with opposite remedies:\n *\n * - **undeclared** — the app never asked for this package. Remedy: declare it\n * in the app's `package.json` and install.\n * - **declared but unresolvable** — the app asked for it and the install is\n * broken/pruned/unbuilt. Remedy: fix the install. Re-reading the\n * `package.json` is wasted effort; the declaration is right there.\n *\n * {@link hostImportFailureKind} exposes that classification to callers so their\n * fail-fast text can say which one it is (`packages/cli` ADR-0093 D5,\n * `packages/verify` `bootStack`, `packages/qa/dogfood`'s enterprise probe).\n */\n\nimport { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { isModuleNotFoundError } from './module-not-found.js';\n\n/**\n * Imports a package as the host app would see it.\n *\n * `any` is the module namespace of a package this repo does not compile against\n * (it is not a dependency of the importing package at all) — every call site\n * reads an export off it dynamically, exactly as the bare `import()` it replaces\n * did.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type HostImporter = (pkg: string) => Promise<any>;\n\n/**\n * A `require` anchored at the **host app's** `package.json` — i.e. the project\n * `objectstack serve` was invoked in, or the app `bootStack` is verifying, whose\n * `node_modules` carries the packages it declares.\n *\n * @param hostRoot Directory holding the host app's `package.json` (default: the\n * process CWD, which is where the CLI reads `objectstack.config.ts` from too).\n */\nexport function createHostRequire(hostRoot: string = process.cwd()): NodeRequire {\n return createRequire(join(hostRoot, 'package.json'));\n}\n\n/**\n * The `package.json` fields whose KEYS count as a host-app declaration (#4719).\n *\n * All four are deliberate authoring acts in the app's own manifest that name the\n * package, which is the signal this gate is built on — not \"is it reachable\".\n * Why each is in:\n *\n * - `dependencies` — the obvious one: the app runs with it.\n * - `devDependencies` — the app being served / verified / dogfooded IS the\n * project, not a library someone else consumes, so its dev deps are installed\n * in exactly the environment this resolver runs in.\n * - `optionalDependencies` — npm/pnpm install them and tolerate an install\n * failure. \"Installed ⇒ declared\" holds; and if it did NOT install, the\n * declared-but-unresolvable branch says so precisely instead of pretending the\n * app never asked.\n * - `peerDependencies` — an app is nobody's peer, so this is an unusual place to\n * put an enterprise add-on; but it still NAMES the package on purpose, and\n * `packages/cli`'s own edition gate (`serve`'s AI-service opt-in, #1597) has\n * read all four since it was written. Accepting three here and four there\n * would fork \"declared\" into two dialects for one question — the shape Prime\n * Directive #12 exists to prevent. That gate now delegates to this list, so\n * there is one owner and one answer.\n *\n * `bundleDependencies` is absent on purpose: it is an array of names that must\n * ALSO appear in `dependencies`, so it can never be the only declaration.\n */\nexport const HOST_DECLARATION_FIELDS = [\n 'dependencies',\n 'devDependencies',\n 'optionalDependencies',\n 'peerDependencies',\n] as const;\n\nexport type HostDeclarationField = (typeof HOST_DECLARATION_FIELDS)[number];\n\n/** What the host app's `package.json` says about one package name. */\nexport interface HostDeclaration {\n /** Bare package name the specifier belongs to (subpath stripped). */\n packageName: string;\n /** Directory whose `package.json` was consulted. */\n hostRoot: string;\n /** True when {@link packageName} is a key of one of {@link HOST_DECLARATION_FIELDS}. */\n declared: boolean;\n /** Which field carried it (first match, in {@link HOST_DECLARATION_FIELDS} order). */\n field?: HostDeclarationField;\n /** The version range AS WRITTEN — `^1.2.3`, `workspace:*`, `npm:@acme/x@1`, `link:../x`. */\n specifier?: string;\n /** True when `hostRoot` has no readable / parseable `package.json` at all. */\n manifestMissing?: boolean;\n}\n\n/**\n * The package a bare specifier belongs to, or `undefined` when the specifier is\n * not a bare package name at all (a relative/absolute path, a `file:`/`data:`\n * URL, or a `node:`-prefixed builtin). Those bypass the declaration gate: they\n * are not things a `package.json` can declare.\n *\n * Subpaths are stripped, so `@objectstack/platform-objects/plugin` is declared\n * by `\"@objectstack/platform-objects\"`, which is the only key that can exist.\n * Scoped names keep both segments.\n *\n * Alias dependencies need no special case, and that is the point: with\n * `\"foo\": \"npm:bar@1\"` the importable specifier is `foo` and the manifest key is\n * `foo`, so keying on the KEY (never the value) is exactly right — `import('bar')`\n * correctly reads as undeclared unless `bar` is itself a key. Same for\n * `workspace:` / `link:` / `file:` specifiers: the key is the name, the value is\n * the package manager's business.\n */\nexport function packageNameFromSpecifier(specifier: string): string | undefined {\n if (!specifier || specifier.startsWith('.') || specifier.startsWith('/')) return undefined;\n // A URL-ish or protocol-prefixed specifier (`node:fs`, `file:///…`, `data:…`).\n if (/^[a-z][a-z0-9+.-]*:/i.test(specifier)) return undefined;\n const segments = specifier.split('/');\n if (specifier.startsWith('@')) {\n if (segments.length < 2 || !segments[0] || !segments[1]) return undefined;\n return `${segments[0]}/${segments[1]}`;\n }\n return segments[0] || undefined;\n}\n\n/**\n * Read what the host app's `package.json` declares about `specifier`.\n *\n * Deliberately a plain manifest READ, never a resolution attempt: resolvability\n * is the property #4719 proved unreliable (it moved with `NODE_PATH` and the\n * hoist layout), while the manifest is the same fact in every launcher.\n */\nexport function readHostDeclaration(\n specifier: string,\n hostRoot: string = process.cwd(),\n): HostDeclaration {\n const packageName = packageNameFromSpecifier(specifier) ?? specifier;\n const base: HostDeclaration = { packageName, hostRoot, declared: false };\n\n let manifest: Record<string, unknown>;\n try {\n manifest = JSON.parse(readFileSync(join(hostRoot, 'package.json'), 'utf8')) as Record<\n string,\n unknown\n >;\n } catch {\n // No manifest ⇒ nothing is declared. Recorded rather than swallowed so the\n // failure text can say \"there is no package.json here\" instead of the\n // misleading \"you did not declare it\".\n return { ...base, manifestMissing: true };\n }\n\n for (const field of HOST_DECLARATION_FIELDS) {\n const entries = manifest[field];\n if (!entries || typeof entries !== 'object') continue;\n const specifierValue = (entries as Record<string, unknown>)[packageName];\n if (specifierValue === undefined) continue;\n return { ...base, declared: true, field, specifier: String(specifierValue) };\n }\n return base;\n}\n\n/** Convenience predicate over {@link readHostDeclaration}. */\nexport function isDeclaredByHost(specifier: string, hostRoot?: string): boolean {\n return readHostDeclaration(specifier, hostRoot).declared;\n}\n\n/**\n * Why a {@link HostImporter} could not produce a module.\n *\n * - `undeclared` — the host app's `package.json` never names the package, and\n * the importing framework package cannot supply it either. Remedy: DECLARE it\n * in the app and install.\n * - `declared-unresolvable` — the app declares it and it still would not\n * resolve. Remedy: fix the INSTALL. Re-reading the manifest is wasted effort.\n *\n * An evaluation crash is neither: it propagates untouched and carries no kind.\n */\nexport type HostImportFailureKind = 'undeclared' | 'declared-unresolvable';\n\n/**\n * Property carrying {@link HostImportFailureKind} on a thrown error.\n *\n * A string property, read by {@link hostImportFailureKind} — never `instanceof`.\n * `serve` loads plugins through this importer, so CLI and package can hold\n * different module instances of anything class-shaped; the #4818 comment in\n * `serve.ts` names that trap explicitly.\n */\nexport const HOST_IMPORT_FAILURE_KIND = 'objectstackHostImportFailureKind';\n\n/** The classification on an error thrown by a {@link HostImporter}, if any. */\nexport function hostImportFailureKind(err: unknown): HostImportFailureKind | undefined {\n const kind = (err as Record<string, unknown> | null | undefined)?.[HOST_IMPORT_FAILURE_KIND];\n return kind === 'undeclared' || kind === 'declared-unresolvable' ? kind : undefined;\n}\n\nfunction hostImportError(\n kind: HostImportFailureKind,\n message: string,\n cause: unknown,\n): Error {\n // `cause` is assigned rather than passed to the constructor: this package\n // compiles against a lib without the ES2022 `ErrorOptions` overload.\n const err = new Error(message);\n // Every caller classifies \"missing vs crashed\" through\n // `isModuleNotFoundError`; both of these ARE the missing case, just with\n // different remedies, so they must keep answering true to it.\n return Object.assign(err, {\n cause,\n code: 'MODULE_NOT_FOUND',\n [HOST_IMPORT_FAILURE_KIND]: kind,\n });\n}\n\nfunction undeclaredMessage(declaration: HostDeclaration, cause: unknown): string {\n const { packageName, hostRoot, manifestMissing } = declaration;\n const detail = cause instanceof Error ? cause.message : String(cause);\n return (\n `Cannot find package '${packageName}': the host app does not declare it.\\n` +\n ` host app: ${hostRoot}\\n` +\n (manifestMissing\n ? ' no readable package.json was found there — nothing can be declared\\n'\n : ` checked: ${HOST_DECLARATION_FIELDS.join(', ')}\\n`) +\n `\\n Declare it in that app's package.json and install it, e.g.\\n` +\n ` cd ${hostRoot} && pnpm add ${packageName}\\n` +\n '\\n Being merely REACHABLE is not enough and is rejected on purpose (#4719):\\n' +\n ' a package hoisted into a workspace store — which is what NODE_PATH points\\n' +\n \" at in every pnpm bin shim — used to resolve here regardless of the app's\\n\" +\n ' package.json, so the same app booted or refused depending on how the\\n' +\n ' process was launched. The declaration is the contract.\\n' +\n ` (fallback resolution also failed: ${detail})`\n );\n}\n\nfunction unresolvableMessage(declaration: HostDeclaration, cause: unknown): string {\n const { packageName, hostRoot, field, specifier } = declaration;\n const detail = cause instanceof Error ? cause.message : String(cause);\n return (\n `Cannot find module '${packageName}': the host app DECLARES it ` +\n `(${field}: ${JSON.stringify(specifier)}) but it could not be resolved.\\n` +\n ` host app: ${hostRoot}\\n` +\n '\\n This is an INSTALL problem, not a declaration problem — the declaration is\\n' +\n ' already there, so re-reading the package.json will not help. Check:\\n' +\n ` • dependencies never installed, or installed before the declaration was added → run \\`pnpm install\\` in ${hostRoot}\\n` +\n ' • a production prune / filtered deploy dropped it (devDependencies and\\n' +\n ' optionalDependencies go first)\\n' +\n ' • it IS installed but its \"main\"/\"exports\" points at a dist that was never built\\n' +\n ` (resolver: ${detail})`\n );\n}\n\n/**\n * Build an importer that loads a package **as the host app declares it**, and\n * otherwise falls back to the importing package's own resolution.\n *\n * Order of operations, and why (#4719):\n *\n * 1. The host `package.json` is READ. Only a declared name is looked up in the\n * host's `node_modules`. An undeclared name never reaches the host resolver,\n * so no amount of `NODE_PATH` / hoisting can make it appear to be the app's.\n * 2. Declared but unresolvable is reported AS SUCH — the app asked for it and\n * the install is broken. It is not retried bare: falling back there would\n * reintroduce exactly the \"some other package happens to supply it\" accident\n * this gate closes, and would report an install problem as an absence.\n * 3. Undeclared falls back to the importing package's own resolution, which is\n * what keeps every framework-owned load working (`serve`'s plugin-auth /\n * service-i18n path, `bootStack`'s service plugins). Bare `import()` is ESM,\n * and ESM does not honour `NODE_PATH`, so the fallback cannot re-open the\n * hole either. Only when that fails as module-not-found does the undeclared\n * error surface; a package that RESOLVES and then throws while evaluating is\n * a genuine crash and propagates untouched, as before.\n *\n * @param hostRoot Directory holding the host app's `package.json` (default: the\n * process CWD, which is where the CLI reads `objectstack.config.ts` from too).\n * Note this used to take a pre-built `NodeRequire`; it needs the ROOT now,\n * because a `NodeRequire` cannot be asked where it was anchored and the manifest\n * has to be read from there.\n */\nexport function createHostImporter(hostRoot: string = process.cwd()): HostImporter {\n const hostRequire = createHostRequire(hostRoot);\n return async (pkg: string): Promise<any> => {\n // Not a bare package name (a path, a URL, a `node:` builtin) — nothing a\n // manifest could declare. Hand it to the normal resolver untouched.\n if (packageNameFromSpecifier(pkg) === undefined) {\n return import(/* webpackIgnore: true */ pkg);\n }\n\n const declaration = readHostDeclaration(pkg, hostRoot);\n\n if (declaration.declared) {\n let resolved: string;\n try {\n resolved = hostRequire.resolve(pkg);\n } catch (cause) {\n throw hostImportError(\n 'declared-unresolvable',\n unresolvableMessage(declaration, cause),\n cause,\n );\n }\n return import(pathToFileURL(resolved).href);\n }\n\n try {\n return await import(/* webpackIgnore: true */ pkg);\n } catch (cause) {\n // A package that resolved and then exploded is a crash, not an absence.\n if (!isModuleNotFoundError(cause)) throw cause;\n throw hostImportError('undeclared', undeclaredMessage(declaration, cause), cause);\n }\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * True when a dynamic `import()` / `require.resolve()` failed because the\n * module is simply NOT INSTALLED — as opposed to the module being present but\n * throwing while it loads (a real crash). Checking `err.code` FIRST matters:\n * ESM reports a missing package as `err.code === 'ERR_MODULE_NOT_FOUND'` with\n * the human message `Cannot find package '...'`; matching only the older\n * `Cannot find module` string mis-classifies that as a crash (framework#1595).\n *\n * Single shared owner for this classification (framework#3265): the CLI's\n * optional-plugin guards and `requires` capability resolver delegate here, and\n * cloud's `objectos-runtime` capability loader is expected to adopt it at its\n * next framework pin bump — so the parallel loaders cannot drift apart and\n * re-introduce the #1595 false-alarm class.\n */\nexport function isModuleNotFoundError(err: unknown): boolean {\n const code = (err as { code?: string } | null | undefined)?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;\n const msg = err instanceof Error ? err.message : String(err);\n return msg.includes('Cannot find module') || msg.includes('Cannot find package');\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/node.ts","../src/module-not-found.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/types/node` — the **node-only** slice of the shared utilities.\n *\n * WHY A SUBPATH AND NOT THE ROOT EXPORT. `@objectstack/types` is a dependency of\n * `@objectstack/hono`, whose whole reason to exist is \"edge-compatible REST API\n * server for Cloudflare Workers, Deno, Bun, and Node\" — and of the plugin/service\n * layer a `LiteKernel` boots on Workers. The root entry (`src/index.ts`) reaches\n * **zero** `node:` builtins today, and that is a property those consumers depend\n * on: a Workers bundle that pulls in `node:module` fails to build (or dies at\n * first call) even when nothing ever invokes it. Everything here needs\n * `node:module` / `node:url` by definition — it exists to drive Node's own\n * resolver — so it lives behind its own entry point instead.\n *\n * The isolation is structural, not conventional: `tsup` builds `src/index.ts` and\n * `src/node.ts` as separate entries with `splitting: false`, so the root bundle\n * contains no reference to this file, and `node-isolation.test.ts` fails the\n * build if anything reachable from the root ever imports a `node:` builtin. Same\n * arrangement `@objectstack/metadata` already ships for `./node`.\n *\n * ── What lives here ──────────────────────────────────────────────────────────\n *\n * Resolving optional packages from the **host app**, not from the framework\n * package doing the importing.\n *\n * Node ESM resolves a bare `import('pkg')` against the **importer's own\n * realpath**. Framework packages (the CLI, `@objectstack/verify`,\n * `@objectstack/dogfood`) are reached through `link:`/workspace dependencies, so\n * their realpath is inside the *framework* workspace — a bare import from any of\n * them can only ever see packages installed in the framework's own\n * `node_modules`. Every package that lives OUTSIDE that workspace and is supplied\n * by the app being served, verified or tested — a cloud-private package such as\n * `@objectstack/organizations` or `@objectstack/service-ai-studio`, or anything a\n * customer installs into their own project — is therefore invisible to a bare\n * import, no matter what the host app declares in its `package.json`\n * (cloud#1013: `objectstack serve` could never load the enterprise multi-org\n * runtime, so every self-hosted walled-posture deployment hit the ADR-0093 D5\n * fail-fast and exited 1; framework#4700: `bootStack({ multiTenant: true })` told\n * apps to install a package they had already installed, and the dogfood\n * multi-org probes were constant-false).\n *\n * The fix is to resolve from the host app's root and import the resolved\n * absolute path. The importing package's own resolution stays as the fallback,\n * for the framework-owned packages it depends on and the host does not declare\n * — and since #10943 that fallback is the base the CALLER hands in\n * ({@link HostImporterOptions.fallbackImport}), because a fallback written here\n * resolved from `@objectstack/types` and could only ever see\n * `@objectstack/spec`. Same defect class as the paragraph above, one level up:\n * a bare import resolves against the module that CONTAINS it, and this module\n * is not the one doing the asking.\n *\n * Resolution failure is the ONLY thing that falls back. A package the host\n * resolves but that throws while it evaluates is a genuine crash and propagates\n * unchanged: re-importing it bare would replace the real cause with a\n * `MODULE_NOT_FOUND`, which every caller here classifies as \"not installed\" —\n * turning a broken package into a silent skip (or, on the organizations path,\n * into a fatal message telling the operator to install what is already there).\n *\n * ── #4719: the host's DECLARATION gates the lookup, not its resolvability ────\n *\n * \"Resolve from the host app\" was implemented as a CJS `createRequire` anchored\n * at the host's `package.json`, and **CJS resolution honours `NODE_PATH`**\n * (`Module.globalPaths`). The first thing a pnpm-generated bin shim does is\n *\n * export NODE_PATH=\"<workspace>/node_modules/.pnpm/node_modules\"\n *\n * and every `serve` / `dev` child process inherits it. Everything any package in\n * the workspace transitively depends on lives in that hoisted store, so\n * `hostRequire.resolve(pkg)` succeeded for packages the host app had never\n * declared — the answer depended on HOW THE PROCESS WAS LAUNCHED, not on the\n * app. Measured on cloud's `apps/objectos-ee`, which did not declare\n * `@objectstack/organizations`: `pnpm start` (through the shim) booted with the\n * organizations plugin mounted and ADR-0093 D5 silent, while\n * `node node_modules/@objectstack/cli/bin/run.js serve` (no shim, no NODE_PATH)\n * hit the D5 fail-fast and exited 1. Same app, same `package.json`, same\n * posture. D5's own message told operators to \"declare it in the app's\n * package.json\" — the one thing the CLI never checked.\n *\n * So the host lookup is now gated on the host's **declaration**: a package name\n * is looked up in the host's `node_modules` only when it appears in the host\n * `package.json` (see {@link HOST_DECLARATION_FIELDS}). Reachability through a\n * hoisted store or `NODE_PATH` is deliberately not accepted — it is precisely\n * the accident that made the contract unenforced. This is the \"declared =\n * enforced\" shape the rest of the repo uses (Prime Directive #10): the\n * declaration is a deliberate authoring act, machine-checkable at the moment of\n * boot, and independent of launcher, package manager and hoist layout.\n *\n * The two failures it separates were, until now, one indistinguishable\n * `MODULE_NOT_FOUND`, with opposite remedies:\n *\n * - **undeclared** — the app never asked for this package. Remedy: declare it\n * in the app's `package.json` and install.\n * - **declared but unresolvable** — the app asked for it and the install is\n * broken/pruned/unbuilt. Remedy: fix the install. Re-reading the\n * `package.json` is wasted effort; the declaration is right there.\n *\n * {@link hostImportFailureKind} exposes that classification to callers so their\n * fail-fast text can say which one it is (`packages/cli` ADR-0093 D5,\n * `packages/verify` `bootStack`, `packages/qa/dogfood`'s enterprise probe).\n */\n\nimport { readFileSync } from 'node:fs';\nimport { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { isModuleNotFoundError } from './module-not-found.js';\n\n/**\n * Imports a package as the host app would see it.\n *\n * `any` is the module namespace of a package this repo does not compile against\n * (it is not a dependency of the importing package at all) — every call site\n * reads an export off it dynamically, exactly as the bare `import()` it replaces\n * did.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type HostImporter = (pkg: string) => Promise<any>;\n\n/**\n * The importing package's OWN dynamic import — write it literally, in the\n * calling module:\n *\n * createHostImporter(hostRoot, { fallbackImport: (s) => import(s) })\n *\n * `any` for the same reason {@link HostImporter} uses it: the module namespace\n * belongs to a package this repo does not compile against.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type FallbackImport = (specifier: string) => Promise<any>;\n\n/** Options for {@link createHostImporter}. */\nexport interface HostImporterOptions {\n /**\n * The resolution base for everything the host app does NOT declare — supplied\n * as the caller's own `import()` rather than as a URL string, because a\n * string base was MEASURED to be unimplementable without a regression. See\n * {@link createHostImporter}'s \"why a function\" note for both measurements.\n *\n * Omitted ⇒ the fallback resolves from `@objectstack/types`, which sees only\n * `@objectstack/types`'s own dependencies. That default is retained so an\n * out-of-tree caller cannot be broken by this parameter's arrival, and the\n * `undeclared` failure text names it explicitly so the gap reports itself\n * instead of being rediscovered.\n */\n fallbackImport?: FallbackImport;\n}\n\n/**\n * A `require` anchored at the **host app's** `package.json` — i.e. the project\n * `objectstack serve` was invoked in, or the app `bootStack` is verifying, whose\n * `node_modules` carries the packages it declares.\n *\n * @param hostRoot Directory holding the host app's `package.json` (default: the\n * process CWD, which is where the CLI reads `objectstack.config.ts` from too).\n */\nexport function createHostRequire(hostRoot: string = process.cwd()): NodeRequire {\n return createRequire(join(hostRoot, 'package.json'));\n}\n\n/**\n * The `package.json` fields whose KEYS count as a host-app declaration (#4719).\n *\n * All four are deliberate authoring acts in the app's own manifest that name the\n * package, which is the signal this gate is built on — not \"is it reachable\".\n * Why each is in:\n *\n * - `dependencies` — the obvious one: the app runs with it.\n * - `devDependencies` — the app being served / verified / dogfooded IS the\n * project, not a library someone else consumes, so its dev deps are installed\n * in exactly the environment this resolver runs in.\n * - `optionalDependencies` — npm/pnpm install them and tolerate an install\n * failure. \"Installed ⇒ declared\" holds; and if it did NOT install, the\n * declared-but-unresolvable branch says so precisely instead of pretending the\n * app never asked.\n * - `peerDependencies` — an app is nobody's peer, so this is an unusual place to\n * put an enterprise add-on; but it still NAMES the package on purpose, and\n * `packages/cli`'s own edition gate (`serve`'s AI-service opt-in, #1597) has\n * read all four since it was written. Accepting three here and four there\n * would fork \"declared\" into two dialects for one question — the shape Prime\n * Directive #12 exists to prevent. That gate now delegates to this list, so\n * there is one owner and one answer.\n *\n * `bundleDependencies` is absent on purpose: it is an array of names that must\n * ALSO appear in `dependencies`, so it can never be the only declaration.\n */\nexport const HOST_DECLARATION_FIELDS = [\n 'dependencies',\n 'devDependencies',\n 'optionalDependencies',\n 'peerDependencies',\n] as const;\n\nexport type HostDeclarationField = (typeof HOST_DECLARATION_FIELDS)[number];\n\n/** What the host app's `package.json` says about one package name. */\nexport interface HostDeclaration {\n /** Bare package name the specifier belongs to (subpath stripped). */\n packageName: string;\n /** Directory whose `package.json` was consulted. */\n hostRoot: string;\n /** True when {@link packageName} is a key of one of {@link HOST_DECLARATION_FIELDS}. */\n declared: boolean;\n /** Which field carried it (first match, in {@link HOST_DECLARATION_FIELDS} order). */\n field?: HostDeclarationField;\n /** The version range AS WRITTEN — `^1.2.3`, `workspace:*`, `npm:@acme/x@1`, `link:../x`. */\n specifier?: string;\n /** True when `hostRoot` has no readable / parseable `package.json` at all. */\n manifestMissing?: boolean;\n}\n\n/**\n * The package a bare specifier belongs to, or `undefined` when the specifier is\n * not a bare package name at all (a relative/absolute path, a `file:`/`data:`\n * URL, or a `node:`-prefixed builtin). Those bypass the declaration gate: they\n * are not things a `package.json` can declare.\n *\n * Subpaths are stripped, so `@objectstack/platform-objects/plugin` is declared\n * by `\"@objectstack/platform-objects\"`, which is the only key that can exist.\n * Scoped names keep both segments.\n *\n * Alias dependencies need no special case, and that is the point: with\n * `\"foo\": \"npm:bar@1\"` the importable specifier is `foo` and the manifest key is\n * `foo`, so keying on the KEY (never the value) is exactly right — `import('bar')`\n * correctly reads as undeclared unless `bar` is itself a key. Same for\n * `workspace:` / `link:` / `file:` specifiers: the key is the name, the value is\n * the package manager's business.\n */\nexport function packageNameFromSpecifier(specifier: string): string | undefined {\n if (!specifier || specifier.startsWith('.') || specifier.startsWith('/')) return undefined;\n // A URL-ish or protocol-prefixed specifier (`node:fs`, `file:///…`, `data:…`).\n if (/^[a-z][a-z0-9+.-]*:/i.test(specifier)) return undefined;\n const segments = specifier.split('/');\n if (specifier.startsWith('@')) {\n if (segments.length < 2 || !segments[0] || !segments[1]) return undefined;\n return `${segments[0]}/${segments[1]}`;\n }\n return segments[0] || undefined;\n}\n\n/**\n * Read what the host app's `package.json` declares about `specifier`.\n *\n * Deliberately a plain manifest READ, never a resolution attempt: resolvability\n * is the property #4719 proved unreliable (it moved with `NODE_PATH` and the\n * hoist layout), while the manifest is the same fact in every launcher.\n */\nexport function readHostDeclaration(\n specifier: string,\n hostRoot: string = process.cwd(),\n): HostDeclaration {\n const packageName = packageNameFromSpecifier(specifier) ?? specifier;\n const base: HostDeclaration = { packageName, hostRoot, declared: false };\n\n let manifest: Record<string, unknown>;\n try {\n manifest = JSON.parse(readFileSync(join(hostRoot, 'package.json'), 'utf8')) as Record<\n string,\n unknown\n >;\n } catch {\n // No manifest ⇒ nothing is declared. Recorded rather than swallowed so the\n // failure text can say \"there is no package.json here\" instead of the\n // misleading \"you did not declare it\".\n return { ...base, manifestMissing: true };\n }\n\n for (const field of HOST_DECLARATION_FIELDS) {\n const entries = manifest[field];\n if (!entries || typeof entries !== 'object') continue;\n const specifierValue = (entries as Record<string, unknown>)[packageName];\n if (specifierValue === undefined) continue;\n return { ...base, declared: true, field, specifier: String(specifierValue) };\n }\n return base;\n}\n\n/** Convenience predicate over {@link readHostDeclaration}. */\nexport function isDeclaredByHost(specifier: string, hostRoot?: string): boolean {\n return readHostDeclaration(specifier, hostRoot).declared;\n}\n\n/**\n * Why a {@link HostImporter} could not produce a module.\n *\n * - `undeclared` — the host app's `package.json` never names the package, and\n * the importing framework package cannot supply it either. Remedy: DECLARE it\n * in the app and install.\n * - `declared-unresolvable` — the app declares it and it still would not\n * resolve. Remedy: fix the INSTALL. Re-reading the manifest is wasted effort.\n *\n * An evaluation crash is neither: it propagates untouched and carries no kind.\n */\nexport type HostImportFailureKind = 'undeclared' | 'declared-unresolvable';\n\n/**\n * Property carrying {@link HostImportFailureKind} on a thrown error.\n *\n * A string property, read by {@link hostImportFailureKind} — never `instanceof`.\n * `serve` loads plugins through this importer, so CLI and package can hold\n * different module instances of anything class-shaped; the #4818 comment in\n * `serve.ts` names that trap explicitly.\n */\nexport const HOST_IMPORT_FAILURE_KIND = 'objectstackHostImportFailureKind';\n\n/** The classification on an error thrown by a {@link HostImporter}, if any. */\nexport function hostImportFailureKind(err: unknown): HostImportFailureKind | undefined {\n const kind = (err as Record<string, unknown> | null | undefined)?.[HOST_IMPORT_FAILURE_KIND];\n return kind === 'undeclared' || kind === 'declared-unresolvable' ? kind : undefined;\n}\n\nfunction hostImportError(\n kind: HostImportFailureKind,\n message: string,\n cause: unknown,\n): Error {\n // `cause` is assigned rather than passed to the constructor: this package\n // compiles against a lib without the ES2022 `ErrorOptions` overload.\n const err = new Error(message);\n // Every caller classifies \"missing vs crashed\" through\n // `isModuleNotFoundError`; both of these ARE the missing case, just with\n // different remedies, so they must keep answering true to it.\n return Object.assign(err, {\n cause,\n code: 'MODULE_NOT_FOUND',\n [HOST_IMPORT_FAILURE_KIND]: kind,\n });\n}\n\n/**\n * @param callerBaseSupplied Did the caller state its own resolution base\n * ({@link HostImporterOptions.fallbackImport})? When it did not, the fallback\n * ran from `@objectstack/types`, which sees only `@objectstack/spec` — so the\n * absence being reported may be an artefact of the missing base rather than a\n * real one. #10943 kept that default for out-of-tree callers; saying so here is\n * what stops it being silent, because the alternative is a reader re-deriving\n * the whole measurement from a `MODULE_NOT_FOUND` that names nothing.\n */\nfunction undeclaredMessage(\n declaration: HostDeclaration,\n cause: unknown,\n callerBaseSupplied: boolean,\n): string {\n const { packageName, hostRoot, manifestMissing } = declaration;\n const detail = cause instanceof Error ? cause.message : String(cause);\n const baseNote = callerBaseSupplied\n ? ''\n : '\\n (the caller did not pass `fallbackImport`, so that fallback resolved from\\n' +\n \" @objectstack/types, which can see only its own dependencies — a caller that\\n\" +\n ' needs its own resolution passes `{ fallbackImport: (s) => import(s) }`, #10943)';\n return (\n `Cannot find package '${packageName}': the host app does not declare it.\\n` +\n ` host app: ${hostRoot}\\n` +\n (manifestMissing\n ? ' no readable package.json was found there — nothing can be declared\\n'\n : ` checked: ${HOST_DECLARATION_FIELDS.join(', ')}\\n`) +\n `\\n Declare it in that app's package.json and install it, e.g.\\n` +\n ` cd ${hostRoot} && pnpm add ${packageName}\\n` +\n '\\n Being merely REACHABLE is not enough and is rejected on purpose (#4719):\\n' +\n ' a package hoisted into a workspace store — which is what NODE_PATH points\\n' +\n \" at in every pnpm bin shim — used to resolve here regardless of the app's\\n\" +\n ' package.json, so the same app booted or refused depending on how the\\n' +\n ' process was launched. The declaration is the contract.\\n' +\n ` (fallback resolution also failed: ${detail})${baseNote}`\n );\n}\n\nfunction unresolvableMessage(declaration: HostDeclaration, cause: unknown): string {\n const { packageName, hostRoot, field, specifier } = declaration;\n const detail = cause instanceof Error ? cause.message : String(cause);\n return (\n `Cannot find module '${packageName}': the host app DECLARES it ` +\n `(${field}: ${JSON.stringify(specifier)}) but it could not be resolved.\\n` +\n ` host app: ${hostRoot}\\n` +\n '\\n This is an INSTALL problem, not a declaration problem — the declaration is\\n' +\n ' already there, so re-reading the package.json will not help. Check:\\n' +\n ` • dependencies never installed, or installed before the declaration was added → run \\`pnpm install\\` in ${hostRoot}\\n` +\n ' • a production prune / filtered deploy dropped it (devDependencies and\\n' +\n ' optionalDependencies go first)\\n' +\n ' • it IS installed but its \"main\"/\"exports\" points at a dist that was never built\\n' +\n ` (resolver: ${detail})`\n );\n}\n\n/**\n * Build an importer that loads a package **as the host app declares it**, and\n * otherwise falls back to the importing package's own resolution.\n *\n * Order of operations, and why (#4719):\n *\n * 1. The host `package.json` is READ. Only a declared name is looked up in the\n * host's `node_modules`. An undeclared name never reaches the host resolver,\n * so no amount of `NODE_PATH` / hoisting can make it appear to be the app's.\n * 2. Declared but unresolvable is reported AS SUCH — the app asked for it and\n * the install is broken. It is not retried bare: falling back there would\n * reintroduce exactly the \"some other package happens to supply it\" accident\n * this gate closes, and would report an install problem as an absence.\n * 3. Undeclared falls back to the CALLER's own resolution, which is what keeps\n * every framework-owned load working (`serve`'s plugin-auth / service-i18n\n * path, `bootStack`'s service plugins). Bare `import()` is ESM, and ESM does\n * not honour `NODE_PATH`, so the fallback cannot re-open the hole either.\n * Only when that fails as module-not-found does the undeclared error\n * surface; a package that RESOLVES and then throws while evaluating is a\n * genuine crash and propagates untouched, as before.\n *\n * ── The caller supplies that base, and why it is a FUNCTION (#10943) ─────────\n *\n * Step 3 said \"the importing package's own resolution\" long before anything\n * made it true. The fallback was a bare `import()` written HERE, and ESM\n * resolves a bare specifier against the module containing the call — so it\n * resolved from `@objectstack/types`, which under a pnpm-isolated layout can\n * see only `@objectstack/types`'s own dependencies. Measured on `main` from an\n * app declaring nothing, `@objectstack/plugin-auth`, `@objectstack/plugin-audit`\n * and `chalk` all resolve from `packages/cli` and all failed through this\n * helper; `@objectstack/spec` — the one dependency this package declares — was\n * the only name that came back OK, which is the whole pattern. Under a hoisted\n * npm/yarn layout the same fallback usually DOES find the caller's\n * dependencies, so the claim was green in some installs and absent in others:\n * the layout-dependence class cloud#1013 and #10645 exist to close, one level\n * up. A declared contract the implementation does not keep is the thing this\n * repo fixes at the producer (Prime Directive #12), so the mechanism moved\n * rather than the sentence.\n *\n * The base arrives as the caller's own `import()` and NOT as a `parentURL` /\n * `import.meta.url` string. Both string spellings were measured on Node\n * v22.22.2 and both are wrong:\n *\n * - `import.meta.resolve(specifier, parentURL)` — the parent argument is\n * SILENTLY IGNORED without `--experimental-import-meta-resolve`. Measured:\n * resolving `@objectstack/plugin-auth` against a `packages/types` parent\n * returned `packages/cli/node_modules/...`, i.e. the caller's own answer,\n * byte-identical to passing no parent at all. It would have compiled, run,\n * and pinned green while ignoring the base — a phantom fix of exactly the\n * kind this card is about.\n * - `createRequire(parentURL).resolve(specifier)` — CJS resolution, which\n * honours `NODE_PATH`. Measured against a store reachable only through\n * `NODE_PATH`: the CJS resolve found it (with and without the `paths`\n * option, since GLOBAL_FOLDERS are always appended) while the ESM bare\n * `import()` did not. That is #4719's hole re-opened on the fallback path,\n * and it would have falsified the \"ESM does not honour NODE_PATH\" sentence\n * three lines above.\n *\n * A function written in the calling module is the only spelling that uses\n * Node's real ESM resolver anchored where the caller actually lives: no flag,\n * no `NODE_PATH`, no second resolution algorithm to drift from the first.\n *\n * @param hostRoot Directory holding the host app's `package.json` (default: the\n * process CWD, which is where the CLI reads `objectstack.config.ts` from too).\n * Note this used to take a pre-built `NodeRequire`; it needs the ROOT now,\n * because a `NodeRequire` cannot be asked where it was anchored and the manifest\n * has to be read from there.\n * @param options {@link HostImporterOptions.fallbackImport} carries the caller's\n * resolution base. Omitting it keeps the pre-#10943 behaviour (this package's\n * own resolution) so no out-of-tree caller changes under its feet.\n */\nexport function createHostImporter(\n hostRoot: string = process.cwd(),\n options: HostImporterOptions = {},\n): HostImporter {\n const hostRequire = createHostRequire(hostRoot);\n const { fallbackImport } = options;\n const importAsCaller: FallbackImport =\n fallbackImport ?? ((specifier) => import(/* webpackIgnore: true */ specifier));\n return async (pkg: string): Promise<any> => {\n // Not a bare package name (a path, a URL, a `node:` builtin) — nothing a\n // manifest could declare. Hand it to the normal resolver untouched.\n //\n // ⚠️ Deliberately NOT re-based onto `fallbackImport` (#10943). Every\n // base-INDEPENDENT spelling here — `file://`, `node:`, `data:`, an absolute\n // path — means the same module whoever imports it, so the base is not a\n // question they can even ask. The one spelling it WOULD move is a RELATIVE\n // one, and where that should resolve from is an open policy question owned\n // by #10944 (`serve` refuses a relative `plugins: [...]` entry rather than\n // silently re-basing it) — with a measured consumer count of zero here:\n // `serve` handles non-package specifiers before this helper is reached, and\n // `bootStack` / the dogfood probe pass package names only. Answering half\n // of another card's undecided question, for nobody, is not a repair.\n if (packageNameFromSpecifier(pkg) === undefined) {\n return import(/* webpackIgnore: true */ pkg);\n }\n\n const declaration = readHostDeclaration(pkg, hostRoot);\n\n if (declaration.declared) {\n let resolved: string;\n try {\n resolved = hostRequire.resolve(pkg);\n } catch (cause) {\n throw hostImportError(\n 'declared-unresolvable',\n unresolvableMessage(declaration, cause),\n cause,\n );\n }\n return import(pathToFileURL(resolved).href);\n }\n\n try {\n return await importAsCaller(pkg);\n } catch (cause) {\n // A package that resolved and then exploded is a crash, not an absence.\n if (!isModuleNotFoundError(cause)) throw cause;\n throw hostImportError(\n 'undeclared',\n undeclaredMessage(declaration, cause, fallbackImport !== undefined),\n cause,\n );\n }\n };\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * True when a dynamic `import()` / `require.resolve()` failed because the\n * module is simply NOT INSTALLED — as opposed to the module being present but\n * throwing while it loads (a real crash). Checking `err.code` FIRST matters:\n * ESM reports a missing package as `err.code === 'ERR_MODULE_NOT_FOUND'` with\n * the human message `Cannot find package '...'`; matching only the older\n * `Cannot find module` string mis-classifies that as a crash (framework#1595).\n *\n * Single shared owner for this classification (framework#3265): the CLI's\n * optional-plugin guards and `requires` capability resolver delegate here, and\n * cloud's `objectos-runtime` capability loader is expected to adopt it at its\n * next framework pin bump — so the parallel loaders cannot drift apart and\n * re-introduce the #1595 false-alarm class.\n */\nexport function isModuleNotFoundError(err: unknown): boolean {\n const code = (err as { code?: string } | null | undefined)?.code;\n if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') return true;\n const msg = err instanceof Error ? err.message : String(err);\n return msg.includes('Cannot find module') || msg.includes('Cannot find package');\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/types",
3
- "version": "17.1.0",
3
+ "version": "17.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Shared interfaces describing the ObjectStack Runtime environment",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,7 @@
18
18
  }
19
19
  },
20
20
  "dependencies": {
21
- "@objectstack/spec": "17.1.0"
21
+ "@objectstack/spec": "17.2.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "typescript": "^6.0.3",