@open-mercato/shared 0.6.7 → 0.6.8-develop.6874.1.982d6097d8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +57 -6
  3. package/build.mjs +2 -4
  4. package/dist/lib/ai/safety-identifier.js +32 -0
  5. package/dist/lib/ai/safety-identifier.js.map +7 -0
  6. package/dist/lib/auth/organizationScope.js +34 -0
  7. package/dist/lib/auth/organizationScope.js.map +7 -0
  8. package/dist/lib/auth/server.js +15 -2
  9. package/dist/lib/auth/server.js.map +2 -2
  10. package/dist/lib/bootstrap/clientOnlyModules.js +55 -0
  11. package/dist/lib/bootstrap/clientOnlyModules.js.map +7 -0
  12. package/dist/lib/bootstrap/dynamicLoader.js +117 -43
  13. package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
  14. package/dist/lib/commands/command-bus.js +5 -1
  15. package/dist/lib/commands/command-bus.js.map +2 -2
  16. package/dist/lib/commands/command-interceptor-runner.js +2 -2
  17. package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
  18. package/dist/lib/crud/enricher-runner.js +2 -11
  19. package/dist/lib/crud/enricher-runner.js.map +2 -2
  20. package/dist/lib/crud/factory.js +72 -9
  21. package/dist/lib/crud/factory.js.map +2 -2
  22. package/dist/lib/crud/interceptor-runner.js +2 -2
  23. package/dist/lib/crud/interceptor-runner.js.map +2 -2
  24. package/dist/lib/crud/mutation-guard-registry.js +2 -2
  25. package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
  26. package/dist/lib/crud/types.js +4 -0
  27. package/dist/lib/crud/types.js.map +3 -3
  28. package/dist/lib/data/consistency.js +19 -0
  29. package/dist/lib/data/consistency.js.map +7 -0
  30. package/dist/lib/data/engine.js +37 -11
  31. package/dist/lib/data/engine.js.map +2 -2
  32. package/dist/lib/db/entityMetadata.js +26 -0
  33. package/dist/lib/db/entityMetadata.js.map +7 -0
  34. package/dist/lib/db/pg-errors.js +42 -0
  35. package/dist/lib/db/pg-errors.js.map +2 -2
  36. package/dist/lib/di/container.js +48 -6
  37. package/dist/lib/di/container.js.map +2 -2
  38. package/dist/lib/email/send.js +56 -0
  39. package/dist/lib/email/send.js.map +2 -2
  40. package/dist/lib/encryption/subscriber.js +2 -10
  41. package/dist/lib/encryption/subscriber.js.map +2 -2
  42. package/dist/lib/encryption/tenantDataEncryptionService.js +52 -17
  43. package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
  44. package/dist/lib/i18n/config.js +1 -1
  45. package/dist/lib/i18n/config.js.map +2 -2
  46. package/dist/lib/i18n/server.js +4 -4
  47. package/dist/lib/i18n/server.js.map +3 -3
  48. package/dist/lib/logger/extension.js +29 -0
  49. package/dist/lib/logger/extension.js.map +7 -0
  50. package/dist/lib/logger/index.js +47 -3
  51. package/dist/lib/logger/index.js.map +3 -3
  52. package/dist/lib/modules/registry.js +5 -1
  53. package/dist/lib/modules/registry.js.map +2 -2
  54. package/dist/lib/modules/surfaceFingerprint.js +47 -0
  55. package/dist/lib/modules/surfaceFingerprint.js.map +7 -0
  56. package/dist/lib/query/ciphertext-search-warning.js +45 -0
  57. package/dist/lib/query/ciphertext-search-warning.js.map +7 -0
  58. package/dist/lib/query/engine.js +54 -40
  59. package/dist/lib/query/engine.js.map +2 -2
  60. package/dist/lib/query/types.js.map +1 -1
  61. package/dist/lib/redis/connection.js +7 -2
  62. package/dist/lib/redis/connection.js.map +2 -2
  63. package/dist/lib/search/auto-indexing.js +14 -0
  64. package/dist/lib/search/auto-indexing.js.map +7 -0
  65. package/dist/lib/search/availability.js +107 -0
  66. package/dist/lib/search/availability.js.map +7 -0
  67. package/dist/lib/search/config.js +61 -2
  68. package/dist/lib/search/config.js.map +2 -2
  69. package/dist/lib/search/tokenLookup.js +46 -0
  70. package/dist/lib/search/tokenLookup.js.map +7 -0
  71. package/dist/lib/search/tokenize.js +23 -11
  72. package/dist/lib/search/tokenize.js.map +2 -2
  73. package/dist/lib/telemetry/runtime.js +35 -0
  74. package/dist/lib/telemetry/runtime.js.map +7 -0
  75. package/dist/lib/version.js +1 -1
  76. package/dist/lib/version.js.map +1 -1
  77. package/dist/lib/webhooks/body.js +57 -0
  78. package/dist/lib/webhooks/body.js.map +7 -0
  79. package/dist/lib/webhooks/inbound-types.js +1 -0
  80. package/dist/lib/webhooks/inbound-types.js.map +7 -0
  81. package/dist/lib/webhooks/index.js +13 -1
  82. package/dist/lib/webhooks/index.js.map +2 -2
  83. package/dist/lib/webhooks/verify.js +12 -7
  84. package/dist/lib/webhooks/verify.js.map +2 -2
  85. package/dist/modules/analytics.js.map +2 -2
  86. package/dist/modules/overrides.js +50 -1
  87. package/dist/modules/overrides.js.map +2 -2
  88. package/dist/modules/payment_gateways/types.js +8 -1
  89. package/dist/modules/payment_gateways/types.js.map +2 -2
  90. package/dist/modules/registry.js +1 -0
  91. package/dist/modules/registry.js.map +2 -2
  92. package/dist/modules/widgets/extension-points.js +96 -0
  93. package/dist/modules/widgets/extension-points.js.map +7 -0
  94. package/dist/security/enabledModulesRegistry.js +56 -11
  95. package/dist/security/enabledModulesRegistry.js.map +2 -2
  96. package/dist/security/featurePolicy.js +62 -0
  97. package/dist/security/featurePolicy.js.map +7 -0
  98. package/jest.config.cjs +2 -1
  99. package/package.json +19 -9
  100. package/scripts/versionSource.cjs +65 -0
  101. package/src/lib/__tests__/versionSource.test.ts +46 -0
  102. package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
  103. package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
  104. package/src/lib/ai/llm-provider.ts +33 -0
  105. package/src/lib/ai/safety-identifier.ts +73 -0
  106. package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
  107. package/src/lib/auth/__tests__/server.test.ts +23 -0
  108. package/src/lib/auth/organizationScope.ts +65 -0
  109. package/src/lib/auth/server.ts +23 -2
  110. package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +189 -0
  111. package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
  112. package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
  113. package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +63 -8
  114. package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +256 -0
  115. package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +21 -1
  116. package/src/lib/bootstrap/clientOnlyModules.ts +85 -0
  117. package/src/lib/bootstrap/dynamicLoader.ts +213 -65
  118. package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +28 -0
  119. package/src/lib/commands/command-bus.ts +5 -1
  120. package/src/lib/commands/command-interceptor-runner.ts +2 -2
  121. package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +171 -0
  122. package/src/lib/crud/__tests__/crud-factory.test.ts +351 -0
  123. package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
  124. package/src/lib/crud/enricher-runner.ts +2 -11
  125. package/src/lib/crud/factory.ts +82 -6
  126. package/src/lib/crud/interceptor-runner.ts +2 -2
  127. package/src/lib/crud/mutation-guard-registry.ts +2 -2
  128. package/src/lib/crud/types.ts +3 -0
  129. package/src/lib/data/__tests__/consistency.test.ts +38 -0
  130. package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
  131. package/src/lib/data/consistency.ts +17 -0
  132. package/src/lib/data/engine.ts +50 -16
  133. package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
  134. package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
  135. package/src/lib/db/entityMetadata.ts +43 -0
  136. package/src/lib/db/pg-errors.ts +58 -0
  137. package/src/lib/di/__tests__/container-app-di-absent.test.ts +155 -0
  138. package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
  139. package/src/lib/di/__tests__/registrar-error-log.test.ts +154 -0
  140. package/src/lib/di/container.ts +72 -8
  141. package/src/lib/email/__tests__/send.test.ts +37 -1
  142. package/src/lib/email/send.ts +79 -0
  143. package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
  144. package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
  145. package/src/lib/encryption/subscriber.ts +2 -14
  146. package/src/lib/encryption/tenantDataEncryptionService.ts +64 -18
  147. package/src/lib/i18n/__tests__/config.test.ts +8 -0
  148. package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
  149. package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
  150. package/src/lib/i18n/config.ts +2 -2
  151. package/src/lib/i18n/server.ts +6 -2
  152. package/src/lib/logger/__tests__/logger.test.ts +77 -0
  153. package/src/lib/logger/extension.ts +59 -0
  154. package/src/lib/logger/index.ts +59 -1
  155. package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +122 -0
  156. package/src/lib/modules/registry.ts +10 -0
  157. package/src/lib/modules/surfaceFingerprint.ts +87 -0
  158. package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +178 -0
  159. package/src/lib/query/__tests__/engine.test.ts +65 -13
  160. package/src/lib/query/ciphertext-search-warning.ts +95 -0
  161. package/src/lib/query/engine.ts +75 -58
  162. package/src/lib/query/types.ts +4 -5
  163. package/src/lib/redis/__tests__/connection.test.ts +16 -0
  164. package/src/lib/redis/connection.ts +10 -7
  165. package/src/lib/search/__tests__/availability.test.ts +211 -0
  166. package/src/lib/search/__tests__/config.test.ts +177 -0
  167. package/src/lib/search/__tests__/tokenLookup.test.ts +206 -0
  168. package/src/lib/search/__tests__/tokenize.test.ts +49 -0
  169. package/src/lib/search/auto-indexing.ts +22 -0
  170. package/src/lib/search/availability.ts +232 -0
  171. package/src/lib/search/config.ts +106 -8
  172. package/src/lib/search/tokenLookup.ts +133 -0
  173. package/src/lib/search/tokenize.ts +33 -11
  174. package/src/lib/telemetry/runtime.ts +72 -0
  175. package/src/lib/webhooks/__tests__/body.test.ts +95 -0
  176. package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
  177. package/src/lib/webhooks/body.ts +70 -0
  178. package/src/lib/webhooks/inbound-types.ts +125 -0
  179. package/src/lib/webhooks/index.ts +19 -1
  180. package/src/lib/webhooks/verify.ts +17 -9
  181. package/src/modules/__tests__/nav-group-order-override.test.ts +183 -0
  182. package/src/modules/__tests__/registry.test.ts +10 -2
  183. package/src/modules/analytics.ts +7 -0
  184. package/src/modules/customer-auth.ts +1 -0
  185. package/src/modules/encryption.ts +3 -0
  186. package/src/modules/navigation/backendChrome.ts +15 -0
  187. package/src/modules/overrides.ts +103 -0
  188. package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
  189. package/src/modules/payment_gateways/types.ts +41 -0
  190. package/src/modules/registry.ts +3 -0
  191. package/src/modules/widgets/__tests__/extension-points.test.ts +101 -0
  192. package/src/modules/widgets/extension-points.ts +524 -0
  193. package/src/security/__tests__/featurePolicy.test.ts +166 -0
  194. package/src/security/enabledModulesRegistry.ts +64 -12
  195. package/src/security/featurePolicy.ts +89 -0
