@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +57 -6
- package/build.mjs +2 -4
- package/dist/lib/ai/safety-identifier.js +32 -0
- package/dist/lib/ai/safety-identifier.js.map +7 -0
- package/dist/lib/auth/organizationScope.js +34 -0
- package/dist/lib/auth/organizationScope.js.map +7 -0
- package/dist/lib/auth/server.js +15 -2
- package/dist/lib/auth/server.js.map +2 -2
- package/dist/lib/bootstrap/clientOnlyModules.js +55 -0
- package/dist/lib/bootstrap/clientOnlyModules.js.map +7 -0
- package/dist/lib/bootstrap/dynamicLoader.js +117 -43
- package/dist/lib/bootstrap/dynamicLoader.js.map +2 -2
- package/dist/lib/commands/command-bus.js +5 -1
- package/dist/lib/commands/command-bus.js.map +2 -2
- package/dist/lib/commands/command-interceptor-runner.js +2 -2
- package/dist/lib/commands/command-interceptor-runner.js.map +2 -2
- package/dist/lib/crud/enricher-runner.js +2 -11
- package/dist/lib/crud/enricher-runner.js.map +2 -2
- package/dist/lib/crud/factory.js +72 -9
- package/dist/lib/crud/factory.js.map +2 -2
- package/dist/lib/crud/interceptor-runner.js +2 -2
- package/dist/lib/crud/interceptor-runner.js.map +2 -2
- package/dist/lib/crud/mutation-guard-registry.js +2 -2
- package/dist/lib/crud/mutation-guard-registry.js.map +2 -2
- package/dist/lib/crud/types.js +4 -0
- package/dist/lib/crud/types.js.map +3 -3
- package/dist/lib/data/consistency.js +19 -0
- package/dist/lib/data/consistency.js.map +7 -0
- package/dist/lib/data/engine.js +37 -11
- package/dist/lib/data/engine.js.map +2 -2
- package/dist/lib/db/entityMetadata.js +26 -0
- package/dist/lib/db/entityMetadata.js.map +7 -0
- package/dist/lib/db/pg-errors.js +42 -0
- package/dist/lib/db/pg-errors.js.map +2 -2
- package/dist/lib/di/container.js +48 -6
- package/dist/lib/di/container.js.map +2 -2
- package/dist/lib/email/send.js +56 -0
- package/dist/lib/email/send.js.map +2 -2
- package/dist/lib/encryption/subscriber.js +2 -10
- package/dist/lib/encryption/subscriber.js.map +2 -2
- package/dist/lib/encryption/tenantDataEncryptionService.js +52 -17
- package/dist/lib/encryption/tenantDataEncryptionService.js.map +2 -2
- package/dist/lib/i18n/config.js +1 -1
- package/dist/lib/i18n/config.js.map +2 -2
- package/dist/lib/i18n/server.js +4 -4
- package/dist/lib/i18n/server.js.map +3 -3
- package/dist/lib/logger/extension.js +29 -0
- package/dist/lib/logger/extension.js.map +7 -0
- package/dist/lib/logger/index.js +47 -3
- package/dist/lib/logger/index.js.map +3 -3
- package/dist/lib/modules/registry.js +5 -1
- package/dist/lib/modules/registry.js.map +2 -2
- package/dist/lib/modules/surfaceFingerprint.js +47 -0
- package/dist/lib/modules/surfaceFingerprint.js.map +7 -0
- package/dist/lib/query/ciphertext-search-warning.js +45 -0
- package/dist/lib/query/ciphertext-search-warning.js.map +7 -0
- package/dist/lib/query/engine.js +54 -40
- package/dist/lib/query/engine.js.map +2 -2
- package/dist/lib/query/types.js.map +1 -1
- package/dist/lib/redis/connection.js +7 -2
- package/dist/lib/redis/connection.js.map +2 -2
- package/dist/lib/search/auto-indexing.js +14 -0
- package/dist/lib/search/auto-indexing.js.map +7 -0
- package/dist/lib/search/availability.js +107 -0
- package/dist/lib/search/availability.js.map +7 -0
- package/dist/lib/search/config.js +61 -2
- package/dist/lib/search/config.js.map +2 -2
- package/dist/lib/search/tokenLookup.js +46 -0
- package/dist/lib/search/tokenLookup.js.map +7 -0
- package/dist/lib/search/tokenize.js +23 -11
- package/dist/lib/search/tokenize.js.map +2 -2
- package/dist/lib/telemetry/runtime.js +35 -0
- package/dist/lib/telemetry/runtime.js.map +7 -0
- package/dist/lib/version.js +1 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/webhooks/body.js +57 -0
- package/dist/lib/webhooks/body.js.map +7 -0
- package/dist/lib/webhooks/inbound-types.js +1 -0
- package/dist/lib/webhooks/inbound-types.js.map +7 -0
- package/dist/lib/webhooks/index.js +13 -1
- package/dist/lib/webhooks/index.js.map +2 -2
- package/dist/lib/webhooks/verify.js +12 -7
- package/dist/lib/webhooks/verify.js.map +2 -2
- package/dist/modules/analytics.js.map +2 -2
- package/dist/modules/overrides.js +50 -1
- package/dist/modules/overrides.js.map +2 -2
- package/dist/modules/payment_gateways/types.js +8 -1
- package/dist/modules/payment_gateways/types.js.map +2 -2
- package/dist/modules/registry.js +1 -0
- package/dist/modules/registry.js.map +2 -2
- package/dist/modules/widgets/extension-points.js +96 -0
- package/dist/modules/widgets/extension-points.js.map +7 -0
- package/dist/security/enabledModulesRegistry.js +56 -11
- package/dist/security/enabledModulesRegistry.js.map +2 -2
- package/dist/security/featurePolicy.js +62 -0
- package/dist/security/featurePolicy.js.map +7 -0
- package/jest.config.cjs +2 -1
- package/package.json +19 -9
- package/scripts/versionSource.cjs +65 -0
- package/src/lib/__tests__/versionSource.test.ts +46 -0
- package/src/lib/ai/__tests__/llm-provider-contract.test.ts +59 -0
- package/src/lib/ai/__tests__/safety-identifier.test.ts +71 -0
- package/src/lib/ai/llm-provider.ts +33 -0
- package/src/lib/ai/safety-identifier.ts +73 -0
- package/src/lib/auth/__tests__/organizationScope.test.ts +107 -0
- package/src/lib/auth/__tests__/server.test.ts +23 -0
- package/src/lib/auth/organizationScope.ts +65 -0
- package/src/lib/auth/server.ts +23 -2
- package/src/lib/bootstrap/__tests__/clientOnlyModules.test.ts +189 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.aliasResolver.test.ts +98 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.appDi.test.ts +211 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.commandInterceptors.test.ts +63 -8
- package/src/lib/bootstrap/__tests__/dynamicLoader.generatedCacheRecovery.test.ts +256 -0
- package/src/lib/bootstrap/__tests__/dynamicLoader.tsconfig.test.ts +21 -1
- package/src/lib/bootstrap/clientOnlyModules.ts +85 -0
- package/src/lib/bootstrap/dynamicLoader.ts +213 -65
- package/src/lib/commands/__tests__/command-interceptor-runner.test.ts +28 -0
- package/src/lib/commands/command-bus.ts +5 -1
- package/src/lib/commands/command-interceptor-runner.ts +2 -2
- package/src/lib/crud/__tests__/crud-factory.cache-user-scope.test.ts +171 -0
- package/src/lib/crud/__tests__/crud-factory.test.ts +351 -0
- package/src/lib/crud/__tests__/mutation-guard-registry.test.ts +26 -0
- package/src/lib/crud/enricher-runner.ts +2 -11
- package/src/lib/crud/factory.ts +82 -6
- package/src/lib/crud/interceptor-runner.ts +2 -2
- package/src/lib/crud/mutation-guard-registry.ts +2 -2
- package/src/lib/crud/types.ts +3 -0
- package/src/lib/data/__tests__/consistency.test.ts +38 -0
- package/src/lib/data/__tests__/engine.bulk-suppress.test.ts +18 -3
- package/src/lib/data/consistency.ts +17 -0
- package/src/lib/data/engine.ts +50 -16
- package/src/lib/db/__tests__/entityMetadata.test.ts +38 -0
- package/src/lib/db/__tests__/pg-errors.test.ts +48 -0
- package/src/lib/db/entityMetadata.ts +43 -0
- package/src/lib/db/pg-errors.ts +58 -0
- package/src/lib/di/__tests__/container-app-di-absent.test.ts +155 -0
- package/src/lib/di/__tests__/container-app-di.test.ts +173 -0
- package/src/lib/di/__tests__/registrar-error-log.test.ts +154 -0
- package/src/lib/di/container.ts +72 -8
- package/src/lib/email/__tests__/send.test.ts +37 -1
- package/src/lib/email/send.ts +79 -0
- package/src/lib/encryption/__tests__/subscriber.test.ts +54 -2
- package/src/lib/encryption/__tests__/tenantDataEncryptionService.test.ts +58 -0
- package/src/lib/encryption/subscriber.ts +2 -14
- package/src/lib/encryption/tenantDataEncryptionService.ts +64 -18
- package/src/lib/i18n/__tests__/config.test.ts +8 -0
- package/src/lib/i18n/__tests__/server-dictionary-cache.test.ts +30 -0
- package/src/lib/i18n/__tests__/server-unbootstrapped.test.ts +29 -0
- package/src/lib/i18n/config.ts +2 -2
- package/src/lib/i18n/server.ts +6 -2
- package/src/lib/logger/__tests__/logger.test.ts +77 -0
- package/src/lib/logger/extension.ts +59 -0
- package/src/lib/logger/index.ts +59 -1
- package/src/lib/modules/__tests__/surfaceFingerprint.test.ts +122 -0
- package/src/lib/modules/registry.ts +10 -0
- package/src/lib/modules/surfaceFingerprint.ts +87 -0
- package/src/lib/query/__tests__/ciphertext-search-warning.test.ts +178 -0
- package/src/lib/query/__tests__/engine.test.ts +65 -13
- package/src/lib/query/ciphertext-search-warning.ts +95 -0
- package/src/lib/query/engine.ts +75 -58
- package/src/lib/query/types.ts +4 -5
- package/src/lib/redis/__tests__/connection.test.ts +16 -0
- package/src/lib/redis/connection.ts +10 -7
- package/src/lib/search/__tests__/availability.test.ts +211 -0
- package/src/lib/search/__tests__/config.test.ts +177 -0
- package/src/lib/search/__tests__/tokenLookup.test.ts +206 -0
- package/src/lib/search/__tests__/tokenize.test.ts +49 -0
- package/src/lib/search/auto-indexing.ts +22 -0
- package/src/lib/search/availability.ts +232 -0
- package/src/lib/search/config.ts +106 -8
- package/src/lib/search/tokenLookup.ts +133 -0
- package/src/lib/search/tokenize.ts +33 -11
- package/src/lib/telemetry/runtime.ts +72 -0
- package/src/lib/webhooks/__tests__/body.test.ts +95 -0
- package/src/lib/webhooks/__tests__/verify.test.ts +20 -1
- package/src/lib/webhooks/body.ts +70 -0
- package/src/lib/webhooks/inbound-types.ts +125 -0
- package/src/lib/webhooks/index.ts +19 -1
- package/src/lib/webhooks/verify.ts +17 -9
- package/src/modules/__tests__/nav-group-order-override.test.ts +183 -0
- package/src/modules/__tests__/registry.test.ts +10 -2
- package/src/modules/analytics.ts +7 -0
- package/src/modules/customer-auth.ts +1 -0
- package/src/modules/encryption.ts +3 -0
- package/src/modules/navigation/backendChrome.ts +15 -0
- package/src/modules/overrides.ts +103 -0
- package/src/modules/payment_gateways/__tests__/types.test.ts +37 -0
- package/src/modules/payment_gateways/types.ts +41 -0
- package/src/modules/registry.ts +3 -0
- package/src/modules/widgets/__tests__/extension-points.test.ts +101 -0
- package/src/modules/widgets/extension-points.ts +524 -0
- package/src/security/__tests__/featurePolicy.test.ts +166 -0
- package/src/security/enabledModulesRegistry.ts +64 -12
- 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
|
-
{
|
|
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
|
+
}
|