@open-mercato/cli 0.6.6-develop.6361.1.e790d2aa34 → 0.6.6-develop.6363.1.03c5e3429a

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/utils.js CHANGED
@@ -170,7 +170,7 @@ function writeGeneratedFile(options) {
170
170
  const { outFile, checksumFile, content, structureChecksum, result, quiet } = options;
171
171
  const checksum = { content: calculateChecksum(content), structure: structureChecksum };
172
172
  const existing = readChecksumRecord(checksumFile);
173
- const shouldWrite = !existing || existing.content !== checksum.content || existing.structure !== checksum.structure;
173
+ const shouldWrite = !fs.existsSync(outFile) || !existing || existing.content !== checksum.content || existing.structure !== checksum.structure;
174
174
  if (shouldWrite) {
175
175
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
176
176
  fs.writeFileSync(outFile, content);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/utils.ts"],
4
- "sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\nimport crypto from 'node:crypto'\nimport { pathToFileURL } from 'node:url'\n\nexport type ChecksumRecord = {\n content: string\n structure: string\n}\n\nexport interface GeneratorResult {\n filesWritten: string[]\n filesUnchanged: string[]\n errors: string[]\n}\n\nexport function calculateChecksum(content: string): string {\n return crypto.createHash('md5').update(content).digest('hex')\n}\n\nexport function readChecksumRecord(filePath: string): ChecksumRecord | null {\n if (!fs.existsSync(filePath)) {\n return null\n }\n try {\n const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Partial<ChecksumRecord>\n if (parsed && typeof parsed.content === 'string' && typeof parsed.structure === 'string') {\n return { content: parsed.content, structure: parsed.structure }\n }\n } catch {\n // Invalid checksum file\n }\n return null\n}\n\nexport function writeChecksumRecord(filePath: string, record: ChecksumRecord): void {\n fs.writeFileSync(filePath, JSON.stringify(record) + '\\n')\n}\n\nfunction collectStructureEntries(target: string, base: string, acc: string[]): void {\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(target, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))\n } catch (err) {\n acc.push(`error:${path.relative(base, target)}:${(err as Error).message}`)\n return\n }\n\n for (const entry of entries) {\n const fullPath = path.join(target, entry.name)\n const rel = path.relative(base, fullPath)\n try {\n const stat = fs.statSync(fullPath)\n if (entry.isDirectory()) {\n acc.push(`dir:${rel}:${stat.mtimeMs}`)\n collectStructureEntries(fullPath, base, acc)\n } else if (entry.isFile()) {\n acc.push(`file:${rel}:${stat.size}:${stat.mtimeMs}`)\n } else {\n acc.push(`other:${rel}:${stat.mtimeMs}`)\n }\n } catch {\n // File was deleted between readdir and stat - skip it\n continue\n }\n }\n}\n\nexport function calculateStructureChecksum(paths: string[]): string {\n const normalized = Array.from(new Set(paths.map((p) => path.resolve(p)))).sort((a, b) => a.localeCompare(b))\n const entries: string[] = []\n for (const target of normalized) {\n if (!fs.existsSync(target)) {\n entries.push(`missing:${target}`)\n continue\n }\n const stat = fs.statSync(target)\n entries.push(`${stat.isDirectory() ? 'dir' : 'file'}:${target}:${stat.mtimeMs}`)\n if (stat.isDirectory()) collectStructureEntries(target, target, entries)\n }\n return calculateChecksum(entries.join('\\n'))\n}\n\nexport function writeIfChanged(\n filePath: string,\n content: string,\n checksumPath?: string,\n structureChecksum?: string\n): boolean {\n const newChecksum = calculateChecksum(content)\n\n if (checksumPath) {\n const existingRecord = readChecksumRecord(checksumPath)\n const newRecord: ChecksumRecord = {\n content: newChecksum,\n structure: structureChecksum || '',\n }\n\n const shouldWrite =\n !existingRecord ||\n existingRecord.content !== newRecord.content ||\n (structureChecksum && existingRecord.structure !== newRecord.structure)\n\n if (shouldWrite) {\n ensureDir(filePath)\n fs.writeFileSync(filePath, content)\n writeChecksumRecord(checksumPath, newRecord)\n return true\n }\n return false\n }\n\n // Simple comparison without checksum file\n if (fs.existsSync(filePath)) {\n const existing = fs.readFileSync(filePath, 'utf8')\n if (existing === content) {\n return false\n }\n }\n\n ensureDir(filePath)\n fs.writeFileSync(filePath, content)\n return true\n}\n\nexport function ensureDir(filePath: string): void {\n fs.mkdirSync(path.dirname(filePath), { recursive: true })\n}\n\n// Allowed path substrings for safe deletion. Include both POSIX and Windows separators.\nconst ALLOWED_RIMRAF_PATTERNS = [\n '/generated/',\n '/dist/',\n '/.mercato/',\n '/entities/',\n '\\\\generated\\\\',\n '\\\\dist\\\\',\n '\\\\.mercato\\\\',\n '\\\\entities\\\\',\n]\n\nexport function rimrafDir(dir: string, opts?: { allowedPatterns?: string[] }): void {\n if (!fs.existsSync(dir)) return\n\n // Safety check: only allow deletion within known safe directories\n const resolved = path.resolve(dir)\n const allowed = opts?.allowedPatterns ?? ALLOWED_RIMRAF_PATTERNS\n\n // Normalize resolved path to support matching against both POSIX and Windows patterns\n const normalized = {\n posix: resolved.replace(/\\\\/g, '/'),\n win: resolved.replace(/\\//g, '\\\\'),\n }\n\n if (!allowed.some((pattern) => normalized.posix.includes(pattern) || normalized.win.includes(pattern))) {\n throw new Error(`Refusing to delete directory outside allowed paths: ${resolved}. Allowed patterns: ${allowed.join(', ')}`)\n }\n\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const p = path.join(dir, entry.name)\n if (entry.isDirectory()) rimrafDir(p, opts)\n else fs.unlinkSync(p)\n }\n fs.rmdirSync(dir)\n}\n\nexport function toVar(s: string): string {\n return s.replace(/[^a-zA-Z0-9_]/g, '_')\n}\n\nexport function toSnake(s: string): string {\n return s\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/\\W+/g, '_')\n .replace(/_{2,}/g, '_')\n .replace(/(?:^_+|_+$)/g, '')\n .toLowerCase()\n}\n\nexport function sourceFileHasNamedExport(filePath: string, exportName: string): boolean {\n let source: string\n try {\n source = fs.readFileSync(filePath, 'utf8')\n } catch {\n return false\n }\n\n const escaped = exportName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n if (new RegExp(`export\\\\s+(?:const|let|var|function|class|async\\\\s+function)\\\\s+${escaped}\\\\b`).test(source)) {\n return true\n }\n if (new RegExp(`export\\\\s+\\\\{[^}]*\\\\b${escaped}\\\\b[^}]*\\\\}`).test(source)) {\n return true\n }\n return false\n}\n\nexport async function moduleHasExport(filePath: string, exportName: string): Promise<boolean> {\n if (sourceFileHasNamedExport(filePath, exportName)) {\n return true\n }\n try {\n const isAbsolutePath = path.isAbsolute(filePath)\n const importUrl = isAbsolutePath ? pathToFileURL(filePath).href : filePath\n const mod = await import(importUrl)\n return mod != null && Object.prototype.hasOwnProperty.call(mod, exportName)\n } catch {\n return false\n }\n}\n\nexport function logGenerationResult(label: string, changed: boolean): void {\n if (changed) {\n console.log(`Generated ${label}`)\n }\n}\n\nexport function createGeneratorResult(): GeneratorResult {\n return {\n filesWritten: [],\n filesUnchanged: [],\n errors: [],\n }\n}\n\nexport function writeGeneratedFile(options: {\n outFile: string\n checksumFile: string\n content: string\n structureChecksum: string\n result: GeneratorResult\n quiet?: boolean\n}): void {\n const { outFile, checksumFile, content, structureChecksum, result, quiet } = options\n const checksum = { content: calculateChecksum(content), structure: structureChecksum }\n const existing = readChecksumRecord(checksumFile)\n const shouldWrite =\n !existing ||\n existing.content !== checksum.content ||\n existing.structure !== checksum.structure\n if (shouldWrite) {\n fs.mkdirSync(path.dirname(outFile), { recursive: true })\n fs.writeFileSync(outFile, content)\n writeChecksumRecord(checksumFile, checksum)\n result.filesWritten.push(outFile)\n } else {\n result.filesUnchanged.push(outFile)\n }\n if (!quiet) logGenerationResult(path.relative(process.cwd(), outFile), shouldWrite)\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\nimport crypto from 'node:crypto'\nimport { pathToFileURL } from 'node:url'\n\nexport type ChecksumRecord = {\n content: string\n structure: string\n}\n\nexport interface GeneratorResult {\n filesWritten: string[]\n filesUnchanged: string[]\n errors: string[]\n}\n\nexport function calculateChecksum(content: string): string {\n return crypto.createHash('md5').update(content).digest('hex')\n}\n\nexport function readChecksumRecord(filePath: string): ChecksumRecord | null {\n if (!fs.existsSync(filePath)) {\n return null\n }\n try {\n const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Partial<ChecksumRecord>\n if (parsed && typeof parsed.content === 'string' && typeof parsed.structure === 'string') {\n return { content: parsed.content, structure: parsed.structure }\n }\n } catch {\n // Invalid checksum file\n }\n return null\n}\n\nexport function writeChecksumRecord(filePath: string, record: ChecksumRecord): void {\n fs.writeFileSync(filePath, JSON.stringify(record) + '\\n')\n}\n\nfunction collectStructureEntries(target: string, base: string, acc: string[]): void {\n let entries: fs.Dirent[]\n try {\n entries = fs.readdirSync(target, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))\n } catch (err) {\n acc.push(`error:${path.relative(base, target)}:${(err as Error).message}`)\n return\n }\n\n for (const entry of entries) {\n const fullPath = path.join(target, entry.name)\n const rel = path.relative(base, fullPath)\n try {\n const stat = fs.statSync(fullPath)\n if (entry.isDirectory()) {\n acc.push(`dir:${rel}:${stat.mtimeMs}`)\n collectStructureEntries(fullPath, base, acc)\n } else if (entry.isFile()) {\n acc.push(`file:${rel}:${stat.size}:${stat.mtimeMs}`)\n } else {\n acc.push(`other:${rel}:${stat.mtimeMs}`)\n }\n } catch {\n // File was deleted between readdir and stat - skip it\n continue\n }\n }\n}\n\nexport function calculateStructureChecksum(paths: string[]): string {\n const normalized = Array.from(new Set(paths.map((p) => path.resolve(p)))).sort((a, b) => a.localeCompare(b))\n const entries: string[] = []\n for (const target of normalized) {\n if (!fs.existsSync(target)) {\n entries.push(`missing:${target}`)\n continue\n }\n const stat = fs.statSync(target)\n entries.push(`${stat.isDirectory() ? 'dir' : 'file'}:${target}:${stat.mtimeMs}`)\n if (stat.isDirectory()) collectStructureEntries(target, target, entries)\n }\n return calculateChecksum(entries.join('\\n'))\n}\n\nexport function writeIfChanged(\n filePath: string,\n content: string,\n checksumPath?: string,\n structureChecksum?: string\n): boolean {\n const newChecksum = calculateChecksum(content)\n\n if (checksumPath) {\n const existingRecord = readChecksumRecord(checksumPath)\n const newRecord: ChecksumRecord = {\n content: newChecksum,\n structure: structureChecksum || '',\n }\n\n const shouldWrite =\n !existingRecord ||\n existingRecord.content !== newRecord.content ||\n (structureChecksum && existingRecord.structure !== newRecord.structure)\n\n if (shouldWrite) {\n ensureDir(filePath)\n fs.writeFileSync(filePath, content)\n writeChecksumRecord(checksumPath, newRecord)\n return true\n }\n return false\n }\n\n // Simple comparison without checksum file\n if (fs.existsSync(filePath)) {\n const existing = fs.readFileSync(filePath, 'utf8')\n if (existing === content) {\n return false\n }\n }\n\n ensureDir(filePath)\n fs.writeFileSync(filePath, content)\n return true\n}\n\nexport function ensureDir(filePath: string): void {\n fs.mkdirSync(path.dirname(filePath), { recursive: true })\n}\n\n// Allowed path substrings for safe deletion. Include both POSIX and Windows separators.\nconst ALLOWED_RIMRAF_PATTERNS = [\n '/generated/',\n '/dist/',\n '/.mercato/',\n '/entities/',\n '\\\\generated\\\\',\n '\\\\dist\\\\',\n '\\\\.mercato\\\\',\n '\\\\entities\\\\',\n]\n\nexport function rimrafDir(dir: string, opts?: { allowedPatterns?: string[] }): void {\n if (!fs.existsSync(dir)) return\n\n // Safety check: only allow deletion within known safe directories\n const resolved = path.resolve(dir)\n const allowed = opts?.allowedPatterns ?? ALLOWED_RIMRAF_PATTERNS\n\n // Normalize resolved path to support matching against both POSIX and Windows patterns\n const normalized = {\n posix: resolved.replace(/\\\\/g, '/'),\n win: resolved.replace(/\\//g, '\\\\'),\n }\n\n if (!allowed.some((pattern) => normalized.posix.includes(pattern) || normalized.win.includes(pattern))) {\n throw new Error(`Refusing to delete directory outside allowed paths: ${resolved}. Allowed patterns: ${allowed.join(', ')}`)\n }\n\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const p = path.join(dir, entry.name)\n if (entry.isDirectory()) rimrafDir(p, opts)\n else fs.unlinkSync(p)\n }\n fs.rmdirSync(dir)\n}\n\nexport function toVar(s: string): string {\n return s.replace(/[^a-zA-Z0-9_]/g, '_')\n}\n\nexport function toSnake(s: string): string {\n return s\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .replace(/\\W+/g, '_')\n .replace(/_{2,}/g, '_')\n .replace(/(?:^_+|_+$)/g, '')\n .toLowerCase()\n}\n\nexport function sourceFileHasNamedExport(filePath: string, exportName: string): boolean {\n let source: string\n try {\n source = fs.readFileSync(filePath, 'utf8')\n } catch {\n return false\n }\n\n const escaped = exportName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n if (new RegExp(`export\\\\s+(?:const|let|var|function|class|async\\\\s+function)\\\\s+${escaped}\\\\b`).test(source)) {\n return true\n }\n if (new RegExp(`export\\\\s+\\\\{[^}]*\\\\b${escaped}\\\\b[^}]*\\\\}`).test(source)) {\n return true\n }\n return false\n}\n\nexport async function moduleHasExport(filePath: string, exportName: string): Promise<boolean> {\n if (sourceFileHasNamedExport(filePath, exportName)) {\n return true\n }\n try {\n const isAbsolutePath = path.isAbsolute(filePath)\n const importUrl = isAbsolutePath ? pathToFileURL(filePath).href : filePath\n const mod = await import(importUrl)\n return mod != null && Object.prototype.hasOwnProperty.call(mod, exportName)\n } catch {\n return false\n }\n}\n\nexport function logGenerationResult(label: string, changed: boolean): void {\n if (changed) {\n console.log(`Generated ${label}`)\n }\n}\n\nexport function createGeneratorResult(): GeneratorResult {\n return {\n filesWritten: [],\n filesUnchanged: [],\n errors: [],\n }\n}\n\nexport function writeGeneratedFile(options: {\n outFile: string\n checksumFile: string\n content: string\n structureChecksum: string\n result: GeneratorResult\n quiet?: boolean\n}): void {\n const { outFile, checksumFile, content, structureChecksum, result, quiet } = options\n const checksum = { content: calculateChecksum(content), structure: structureChecksum }\n const existing = readChecksumRecord(checksumFile)\n const shouldWrite =\n !fs.existsSync(outFile) ||\n !existing ||\n existing.content !== checksum.content ||\n existing.structure !== checksum.structure\n if (shouldWrite) {\n fs.mkdirSync(path.dirname(outFile), { recursive: true })\n fs.writeFileSync(outFile, content)\n writeChecksumRecord(checksumFile, checksum)\n result.filesWritten.push(outFile)\n } else {\n result.filesUnchanged.push(outFile)\n }\n if (!quiet) logGenerationResult(path.relative(process.cwd(), outFile), shouldWrite)\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.6.6-develop.6361.1.e790d2aa34",
3
+ "version": "0.6.6-develop.6363.1.03c5e3429a",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,8 +60,8 @@
60
60
  "@mikro-orm/decorators": "^7.1.4",
61
61
  "@mikro-orm/migrations": "^7.1.4",
62
62
  "@mikro-orm/postgresql": "^7.1.4",
63
- "@open-mercato/queue": "0.6.6-develop.6361.1.e790d2aa34",
64
- "@open-mercato/shared": "0.6.6-develop.6361.1.e790d2aa34",
63
+ "@open-mercato/queue": "0.6.6-develop.6363.1.03c5e3429a",
64
+ "@open-mercato/shared": "0.6.6-develop.6363.1.03c5e3429a",
65
65
  "cross-spawn": "^7.0.6",
66
66
  "pg": "8.22.0",
67
67
  "semver": "^7.8.5",
@@ -71,10 +71,10 @@
71
71
  "typescript": "^6.0.3"
72
72
  },
73
73
  "peerDependencies": {
74
- "@open-mercato/shared": "0.6.6-develop.6361.1.e790d2aa34"
74
+ "@open-mercato/shared": "0.6.6-develop.6363.1.03c5e3429a"
75
75
  },
76
76
  "devDependencies": {
77
- "@open-mercato/shared": "0.6.6-develop.6361.1.e790d2aa34",
77
+ "@open-mercato/shared": "0.6.6-develop.6363.1.03c5e3429a",
78
78
  "@types/jest": "^30.0.0",
79
79
  "jest": "^30.4.2",
80
80
  "ts-jest": "^29.4.11"
@@ -214,7 +214,7 @@ describe('generateModuleRegistry with module subsets', () => {
214
214
  `))
215
215
  expect(readGenerated(tmpDir, 'frontend-routes.generated.ts')).toBe(dedent(`
216
216
  // AUTO-GENERATED by mercato generate registry
217
- import type { FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
217
+ import { resolvePageRouteMetadata, type FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
218
218
 
219
219
  export const frontendRoutes: FrontendRouteManifestEntry[] = [
220
220
 
@@ -224,7 +224,7 @@ describe('generateModuleRegistry with module subsets', () => {
224
224
  `))
225
225
  expect(readGenerated(tmpDir, 'backend-routes.generated.ts')).toBe(dedent(`
226
226
  // AUTO-GENERATED by mercato generate registry
227
- import type { BackendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
227
+ import { resolvePageRouteMetadata, type BackendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
228
228
 
229
229
  export const backendRoutes: BackendRouteManifestEntry[] = [
230
230
 
@@ -309,20 +309,20 @@ describe('generateModuleRegistry with module subsets', () => {
309
309
  `))
310
310
  expect(readGenerated(tmpDir, 'frontend-routes.generated.ts')).toBe(dedent(`
311
311
  // AUTO-GENERATED by mercato generate registry
312
- import type { FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
312
+ import { resolvePageRouteMetadata, type FrontendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
313
313
 
314
314
  export const frontendRoutes: FrontendRouteManifestEntry[] = [
315
- { moduleId: "orders", pattern: "/", requireAuth: ((undefined as any))?.requireAuth, requireRoles: ((undefined as any))?.requireRoles, requireFeatures: ((undefined as any))?.requireFeatures, requireCustomerAuth: ((undefined as any))?.requireCustomerAuth, requireCustomerFeatures: ((undefined as any))?.requireCustomerFeatures, nav: ((undefined as any))?.nav, title: ((undefined as any))?.pageTitle ?? ((undefined as any))?.title, titleKey: ((undefined as any))?.pageTitleKey ?? ((undefined as any))?.titleKey, group: ((undefined as any))?.pageGroup ?? ((undefined as any))?.group, groupKey: ((undefined as any))?.pageGroupKey ?? ((undefined as any))?.groupKey, icon: ((undefined as any))?.icon, order: ((undefined as any))?.pageOrder ?? ((undefined as any))?.order, priority: ((undefined as any))?.pagePriority ?? ((undefined as any))?.priority, navHidden: ((undefined as any))?.navHidden, visible: ((undefined as any))?.visible, enabled: ((undefined as any))?.enabled, breadcrumb: ((undefined as any))?.breadcrumb, pageContext: ((undefined as any))?.pageContext, placement: ((undefined as any))?.placement, load: async () => { const mod = await import("@open-mercato/core/modules/orders/frontend/page"); return (mod.default ?? mod) as any } }
315
+ { moduleId: "orders", ...resolvePageRouteMetadata("/", (undefined as any)), load: async () => { const mod = await import("@open-mercato/core/modules/orders/frontend/page"); return (mod.default ?? mod) as any } }
316
316
  ]
317
317
 
318
318
  export default frontendRoutes
319
319
  `))
320
320
  expect(readGenerated(tmpDir, 'backend-routes.generated.ts')).toBe(dedent(`
321
321
  // AUTO-GENERATED by mercato generate registry
322
- import type { BackendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
322
+ import { resolvePageRouteMetadata, type BackendRouteManifestEntry } from '@open-mercato/shared/modules/registry'
323
323
 
324
324
  export const backendRoutes: BackendRouteManifestEntry[] = [
325
- { moduleId: "orders", pattern: "/backend/orders", requireAuth: ((undefined as any))?.requireAuth, requireRoles: ((undefined as any))?.requireRoles, requireFeatures: ((undefined as any))?.requireFeatures, requireCustomerAuth: ((undefined as any))?.requireCustomerAuth, requireCustomerFeatures: ((undefined as any))?.requireCustomerFeatures, nav: ((undefined as any))?.nav, title: ((undefined as any))?.pageTitle ?? ((undefined as any))?.title, titleKey: ((undefined as any))?.pageTitleKey ?? ((undefined as any))?.titleKey, group: ((undefined as any))?.pageGroup ?? ((undefined as any))?.group, groupKey: ((undefined as any))?.pageGroupKey ?? ((undefined as any))?.groupKey, icon: ((undefined as any))?.icon, order: ((undefined as any))?.pageOrder ?? ((undefined as any))?.order, priority: ((undefined as any))?.pagePriority ?? ((undefined as any))?.priority, navHidden: ((undefined as any))?.navHidden, visible: ((undefined as any))?.visible, enabled: ((undefined as any))?.enabled, breadcrumb: ((undefined as any))?.breadcrumb, pageContext: ((undefined as any))?.pageContext, placement: ((undefined as any))?.placement, load: async () => { const mod = await import("@open-mercato/core/modules/orders/backend/page"); return (mod.default ?? mod) as any } }
325
+ { moduleId: "orders", ...resolvePageRouteMetadata("/backend/orders", (undefined as any)), load: async () => { const mod = await import("@open-mercato/core/modules/orders/backend/page"); return (mod.default ?? mod) as any } }
326
326
  ]
327
327
 
328
328
  export default backendRoutes
@@ -463,14 +463,22 @@ describe('generateModuleRegistry with module subsets', () => {
463
463
  expect(output).toContain('subscriber_event_meta:on-created')
464
464
  })
465
465
 
466
- it('keeps backend route metadata runtime-backed when icons are non-serializable', async () => {
466
+ it('keeps runtime metadata fallback for settings/profile route icons that are not serializable yet', async () => {
467
467
  touchFile(
468
- path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'iconic', 'backend', 'dashboard', 'page.tsx'),
468
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'iconic', 'backend', 'settings', 'page.tsx'),
469
469
  'export default function Page() { return null }\n',
470
470
  )
471
471
  touchFile(
472
- path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'iconic', 'backend', 'dashboard', 'page.meta.ts'),
473
- "import React from 'react'\nconst icon = React.createElement('svg', null)\nexport const metadata = { requireAuth: true, pageTitle: 'Dashboard', icon }\n",
472
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'iconic', 'backend', 'settings', 'page.meta.ts'),
473
+ "import React from 'react'\nconst icon = React.createElement('svg', null)\nexport const metadata = { requireAuth: true, pageTitle: 'Settings', pageContext: 'settings', icon }\n",
474
+ )
475
+ touchFile(
476
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'iconic', 'backend', 'profile', 'page.tsx'),
477
+ 'export default function Page() { return null }\n',
478
+ )
479
+ touchFile(
480
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'iconic', 'backend', 'profile', 'page.meta.ts'),
481
+ "import React from 'react'\nconst icon = React.createElement('svg', null)\nexport const metadata = { requireAuth: true, pageTitle: 'Profile', pageContext: 'profile', icon }\n",
474
482
  )
475
483
 
476
484
  const enabled: ModuleEntry[] = [
@@ -481,9 +489,12 @@ describe('generateModuleRegistry with module subsets', () => {
481
489
 
482
490
  expect(result.errors).toEqual([])
483
491
  const output = readGenerated(tmpDir, 'backend-routes.generated.ts')!
484
- expect(output).toMatch(/import \* as .* from "@open-mercato\/core\/modules\/iconic\/backend\/dashboard\/page\.meta"/)
485
- expect(output).not.toContain('"pageTitle":"Dashboard"')
486
- expect(output).toContain('pattern: "/backend/dashboard"')
492
+ expect(output).toMatch(/import \* as .* from "@open-mercato\/core\/modules\/iconic\/backend\/settings\/page\.meta"/)
493
+ expect(output).toMatch(/import \* as .* from "@open-mercato\/core\/modules\/iconic\/backend\/profile\/page\.meta"/)
494
+ expect(output).toContain('resolvePageRouteMetadata("/backend/settings", (((BMManifest')
495
+ expect(output).toContain('resolvePageRouteMetadata("/backend/profile", (((BMManifest')
496
+ expect(output).not.toContain('"pageContext":"settings"')
497
+ expect(output).not.toContain('"pageContext":"profile"')
487
498
  })
488
499
 
489
500
  it('handles module with only widgets — no pages or subscribers', async () => {
@@ -1447,6 +1458,10 @@ describe('generateModuleRegistryApp with module subsets', () => {
1447
1458
  const output = readGenerated(tmpDir, 'modules.app.generated.ts')
1448
1459
  expect(output).not.toBeNull()
1449
1460
  expect(output).toContain('export const modules: Module[] = [')
1461
+
1462
+ const bootstrapOutput = readGenerated(tmpDir, 'modules.bootstrap.generated.ts')
1463
+ expect(bootstrapOutput).not.toBeNull()
1464
+ expect(bootstrapOutput).toContain('export const modules: Module[] = [')
1450
1465
  })
1451
1466
 
1452
1467
  it('app output excludes CLI commands and keeps lazy workers/subscribers', async () => {
@@ -1482,6 +1497,15 @@ describe('generateModuleRegistryApp with module subsets', () => {
1482
1497
  expect(output).toContain('createLazyModuleSubscriber')
1483
1498
  expect(output).toContain('createLazyModuleWorker')
1484
1499
  expect(output).toContain('dashboardWidgets:')
1500
+
1501
+ const bootstrapOutput = readGenerated(tmpDir, 'modules.bootstrap.generated.ts')!
1502
+ expect(bootstrapOutput).toContain('id: "runtime_mod"')
1503
+ expect(bootstrapOutput).not.toContain('cli:')
1504
+ expect(bootstrapOutput).toContain('subscribers:')
1505
+ expect(bootstrapOutput).toContain('workers:')
1506
+ expect(bootstrapOutput).toContain('createLazyModuleSubscriber')
1507
+ expect(bootstrapOutput).toContain('createLazyModuleWorker')
1508
+ expect(bootstrapOutput).toContain('dashboardWidgets:')
1485
1509
  })
1486
1510
  })
1487
1511
 
@@ -691,6 +691,7 @@ describe('generator output compatibility', () => {
691
691
  'message-types.generated.ts',
692
692
  'messages.client.generated.ts',
693
693
  'modules.generated.ts',
694
+ 'modules.i18n.generated.ts',
694
695
  'modules.runtime.generated.ts',
695
696
  'notification-handlers.generated.ts',
696
697
  'notifications.client.generated.ts',
@@ -720,6 +721,7 @@ describe('generator output compatibility', () => {
720
721
  'entities.ids.generated.ts',
721
722
  'entity-fields-registry.ts',
722
723
  'modules.app.generated.ts',
724
+ 'modules.bootstrap.generated.ts',
723
725
  'modules.cli.generated.ts',
724
726
  'enabled-module-ids.generated.ts',
725
727
  ]))
@@ -572,7 +572,7 @@ describe('frontend-routes.generated.ts', () => {
572
572
 
573
573
  it('contains orders frontend route with correct pattern', () => {
574
574
  expect(content).toContain('moduleId: "orders"')
575
- expect(content).toContain('pattern: "/"')
575
+ expect(content).toContain('resolvePageRouteMetadata("/",')
576
576
  })
577
577
  })
578
578
 
@@ -602,10 +602,15 @@ describe('backend-routes.generated.ts', () => {
602
602
  })
603
603
 
604
604
  it('each route entry has pattern, moduleId, and load function', () => {
605
- expect(content).toContain('pattern:')
606
- expect(content).toContain('requireAuth:')
605
+ expect(content).toContain('resolvePageRouteMetadata(')
607
606
  expect(content).toContain('load: async () =>')
608
607
  })
608
+
609
+ it('keeps backend page imports lazy in route entries', () => {
610
+ expect(content).toContain('@open-mercato/core/modules/orders/backend')
611
+ expect(content).toContain('load: async () =>')
612
+ expect(content).toContain('import("@open-mercato/core/modules/orders/backend')
613
+ })
609
614
  })
610
615
 
611
616
  describe('api-routes.generated.ts', () => {
@@ -632,6 +637,11 @@ describe('api-routes.generated.ts', () => {
632
637
  expect(content).toContain('path: "/products"')
633
638
  expect(content).toMatch(/methods:.*GET.*POST.*PUT.*DELETE/)
634
639
  })
640
+
641
+ it('keeps API handler imports lazy in route entries', () => {
642
+ expect(content).toContain('@open-mercato/core/modules/orders/api')
643
+ expect(content).toContain('load: async () => import(')
644
+ })
635
645
  })
636
646
 
637
647
  // ---------------------------------------------------------------------------
@@ -1241,14 +1251,39 @@ describe('modules.app.generated.ts', () => {
1241
1251
  })
1242
1252
  })
1243
1253
 
1254
+ describe('modules.bootstrap.generated.ts', () => {
1255
+ it('exports a bootstrap-only module manifest without route components', async () => {
1256
+ const enabled = scaffoldFixture()
1257
+ const resolver = createMockResolver(enabled)
1258
+ await generateModuleRegistryApp({ resolver, quiet: true })
1259
+ const content = readGenerated('modules.bootstrap.generated.ts')
1260
+
1261
+ expect(content).toContain('export const modules: Module[] = [')
1262
+ expect(content).toContain('export default modules')
1263
+ expect(content).toContain('id: "orders"')
1264
+ expect(content).toContain('subscribers:')
1265
+ expect(content).toContain('orders.order.created')
1266
+ expect(content).toContain('setup:')
1267
+ expect(content).toContain('features:')
1268
+
1269
+ expect(content).not.toContain('frontendRoutes:')
1270
+ expect(content).not.toContain('backendRoutes:')
1271
+ expect(content).not.toContain('createElement')
1272
+ expect(content).not.toContain('/page.meta')
1273
+ expect(content).not.toContain('/frontend/')
1274
+ expect(content).not.toContain('/backend/')
1275
+ expect(content).not.toContain('cli:')
1276
+ })
1277
+ })
1278
+
1244
1279
  describe('bootstrap-modules.generated.ts', () => {
1245
- it('exports legacy bootstrapModules alias from modules.app.generated.ts', async () => {
1280
+ it('exports legacy bootstrapModules alias from modules.bootstrap.generated.ts', async () => {
1246
1281
  const enabled = scaffoldFixture()
1247
1282
  const resolver = createMockResolver(enabled)
1248
1283
  await generateModuleRegistryApp({ resolver, quiet: true })
1249
1284
  const content = readGenerated('bootstrap-modules.generated.ts')
1250
1285
 
1251
- expect(content).toContain('modules.app.generated')
1286
+ expect(content).toContain('modules.bootstrap.generated')
1252
1287
  expect(content).toContain('export const bootstrapModules: Module[] = modules')
1253
1288
  })
1254
1289
  })
@@ -1377,6 +1377,10 @@ function buildPageRouteProps(metaExpr: string, routePath: string): string {
1377
1377
  return `pattern: ${toLiteral(routePath || '/')}, requireAuth: (${metaExpr})?.requireAuth, requireRoles: (${metaExpr})?.requireRoles, requireFeatures: (${metaExpr})?.requireFeatures, requireCustomerAuth: (${metaExpr})?.requireCustomerAuth, requireCustomerFeatures: (${metaExpr})?.requireCustomerFeatures, nav: (${metaExpr})?.nav, title: (${metaExpr})?.pageTitle ?? (${metaExpr})?.title, titleKey: (${metaExpr})?.pageTitleKey ?? (${metaExpr})?.titleKey, group: (${metaExpr})?.pageGroup ?? (${metaExpr})?.group, groupKey: (${metaExpr})?.pageGroupKey ?? (${metaExpr})?.groupKey, icon: (${metaExpr})?.icon, order: (${metaExpr})?.pageOrder ?? (${metaExpr})?.order, priority: (${metaExpr})?.pagePriority ?? (${metaExpr})?.priority, navHidden: (${metaExpr})?.navHidden, visible: (${metaExpr})?.visible, enabled: (${metaExpr})?.enabled, breadcrumb: (${metaExpr})?.breadcrumb, pageContext: (${metaExpr})?.pageContext, placement: (${metaExpr})?.placement`
1378
1378
  }
1379
1379
 
1380
+ function buildPageRouteManifestSpread(metaExpr: string, routePath: string): string {
1381
+ return `...resolvePageRouteMetadata(${toLiteral(routePath || '/')}, ${metaExpr})`
1382
+ }
1383
+
1380
1384
  function normalizeBreadcrumb(raw: unknown): SerializablePageMetadata['breadcrumb'] {
1381
1385
  if (!Array.isArray(raw)) return undefined
1382
1386
  const breadcrumb = raw
@@ -1583,7 +1587,7 @@ async function processPageFiles(options: {
1583
1587
  sourceFile: metaPath,
1584
1588
  metaPath,
1585
1589
  runtimeExpr: `(((${manifestMetaImportName} as any).metadata) as any)`,
1586
- allowRuntimeFallback: type === 'backend',
1590
+ allowRuntimeFallback: true,
1587
1591
  })
1588
1592
  manifestMetaExpr = manifestMetadata.manifestExpr
1589
1593
  if (manifestMetadata.requiresRuntimeImport) {
@@ -1601,7 +1605,7 @@ async function processPageFiles(options: {
1601
1605
  const manifestMetadata = await loadPageMetadataForManifest({
1602
1606
  sourceFile,
1603
1607
  runtimeExpr: `(((${manifestMetaImportName} as any).metadata) as any)`,
1604
- allowRuntimeFallback: type === 'backend',
1608
+ allowRuntimeFallback: true,
1605
1609
  })
1606
1610
  manifestMetaExpr = manifestMetadata.manifestExpr
1607
1611
  if (manifestMetadata.requiresRuntimeImport) {
@@ -1614,7 +1618,7 @@ async function processPageFiles(options: {
1614
1618
  }
1615
1619
  const baseProps = buildPageRouteProps(metaExpr, routePath)
1616
1620
  const runtimeBaseProps = buildPageRouteProps(runtimeMetaExpr, routePath)
1617
- const manifestBaseProps = buildPageRouteProps(manifestMetaExpr, routePath)
1621
+ const manifestBaseProps = buildPageRouteManifestSpread(manifestMetaExpr, routePath)
1618
1622
  eagerRoutes.push(`{ ${baseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
1619
1623
  runtimeRoutes.push(`{ ${runtimeBaseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
1620
1624
  manifestRoutes.push(`{ moduleId: ${toLiteral(modId)}, ${manifestBaseProps}, load: async () => { const mod = await ${buildDynamicImportExpression(importPath)}; return (mod.default ?? mod) as any } }`)
@@ -1661,7 +1665,7 @@ async function processPageFiles(options: {
1661
1665
  sourceFile: metaPath,
1662
1666
  metaPath,
1663
1667
  runtimeExpr: `(((${manifestMetaImportName} as any).metadata) as any)`,
1664
- allowRuntimeFallback: type === 'backend',
1668
+ allowRuntimeFallback: false,
1665
1669
  })
1666
1670
  manifestMetaExpr = manifestMetadata.manifestExpr
1667
1671
  if (manifestMetadata.requiresRuntimeImport) {
@@ -1679,7 +1683,7 @@ async function processPageFiles(options: {
1679
1683
  const manifestMetadata = await loadPageMetadataForManifest({
1680
1684
  sourceFile,
1681
1685
  runtimeExpr: `(((${manifestMetaImportName} as any).metadata) as any)`,
1682
- allowRuntimeFallback: type === 'backend',
1686
+ allowRuntimeFallback: false,
1683
1687
  })
1684
1688
  manifestMetaExpr = manifestMetadata.manifestExpr
1685
1689
  if (manifestMetadata.requiresRuntimeImport) {
@@ -1692,7 +1696,7 @@ async function processPageFiles(options: {
1692
1696
  }
1693
1697
  const baseProps = buildPageRouteProps(metaExpr, routePath)
1694
1698
  const runtimeBaseProps = buildPageRouteProps(runtimeMetaExpr, routePath)
1695
- const manifestBaseProps = buildPageRouteProps(manifestMetaExpr, routePath)
1699
+ const manifestBaseProps = buildPageRouteManifestSpread(manifestMetaExpr, routePath)
1696
1700
  eagerRoutes.push(`{ ${baseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
1697
1701
  runtimeRoutes.push(`{ ${runtimeBaseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
1698
1702
  manifestRoutes.push(`{ moduleId: ${toLiteral(modId)}, ${manifestBaseProps}, load: async () => { const mod = await ${buildDynamicImportExpression(importPath)}; return (mod.default ?? mod) as any } }`)
@@ -2291,8 +2295,12 @@ function renderAstLegacyManifestOutput(options: {
2291
2295
  imports?: GeneratedImportStatement[]
2292
2296
  entries: string[]
2293
2297
  }): string {
2298
+ const usesPageRouteMetadataHelper = options.typeName === 'FrontendRouteManifestEntry'
2299
+ || options.typeName === 'BackendRouteManifestEntry'
2294
2300
  const importSection = [
2295
- `import type { ${options.typeName} } from '@open-mercato/shared/modules/registry'`,
2301
+ usesPageRouteMetadataHelper
2302
+ ? `import { resolvePageRouteMetadata, type ${options.typeName} } from '@open-mercato/shared/modules/registry'`
2303
+ : `import type { ${options.typeName} } from '@open-mercato/shared/modules/registry'`,
2296
2304
  ...(options.imports ?? []).map((entry) => serializeGeneratedImport(entry)),
2297
2305
  ].join('\n')
2298
2306
 
@@ -2631,8 +2639,9 @@ function processTranslationsAst(options: {
2631
2639
  appImportBase: string
2632
2640
  pkgImportBase: string
2633
2641
  imports: string[]
2642
+ extraImports?: string[]
2634
2643
  }): GeneratedObjectEntry[] {
2635
- const { roots, modId, appImportBase, pkgImportBase, imports } = options
2644
+ const { roots, modId, appImportBase, pkgImportBase, imports, extraImports } = options
2636
2645
  const i18nApp = path.join(roots.appBase, 'i18n')
2637
2646
  const pkgI18nBase = resolveStandaloneSourceMirrorBase(roots.pkgBase) ?? roots.pkgBase
2638
2647
  const i18nCore = path.join(pkgI18nBase, 'i18n')
@@ -2665,6 +2674,8 @@ function processTranslationsAst(options: {
2665
2674
  const appName = `T_${toVar(modId)}_${toVar(locale)}_A`
2666
2675
  imports.push(buildImportStatement(coreName, `${pkgImportBase}/i18n/${locale}.json`))
2667
2676
  imports.push(buildImportStatement(appName, `${appImportBase}/i18n/${locale}.json`))
2677
+ extraImports?.push(buildImportStatement(coreName, `${pkgImportBase}/i18n/${locale}.json`))
2678
+ extraImports?.push(buildImportStatement(appName, `${appImportBase}/i18n/${locale}.json`))
2668
2679
  translations.push({
2669
2680
  name: JSON.stringify(locale),
2670
2681
  value: objectLiteral([
@@ -2678,6 +2689,7 @@ function processTranslationsAst(options: {
2678
2689
  if (appHas) {
2679
2690
  const appName = `T_${toVar(modId)}_${toVar(locale)}_A`
2680
2691
  imports.push(buildImportStatement(appName, `${appImportBase}/i18n/${locale}.json`))
2692
+ extraImports?.push(buildImportStatement(appName, `${appImportBase}/i18n/${locale}.json`))
2681
2693
  translations.push({
2682
2694
  name: JSON.stringify(locale),
2683
2695
  value: asExpression(identifier(appName), 'unknown as Record<string, string>'),
@@ -2688,6 +2700,7 @@ function processTranslationsAst(options: {
2688
2700
  if (coreHas) {
2689
2701
  const coreName = `T_${toVar(modId)}_${toVar(locale)}_C`
2690
2702
  imports.push(buildImportStatement(coreName, `${pkgImportBase}/i18n/${locale}.json`))
2703
+ extraImports?.push(buildImportStatement(coreName, `${pkgImportBase}/i18n/${locale}.json`))
2691
2704
  translations.push({
2692
2705
  name: JSON.stringify(locale),
2693
2706
  value: asExpression(identifier(coreName), 'unknown as Record<string, string>'),
@@ -3367,6 +3380,10 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3367
3380
  const outputDir = resolver.getOutputDir()
3368
3381
  const outFile = path.join(outputDir, 'modules.app.generated.ts')
3369
3382
  const checksumFile = path.join(outputDir, 'modules.app.generated.checksum')
3383
+ const bootstrapOutFile = path.join(outputDir, 'modules.bootstrap.generated.ts')
3384
+ const bootstrapChecksumFile = path.join(outputDir, 'modules.bootstrap.generated.checksum')
3385
+ const i18nOutFile = path.join(outputDir, 'modules.i18n.generated.ts')
3386
+ const i18nChecksumFile = path.join(outputDir, 'modules.i18n.generated.checksum')
3370
3387
  const legacyOutFile = path.join(outputDir, 'bootstrap-modules.generated.ts')
3371
3388
  const legacyChecksumFile = path.join(outputDir, 'bootstrap-modules.generated.checksum')
3372
3389
  const enabledIdsOutFile = path.join(outputDir, 'enabled-module-ids.generated.ts')
@@ -3379,7 +3396,11 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3379
3396
  verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
3380
3397
  }
3381
3398
  const imports: string[] = []
3399
+ const bootstrapImports: string[] = []
3400
+ const i18nImports: string[] = []
3382
3401
  const moduleDecls: WriterFunction[] = []
3402
+ const bootstrapModuleDecls: WriterFunction[] = []
3403
+ const i18nModuleDecls: WriterFunction[] = []
3383
3404
  const importIdRef = { value: 0 }
3384
3405
  const trackedRoots = new Set<string>()
3385
3406
  const requiresByModule = new Map<string, string[]>()
@@ -3418,6 +3439,7 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3418
3439
  infoImportName = `I${importIdRef.value++}_${toVar(modId)}`
3419
3440
  const importPath = sanitizeGeneratedModuleSpecifier(indexResolved.importPath)
3420
3441
  imports.push(buildImportStatement(`* as ${infoImportName}`, importPath))
3442
+ bootstrapImports.push(buildImportStatement(`* as ${infoImportName}`, importPath))
3421
3443
  try {
3422
3444
  // eslint-disable-next-line @typescript-eslint/no-var-requires
3423
3445
  const mod = require(indexResolved.absolutePath)
@@ -3429,11 +3451,13 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3429
3451
 
3430
3452
  {
3431
3453
  const setup = resolveConventionFile(roots, imps, 'setup.ts', 'SETUP', modId, importIdRef, imports)
3454
+ if (setup) bootstrapImports.push(buildImportStatement(`* as ${setup.importName}`, setup.importPath))
3432
3455
  if (setup) setupImportName = setup.importName
3433
3456
  }
3434
3457
 
3435
3458
  {
3436
3459
  const encryption = resolveConventionFile(roots, imps, 'encryption.ts', 'ENCRYPTION', modId, importIdRef, imports)
3460
+ if (encryption) bootstrapImports.push(buildImportStatement(`* as ${encryption.importName}`, encryption.importPath))
3437
3461
  if (encryption) encryptionImportName = encryption.importName
3438
3462
  }
3439
3463
 
@@ -3442,12 +3466,13 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3442
3466
  if (resolved) {
3443
3467
  const importName = `INTEGRATION_${toVar(modId)}_${importIdRef.value++}`
3444
3468
  imports.push(buildImportStatement(`* as ${importName}`, resolved.importPath))
3469
+ bootstrapImports.push(buildImportStatement(`* as ${importName}`, resolved.importPath))
3445
3470
  integrationImportName = importName
3446
3471
  }
3447
3472
  }
3448
3473
 
3449
3474
  {
3450
- const ext = resolveConventionFile(roots, imps, 'data/extensions.ts', 'X', modId, importIdRef, imports)
3475
+ const ext = resolveConventionFile(roots, imps, 'data/extensions.ts', 'X', modId, importIdRef, imports, bootstrapImports)
3451
3476
  if (ext) extensionsImportName = ext.importName
3452
3477
  }
3453
3478
 
@@ -3457,17 +3482,18 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3457
3482
  const importName = `ACL_${toVar(modId)}_${importIdRef.value++}`
3458
3483
  const importPath = sanitizeGeneratedModuleSpecifier(aclResolved.importPath)
3459
3484
  imports.push(buildImportStatement(`* as ${importName}`, importPath))
3485
+ bootstrapImports.push(buildImportStatement(`* as ${importName}`, importPath))
3460
3486
  featuresImportName = importName
3461
3487
  }
3462
3488
  }
3463
3489
 
3464
3490
  {
3465
- const ce = resolveConventionFile(roots, imps, 'ce.ts', 'CE', modId, importIdRef, imports)
3491
+ const ce = resolveConventionFile(roots, imps, 'ce.ts', 'CE', modId, importIdRef, imports, bootstrapImports)
3466
3492
  if (ce) customEntitiesImportName = ce.importName
3467
3493
  }
3468
3494
 
3469
3495
  {
3470
- const fields = resolveConventionFile(roots, imps, 'data/fields.ts', 'F', modId, importIdRef, imports)
3496
+ const fields = resolveConventionFile(roots, imps, 'data/fields.ts', 'F', modId, importIdRef, imports, bootstrapImports)
3471
3497
  if (fields) fieldsImportName = fields.importName
3472
3498
  }
3473
3499
 
@@ -3511,13 +3537,17 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3511
3537
  }
3512
3538
  }
3513
3539
 
3540
+ const translationImports: string[] = []
3514
3541
  translations.push(...processTranslationsAst({
3515
3542
  roots,
3516
3543
  modId,
3517
3544
  appImportBase,
3518
3545
  pkgImportBase: imps.pkgBase,
3519
3546
  imports,
3547
+ extraImports: translationImports,
3520
3548
  }))
3549
+ bootstrapImports.push(...translationImports)
3550
+ i18nImports.push(...translationImports)
3521
3551
 
3522
3552
  subscribers.push(...await processSubscribersAst({
3523
3553
  roots,
@@ -3547,18 +3577,16 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3547
3577
  { name: 'id', value: modId },
3548
3578
  { name: 'customFieldSets', value: buildModuleCustomFieldSetsValue({ fieldsImportName, customEntitiesImportName, moduleId: modId }) },
3549
3579
  ]
3580
+ const i18nModuleEntries: GeneratedObjectEntry[] = [
3581
+ { name: 'id', value: modId },
3582
+ ]
3550
3583
 
3551
3584
  if (infoImportName) {
3552
3585
  moduleEntries.push({ name: 'info', value: propertyAccess(identifier(infoImportName), 'metadata') })
3553
3586
  }
3554
- if (frontendRoutes.length > 0) {
3555
- moduleEntries.push({ name: 'frontendRoutes', value: arrayLiteral(frontendRoutes, writeValue) })
3556
- }
3557
- if (backendRoutes.length > 0) {
3558
- moduleEntries.push({ name: 'backendRoutes', value: arrayLiteral(backendRoutes, writeValue) })
3559
- }
3560
3587
  if (translations.length > 0) {
3561
3588
  moduleEntries.push({ name: 'translations', value: objectLiteral(translations) })
3589
+ i18nModuleEntries.push({ name: 'translations', value: objectLiteral(translations) })
3562
3590
  }
3563
3591
  if (subscribers.length > 0) {
3564
3592
  moduleEntries.push({ name: 'subscribers', value: arrayLiteral(subscribers, writeValue) })
@@ -3642,7 +3670,17 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3642
3670
  })
3643
3671
  }
3644
3672
 
3645
- moduleDecls.push(objectLiteral(moduleEntries))
3673
+ const routeAwareModuleEntries = [...moduleEntries]
3674
+ if (frontendRoutes.length > 0) {
3675
+ routeAwareModuleEntries.push({ name: 'frontendRoutes', value: arrayLiteral(frontendRoutes, writeValue) })
3676
+ }
3677
+ if (backendRoutes.length > 0) {
3678
+ routeAwareModuleEntries.push({ name: 'backendRoutes', value: arrayLiteral(backendRoutes, writeValue) })
3679
+ }
3680
+
3681
+ moduleDecls.push(objectLiteral(routeAwareModuleEntries))
3682
+ bootstrapModuleDecls.push(objectLiteral(moduleEntries))
3683
+ i18nModuleDecls.push(objectLiteral(i18nModuleEntries))
3646
3684
  }
3647
3685
 
3648
3686
  const output = renderAstModuleRegistryFile({
@@ -3652,11 +3690,23 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3652
3690
  moduleEntries: moduleDecls,
3653
3691
  includeCreateElementImport: hasRouteComponents,
3654
3692
  })
3693
+ const bootstrapOutput = renderAstModuleRegistryFile({
3694
+ fileName: 'modules.bootstrap.generated.ts',
3695
+ generator: 'registry (bootstrap version)',
3696
+ imports: bootstrapImports,
3697
+ moduleEntries: bootstrapModuleDecls,
3698
+ })
3699
+ const i18nOutput = renderAstModuleRegistryFile({
3700
+ fileName: 'modules.i18n.generated.ts',
3701
+ generator: 'registry (i18n version)',
3702
+ imports: i18nImports,
3703
+ moduleEntries: i18nModuleDecls,
3704
+ })
3655
3705
  const legacyOutput = renderAstLegacyAliasFile({
3656
3706
  fileName: 'bootstrap-modules.generated.ts',
3657
3707
  exportName: 'bootstrapModules',
3658
3708
  exportType: 'Module[]',
3659
- sourceFileName: 'modules.app.generated.ts',
3709
+ sourceFileName: 'modules.bootstrap.generated.ts',
3660
3710
  initializer: identifier('modules'),
3661
3711
  })
3662
3712
 
@@ -3684,6 +3734,8 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3684
3734
 
3685
3735
  const structureChecksum = calculateStructureChecksum(Array.from(trackedRoots))
3686
3736
  writeGeneratedFile({ outFile, checksumFile, content: output, structureChecksum, result, quiet })
3737
+ writeGeneratedFile({ outFile: bootstrapOutFile, checksumFile: bootstrapChecksumFile, content: bootstrapOutput, structureChecksum, result, quiet })
3738
+ writeGeneratedFile({ outFile: i18nOutFile, checksumFile: i18nChecksumFile, content: i18nOutput, structureChecksum, result, quiet })
3687
3739
  writeGeneratedFile({ outFile: legacyOutFile, checksumFile: legacyChecksumFile, content: legacyOutput, structureChecksum, result, quiet })
3688
3740
 
3689
3741
  const enabledIdsOutput = renderEnabledModuleIdsFile(enabled.map((entry) => entry.id))
package/src/lib/utils.ts CHANGED
@@ -235,6 +235,7 @@ export function writeGeneratedFile(options: {
235
235
  const checksum = { content: calculateChecksum(content), structure: structureChecksum }
236
236
  const existing = readChecksumRecord(checksumFile)
237
237
  const shouldWrite =
238
+ !fs.existsSync(outFile) ||
238
239
  !existing ||
239
240
  existing.content !== checksum.content ||
240
241
  existing.structure !== checksum.structure