@open-mercato/shared 0.6.7-develop.6860.1.f796b22e80 → 0.6.7-develop.6862.1.c11a64ce0a
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/dist/lib/version.js
CHANGED
package/dist/lib/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/version.ts"],
|
|
4
|
-
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.
|
|
4
|
+
"sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.7-develop.6862.1.c11a64ce0a';\nexport const appVersion = APP_VERSION;\n"],
|
|
5
5
|
"mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/shared",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6862.1.c11a64ce0a",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"@mikro-orm/core": "^7.1.8",
|
|
106
106
|
"@mikro-orm/decorators": "^7.1.8",
|
|
107
107
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
108
|
-
"@open-mercato/cache": "0.6.7-develop.
|
|
108
|
+
"@open-mercato/cache": "0.6.7-develop.6862.1.c11a64ce0a",
|
|
109
109
|
"@types/sanitize-html": "^2.16.1",
|
|
110
110
|
"dotenv": "^17.4.2",
|
|
111
111
|
"pino": "^10.3.1",
|
|
@@ -29,21 +29,73 @@
|
|
|
29
29
|
* and then rejects with the error Node raises for a v6 decorator import, which
|
|
30
30
|
* is what the import-time recovery is meant to repair.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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.
|
|
39
45
|
*/
|
|
46
|
+
import { spawnSync } from 'node:child_process'
|
|
40
47
|
import crypto from 'node:crypto'
|
|
41
48
|
import fs from 'node:fs'
|
|
49
|
+
import { createRequire } from 'node:module'
|
|
42
50
|
import os from 'node:os'
|
|
43
51
|
import path from 'node:path'
|
|
44
|
-
import { loadBootstrapData } from '../dynamicLoader'
|
|
45
52
|
|
|
46
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('')
|
|
47
99
|
|
|
48
100
|
/**
|
|
49
101
|
* The decorator import is assembled at runtime rather than written literally:
|
|
@@ -52,35 +104,37 @@ const STALE_SIBLING_BASENAME = 'stale-entity.generated.mjs'
|
|
|
52
104
|
* the import-time path under test would never be exercised.
|
|
53
105
|
*/
|
|
54
106
|
const REJECTING_COMPILED_SOURCE = [
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
107
|
+
"import nodeFs from 'node:fs'",
|
|
108
|
+
"import nodePath from 'node:path'",
|
|
109
|
+
"import { fileURLToPath } from 'node:url'",
|
|
110
|
+
'const quote = String.fromCharCode(39)',
|
|
58
111
|
"const staleImport = 'import { Entity, PrimaryKey } from ' + quote + '@mikro-orm/core' + quote",
|
|
59
|
-
`
|
|
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')`,
|
|
60
114
|
`throw new Error('The requested module ' + quote + '@mikro-orm/core' + quote + ' does not provide an export named ' + quote + 'Entity' + quote)`,
|
|
61
115
|
].join('\n')
|
|
62
116
|
|
|
63
117
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* sources stay honest ESM: they are what the loader recompiles from once
|
|
68
|
-
* recovery has deleted the cache.
|
|
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.
|
|
69
121
|
*/
|
|
70
|
-
const GENERATED_MODULES: Record<string,
|
|
71
|
-
'entities.ids.generated':
|
|
72
|
-
'modules.cli.generated':
|
|
73
|
-
'entities.generated':
|
|
74
|
-
'di.generated':
|
|
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 = []',
|
|
75
127
|
}
|
|
76
128
|
|
|
129
|
+
type BootstrapRunResult = { loaded: boolean; message?: string }
|
|
130
|
+
|
|
77
131
|
function contentHash(content: Buffer | string): string {
|
|
78
132
|
return crypto.createHash('sha256').update(content).digest('hex')
|
|
79
133
|
}
|
|
80
134
|
|
|
81
135
|
/**
|
|
82
|
-
* Replace a compiled cache entry with staged content the
|
|
83
|
-
*
|
|
136
|
+
* Replace a compiled cache entry with staged content, keeping the loader's own
|
|
137
|
+
* cache metadata authoritative.
|
|
84
138
|
*
|
|
85
139
|
* The loader validates a cache entry by hashing (#4724): it recompiles unless
|
|
86
140
|
* the sibling `.cache.json` matches both the source and the compiled output.
|
|
@@ -99,79 +153,87 @@ function stageCompiledCache(generatedDir: string, baseName: string, compiled: st
|
|
|
99
153
|
fs.writeFileSync(metadataPath, JSON.stringify(metadata))
|
|
100
154
|
}
|
|
101
155
|
|
|
102
|
-
function hasCacheMetadata(generatedDir: string, baseName: string): boolean {
|
|
103
|
-
return fs.existsSync(path.join(generatedDir, `${baseName}.mjs.cache.json`))
|
|
104
|
-
}
|
|
105
|
-
|
|
106
156
|
describe('compileAndImport — generated-cache recovery is reachable (#4526)', () => {
|
|
157
|
+
let runnerDir: string
|
|
158
|
+
let runnerPath: string
|
|
159
|
+
let templateRoot: string
|
|
107
160
|
let appRoot: string
|
|
108
161
|
let generatedDir: string
|
|
109
|
-
let warnSpy: jest.SpyInstance
|
|
110
162
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
*
|
|
125
|
-
* jest.resetModules() between passes is load-bearing. Jest keys its registry
|
|
126
|
-
* on the resolved path and ignores the loader's `?cache=` query, so without
|
|
127
|
-
* the reset a module would keep being served from its first (ESM, failing)
|
|
128
|
-
* evaluation and no later pass could make progress.
|
|
129
|
-
*
|
|
130
|
-
* None of these failures carries a decorator-export error, so no recovery
|
|
131
|
-
* runs and no marker is written during setup; both tests below would fail
|
|
132
|
-
* loudly if one were.
|
|
133
|
-
*/
|
|
134
|
-
async function primeAndStageCompiledCaches() {
|
|
135
|
-
const baseNames = Object.keys(GENERATED_MODULES)
|
|
136
|
-
for (let pass = 0; pass <= baseNames.length; pass += 1) {
|
|
137
|
-
let loadedCleanly = true
|
|
138
|
-
await loadBootstrapData(appRoot).catch(() => { loadedCleanly = false })
|
|
139
|
-
jest.resetModules()
|
|
140
|
-
if (loadedCleanly) return
|
|
141
|
-
for (const baseName of baseNames) {
|
|
142
|
-
if (!hasCacheMetadata(generatedDir, baseName)) continue
|
|
143
|
-
stageCompiledCache(generatedDir, baseName, GENERATED_MODULES[baseName].compiled)
|
|
144
|
-
}
|
|
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
|
+
)
|
|
145
176
|
}
|
|
146
|
-
|
|
177
|
+
|
|
178
|
+
return JSON.parse(run.stdout.slice(markerIndex + RESULT_MARKER.length)) as BootstrapRunResult
|
|
147
179
|
}
|
|
148
180
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
fs.mkdirSync(generatedDir, { recursive: true })
|
|
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 })
|
|
153
184
|
fs.writeFileSync(
|
|
154
|
-
path.join(
|
|
185
|
+
path.join(root, 'tsconfig.json'),
|
|
155
186
|
JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'ESNext' } }),
|
|
156
187
|
)
|
|
157
188
|
for (const [baseName, source] of Object.entries(GENERATED_MODULES)) {
|
|
158
|
-
fs.writeFileSync(path.join(
|
|
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')
|
|
159
213
|
}
|
|
160
|
-
|
|
214
|
+
}, CHILD_RUN_TIMEOUT_MS)
|
|
161
215
|
|
|
162
|
-
|
|
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')
|
|
163
226
|
})
|
|
164
227
|
|
|
165
228
|
afterEach(() => {
|
|
166
|
-
warnSpy.mockRestore()
|
|
167
229
|
fs.rmSync(appRoot, { recursive: true, force: true })
|
|
168
230
|
})
|
|
169
231
|
|
|
170
|
-
it('runs cache recovery when the compiled cache rejects on import',
|
|
232
|
+
it('runs cache recovery when the compiled cache rejects on import', () => {
|
|
171
233
|
const rejectingPath = path.join(generatedDir, 'entities.ids.generated.mjs')
|
|
172
234
|
stageCompiledCache(generatedDir, 'entities.ids.generated', REJECTING_COMPILED_SOURCE)
|
|
173
235
|
|
|
174
|
-
|
|
236
|
+
const run = runLoaderInChildProcess(appRoot)
|
|
175
237
|
|
|
176
238
|
const markerPath = path.join(generatedDir, '.mikro-orm-v7-cache-recovery.json')
|
|
177
239
|
expect(fs.existsSync(markerPath)).toBe(true)
|
|
@@ -182,11 +244,13 @@ describe('compileAndImport — generated-cache recovery is reachable (#4526)', (
|
|
|
182
244
|
expect(marker.deletedFiles).toContain(path.join(generatedDir, STALE_SIBLING_BASENAME))
|
|
183
245
|
|
|
184
246
|
expect(fs.readFileSync(rejectingPath, 'utf8')).not.toContain('does not provide an export named')
|
|
185
|
-
|
|
247
|
+
expect(run).toEqual({ loaded: true })
|
|
248
|
+
}, CHILD_RUN_TIMEOUT_MS)
|
|
186
249
|
|
|
187
|
-
it('does not run recovery when the compiled cache imports cleanly',
|
|
188
|
-
|
|
250
|
+
it('does not run recovery when the compiled cache imports cleanly', () => {
|
|
251
|
+
const run = runLoaderInChildProcess(appRoot)
|
|
189
252
|
|
|
253
|
+
expect(run).toEqual({ loaded: true })
|
|
190
254
|
expect(fs.existsSync(path.join(generatedDir, '.mikro-orm-v7-cache-recovery.json'))).toBe(false)
|
|
191
|
-
})
|
|
255
|
+
}, CHILD_RUN_TIMEOUT_MS)
|
|
192
256
|
})
|