@open-mercato/cli 0.6.6-develop.6381.1.042df302c3 → 0.6.6-develop.6382.1.4b9b9091ab
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/dist/agentic/guides/core.md +3 -38
- package/dist/agentic/guides/module-system.md +130 -0
- package/dist/agentic/shared/AGENTS.md.template +4 -12
- package/dist/lib/generators/index.js +2 -0
- package/dist/lib/generators/index.js.map +2 -2
- package/dist/lib/generators/module-facts-generate.js +52 -0
- package/dist/lib/generators/module-facts-generate.js.map +7 -0
- package/dist/lib/generators/module-facts.js +734 -0
- package/dist/lib/generators/module-facts.js.map +7 -0
- package/dist/mercato.js +3 -1
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/__tests__/mercato.test.ts +5 -0
- package/src/lib/generators/__tests__/module-facts.auth-source.test.ts +83 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +56 -0
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +69 -0
- package/src/lib/generators/__tests__/module-facts.malformed.test.ts +76 -0
- package/src/lib/generators/index.ts +1 -0
- package/src/lib/generators/module-facts-generate.ts +68 -0
- package/src/lib/generators/module-facts.ts +958 -0
- package/src/mercato.ts +2 -0
- package/dist/agentic/guides/core.auth.md +0 -101
- package/dist/agentic/guides/core.catalog.md +0 -79
- package/dist/agentic/guides/core.currencies.md +0 -43
- package/dist/agentic/guides/core.customer_accounts.md +0 -129
- package/dist/agentic/guides/core.customers.md +0 -138
- package/dist/agentic/guides/core.data_sync.md +0 -107
- package/dist/agentic/guides/core.integrations.md +0 -113
- package/dist/agentic/guides/core.sales.md +0 -104
- package/dist/agentic/guides/core.workflows.md +0 -152
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6382.1.4b9b9091ab",
|
|
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.
|
|
64
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
63
|
+
"@open-mercato/queue": "0.6.6-develop.6382.1.4b9b9091ab",
|
|
64
|
+
"@open-mercato/shared": "0.6.6-develop.6382.1.4b9b9091ab",
|
|
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.
|
|
74
|
+
"@open-mercato/shared": "0.6.6-develop.6382.1.4b9b9091ab"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
77
|
+
"@open-mercato/shared": "0.6.6-develop.6382.1.4b9b9091ab",
|
|
78
78
|
"@types/jest": "^30.0.0",
|
|
79
79
|
"jest": "^30.4.2",
|
|
80
80
|
"ts-jest": "^29.4.11"
|
|
@@ -359,6 +359,7 @@ describe('init command failure output', () => {
|
|
|
359
359
|
generateModuleDi: jest.fn().mockResolvedValue(undefined),
|
|
360
360
|
generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
|
|
361
361
|
generateOpenApi: jest.fn().mockResolvedValue(undefined),
|
|
362
|
+
generateModuleFacts: jest.fn().mockResolvedValue(undefined),
|
|
362
363
|
}))
|
|
363
364
|
jest.doMock('../lib/resolver', () => ({
|
|
364
365
|
createResolver: () => ({
|
|
@@ -407,6 +408,7 @@ describe('init command failure output', () => {
|
|
|
407
408
|
generateModuleDi: jest.fn().mockResolvedValue(undefined),
|
|
408
409
|
generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
|
|
409
410
|
generateOpenApi: jest.fn().mockResolvedValue(undefined),
|
|
411
|
+
generateModuleFacts: jest.fn().mockResolvedValue(undefined),
|
|
410
412
|
}))
|
|
411
413
|
jest.doMock('../lib/resolver', () => ({
|
|
412
414
|
createResolver: () => ({
|
|
@@ -458,6 +460,7 @@ describe('init command failure output', () => {
|
|
|
458
460
|
generateModuleDi: jest.fn().mockResolvedValue(undefined),
|
|
459
461
|
generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
|
|
460
462
|
generateOpenApi: jest.fn().mockResolvedValue(undefined),
|
|
463
|
+
generateModuleFacts: jest.fn().mockResolvedValue(undefined),
|
|
461
464
|
}))
|
|
462
465
|
jest.doMock('../lib/db', () => ({
|
|
463
466
|
dbMigrate: jest.fn().mockResolvedValue(undefined),
|
|
@@ -566,6 +569,7 @@ describe('generate post-step structural cache purge', () => {
|
|
|
566
569
|
generateModuleDi,
|
|
567
570
|
generateModulePackageSources,
|
|
568
571
|
generateOpenApi,
|
|
572
|
+
generateModuleFacts: jest.fn().mockResolvedValue(undefined),
|
|
569
573
|
}))
|
|
570
574
|
jest.doMock('../lib/resolver', () => ({
|
|
571
575
|
createResolver: () => ({
|
|
@@ -615,6 +619,7 @@ describe('generate post-step structural cache purge', () => {
|
|
|
615
619
|
generateModuleDi,
|
|
616
620
|
generateModulePackageSources,
|
|
617
621
|
generateOpenApi,
|
|
622
|
+
generateModuleFacts: jest.fn().mockResolvedValue(undefined),
|
|
618
623
|
}))
|
|
619
624
|
jest.doMock('../lib/resolver', () => ({
|
|
620
625
|
createResolver: () => ({
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { extractModuleFacts } from '../module-facts'
|
|
4
|
+
|
|
5
|
+
function findCoreSrcRoot(): string {
|
|
6
|
+
let dir = __dirname
|
|
7
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
8
|
+
const candidate = path.join(dir, 'packages', 'core', 'src', 'modules')
|
|
9
|
+
if (fs.existsSync(candidate)) return candidate
|
|
10
|
+
dir = path.dirname(dir)
|
|
11
|
+
}
|
|
12
|
+
throw new Error('[internal] could not locate packages/core/src/modules from the test directory')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const REGISTRY_SOURCE = `
|
|
16
|
+
export const modules = [
|
|
17
|
+
{
|
|
18
|
+
id: 'customers',
|
|
19
|
+
apis: [
|
|
20
|
+
{
|
|
21
|
+
path: '/api/customers/people',
|
|
22
|
+
handlers: { GET: handlerGet, POST: handlerPost },
|
|
23
|
+
metadata: {
|
|
24
|
+
GET: { requireAuth: true, requireFeatures: ['customers.people.view'] },
|
|
25
|
+
POST: { requireAuth: true, requireFeatures: ['customers.people.manage'] },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: '/api/customers/people/:id',
|
|
30
|
+
handlers: { GET: handlerGet, PUT: handlerPut, DELETE: handlerDelete },
|
|
31
|
+
metadata: {
|
|
32
|
+
GET: { requireFeatures: ['customers.people.view'] },
|
|
33
|
+
PUT: { requireFeatures: ['customers.people.manage'] },
|
|
34
|
+
DELETE: { requireFeatures: ['customers.people.manage'] },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'sales',
|
|
41
|
+
apis: [
|
|
42
|
+
{
|
|
43
|
+
path: '/api/sales/orders',
|
|
44
|
+
handlers: { GET: handlerGet },
|
|
45
|
+
metadata: { GET: { requireFeatures: ['sales.orders.view'] } },
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
`
|
|
51
|
+
|
|
52
|
+
describe('module-facts API route auth source (T2)', () => {
|
|
53
|
+
const coreSrcRoot = findCoreSrcRoot()
|
|
54
|
+
const facts = extractModuleFacts({ moduleId: 'customers', coreSrcRoot, registrySource: REGISTRY_SOURCE })
|
|
55
|
+
|
|
56
|
+
it('reads per-method auth from the registry apis[].metadata for the requested module', () => {
|
|
57
|
+
const list = facts.apiRoutes.find((route) => route.path === '/api/customers/people')
|
|
58
|
+
expect(list).toBeDefined()
|
|
59
|
+
expect(list?.methods).toEqual(['GET', 'POST'])
|
|
60
|
+
expect(list?.auth.GET).toEqual({ requireAuth: true, requireFeatures: ['customers.people.view'] })
|
|
61
|
+
expect(list?.auth.POST).toEqual({ requireAuth: true, requireFeatures: ['customers.people.manage'] })
|
|
62
|
+
|
|
63
|
+
const detail = facts.apiRoutes.find((route) => route.path === '/api/customers/people/:id')
|
|
64
|
+
expect(detail?.methods).toEqual(['GET', 'PUT', 'DELETE'])
|
|
65
|
+
expect(detail?.auth.PUT).toEqual({ requireFeatures: ['customers.people.manage'] })
|
|
66
|
+
expect(detail?.auth.DELETE).toEqual({ requireFeatures: ['customers.people.manage'] })
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('scopes routes to the requested module and ignores other modules in the registry', () => {
|
|
70
|
+
expect(facts.apiRoutes.some((route) => route.path.startsWith('/api/sales/'))).toBe(false)
|
|
71
|
+
expect(facts.warnings.some((warning) => warning.includes('module registry unavailable'))).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('omits API routes (and warns) when a registry path is provided but the file is missing', () => {
|
|
75
|
+
const missing = extractModuleFacts({
|
|
76
|
+
moduleId: 'customers',
|
|
77
|
+
coreSrcRoot,
|
|
78
|
+
registryPath: path.join(coreSrcRoot, '__does_not_exist__', 'modules.runtime.generated.ts'),
|
|
79
|
+
})
|
|
80
|
+
expect(missing.apiRoutes).toEqual([])
|
|
81
|
+
expect(missing.warnings.some((warning) => warning.includes('module registry unavailable'))).toBe(true)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { extractAllModuleFacts, MODULE_FACTS_ALLOWLIST } from '../module-facts'
|
|
4
|
+
|
|
5
|
+
function findCoreSrcRoot(): string {
|
|
6
|
+
let dir = __dirname
|
|
7
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
8
|
+
const candidate = path.join(dir, 'packages', 'core', 'src', 'modules')
|
|
9
|
+
if (fs.existsSync(candidate)) return candidate
|
|
10
|
+
dir = path.dirname(dir)
|
|
11
|
+
}
|
|
12
|
+
throw new Error('[internal] could not locate packages/core/src/modules from the test directory')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isUnique(values: string[]): boolean {
|
|
16
|
+
return values.length === new Set(values).size
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('module-facts BC resolve guard (T3)', () => {
|
|
20
|
+
const coreSrcRoot = findCoreSrcRoot()
|
|
21
|
+
const { factsByModule } = extractAllModuleFacts({ coreSrcRoot })
|
|
22
|
+
|
|
23
|
+
it('emits facts for every allowlisted module', () => {
|
|
24
|
+
expect(Object.keys(factsByModule).sort()).toEqual([...MODULE_FACTS_ALLOWLIST].sort())
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
for (const moduleId of MODULE_FACTS_ALLOWLIST) {
|
|
28
|
+
describe(`${moduleId}`, () => {
|
|
29
|
+
it('namespaces entity / event / acl / notification ids under the module and keeps them unique', () => {
|
|
30
|
+
const facts = factsByModule[moduleId]
|
|
31
|
+
const entityIds = facts.entities.map((entity) => entity.id)
|
|
32
|
+
|
|
33
|
+
expect(entityIds.every((id) => id.startsWith(`${moduleId}:`))).toBe(true)
|
|
34
|
+
expect(isUnique(entityIds)).toBe(true)
|
|
35
|
+
expect(facts.events.every((event) => event.id.startsWith(`${moduleId}.`))).toBe(true)
|
|
36
|
+
expect(isUnique(facts.events.map((event) => event.id))).toBe(true)
|
|
37
|
+
expect(facts.aclFeatures.every((feature) => feature.startsWith(`${moduleId}.`))).toBe(true)
|
|
38
|
+
expect(isUnique(facts.aclFeatures)).toBe(true)
|
|
39
|
+
expect(facts.notifications.every((notification) => notification.startsWith(`${moduleId}.`))).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('resolves search and host-token references against the module entity set', () => {
|
|
43
|
+
const facts = factsByModule[moduleId]
|
|
44
|
+
const entityIds = new Set(facts.entities.map((entity) => entity.id))
|
|
45
|
+
|
|
46
|
+
for (const searchEntity of facts.searchEntities) {
|
|
47
|
+
expect(entityIds.has(searchEntity)).toBe(true)
|
|
48
|
+
}
|
|
49
|
+
for (const hostEntityId of facts.hostTokens.entityIds) {
|
|
50
|
+
expect(entityIds.has(hostEntityId)).toBe(true)
|
|
51
|
+
expect(hostEntityId.endsWith('_entity')).toBe(true)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { extractModuleFacts } from '../module-facts'
|
|
4
|
+
|
|
5
|
+
function findCoreSrcRoot(): string {
|
|
6
|
+
let dir = __dirname
|
|
7
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
8
|
+
const candidate = path.join(dir, 'packages', 'core', 'src', 'modules')
|
|
9
|
+
if (fs.existsSync(candidate)) return candidate
|
|
10
|
+
dir = path.dirname(dir)
|
|
11
|
+
}
|
|
12
|
+
throw new Error('[internal] could not locate packages/core/src/modules from the test directory')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('module-facts customers fixture (T1 anti-drift guard)', () => {
|
|
16
|
+
const coreSrcRoot = findCoreSrcRoot()
|
|
17
|
+
const facts = extractModuleFacts({ moduleId: 'customers', coreSrcRoot })
|
|
18
|
+
|
|
19
|
+
it('locks the source-derived customers entity surface in colon-form ids', () => {
|
|
20
|
+
expect(facts.module).toBe('customers')
|
|
21
|
+
expect(facts.entities).toHaveLength(25)
|
|
22
|
+
expect(facts.entities.every((entity) => entity.id.startsWith('customers:'))).toBe(true)
|
|
23
|
+
expect(facts.entities[0]).toMatchObject({ id: 'customers:customer_entity' })
|
|
24
|
+
for (const entity of facts.entities) {
|
|
25
|
+
expect(entity.class.length).toBeGreaterThan(0)
|
|
26
|
+
expect(entity.table.length).toBeGreaterThan(0)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('locks customers events, acl, search, and notification surfaces', () => {
|
|
31
|
+
expect(facts.events).toHaveLength(49)
|
|
32
|
+
expect(facts.aclFeatures).toHaveLength(21)
|
|
33
|
+
expect(facts.searchEntities).toEqual([
|
|
34
|
+
'customers:customer_person_profile',
|
|
35
|
+
'customers:customer_company_profile',
|
|
36
|
+
'customers:customer_comment',
|
|
37
|
+
'customers:customer_deal',
|
|
38
|
+
'customers:customer_activity',
|
|
39
|
+
'customers:customer_todo_link',
|
|
40
|
+
])
|
|
41
|
+
expect(facts.notifications).toEqual(['customers.deal.won', 'customers.deal.lost'])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('locks the real cli commands and host table ids, not the abbreviated spec example', () => {
|
|
45
|
+
expect(facts.cli).toEqual([
|
|
46
|
+
'seed-dictionaries',
|
|
47
|
+
'seed-examples',
|
|
48
|
+
'seed-stresstest',
|
|
49
|
+
'interactions:backfill',
|
|
50
|
+
])
|
|
51
|
+
expect(facts.hostTokens.tableIds).toEqual([
|
|
52
|
+
'customers.companies.list',
|
|
53
|
+
'customers.deals.list',
|
|
54
|
+
'customers.people.list',
|
|
55
|
+
])
|
|
56
|
+
expect(facts.hostTokens.entityIds).toEqual(['customers:customer_entity'])
|
|
57
|
+
expect(facts.diTokens).toEqual([])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('omits API route auth and warns when no module registry is provided', () => {
|
|
61
|
+
expect(facts.apiRoutes).toEqual([])
|
|
62
|
+
expect(facts.warnings.some((warning) => warning.includes('module registry unavailable'))).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('locks the module metadata surface extracted from index.ts', () => {
|
|
66
|
+
expect(facts.title).toBe('Customer Relationship Management')
|
|
67
|
+
expect(facts.description).toBe('Core CRM capabilities for people, companies, deals, and activities.')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { extractModuleFacts } from '../module-facts'
|
|
5
|
+
|
|
6
|
+
let tmpRoot: string
|
|
7
|
+
|
|
8
|
+
function writeModuleFile(moduleId: string, relativePath: string, content: string): void {
|
|
9
|
+
const fullPath = path.join(tmpRoot, moduleId, relativePath)
|
|
10
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true })
|
|
11
|
+
fs.writeFileSync(fullPath, content)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'module-facts-malformed-'))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('module-facts malformed source handling (T4)', () => {
|
|
23
|
+
it('returns empty sections plus warnings (never throws) for malformed convention files', () => {
|
|
24
|
+
writeModuleFile('broken', 'data/entities.ts', 'export const notAnEntity = 1\nexport class LooseClass {}\n')
|
|
25
|
+
writeModuleFile('broken', 'search.ts', 'export const unrelatedConfig = { entities: [] }\n')
|
|
26
|
+
writeModuleFile('broken', 'notifications.ts', 'export const notifications = "not-an-array"\n')
|
|
27
|
+
writeModuleFile('broken', 'cli.ts', 'export const commands = []\n')
|
|
28
|
+
|
|
29
|
+
const run = (): ReturnType<typeof extractModuleFacts> =>
|
|
30
|
+
extractModuleFacts({ moduleId: 'broken', coreSrcRoot: tmpRoot })
|
|
31
|
+
expect(run).not.toThrow()
|
|
32
|
+
|
|
33
|
+
const facts = run()
|
|
34
|
+
expect(facts.entities).toEqual([])
|
|
35
|
+
expect(facts.events).toEqual([])
|
|
36
|
+
expect(facts.aclFeatures).toEqual([])
|
|
37
|
+
expect(facts.searchEntities).toEqual([])
|
|
38
|
+
expect(facts.notifications).toEqual([])
|
|
39
|
+
expect(facts.cli).toEqual([])
|
|
40
|
+
expect(facts.apiRoutes).toEqual([])
|
|
41
|
+
|
|
42
|
+
const warnings = facts.warnings.join('\n')
|
|
43
|
+
expect(warnings).toContain('search.ts present but no searchConfig')
|
|
44
|
+
expect(warnings).toContain('notifications.ts present but no notificationTypes array')
|
|
45
|
+
expect(warnings).toContain('cli.ts present but no default export')
|
|
46
|
+
expect(warnings).toContain('module registry unavailable')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('survives a syntactically broken source file without throwing', () => {
|
|
50
|
+
writeModuleFile('broken', 'search.ts', 'export const searchConfig = { entities: [ this is not valid typescript\n')
|
|
51
|
+
|
|
52
|
+
const run = (): ReturnType<typeof extractModuleFacts> =>
|
|
53
|
+
extractModuleFacts({ moduleId: 'broken', coreSrcRoot: tmpRoot })
|
|
54
|
+
expect(run).not.toThrow()
|
|
55
|
+
expect(run().searchEntities).toEqual([])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns all-empty facts (never throws) for a module with no convention files', () => {
|
|
59
|
+
fs.mkdirSync(path.join(tmpRoot, 'empty'), { recursive: true })
|
|
60
|
+
|
|
61
|
+
const run = (): ReturnType<typeof extractModuleFacts> =>
|
|
62
|
+
extractModuleFacts({ moduleId: 'empty', coreSrcRoot: tmpRoot })
|
|
63
|
+
expect(run).not.toThrow()
|
|
64
|
+
|
|
65
|
+
expect(run()).toMatchObject({
|
|
66
|
+
module: 'empty',
|
|
67
|
+
entities: [],
|
|
68
|
+
events: [],
|
|
69
|
+
aclFeatures: [],
|
|
70
|
+
searchEntities: [],
|
|
71
|
+
notifications: [],
|
|
72
|
+
cli: [],
|
|
73
|
+
diTokens: [],
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -4,3 +4,4 @@ export { generateModuleEntities, type ModuleEntitiesOptions } from './module-ent
|
|
|
4
4
|
export { generateModuleDi, type ModuleDiOptions } from './module-di'
|
|
5
5
|
export { generateModulePackageSources, type ModulePackageSourcesOptions } from './module-package-sources'
|
|
6
6
|
export { generateOpenApi, type GenerateOpenApiOptions } from './openapi'
|
|
7
|
+
export { generateModuleFacts, type ModuleFactsOptions } from './module-facts-generate'
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { PackageResolver } from '../resolver'
|
|
4
|
+
import {
|
|
5
|
+
createGeneratorResult,
|
|
6
|
+
ensureDir,
|
|
7
|
+
logGenerationResult,
|
|
8
|
+
type GeneratorResult,
|
|
9
|
+
} from '../utils'
|
|
10
|
+
import { extractAllModuleFacts, renderModuleFactsJson } from './module-facts'
|
|
11
|
+
|
|
12
|
+
export interface ModuleFactsOptions {
|
|
13
|
+
resolver: PackageResolver
|
|
14
|
+
quiet?: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readCoreVersion(coreSrcRoot: string): string | null {
|
|
18
|
+
const corePackageJsonPath = path.resolve(coreSrcRoot, '..', '..', 'package.json')
|
|
19
|
+
if (!fs.existsSync(corePackageJsonPath)) return null
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(corePackageJsonPath, 'utf8')) as { version?: unknown }
|
|
22
|
+
return typeof parsed.version === 'string' ? parsed.version : null
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Emits the versioned `apps/mercato/src/module-facts.generated.json` artifact from
|
|
30
|
+
* monorepo core module sources. Must run AFTER `generateModuleRegistry` because
|
|
31
|
+
* per-route API auth is read from the generated `modules.runtime.generated.ts`
|
|
32
|
+
* registry (`apis[].metadata`). Registry-derived warnings are non-fatal.
|
|
33
|
+
*/
|
|
34
|
+
export async function generateModuleFacts(options: ModuleFactsOptions): Promise<GeneratorResult> {
|
|
35
|
+
const { resolver, quiet } = options
|
|
36
|
+
const result = createGeneratorResult()
|
|
37
|
+
|
|
38
|
+
const rootDir = resolver.getRootDir()
|
|
39
|
+
const coreSrcRoot = path.join(rootDir, 'packages', 'core', 'src', 'modules')
|
|
40
|
+
if (!fs.existsSync(coreSrcRoot)) {
|
|
41
|
+
if (!quiet) {
|
|
42
|
+
console.warn(`[module-facts] core module sources not found at ${coreSrcRoot}; skipping module-facts generation`)
|
|
43
|
+
}
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const registryPath = path.join(resolver.getOutputDir(), 'modules.runtime.generated.ts')
|
|
48
|
+
const coreVersion = readCoreVersion(coreSrcRoot)
|
|
49
|
+
|
|
50
|
+
const { factsByModule, warnings } = extractAllModuleFacts({ coreSrcRoot, registryPath, coreVersion })
|
|
51
|
+
for (const warning of warnings) {
|
|
52
|
+
console.warn(warning)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const content = renderModuleFactsJson(factsByModule)
|
|
56
|
+
const outFile = path.join(resolver.getAppDir(), 'src', 'module-facts.generated.json')
|
|
57
|
+
const existing = fs.existsSync(outFile) ? fs.readFileSync(outFile, 'utf8') : null
|
|
58
|
+
if (existing === content) {
|
|
59
|
+
result.filesUnchanged.push(outFile)
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ensureDir(outFile)
|
|
64
|
+
fs.writeFileSync(outFile, content)
|
|
65
|
+
result.filesWritten.push(outFile)
|
|
66
|
+
logGenerationResult(path.relative(process.cwd(), outFile), true)
|
|
67
|
+
return result
|
|
68
|
+
}
|