@@ -0,0 +1,256 @@
1
+ /**
2
+ * @jest-environment node
3
+ *
4
+ * Integration guard for #4526/#4693: compileAndImport must await its dynamic
5
+ * import so that an import-time rejection settles inside the try block.
6
+ * Returning the promise unawaited let the rejection escape the surrounding try,
7
+ * which made the MikroORM v7 generated-cache recovery in the catch dead code for
8
+ * exactly the failure it exists to repair. The one-line fix shipped in #4540;
9
+ * this file is the end-to-end coverage for it.
10
+ *
11
+ * Two sibling suites cover this area and neither subsumes the other:
12
+ *
13
+ * - dynamicLoader.cacheRecovery.test.ts mocks ../generatedCacheRecovery in full
14
+ * and asserts how the loader *reacts* to a rejecting import (recovery is
15
+ * invoked once, the retry is capped at one, the error propagates when no
16
+ * recovery applies).
17
+ * - generatedCacheRecovery.test.ts exercises the recovery module directly,
18
+ * without going through the loader.
19
+ * - This file runs the REAL recovery module from the loader's catch, so the
20
+ * loader ↔ recovery seam is covered end to end: the on-disk marker, its
21
+ * `runtime-import-error` reason and the deleted-file list are asserted against
22
+ * what the production module actually writes. Nothing else in the repository
23
+ * reaches that reason.
24
+ *
25
+ * The fixture reproduces the window the catch exists for: a stale compiled file
26
+ * that the startup scan (ensureMikroOrmV7GeneratedCacheCompatibility) cannot see
27
+ * because it appears only once loading is already under way. The staged .mjs
28
+ * therefore carries no decorator import at scan time — it writes a stale sibling
29
+ * and then rejects with the error Node raises for a v6 decorator import, which
30
+ * is what the import-time recovery is meant to repair.
31
+ *
32
+ * Every loadBootstrapData call runs in a child process (#4960). Driving the real
33
+ * loader in-process made this suite flaky at ~30-50%: a load that aborts part way
34
+ * leaves the sibling compileAndImport calls of its Promise.all still running, and
35
+ * those in-flight esbuild builds rewrite the very .mjs files the fixture stages
36
+ * between passes, so priming was a race rather than a loop. A child process also
37
+ * gives each load a real Node module registry, so esbuild's ESM output imports
38
+ * natively and the fixture no longer has to stage CommonJS stand-ins for it.
39
+ *
40
+ * Because the child is a real Node process, the guarded retry now completes for
41
+ * real: recovery deletes the stale cache, the loader recompiles from the .ts
42
+ * sources and the load succeeds. That end state is asserted too — under the old
43
+ * in-process fixture it was unreachable, because the runner kept serving the
44
+ * rejecting module from memory no matter what recovery wrote to disk.
45
+ */
46
+ import { spawnSync } from 'node:child_process'
47
+ import crypto from 'node:crypto'
48
+ import fs from 'node:fs'
49
+ import { createRequire } from 'node:module'
50
+ import os from 'node:os'
51
+ import path from 'node:path'
52
+
53
+ const STALE_SIBLING_BASENAME = 'stale-entity.generated.mjs'
54
+ const RESULT_MARKER = '__BOOTSTRAP_RUN_RESULT__'
55
+ /**
56
+ * The Jest budget deliberately exceeds the spawn budget, so a wedged child is
57
+ * reported as this file's own "runner produced no result" error — with the
58
+ * child's output attached — rather than as an opaque Jest timeout.
59
+ */
60
+ const CHILD_SPAWN_TIMEOUT_MS = 120_000
61
+ const CHILD_RUN_TIMEOUT_MS = 180_000
62
+
63
+ const LOADER_PATH = path.resolve(__dirname, '..', 'dynamicLoader.ts')
64
+
65
+ /**
66
+ * Resolved through a real `require` rather than Jest's resolver: this path is
67
+ * handed to a child process, so it must be the location Node itself would load.
68
+ */
69
+ const TSX_CLI_PATH = createRequire(__filename).resolve('tsx/cli')
70
+
71
+ /**
72
+ * Runs one loadBootstrapData against the fixture app root and reports the
73
+ * outcome on stdout behind a marker, so the loader's own logging (and the
74
+ * recovery module's console.warn banner) cannot be mistaken for the result.
75
+ */
76
+ const RUNNER_SOURCE = [
77
+ "import { pathToFileURL } from 'node:url'",
78
+ '',
79
+ 'const [loaderPath, appRoot] = process.argv.slice(2)',
80
+ '',
81
+ 'let result',
82
+ 'try {',
83
+ ' const loader = await import(pathToFileURL(loaderPath).href)',
84
+ ' await loader.loadBootstrapData(appRoot)',
85
+ ' result = { loaded: true }',
86
+ '} catch (error) {',
87
+ ' result = { loaded: false, message: error instanceof Error ? error.message : String(error) }',
88
+ '}',
89
+ '',
90
+ `process.stdout.write('\\n' + ${JSON.stringify(RESULT_MARKER)} + JSON.stringify(result))`,
91
+ ].join('\n')
92
+
93
+ /**
94
+ * Split so the literal never appears in this file: the repository's Jest
95
+ * transformer rewrites every `import.meta` occurrence to a CommonJS stub before
96
+ * ts-jest sees the source, including ones inside a string.
97
+ */
98
+ const IMPORT_META_URL_EXPRESSION = ['import', '.meta.url'].join('')
99
+
100
+ /**
101
+ * The decorator import is assembled at runtime rather than written literally:
102
+ * a literal would make this fixture itself match the startup scan's staleness
103
+ * pattern, so the scan would delete the cache before the import ever runs and
104
+ * the import-time path under test would never be exercised.
105
+ */
106
+ const REJECTING_COMPILED_SOURCE = [
107
+ "import nodeFs from 'node:fs'",
108
+ "import nodePath from 'node:path'",
109
+ "import { fileURLToPath } from 'node:url'",
110
+ 'const quote = String.fromCharCode(39)',
111
+ "const staleImport = 'import { Entity, PrimaryKey } from ' + quote + '@mikro-orm/core' + quote",
112
+ `const generatedDirPath = fileURLToPath(new URL('.', ${IMPORT_META_URL_EXPRESSION}))`,
113
+ `nodeFs.writeFileSync(nodePath.join(generatedDirPath, ${JSON.stringify(STALE_SIBLING_BASENAME)}), staleImport + '\\nexport const stale = true\\n')`,
114
+ `throw new Error('The requested module ' + quote + '@mikro-orm/core' + quote + ' does not provide an export named ' + quote + 'Entity' + quote)`,
115
+ ].join('\n')
116
+
117
+ /**
118
+ * The generated sources stay honest ESM. The loader compiles them with esbuild
119
+ * and the child process imports that output natively, so nothing here has to
120
+ * model the compiled shape — the loader's real output is what gets imported.
121
+ */
122
+ const GENERATED_MODULES: Record<string, string> = {
123
+ 'entities.ids.generated': 'export const E = {}',
124
+ 'modules.cli.generated': 'export const modules = []',
125
+ 'entities.generated': 'export const entities = []',
126
+ 'di.generated': 'export const diRegistrars = []',
127
+ }
128
+
129
+ type BootstrapRunResult = { loaded: boolean; message?: string }
130
+
131
+ function contentHash(content: Buffer | string): string {
132
+ return crypto.createHash('sha256').update(content).digest('hex')
133
+ }
134
+
135
+ /**
136
+ * Replace a compiled cache entry with staged content, keeping the loader's own
137
+ * cache metadata authoritative.
138
+ *
139
+ * The loader validates a cache entry by hashing (#4724): it recompiles unless
140
+ * the sibling `.cache.json` matches both the source and the compiled output.
141
+ * Only `outputHash` is rewritten here — `version`, `inputHash` and
142
+ * `dependencies` stay exactly as the loader wrote them, so the fixture models a
143
+ * cache that is internally consistent and simply stale, and it cannot drift out
144
+ * of step with the cache format the way a hand-built metadata file would.
145
+ */
146
+ function stageCompiledCache(generatedDir: string, baseName: string, compiled: string) {
147
+ const jsPath = path.join(generatedDir, `${baseName}.mjs`)
148
+ const metadataPath = `${jsPath}.cache.json`
149
+
150
+ fs.writeFileSync(jsPath, compiled)
151
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'))
152
+ metadata.outputHash = contentHash(fs.readFileSync(jsPath))
153
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata))
154
+ }
155
+
156
+ describe('compileAndImport — generated-cache recovery is reachable (#4526)', () => {
157
+ let runnerDir: string
158
+ let runnerPath: string
159
+ let templateRoot: string
160
+ let appRoot: string
161
+ let generatedDir: string
162
+
163
+ function runLoaderInChildProcess(target: string): BootstrapRunResult {
164
+ const run = spawnSync(process.execPath, [TSX_CLI_PATH, runnerPath, LOADER_PATH, target], {
165
+ encoding: 'utf8',
166
+ timeout: CHILD_SPAWN_TIMEOUT_MS,
167
+ env: { ...process.env, OM_LOG_LEVEL: 'error' },
168
+ })
169
+
170
+ const markerIndex = (run.stdout ?? '').lastIndexOf(RESULT_MARKER)
171
+ if (markerIndex === -1) {
172
+ throw new Error(
173
+ `[internal] bootstrap runner reported no result (status ${run.status})\n`
174
+ + `stdout: ${run.stdout}\nstderr: ${run.stderr}`,
175
+ )
176
+ }
177
+
178
+ return JSON.parse(run.stdout.slice(markerIndex + RESULT_MARKER.length)) as BootstrapRunResult
179
+ }
180
+
181
+ function createFixtureAppRoot(prefix: string): string {
182
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix))
183
+ fs.mkdirSync(path.join(root, '.mercato', 'generated'), { recursive: true })
184
+ fs.writeFileSync(
185
+ path.join(root, 'tsconfig.json'),
186
+ JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'ESNext' } }),
187
+ )
188
+ for (const [baseName, source] of Object.entries(GENERATED_MODULES)) {
189
+ fs.writeFileSync(path.join(root, '.mercato', 'generated', `${baseName}.ts`), source)
190
+ }
191
+ return root
192
+ }
193
+
194
+ /**
195
+ * Prime once: a single child-process load compiles every generated source and
196
+ * writes the loader's real cache metadata for it. The primed tree is copied
197
+ * per test rather than re-primed — the cache records source and tsconfig
198
+ * hashes plus app-root-relative dependency paths, so it stays valid under a
199
+ * different root.
200
+ */
201
+ beforeAll(() => {
202
+ runnerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'om-bootstrap-4526-runner-'))
203
+ runnerPath = path.join(runnerDir, 'load-bootstrap-data.mjs')
204
+ fs.writeFileSync(runnerPath, RUNNER_SOURCE)
205
+
206
+ templateRoot = createFixtureAppRoot('om-bootstrap-4526-template-')
207
+ const primed = runLoaderInChildProcess(templateRoot)
208
+ if (!primed.loaded) {
209
+ throw new Error(`[internal] fixture priming failed: ${primed.message}`)
210
+ }
211
+ if (fs.existsSync(path.join(templateRoot, '.mercato', 'generated', '.mikro-orm-v7-cache-recovery.json'))) {
212
+ throw new Error('[internal] fixture priming triggered cache recovery')
213
+ }
214
+ }, CHILD_RUN_TIMEOUT_MS)
215
+
216
+ afterAll(() => {
217
+ for (const directory of [templateRoot, runnerDir]) {
218
+ if (directory) fs.rmSync(directory, { recursive: true, force: true })
219
+ }
220
+ })
221
+
222
+ beforeEach(() => {
223
+ appRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-bootstrap-4526-'))
224
+ fs.cpSync(templateRoot, appRoot, { recursive: true })
225
+ generatedDir = path.join(appRoot, '.mercato', 'generated')
226
+ })
227
+
228
+ afterEach(() => {
229
+ fs.rmSync(appRoot, { recursive: true, force: true })
230
+ })
231
+
232
+ it('runs cache recovery when the compiled cache rejects on import', () => {
233
+ const rejectingPath = path.join(generatedDir, 'entities.ids.generated.mjs')
234
+ stageCompiledCache(generatedDir, 'entities.ids.generated', REJECTING_COMPILED_SOURCE)
235
+
236
+ const run = runLoaderInChildProcess(appRoot)
237
+
238
+ const markerPath = path.join(generatedDir, '.mikro-orm-v7-cache-recovery.json')
239
+ expect(fs.existsSync(markerPath)).toBe(true)
240
+
241
+ const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'))
242
+ expect(marker.reason).toBe('runtime-import-error')
243
+ expect(marker.deletedFiles).toContain(rejectingPath)
244
+ expect(marker.deletedFiles).toContain(path.join(generatedDir, STALE_SIBLING_BASENAME))
245
+
246
+ expect(fs.readFileSync(rejectingPath, 'utf8')).not.toContain('does not provide an export named')
247
+ expect(run).toEqual({ loaded: true })
248
+ }, CHILD_RUN_TIMEOUT_MS)
249
+
250
+ it('does not run recovery when the compiled cache imports cleanly', () => {
251
+ const run = runLoaderInChildProcess(appRoot)
252
+
253
+ expect(run).toEqual({ loaded: true })
254
+ expect(fs.existsSync(path.join(generatedDir, '.mikro-orm-v7-cache-recovery.json'))).toBe(false)
255
+ }, CHILD_RUN_TIMEOUT_MS)
256
+ })
@@ -81,7 +81,11 @@ function loadBootstrapDataInNode(appRoot: string): {
81
81
  return JSON.parse(execFileSync(
82
82
  process.execPath,
83
83
  ['--import', 'tsx', '--input-type=module', '-e', script, appRoot],
84
- { cwd: process.cwd(), encoding: 'utf8' },
84
+ {
85
+ cwd: process.cwd(),
86
+ encoding: 'utf8',
87
+ env: { ...process.env, OM_LOG_DESTINATION: 'stderr' },
88
+ },
85
89
  ))
86
90
  }
