@open-mercato/cli 0.4.9-develop-7afbe1e834 → 0.4.9-develop-94fb251ed3
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/AGENTS.md +34 -0
- package/dist/lib/generators/module-registry.js +144 -0
- package/dist/lib/generators/module-registry.js.map +3 -3
- package/dist/lib/testing/integration.js +109 -78
- package/dist/lib/testing/integration.js.map +2 -2
- package/package.json +4 -4
- package/src/lib/generators/__tests__/module-subset.test.ts +107 -1
- package/src/lib/generators/__tests__/scanner.test.ts +2 -1
- package/src/lib/generators/module-registry.ts +144 -0
- package/src/lib/testing/__tests__/integration.test.ts +106 -3
- package/src/lib/testing/integration.ts +137 -85
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.4.9-develop-
|
|
3
|
+
"version": "0.4.9-develop-94fb251ed3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -58,16 +58,16 @@
|
|
|
58
58
|
"@mikro-orm/core": "^6.6.2",
|
|
59
59
|
"@mikro-orm/migrations": "^6.6.2",
|
|
60
60
|
"@mikro-orm/postgresql": "^6.6.2",
|
|
61
|
-
"@open-mercato/shared": "0.4.9-develop-
|
|
61
|
+
"@open-mercato/shared": "0.4.9-develop-94fb251ed3",
|
|
62
62
|
"pg": "8.20.0",
|
|
63
63
|
"testcontainers": "^11.12.0",
|
|
64
64
|
"typescript": "^5.9.3"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
|
-
"@open-mercato/shared": "0.4.9-develop-
|
|
67
|
+
"@open-mercato/shared": "0.4.9-develop-94fb251ed3"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@open-mercato/shared": "0.4.9-develop-
|
|
70
|
+
"@open-mercato/shared": "0.4.9-develop-94fb251ed3",
|
|
71
71
|
"@types/jest": "^30.0.0",
|
|
72
72
|
"jest": "^30.2.0",
|
|
73
73
|
"ts-jest": "^29.4.6"
|
|
@@ -71,6 +71,50 @@ function readGenerated(tmpDir: string, filename: string): string | null {
|
|
|
71
71
|
return fs.readFileSync(filePath, 'utf8')
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// Inline CommonJS generators.ts that provides the security generator plugins.
|
|
75
|
+
// Written as plain JS (no TypeScript) so it can be dynamically imported from tmpDir
|
|
76
|
+
// without ts-jest transpilation.
|
|
77
|
+
const SECURITY_GENERATORS_CONTENT = `'use strict'
|
|
78
|
+
function buildMfaOutput({ importSection, entriesLiteral }) {
|
|
79
|
+
return '// AUTO-GENERATED by mercato generate registry\\n'
|
|
80
|
+
+ (importSection ? importSection + '\\n' : '')
|
|
81
|
+
+ 'type SecurityMfaProviderEntry = { moduleId: string; providers: unknown[] }\\n\\n'
|
|
82
|
+
+ 'export const securityMfaProviderEntries: SecurityMfaProviderEntry[] = [\\n'
|
|
83
|
+
+ (entriesLiteral ? ' ' + entriesLiteral + '\\n' : '')
|
|
84
|
+
+ ']\\n'
|
|
85
|
+
}
|
|
86
|
+
function buildSudoOutput({ importSection, entriesLiteral }) {
|
|
87
|
+
return '// AUTO-GENERATED by mercato generate registry\\n'
|
|
88
|
+
+ "import type { SecuritySudoTarget, SecuritySudoTargetEntry } from '@open-mercato/enterprise/modules/security'\\n"
|
|
89
|
+
+ (importSection ? '\\n' + importSection + '\\n' : '\\n') + '\\n'
|
|
90
|
+
+ 'type SecuritySudoTargetEntryRaw = { moduleId: string; targets: Array<Record<string, unknown>> }\\n\\n'
|
|
91
|
+
+ 'const entriesRaw: SecuritySudoTargetEntryRaw[] = [\\n'
|
|
92
|
+
+ (entriesLiteral ? ' ' + entriesLiteral + '\\n' : '')
|
|
93
|
+
+ ']\\n\\n'
|
|
94
|
+
+ 'export const securitySudoTargetEntries: SecuritySudoTargetEntry[] = entriesRaw.map(function(e) { return { moduleId: e.moduleId, targets: e.targets } })\\n'
|
|
95
|
+
}
|
|
96
|
+
module.exports = {
|
|
97
|
+
generatorPlugins: [
|
|
98
|
+
{
|
|
99
|
+
id: 'security.mfa-providers',
|
|
100
|
+
conventionFile: 'security.mfa-providers.ts',
|
|
101
|
+
importPrefix: 'SECURITY_MFA_PROVIDERS',
|
|
102
|
+
configExpr: function(n, id) { return "{ moduleId: '" + id + "', providers: ((" + n + ".default ?? " + n + ".mfaProviders ?? []) as unknown[]) }" },
|
|
103
|
+
outputFileName: 'security-mfa-providers.generated.ts',
|
|
104
|
+
buildOutput: buildMfaOutput,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'security.sudo',
|
|
108
|
+
conventionFile: 'security.sudo.ts',
|
|
109
|
+
importPrefix: 'SECURITY_SUDO',
|
|
110
|
+
configExpr: function(n, id) { return "{ moduleId: '" + id + "', targets: ((" + n + ".default ?? " + n + ".sudoTargets ?? []) as Array<Record<string, unknown>>) }" },
|
|
111
|
+
outputFileName: 'security-sudo.generated.ts',
|
|
112
|
+
buildOutput: buildSudoOutput,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
`
|
|
117
|
+
|
|
74
118
|
beforeEach(() => {
|
|
75
119
|
tmpDir = createTmpDir()
|
|
76
120
|
})
|
|
@@ -361,7 +405,7 @@ describe('generateModuleRegistryCli with module subsets', () => {
|
|
|
361
405
|
})
|
|
362
406
|
|
|
363
407
|
describe('all generated files are valid with varying subsets', () => {
|
|
364
|
-
it('produces all
|
|
408
|
+
it('produces all generated files even when no modules have matching content', async () => {
|
|
365
409
|
scaffoldModule(tmpDir, 'bare_mod', 'pkg', ['acl.ts'])
|
|
366
410
|
const enabled: ModuleEntry[] = [
|
|
367
411
|
{ id: 'bare_mod', from: '@open-mercato/core' },
|
|
@@ -380,6 +424,8 @@ describe('all generated files are valid with varying subsets', () => {
|
|
|
380
424
|
'events.generated.ts',
|
|
381
425
|
'analytics.generated.ts',
|
|
382
426
|
'translations-fields.generated.ts',
|
|
427
|
+
'frontend-middleware.generated.ts',
|
|
428
|
+
'backend-middleware.generated.ts',
|
|
383
429
|
]
|
|
384
430
|
for (const file of expectedFiles) {
|
|
385
431
|
const content = readGenerated(tmpDir, file)
|
|
@@ -433,6 +479,50 @@ describe('all generated files are valid with varying subsets', () => {
|
|
|
433
479
|
expect(aiTools).not.toContain('no_ai')
|
|
434
480
|
})
|
|
435
481
|
|
|
482
|
+
it('security generated registries are empty when no module provides security convention files', async () => {
|
|
483
|
+
scaffoldModule(tmpDir, 'no_security', 'pkg', ['setup.ts'])
|
|
484
|
+
touchFile(
|
|
485
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'no_security', 'generators.ts'),
|
|
486
|
+
SECURITY_GENERATORS_CONTENT,
|
|
487
|
+
)
|
|
488
|
+
const resolver = createMockResolver(tmpDir, [
|
|
489
|
+
{ id: 'no_security', from: '@open-mercato/core' },
|
|
490
|
+
])
|
|
491
|
+
await generateModuleRegistry({ resolver, quiet: true })
|
|
492
|
+
|
|
493
|
+
const mfaProviders = readGenerated(tmpDir, 'security-mfa-providers.generated.ts')!
|
|
494
|
+
const sudoTargets = readGenerated(tmpDir, 'security-sudo.generated.ts')!
|
|
495
|
+
|
|
496
|
+
expect(mfaProviders).toContain('export const securityMfaProviderEntries')
|
|
497
|
+
expect(mfaProviders).not.toContain('no_security')
|
|
498
|
+
expect(sudoTargets).toContain('export const securitySudoTargetEntries')
|
|
499
|
+
expect(sudoTargets).toContain('const entriesRaw: SecuritySudoTargetEntryRaw[] = [\n]')
|
|
500
|
+
expect(sudoTargets).not.toContain('no_security')
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('discovers security convention files into dedicated generated registries', async () => {
|
|
504
|
+
scaffoldModule(tmpDir, 'security_ext', 'pkg', [
|
|
505
|
+
'security.mfa-providers.ts',
|
|
506
|
+
'security.sudo.ts',
|
|
507
|
+
])
|
|
508
|
+
touchFile(
|
|
509
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'security_ext', 'generators.ts'),
|
|
510
|
+
SECURITY_GENERATORS_CONTENT,
|
|
511
|
+
)
|
|
512
|
+
const resolver = createMockResolver(tmpDir, [
|
|
513
|
+
{ id: 'security_ext', from: '@open-mercato/core' },
|
|
514
|
+
])
|
|
515
|
+
await generateModuleRegistry({ resolver, quiet: true })
|
|
516
|
+
|
|
517
|
+
const mfaProviders = readGenerated(tmpDir, 'security-mfa-providers.generated.ts')!
|
|
518
|
+
const sudoTargets = readGenerated(tmpDir, 'security-sudo.generated.ts')!
|
|
519
|
+
|
|
520
|
+
expect(mfaProviders).toContain('security_ext')
|
|
521
|
+
expect(mfaProviders).toContain('mfaProviders')
|
|
522
|
+
expect(sudoTargets).toContain('security_ext')
|
|
523
|
+
expect(sudoTargets).toContain('sudoTargets')
|
|
524
|
+
})
|
|
525
|
+
|
|
436
526
|
it('notifications.generated.ts uses typed fallback for legacy "types" export', async () => {
|
|
437
527
|
scaffoldModule(tmpDir, 'notif_mod', 'pkg', ['notifications.ts'])
|
|
438
528
|
const resolver = createMockResolver(tmpDir, [
|
|
@@ -444,4 +534,20 @@ describe('all generated files are valid with varying subsets', () => {
|
|
|
444
534
|
expect(notifications).toContain('as any).types')
|
|
445
535
|
expect(notifications).toContain('as NotificationTypeDefinition[]')
|
|
446
536
|
})
|
|
537
|
+
|
|
538
|
+
it('discovers frontend and backend middleware conventions', async () => {
|
|
539
|
+
scaffoldModule(tmpDir, 'security', 'pkg', [
|
|
540
|
+
'frontend/middleware.ts',
|
|
541
|
+
'backend/middleware.ts',
|
|
542
|
+
])
|
|
543
|
+
const resolver = createMockResolver(tmpDir, [
|
|
544
|
+
{ id: 'security', from: '@open-mercato/core' },
|
|
545
|
+
])
|
|
546
|
+
await generateModuleRegistry({ resolver, quiet: true })
|
|
547
|
+
|
|
548
|
+
const frontendMiddleware = readGenerated(tmpDir, 'frontend-middleware.generated.ts')!
|
|
549
|
+
const backendMiddleware = readGenerated(tmpDir, 'backend-middleware.generated.ts')!
|
|
550
|
+
expect(frontendMiddleware).toContain("moduleId: 'security'")
|
|
551
|
+
expect(backendMiddleware).toContain("moduleId: 'security'")
|
|
552
|
+
})
|
|
447
553
|
})
|
|
@@ -339,7 +339,8 @@ describe('resolveModuleFile', () => {
|
|
|
339
339
|
const conventionFiles = [
|
|
340
340
|
'acl.ts', 'ce.ts', 'search.ts', 'notifications.ts',
|
|
341
341
|
'ai-tools.ts', 'events.ts', 'analytics.ts', 'setup.ts',
|
|
342
|
-
'translations.ts', '
|
|
342
|
+
'translations.ts', 'security.mfa-providers.ts', 'security.sudo.ts',
|
|
343
|
+
'data/extensions.ts', 'data/fields.ts',
|
|
343
344
|
]
|
|
344
345
|
for (const file of conventionFiles) {
|
|
345
346
|
touch(file, 'pkg')
|
|
@@ -405,6 +405,8 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
405
405
|
const eventsChecksumFile = path.join(outputDir, 'events.generated.checksum')
|
|
406
406
|
const analyticsOutFile = path.join(outputDir, 'analytics.generated.ts')
|
|
407
407
|
const analyticsChecksumFile = path.join(outputDir, 'analytics.generated.checksum')
|
|
408
|
+
const bootstrapRegsOutFile = path.join(outputDir, 'bootstrap-registrations.generated.ts')
|
|
409
|
+
const bootstrapRegsChecksumFile = path.join(outputDir, 'bootstrap-registrations.generated.checksum')
|
|
408
410
|
const transFieldsOutFile = path.join(outputDir, 'translations-fields.generated.ts')
|
|
409
411
|
const transFieldsChecksumFile = path.join(outputDir, 'translations-fields.generated.checksum')
|
|
410
412
|
const enrichersOutFile = path.join(outputDir, 'enrichers.generated.ts')
|
|
@@ -419,8 +421,37 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
419
421
|
const guardsChecksumFile = path.join(outputDir, 'guards.generated.checksum')
|
|
420
422
|
const commandInterceptorsOutFile = path.join(outputDir, 'command-interceptors.generated.ts')
|
|
421
423
|
const commandInterceptorsChecksumFile = path.join(outputDir, 'command-interceptors.generated.checksum')
|
|
424
|
+
const frontendMiddlewareOutFile = path.join(outputDir, 'frontend-middleware.generated.ts')
|
|
425
|
+
const frontendMiddlewareChecksumFile = path.join(outputDir, 'frontend-middleware.generated.checksum')
|
|
426
|
+
const backendMiddlewareOutFile = path.join(outputDir, 'backend-middleware.generated.ts')
|
|
427
|
+
const backendMiddlewareChecksumFile = path.join(outputDir, 'backend-middleware.generated.checksum')
|
|
422
428
|
|
|
423
429
|
const enabled = resolver.loadEnabledModules()
|
|
430
|
+
|
|
431
|
+
// Pre-pass: collect generator plugins from each enabled module's generators.ts
|
|
432
|
+
const pluginRegistry = new Map<string, import('@open-mercato/shared/modules/generators').GeneratorPlugin>()
|
|
433
|
+
const pluginState = new Map<string, { imports: string[]; configs: string[] }>()
|
|
434
|
+
for (const entry of enabled) {
|
|
435
|
+
const roots = resolver.getModulePaths(entry)
|
|
436
|
+
const rawImps = resolver.getModuleImportBase(entry)
|
|
437
|
+
const isAppMod = entry.from === '@app'
|
|
438
|
+
const appImportBase = isAppMod ? `../../src/modules/${entry.id}` : rawImps.appBase
|
|
439
|
+
const imps: ModuleImports = { appBase: appImportBase, pkgBase: rawImps.pkgBase }
|
|
440
|
+
const resolved = resolveModuleFile(roots, imps, 'generators.ts')
|
|
441
|
+
if (!resolved) continue
|
|
442
|
+
try {
|
|
443
|
+
const pluginMod = await import(resolved.absolutePath)
|
|
444
|
+
const plugins: import('@open-mercato/shared/modules/generators').GeneratorPlugin[] =
|
|
445
|
+
pluginMod.generatorPlugins ?? pluginMod.default ?? []
|
|
446
|
+
for (const plugin of plugins) {
|
|
447
|
+
if (!pluginRegistry.has(plugin.id)) {
|
|
448
|
+
pluginRegistry.set(plugin.id, plugin)
|
|
449
|
+
pluginState.set(plugin.id, { imports: [], configs: [] })
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
} catch {}
|
|
453
|
+
}
|
|
454
|
+
|
|
424
455
|
const imports: string[] = []
|
|
425
456
|
const moduleDecls: string[] = []
|
|
426
457
|
// Mutable ref so extracted helper functions can increment the shared counter
|
|
@@ -462,6 +493,10 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
462
493
|
const guardImports: string[] = []
|
|
463
494
|
const commandInterceptorConfigs: string[] = []
|
|
464
495
|
const commandInterceptorImports: string[] = []
|
|
496
|
+
const frontendMiddlewareConfigs: string[] = []
|
|
497
|
+
const frontendMiddlewareImports: string[] = []
|
|
498
|
+
const backendMiddlewareConfigs: string[] = []
|
|
499
|
+
const backendMiddlewareImports: string[] = []
|
|
465
500
|
|
|
466
501
|
// UMES conflict detection: collect file paths during module processing
|
|
467
502
|
const umesConflictSources: Array<{
|
|
@@ -735,6 +770,18 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
735
770
|
configExpr: (n, id) => `{ moduleId: '${id}', fields: (${n}.default ?? ${n}.translatableFields ?? {}) as Record<string, string[]> }`,
|
|
736
771
|
})
|
|
737
772
|
|
|
773
|
+
// Generator plugins: process each registered plugin's convention file
|
|
774
|
+
for (const plugin of pluginRegistry.values()) {
|
|
775
|
+
processStandaloneConfig({
|
|
776
|
+
roots, imps, modId, importIdRef,
|
|
777
|
+
relativePath: plugin.conventionFile,
|
|
778
|
+
prefix: plugin.importPrefix,
|
|
779
|
+
standaloneImports: pluginState.get(plugin.id)!.imports,
|
|
780
|
+
standaloneConfigs: pluginState.get(plugin.id)!.configs,
|
|
781
|
+
configExpr: plugin.configExpr,
|
|
782
|
+
})
|
|
783
|
+
}
|
|
784
|
+
|
|
738
785
|
// Inbox Actions: inbox-actions.ts
|
|
739
786
|
{
|
|
740
787
|
const resolved = resolveModuleFile(roots, imps, 'inbox-actions.ts')
|
|
@@ -768,6 +815,26 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
768
815
|
configExpr: (n, id) => `{ moduleId: '${id}', interceptors: ((${n} as any).interceptors ?? (${n} as any).default ?? []) }`,
|
|
769
816
|
})
|
|
770
817
|
|
|
818
|
+
// 10g. Frontend page middleware: frontend/middleware.ts
|
|
819
|
+
processStandaloneConfig({
|
|
820
|
+
roots, imps, modId, importIdRef,
|
|
821
|
+
relativePath: 'frontend/middleware.ts',
|
|
822
|
+
prefix: 'FRONTEND_MIDDLEWARE',
|
|
823
|
+
standaloneImports: frontendMiddlewareImports,
|
|
824
|
+
standaloneConfigs: frontendMiddlewareConfigs,
|
|
825
|
+
configExpr: (n, id) => `{ moduleId: '${id}', middleware: ((${n} as any).middleware ?? (${n} as any).default ?? []) }`,
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
// 10h. Backend page middleware: backend/middleware.ts
|
|
829
|
+
processStandaloneConfig({
|
|
830
|
+
roots, imps, modId, importIdRef,
|
|
831
|
+
relativePath: 'backend/middleware.ts',
|
|
832
|
+
prefix: 'BACKEND_MIDDLEWARE',
|
|
833
|
+
standaloneImports: backendMiddlewareImports,
|
|
834
|
+
standaloneConfigs: backendMiddlewareConfigs,
|
|
835
|
+
configExpr: (n, id) => `{ moduleId: '${id}', middleware: ((${n} as any).middleware ?? (${n} as any).default ?? []) }`,
|
|
836
|
+
})
|
|
837
|
+
|
|
771
838
|
// 11. Setup: setup.ts
|
|
772
839
|
{
|
|
773
840
|
const setup = resolveConventionFile(roots, imps, 'setup.ts', 'SETUP', modId, importIdRef, imports)
|
|
@@ -1430,6 +1497,43 @@ export const allAiTools = aiToolConfigEntries.flatMap(e => e.tools)
|
|
|
1430
1497
|
writeGeneratedFile({ outFile: analyticsOutFile, checksumFile: analyticsChecksumFile, content: analyticsOutput, structureChecksum, result, quiet })
|
|
1431
1498
|
writeGeneratedFile({ outFile: transFieldsOutFile, checksumFile: transFieldsChecksumFile, content: transFieldsOutput, structureChecksum, result, quiet })
|
|
1432
1499
|
|
|
1500
|
+
// Generator plugin outputs (registered via modules' generators.ts)
|
|
1501
|
+
for (const [pluginId, plugin] of pluginRegistry) {
|
|
1502
|
+
const state = pluginState.get(pluginId)!
|
|
1503
|
+
const importSection = state.imports.join('\n')
|
|
1504
|
+
const entriesLiteral = state.configs.join(',\n ')
|
|
1505
|
+
const content = plugin.buildOutput({ importSection, entriesLiteral })
|
|
1506
|
+
const outFile = path.join(outputDir, plugin.outputFileName)
|
|
1507
|
+
const checksumFile = outFile.replace('.ts', '.checksum')
|
|
1508
|
+
writeGeneratedFile({ outFile, checksumFile, content, structureChecksum, result, quiet })
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// Bootstrap registrations: aggregate all plugin bootstrap-registration hooks into one file.
|
|
1512
|
+
// Always written (even when empty) so bootstrap.ts can unconditionally import it.
|
|
1513
|
+
{
|
|
1514
|
+
const bootstrapPlugins = [...pluginRegistry.values()].filter((p) => p.bootstrapRegistration)
|
|
1515
|
+
const allEntryImports: string[] = []
|
|
1516
|
+
const allRegImports: string[] = []
|
|
1517
|
+
const allCalls: string[] = []
|
|
1518
|
+
for (const plugin of bootstrapPlugins) {
|
|
1519
|
+
const reg = plugin.bootstrapRegistration!
|
|
1520
|
+
const outputBase = plugin.outputFileName.replace('.ts', '')
|
|
1521
|
+
allEntryImports.push(`import { ${reg.entriesExportName} } from './${outputBase}'`)
|
|
1522
|
+
allRegImports.push(...reg.registrationImports)
|
|
1523
|
+
allCalls.push(reg.buildCall(reg.entriesExportName))
|
|
1524
|
+
}
|
|
1525
|
+
const uniqueImports = [...new Set([...allEntryImports, ...allRegImports])]
|
|
1526
|
+
const importSection = uniqueImports.join('\n')
|
|
1527
|
+
const body = allCalls.length ? ` ${allCalls.join('\n ')}` : ''
|
|
1528
|
+
const bootstrapRegsOutput = `// AUTO-GENERATED by mercato generate registry
|
|
1529
|
+
${importSection ? `${importSection}\n` : ''}
|
|
1530
|
+
export function runBootstrapRegistrations(): void {
|
|
1531
|
+
${body}
|
|
1532
|
+
}
|
|
1533
|
+
`
|
|
1534
|
+
writeGeneratedFile({ outFile: bootstrapRegsOutFile, checksumFile: bootstrapRegsChecksumFile, content: bootstrapRegsOutput, structureChecksum, result, quiet })
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1433
1537
|
// Enrichers generated file
|
|
1434
1538
|
const enricherEntriesLiteral = enricherConfigs.join(',\n ')
|
|
1435
1539
|
const enricherImportSection = enricherImports.join('\n')
|
|
@@ -1518,6 +1622,46 @@ ${commandInterceptorEntriesLiteral ? ` ${commandInterceptorEntriesLiteral}\n` :
|
|
|
1518
1622
|
`
|
|
1519
1623
|
writeGeneratedFile({ outFile: commandInterceptorsOutFile, checksumFile: commandInterceptorsChecksumFile, content: commandInterceptorsOutput, structureChecksum, result, quiet })
|
|
1520
1624
|
|
|
1625
|
+
const frontendMiddlewareEntriesLiteral = frontendMiddlewareConfigs.join(',\n ')
|
|
1626
|
+
const frontendMiddlewareImportSection = frontendMiddlewareImports.join('\n')
|
|
1627
|
+
const frontendMiddlewareOutput = `// AUTO-GENERATED by mercato generate registry
|
|
1628
|
+
import type { PageMiddlewareRegistryEntry, PageRouteMiddleware } from '@open-mercato/shared/modules/middleware/page'
|
|
1629
|
+
${frontendMiddlewareImportSection ? `\n${frontendMiddlewareImportSection}\n` : '\n'}type FrontendMiddlewareEntry = { moduleId: string; middleware: PageRouteMiddleware[] }
|
|
1630
|
+
|
|
1631
|
+
const entriesRaw: FrontendMiddlewareEntry[] = [
|
|
1632
|
+
${frontendMiddlewareEntriesLiteral ? ` ${frontendMiddlewareEntriesLiteral}\n` : ''}]
|
|
1633
|
+
|
|
1634
|
+
export const frontendMiddlewareEntries: PageMiddlewareRegistryEntry[] = entriesRaw
|
|
1635
|
+
`
|
|
1636
|
+
writeGeneratedFile({
|
|
1637
|
+
outFile: frontendMiddlewareOutFile,
|
|
1638
|
+
checksumFile: frontendMiddlewareChecksumFile,
|
|
1639
|
+
content: frontendMiddlewareOutput,
|
|
1640
|
+
structureChecksum,
|
|
1641
|
+
result,
|
|
1642
|
+
quiet,
|
|
1643
|
+
})
|
|
1644
|
+
|
|
1645
|
+
const backendMiddlewareEntriesLiteral = backendMiddlewareConfigs.join(',\n ')
|
|
1646
|
+
const backendMiddlewareImportSection = backendMiddlewareImports.join('\n')
|
|
1647
|
+
const backendMiddlewareOutput = `// AUTO-GENERATED by mercato generate registry
|
|
1648
|
+
import type { PageMiddlewareRegistryEntry, PageRouteMiddleware } from '@open-mercato/shared/modules/middleware/page'
|
|
1649
|
+
${backendMiddlewareImportSection ? `\n${backendMiddlewareImportSection}\n` : '\n'}type BackendMiddlewareEntry = { moduleId: string; middleware: PageRouteMiddleware[] }
|
|
1650
|
+
|
|
1651
|
+
const entriesRaw: BackendMiddlewareEntry[] = [
|
|
1652
|
+
${backendMiddlewareEntriesLiteral ? ` ${backendMiddlewareEntriesLiteral}\n` : ''}]
|
|
1653
|
+
|
|
1654
|
+
export const backendMiddlewareEntries: PageMiddlewareRegistryEntry[] = entriesRaw
|
|
1655
|
+
`
|
|
1656
|
+
writeGeneratedFile({
|
|
1657
|
+
outFile: backendMiddlewareOutFile,
|
|
1658
|
+
checksumFile: backendMiddlewareChecksumFile,
|
|
1659
|
+
content: backendMiddlewareOutput,
|
|
1660
|
+
structureChecksum,
|
|
1661
|
+
result,
|
|
1662
|
+
quiet,
|
|
1663
|
+
})
|
|
1664
|
+
|
|
1521
1665
|
return result
|
|
1522
1666
|
}
|
|
1523
1667
|
|
|
@@ -14,10 +14,12 @@ import {
|
|
|
14
14
|
readEphemeralEnvironmentState,
|
|
15
15
|
clearEphemeralEnvironmentState,
|
|
16
16
|
resolveBuildCacheTtlSeconds,
|
|
17
|
+
resolveAppReadyTimeoutMs,
|
|
17
18
|
shouldReuseBuildArtifacts,
|
|
18
19
|
} from '../integration'
|
|
19
20
|
|
|
20
21
|
const CACHE_TTL_ENV_VAR = 'OM_INTEGRATION_BUILD_CACHE_TTL_SECONDS'
|
|
22
|
+
const APP_READY_TIMEOUT_ENV_VAR = 'OM_INTEGRATION_APP_READY_TIMEOUT_SECONDS'
|
|
21
23
|
const resolver = createResolver()
|
|
22
24
|
const projectRootDirectory = resolver.getRootDir()
|
|
23
25
|
|
|
@@ -36,6 +38,7 @@ describe('integration cache and options', () => {
|
|
|
36
38
|
const ephemeralEnvFilePath = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.json')
|
|
37
39
|
const ephemeralLegacyEnvFilePath = path.join(projectRootDirectory, '.ai', 'qa', 'ephemeral-env.md')
|
|
38
40
|
const originalCacheTtl = process.env[CACHE_TTL_ENV_VAR]
|
|
41
|
+
const originalAppReadyTimeout = process.env[APP_READY_TIMEOUT_ENV_VAR]
|
|
39
42
|
let originalEphemeralEnvState: string | null = null
|
|
40
43
|
let originalEphemeralLegacyEnvState: string | null = null
|
|
41
44
|
|
|
@@ -61,6 +64,11 @@ describe('integration cache and options', () => {
|
|
|
61
64
|
} else {
|
|
62
65
|
process.env[CACHE_TTL_ENV_VAR] = originalCacheTtl
|
|
63
66
|
}
|
|
67
|
+
if (originalAppReadyTimeout === undefined) {
|
|
68
|
+
delete process.env[APP_READY_TIMEOUT_ENV_VAR]
|
|
69
|
+
} else {
|
|
70
|
+
process.env[APP_READY_TIMEOUT_ENV_VAR] = originalAppReadyTimeout
|
|
71
|
+
}
|
|
64
72
|
await restoreEphemeralStateFiles(originalEphemeralEnvState, originalEphemeralLegacyEnvState)
|
|
65
73
|
})
|
|
66
74
|
|
|
@@ -68,6 +76,9 @@ describe('integration cache and options', () => {
|
|
|
68
76
|
const baseUrl = 'http://127.0.0.1:5001'
|
|
69
77
|
const fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(async (input) => {
|
|
70
78
|
const url = typeof input === 'string' ? input : String(input)
|
|
79
|
+
if (url.endsWith('/api/auth/login')) {
|
|
80
|
+
return { status: 401, text: async () => '' } as unknown as Response
|
|
81
|
+
}
|
|
71
82
|
if (url.endsWith('/login')) {
|
|
72
83
|
return {
|
|
73
84
|
status: 200,
|
|
@@ -77,9 +88,6 @@ describe('integration cache and options', () => {
|
|
|
77
88
|
if (url.includes('/_next/static/chunks/app-healthcheck.js')) {
|
|
78
89
|
return { status: 200, text: async () => '' } as unknown as Response
|
|
79
90
|
}
|
|
80
|
-
if (url.endsWith('/api/auth/login')) {
|
|
81
|
-
return { status: 401, text: async () => '' } as unknown as Response
|
|
82
|
-
}
|
|
83
91
|
return { status: 200, text: async () => '' } as unknown as Response
|
|
84
92
|
})
|
|
85
93
|
|
|
@@ -113,6 +121,87 @@ describe('integration cache and options', () => {
|
|
|
113
121
|
}
|
|
114
122
|
}, 20000)
|
|
115
123
|
|
|
124
|
+
it('reuses an existing environment when /login returns a redirect status other than 302', async () => {
|
|
125
|
+
const baseUrl = 'http://127.0.0.1:5001'
|
|
126
|
+
const fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(async (input) => {
|
|
127
|
+
const url = typeof input === 'string' ? input : String(input)
|
|
128
|
+
if (url.endsWith('/api/auth/login')) {
|
|
129
|
+
return { status: 401, text: async () => '' } as unknown as Response
|
|
130
|
+
}
|
|
131
|
+
if (url.endsWith('/login')) {
|
|
132
|
+
return { status: 308, text: async () => '' } as unknown as Response
|
|
133
|
+
}
|
|
134
|
+
return { status: 200, text: async () => '' } as unknown as Response
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await writeEphemeralEnvironmentState({
|
|
139
|
+
baseUrl,
|
|
140
|
+
port: 5001,
|
|
141
|
+
logPrefix: 'integration',
|
|
142
|
+
captureScreenshots: true,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const environment = await tryReuseExistingEnvironment({
|
|
146
|
+
verbose: false,
|
|
147
|
+
captureScreenshots: true,
|
|
148
|
+
logPrefix: 'integration',
|
|
149
|
+
forceRebuild: false,
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(environment).not.toBeNull()
|
|
153
|
+
expect(environment).toMatchObject({
|
|
154
|
+
baseUrl,
|
|
155
|
+
port: 5001,
|
|
156
|
+
ownedByCurrentProcess: false,
|
|
157
|
+
})
|
|
158
|
+
} finally {
|
|
159
|
+
fetchSpy.mockRestore()
|
|
160
|
+
}
|
|
161
|
+
}, 20000)
|
|
162
|
+
|
|
163
|
+
it('reuses an existing environment when /login returns healthy HTML without static asset references', async () => {
|
|
164
|
+
const baseUrl = 'http://127.0.0.1:5001'
|
|
165
|
+
const fetchSpy = jest.spyOn(global, 'fetch').mockImplementation(async (input) => {
|
|
166
|
+
const url = typeof input === 'string' ? input : String(input)
|
|
167
|
+
if (url.endsWith('/api/auth/login')) {
|
|
168
|
+
return { status: 401, text: async () => '' } as unknown as Response
|
|
169
|
+
}
|
|
170
|
+
if (url.endsWith('/login')) {
|
|
171
|
+
return {
|
|
172
|
+
status: 200,
|
|
173
|
+
text: async () => '<!doctype html><html><body><form data-auth-ready="0"></form></body></html>',
|
|
174
|
+
} as unknown as Response
|
|
175
|
+
}
|
|
176
|
+
return { status: 200, text: async () => '' } as unknown as Response
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
await writeEphemeralEnvironmentState({
|
|
181
|
+
baseUrl,
|
|
182
|
+
port: 5001,
|
|
183
|
+
logPrefix: 'integration',
|
|
184
|
+
captureScreenshots: false,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const environment = await tryReuseExistingEnvironment({
|
|
188
|
+
verbose: false,
|
|
189
|
+
captureScreenshots: false,
|
|
190
|
+
logPrefix: 'integration',
|
|
191
|
+
forceRebuild: false,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
expect(environment).not.toBeNull()
|
|
195
|
+
expect(environment).toMatchObject({
|
|
196
|
+
baseUrl,
|
|
197
|
+
port: 5001,
|
|
198
|
+
ownedByCurrentProcess: false,
|
|
199
|
+
})
|
|
200
|
+
} finally {
|
|
201
|
+
fetchSpy.mockRestore()
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
116
205
|
it('falls back to rebuilding when the ephemeral environment state is unreachable', async () => {
|
|
117
206
|
const baseUrl = 'http://127.0.0.1:5001'
|
|
118
207
|
const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({ status: 500 } as unknown as Response)
|
|
@@ -223,6 +312,20 @@ describe('integration cache and options', () => {
|
|
|
223
312
|
warn.mockRestore()
|
|
224
313
|
})
|
|
225
314
|
|
|
315
|
+
it('resolves app readiness timeout from env variable', () => {
|
|
316
|
+
delete process.env[APP_READY_TIMEOUT_ENV_VAR]
|
|
317
|
+
expect(resolveAppReadyTimeoutMs('integration')).toBe(90_000)
|
|
318
|
+
|
|
319
|
+
process.env[APP_READY_TIMEOUT_ENV_VAR] = '180'
|
|
320
|
+
expect(resolveAppReadyTimeoutMs('integration')).toBe(180_000)
|
|
321
|
+
|
|
322
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
323
|
+
process.env[APP_READY_TIMEOUT_ENV_VAR] = '0'
|
|
324
|
+
expect(resolveAppReadyTimeoutMs('integration')).toBe(90_000)
|
|
325
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Invalid'))
|
|
326
|
+
warn.mockRestore()
|
|
327
|
+
})
|
|
328
|
+
|
|
226
329
|
it('reuses build artifacts only with matching source fingerprint and fresh cache state', async () => {
|
|
227
330
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'om-int-cache-test-'))
|
|
228
331
|
try {
|