87
91
 
@@ -212,6 +216,22 @@ describe('dynamic loader app tsconfig and content-addressed cache', () => {
212
216
  expect(after.inputHash).not.toBe(before.inputHash)
213
217
  })
214
218
 
219
+ it('excludes client-only virtual inputs from cache dependencies', async () => {
220
+ const appRoot = createAppRoot()
221
+ const generatedDir = path.join(appRoot, '.mercato', 'generated')
222
+ fs.writeFileSync(path.join(generatedDir, 'modules.cli.generated.ts'), `
223
+ export const modules = [{ widget: () => import('./widget.client') }]
224
+ `)
225
+
226
+ loadBootstrapDataInNode(appRoot)
227
+
228
+ const metadata = readCacheMetadata(appRoot, 'modules.cli')
229
+ expect(Object.keys(metadata.dependencies)).not.toContain('om-client-only-stub:./widget.client')
230
+ expect(
231
+ fs.readFileSync(path.join(generatedDir, 'modules.cli.generated.mjs'), 'utf8'),
232
+ ).toContain('clientOnlyModuleUnavailable')
233
+ })
234
+
215
235
  it('invalidates compiled output when only a bundled local dependency changes', async () => {
216
236
  const appRoot = createAppRoot()
217
237
  const generatedSourcePath = path.join(
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Client-only modules (`*.client.tsx`) hold browser components and must never enter the
3
+ * CLI bundle graph. App modules are bundled from source by `compileAndImport`, and esbuild
4
+ * inlines dynamic imports into the single output file, hoisting the client file's static
5
+ * imports to the top of the bundle. A wrapper such as `@open-mercato/ui/backend/charts`
6
+ * would then execute on every CLI start and fail on bare Next.js specifiers that Node's
7
+ * ESM resolver cannot resolve.
8
+ *
9
+ * Resolving these files to an inert stub keeps the owning `widget.ts` importable (the CLI
10
+ * reads its metadata when seeding dashboards) while cutting the browser-only subgraph.
11
+ *
12
+ * Only `import()` expressions are stubbed. That is the documented way a server-side
13
+ * `widget.ts` reaches its browser component (`lazyDashboardWidget(() => import('./widget.client'))`),
14
+ * and its result is consumed as a namespace object, so a default-only stub is sufficient.
15
+ * Static imports are left to the bundler: they may request named bindings the stub cannot
16
+ * provide, which esbuild rejects with `No matching export`, failing the whole CLI bundle —
17
+ * the exact class of breakage this module exists to prevent. Restricting the rewrite to
18
+ * dynamic imports also keeps server-side helpers that merely follow a `*.client.ts` naming
19
+ * convention working untouched.
20
+ */
21
+
22
+ export const CLIENT_ONLY_STUB_NAMESPACE = 'om-client-only-stub'
23
+
24
+ const LOCAL_IMPORT_PATTERN = /^(\.{1,2}\/|@\/)/
25
+ const CLIENT_ONLY_SUFFIX_PATTERN = /(^|[\\/])[^\\/]+\.client(\.[cm]?[jt]sx?)?$/
26
+
27
+ /**
28
+ * Local (relative or `@/` aliased) imports of a `*.client` module. Bare package specifiers
29
+ * are left alone because the bundler already marks them external, so they never get inlined.
30
+ */
31
+ export function isClientOnlyModulePath(importPath: string): boolean {
32
+ if (!LOCAL_IMPORT_PATTERN.test(importPath)) return false
33
+ return CLIENT_ONLY_SUFFIX_PATTERN.test(importPath)
34
+ }
35
+
36
+ /**
37
+ * Render a value as a JavaScript string literal safe to embed in generated source.
38
+ * Everything outside printable ASCII is escaped to a `\uXXXX` sequence, so no character
39
+ * of the input can terminate the literal or be re-interpreted as code — `JSON.stringify`
40
+ * alone is a JSON encoder, not a JavaScript-source escaper.
41
+ */
42
+ export function encodeJsStringLiteral(value: string): string {
43
+ let encoded = ''
44
+ for (let index = 0; index < value.length; index += 1) {
45
+ const char = value[index]
46
+ if (char === '"' || char === '\\') {
47
+ encoded += `\\${char}`
48
+ continue
49
+ }
50
+ const code = value.charCodeAt(index)
51
+ if (code < 0x20 || code > 0x7e) {
52
+ encoded += `\\u${code.toString(16).padStart(4, '0')}`
53
+ continue
54
+ }
55
+ encoded += char
56
+ }
57
+ return `"${encoded}"`
58
+ }
59
+
60
+ export function renderClientOnlyModuleStub(importPath: string): string {
61
+ const message =
62
+ `[internal] Client-only module ${importPath} is not available in the CLI runtime. ` +
63
+ 'It is excluded from the CLI bundle because it renders browser components.'
64
+ return [
65
+ `function clientOnlyModuleUnavailable() { throw new Error(${encodeJsStringLiteral(message)}) }`,
66
+ 'export default clientOnlyModuleUnavailable',
67
+ ].join('\n')
68
+ }
69
+
70
+ export function createClientOnlyStubPlugin(): import('esbuild').Plugin {
71
+ return {
72
+ name: 'client-only-stub',
73
+ setup(build) {
74
+ build.onResolve({ filter: CLIENT_ONLY_SUFFIX_PATTERN }, (args) => {
75
+ if (args.kind !== 'dynamic-import') return null
76
+ if (!isClientOnlyModulePath(args.path)) return null
77
+ return { path: args.path, namespace: CLIENT_ONLY_STUB_NAMESPACE }
78
+ })
79
+ build.onLoad({ filter: /.*/, namespace: CLIENT_ONLY_STUB_NAMESPACE }, (args) => ({
80
+ contents: renderClientOnlyModuleStub(args.path),
81
+ loader: 'js',
82
+ }))
83
+ },
84
+ }
85
+ }