@open-mercato/cli 0.6.7-develop.6553.1.2ec7246e04 → 0.6.7-develop.6565.1.1026d85fff
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 +3 -3
- package/AGENTS.md +1 -0
- package/dist/agentic/guides/module-facts.json +53 -53
- package/dist/agentic/guides/modules/ai_assistant.md +1 -1
- package/dist/agentic/guides/modules/api_docs.md +1 -1
- package/dist/agentic/guides/modules/api_keys.md +1 -1
- package/dist/agentic/guides/modules/attachments.md +1 -1
- package/dist/agentic/guides/modules/audit_logs.md +1 -1
- package/dist/agentic/guides/modules/auth.md +1 -1
- package/dist/agentic/guides/modules/business_rules.md +1 -1
- package/dist/agentic/guides/modules/catalog.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail.md +1 -1
- package/dist/agentic/guides/modules/channel_imap.md +1 -1
- package/dist/agentic/guides/modules/checkout.md +1 -1
- package/dist/agentic/guides/modules/communication_channels.md +1 -1
- package/dist/agentic/guides/modules/configs.md +1 -1
- package/dist/agentic/guides/modules/content.md +1 -1
- package/dist/agentic/guides/modules/currencies.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts.md +1 -1
- package/dist/agentic/guides/modules/customers.md +1 -1
- package/dist/agentic/guides/modules/dashboards.md +1 -1
- package/dist/agentic/guides/modules/data_sync.md +1 -1
- package/dist/agentic/guides/modules/dictionaries.md +1 -1
- package/dist/agentic/guides/modules/directory.md +1 -1
- package/dist/agentic/guides/modules/entities.md +1 -1
- package/dist/agentic/guides/modules/events.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
- package/dist/agentic/guides/modules/generators.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops.md +1 -1
- package/dist/agentic/guides/modules/integrations.md +1 -1
- package/dist/agentic/guides/modules/messages.md +1 -1
- package/dist/agentic/guides/modules/notifications.md +1 -1
- package/dist/agentic/guides/modules/onboarding.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways.md +1 -1
- package/dist/agentic/guides/modules/perspectives.md +1 -1
- package/dist/agentic/guides/modules/planner.md +1 -1
- package/dist/agentic/guides/modules/portal.md +1 -1
- package/dist/agentic/guides/modules/progress.md +1 -1
- package/dist/agentic/guides/modules/query_index.md +1 -1
- package/dist/agentic/guides/modules/record_locks.md +1 -1
- package/dist/agentic/guides/modules/resources.md +1 -1
- package/dist/agentic/guides/modules/sales.md +1 -1
- package/dist/agentic/guides/modules/scheduler.md +1 -1
- package/dist/agentic/guides/modules/search.md +1 -1
- package/dist/agentic/guides/modules/security.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
- package/dist/agentic/guides/modules/sso.md +1 -1
- package/dist/agentic/guides/modules/staff.md +1 -1
- package/dist/agentic/guides/modules/storage_s3.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
- package/dist/agentic/guides/modules/sync_excel.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
- package/dist/agentic/guides/modules/translations.md +1 -1
- package/dist/agentic/guides/modules/webhooks.md +1 -1
- package/dist/agentic/guides/modules/workflows.md +1 -1
- package/dist/agentic/shared/AGENTS.md.template +1 -1
- package/dist/agentic/shared/ai/skills/tiers.schema.json +15 -0
- package/dist/agentic/shared/scripts/install-skills.sh +244 -37
- package/dist/lib/__integration__/TC-INT-008.spec.js +9 -11
- package/dist/lib/__integration__/TC-INT-008.spec.js.map +2 -2
- package/dist/lib/agentic-setup.js +14 -2
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generate-watch-events.js +106 -0
- package/dist/lib/generate-watch-events.js.map +7 -0
- package/dist/lib/generators/entity-ids.js +20 -13
- package/dist/lib/generators/entity-ids.js.map +2 -2
- package/dist/lib/generators/openapi.js +114 -12
- package/dist/lib/generators/openapi.js.map +2 -2
- package/dist/lib/in-process-generate-watcher.js +13 -2
- package/dist/lib/in-process-generate-watcher.js.map +2 -2
- package/dist/mercato.js +68 -23
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/__tests__/mercato.test.ts +53 -2
- package/src/lib/__integration__/TC-INT-008.spec.ts +15 -15
- package/src/lib/__tests__/generate-watch-events.test.ts +112 -0
- package/src/lib/__tests__/generate-watch-structure.test.ts +20 -0
- package/src/lib/__tests__/in-process-generate-watcher.test.ts +193 -2
- package/src/lib/agentic-setup.ts +23 -4
- package/src/lib/generate-watch-events.ts +125 -0
- package/src/lib/generators/__tests__/entity-ids.test.ts +98 -1
- package/src/lib/generators/__tests__/openapi.test.ts +174 -0
- package/src/lib/generators/entity-ids.ts +33 -13
- package/src/lib/generators/openapi.ts +167 -14
- package/src/lib/in-process-generate-watcher.ts +35 -6
- package/src/mercato.ts +75 -27
|
@@ -30,7 +30,10 @@ function createMockResolver(tmpRoot: string, enabled: ModuleEntry[]): PackageRes
|
|
|
30
30
|
appBase: `@/modules/${entry.id}`,
|
|
31
31
|
pkgBase: `@open-mercato/core/modules/${entry.id}`,
|
|
32
32
|
}),
|
|
33
|
-
getPackageOutputDir: () =>
|
|
33
|
+
getPackageOutputDir: (packageName: string) =>
|
|
34
|
+
packageName === '@app'
|
|
35
|
+
? outputDir
|
|
36
|
+
: path.join(tmpRoot, 'packages', 'core', 'generated'),
|
|
34
37
|
getPackageRoot: () => path.join(tmpRoot, 'packages', 'core'),
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -75,6 +78,100 @@ afterEach(() => {
|
|
|
75
78
|
})
|
|
76
79
|
|
|
77
80
|
describe('generateEntityIds', () => {
|
|
81
|
+
it('does not rewrite generated entity outputs when inputs are unchanged', async () => {
|
|
82
|
+
const moduleEntry: ModuleEntry = { id: 'orders', from: '@open-mercato/core' }
|
|
83
|
+
const entitiesFile = path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'orders', 'data', 'entities.ts')
|
|
84
|
+
fs.mkdirSync(path.dirname(entitiesFile), { recursive: true })
|
|
85
|
+
fs.writeFileSync(entitiesFile, 'export class SalesOrder { id!: string\n totalGross!: number\n}\n')
|
|
86
|
+
const resolver = createMockResolver(tmpDir, [moduleEntry])
|
|
87
|
+
|
|
88
|
+
const first = await generateEntityIds({ resolver, quiet: true })
|
|
89
|
+
expect(first.filesWritten.length).toBeGreaterThan(0)
|
|
90
|
+
|
|
91
|
+
const appOutput = resolver.getOutputDir()
|
|
92
|
+
const packageOutput = resolver.getPackageOutputDir('@open-mercato/core')
|
|
93
|
+
const generatedFiles = [
|
|
94
|
+
path.join(appOutput, 'entities.ids.generated.ts'),
|
|
95
|
+
path.join(appOutput, 'entity-fields-registry.ts'),
|
|
96
|
+
path.join(appOutput, 'entities', 'sales_order', 'index.ts'),
|
|
97
|
+
path.join(packageOutput, 'entities.ids.generated.ts'),
|
|
98
|
+
path.join(packageOutput, 'entity-fields-registry.ts'),
|
|
99
|
+
path.join(packageOutput, 'entities', 'sales_order', 'index.ts'),
|
|
100
|
+
]
|
|
101
|
+
const pinnedTime = new Date('2026-01-01T00:00:00.000Z')
|
|
102
|
+
for (const filePath of generatedFiles) {
|
|
103
|
+
fs.utimesSync(filePath, pinnedTime, pinnedTime)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const second = await generateEntityIds({ resolver, quiet: true })
|
|
107
|
+
|
|
108
|
+
expect(second.filesWritten).toEqual([])
|
|
109
|
+
for (const filePath of generatedFiles) {
|
|
110
|
+
expect(fs.statSync(filePath).mtimeMs).toBe(pinnedTime.getTime())
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('reports field-only generated changes without rewriting the entity id map', async () => {
|
|
115
|
+
const moduleEntry: ModuleEntry = { id: 'orders', from: '@open-mercato/core' }
|
|
116
|
+
const entitiesFile = path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'orders', 'data', 'entities.ts')
|
|
117
|
+
fs.mkdirSync(path.dirname(entitiesFile), { recursive: true })
|
|
118
|
+
fs.writeFileSync(entitiesFile, 'export class SalesOrder { id!: string\n}\n')
|
|
119
|
+
const resolver = createMockResolver(tmpDir, [moduleEntry])
|
|
120
|
+
await generateEntityIds({ resolver, quiet: true })
|
|
121
|
+
const rootIds = path.join(resolver.getOutputDir(), 'entities.ids.generated.ts')
|
|
122
|
+
const pinnedTime = new Date('2026-01-01T00:00:00.000Z')
|
|
123
|
+
fs.utimesSync(rootIds, pinnedTime, pinnedTime)
|
|
124
|
+
|
|
125
|
+
fs.writeFileSync(entitiesFile, 'export class SalesOrder { id!: string\n status!: string\n}\n')
|
|
126
|
+
const result = await generateEntityIds({ resolver, quiet: true })
|
|
127
|
+
|
|
128
|
+
expect(result.filesWritten).not.toContain(rootIds)
|
|
129
|
+
expect(fs.statSync(rootIds).mtimeMs).toBe(pinnedTime.getTime())
|
|
130
|
+
expect(result.filesWritten).toContain(
|
|
131
|
+
path.join(resolver.getOutputDir(), 'entities', 'sales_order', 'index.ts'),
|
|
132
|
+
)
|
|
133
|
+
expect(result.filesWritten).toContain(
|
|
134
|
+
path.join(resolver.getOutputDir(), 'entity-fields-registry.ts'),
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('keeps deletion changes visible while removing stale entity field output', async () => {
|
|
139
|
+
const moduleEntry: ModuleEntry = { id: 'orders', from: '@open-mercato/core' }
|
|
140
|
+
const entitiesFile = path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'orders', 'data', 'entities.ts')
|
|
141
|
+
fs.mkdirSync(path.dirname(entitiesFile), { recursive: true })
|
|
142
|
+
fs.writeFileSync(entitiesFile, 'export class SalesOrder { id!: string }\nexport class SalesLine { id!: string }\n')
|
|
143
|
+
const resolver = createMockResolver(tmpDir, [moduleEntry])
|
|
144
|
+
await generateEntityIds({ resolver, quiet: true })
|
|
145
|
+
const staleDirectory = path.join(resolver.getOutputDir(), 'entities', 'sales_line')
|
|
146
|
+
expect(fs.existsSync(staleDirectory)).toBe(true)
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync(entitiesFile, 'export class SalesOrder { id!: string }\n')
|
|
149
|
+
const result = await generateEntityIds({ resolver, quiet: true })
|
|
150
|
+
|
|
151
|
+
expect(fs.existsSync(staleDirectory)).toBe(false)
|
|
152
|
+
expect(result.filesWritten).toContain(
|
|
153
|
+
path.join(resolver.getOutputDir(), 'entities.ids.generated.ts'),
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('reports stale entity directory cleanup when every generated file is unchanged', async () => {
|
|
158
|
+
const moduleEntry: ModuleEntry = { id: 'orders', from: '@open-mercato/core' }
|
|
159
|
+
const entitiesFile = path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'orders', 'data', 'entities.ts')
|
|
160
|
+
fs.mkdirSync(path.dirname(entitiesFile), { recursive: true })
|
|
161
|
+
fs.writeFileSync(entitiesFile, 'export class SalesOrder { id!: string }\n')
|
|
162
|
+
const resolver = createMockResolver(tmpDir, [moduleEntry])
|
|
163
|
+
await generateEntityIds({ resolver, quiet: true })
|
|
164
|
+
|
|
165
|
+
const staleDirectory = path.join(resolver.getOutputDir(), 'entities', 'sales_line')
|
|
166
|
+
fs.mkdirSync(staleDirectory, { recursive: true })
|
|
167
|
+
fs.writeFileSync(path.join(staleDirectory, 'index.ts'), 'export const id = "id"\n')
|
|
168
|
+
|
|
169
|
+
const result = await generateEntityIds({ resolver, quiet: true })
|
|
170
|
+
|
|
171
|
+
expect(fs.existsSync(staleDirectory)).toBe(false)
|
|
172
|
+
expect(result.filesWritten).toEqual([staleDirectory])
|
|
173
|
+
})
|
|
174
|
+
|
|
78
175
|
it('writes an inline entity fields registry without generated entity imports', async () => {
|
|
79
176
|
const moduleEntry: ModuleEntry = { id: 'orders', from: '@open-mercato/core' }
|
|
80
177
|
const entitiesFile = path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'orders', 'data', 'entities.ts')
|
|
@@ -42,6 +42,27 @@ describe('resolveOpenApiGeneratorProjectRoot', () => {
|
|
|
42
42
|
describe('generateOpenApi', () => {
|
|
43
43
|
let tmpDir: string
|
|
44
44
|
|
|
45
|
+
const sharedGeneratorSource = `
|
|
46
|
+
export function buildOpenApiDocument(modules: any[], options: any) {
|
|
47
|
+
const paths: Record<string, any> = {}
|
|
48
|
+
for (const module of modules) {
|
|
49
|
+
for (const api of module.apis ?? []) {
|
|
50
|
+
const methods: Record<string, any> = {}
|
|
51
|
+
for (const [method, spec] of Object.entries(api.handlers.openApi ?? {})) {
|
|
52
|
+
methods[method.toLowerCase()] = spec
|
|
53
|
+
}
|
|
54
|
+
paths[api.path.replace(/\\[([^\\]]+)\\]/g, '{$1}')] = methods
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
openapi: '3.1.0',
|
|
59
|
+
info: { title: options.title, version: options.version, description: options.description },
|
|
60
|
+
servers: options.servers,
|
|
61
|
+
paths,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`
|
|
65
|
+
|
|
45
66
|
function touchFile(filePath: string, content: string): void {
|
|
46
67
|
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
47
68
|
fs.writeFileSync(filePath, content)
|
|
@@ -79,6 +100,14 @@ describe('generateOpenApi', () => {
|
|
|
79
100
|
|
|
80
101
|
beforeEach(() => {
|
|
81
102
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openapi-generator-test-'))
|
|
103
|
+
touchFile(
|
|
104
|
+
path.join(tmpDir, 'packages', 'shared', 'src', 'lib', 'openapi', 'generator.ts'),
|
|
105
|
+
sharedGeneratorSource,
|
|
106
|
+
)
|
|
107
|
+
touchFile(path.join(tmpDir, 'package.json'), '{"private":true}\n')
|
|
108
|
+
touchFile(path.join(tmpDir, 'yarn.lock'), '# test lockfile\n')
|
|
109
|
+
touchFile(path.join(tmpDir, 'app', 'package.json'), '{"private":true}\n')
|
|
110
|
+
touchFile(path.join(tmpDir, 'app', 'tsconfig.json'), '{"compilerOptions":{}}\n')
|
|
82
111
|
})
|
|
83
112
|
|
|
84
113
|
afterEach(() => {
|
|
@@ -130,4 +159,149 @@ describe('generateOpenApi', () => {
|
|
|
130
159
|
expect(openApiDoc.paths['/api/demo/health']?.get?.summary).toBe('app health')
|
|
131
160
|
expect(openApiDoc.paths['/api/demo/details']?.get?.summary).toBe('package details')
|
|
132
161
|
})
|
|
162
|
+
|
|
163
|
+
it('skips route discovery and bundling when all tracked inputs are unchanged', async () => {
|
|
164
|
+
touchFile(
|
|
165
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'demo', 'api', 'health', 'route.ts'),
|
|
166
|
+
[
|
|
167
|
+
'export async function GET() { return new Response("ok") }',
|
|
168
|
+
"export const openApi = { GET: { summary: 'health' } }",
|
|
169
|
+
'',
|
|
170
|
+
].join('\n'),
|
|
171
|
+
)
|
|
172
|
+
const resolver = createMockResolver([{ id: 'demo', from: '@open-mercato/core' }])
|
|
173
|
+
|
|
174
|
+
await generateOpenApi({ resolver, quiet: true })
|
|
175
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
176
|
+
const result = await generateOpenApi({ resolver, quiet: false })
|
|
177
|
+
|
|
178
|
+
const generatedPath = path.join(tmpDir, 'output', 'generated', 'openapi.generated.json')
|
|
179
|
+
expect(result.filesWritten).toEqual([])
|
|
180
|
+
expect(result.filesUnchanged).toEqual([generatedPath])
|
|
181
|
+
expect(consoleSpy).toHaveBeenCalledWith(`[OpenAPI] Skipped (inputs unchanged): ${generatedPath}`)
|
|
182
|
+
expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('[OpenAPI] Found'))
|
|
183
|
+
consoleSpy.mockRestore()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('invalidates the early cache when a transitive route dependency changes', async () => {
|
|
187
|
+
const moduleRoot = path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'demo')
|
|
188
|
+
const schemaPath = path.join(moduleRoot, 'schema.ts')
|
|
189
|
+
touchFile(schemaPath, "export const summary = 'first schema'\n")
|
|
190
|
+
touchFile(
|
|
191
|
+
path.join(moduleRoot, 'api', 'health', 'route.ts'),
|
|
192
|
+
[
|
|
193
|
+
"import { summary } from '../../schema'",
|
|
194
|
+
'export async function GET() { return new Response("ok") }',
|
|
195
|
+
'export const openApi = { GET: { summary } }',
|
|
196
|
+
'',
|
|
197
|
+
].join('\n'),
|
|
198
|
+
)
|
|
199
|
+
const resolver = createMockResolver([{ id: 'demo', from: '@open-mercato/core' }])
|
|
200
|
+
await generateOpenApi({ resolver, quiet: true })
|
|
201
|
+
|
|
202
|
+
touchFile(schemaPath, "export const summary = 'second schema'\n")
|
|
203
|
+
const result = await generateOpenApi({ resolver, quiet: true })
|
|
204
|
+
const generatedPath = path.join(tmpDir, 'output', 'generated', 'openapi.generated.json')
|
|
205
|
+
const openApiDoc = JSON.parse(fs.readFileSync(generatedPath, 'utf8')) as {
|
|
206
|
+
paths: Record<string, { get?: { summary?: string } }>
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
expect(result.filesWritten).toEqual([generatedPath])
|
|
210
|
+
expect(openApiDoc.paths['/api/demo/health']?.get?.summary).toBe('second schema')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('invalidates the early cache when enabled modules change', async () => {
|
|
214
|
+
touchFile(
|
|
215
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'first', 'api', 'route.ts'),
|
|
216
|
+
"export async function GET() {}\nexport const openApi = { GET: { summary: 'first' } }\n",
|
|
217
|
+
)
|
|
218
|
+
touchFile(
|
|
219
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'second', 'api', 'route.ts'),
|
|
220
|
+
"export async function GET() {}\nexport const openApi = { GET: { summary: 'second' } }\n",
|
|
221
|
+
)
|
|
222
|
+
let enabled: ModuleEntry[] = [{ id: 'first', from: '@open-mercato/core' }]
|
|
223
|
+
const delegate = createMockResolver(enabled)
|
|
224
|
+
const resolver: PackageResolver = {
|
|
225
|
+
...delegate,
|
|
226
|
+
loadEnabledModules: () => enabled,
|
|
227
|
+
}
|
|
228
|
+
await generateOpenApi({ resolver, quiet: true })
|
|
229
|
+
|
|
230
|
+
enabled = [{ id: 'second', from: '@open-mercato/core' }]
|
|
231
|
+
await generateOpenApi({ resolver, quiet: true })
|
|
232
|
+
const generatedPath = path.join(tmpDir, 'output', 'generated', 'openapi.generated.json')
|
|
233
|
+
const openApiDoc = JSON.parse(fs.readFileSync(generatedPath, 'utf8')) as {
|
|
234
|
+
paths: Record<string, unknown>
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
expect(openApiDoc.paths['/api/first']).toBeUndefined()
|
|
238
|
+
expect(openApiDoc.paths['/api/second']).toBeDefined()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('rebuilds when the generated document is missing or tampered with', async () => {
|
|
242
|
+
touchFile(
|
|
243
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'demo', 'api', 'health', 'route.ts'),
|
|
244
|
+
"export async function GET() {}\nexport const openApi = { GET: { summary: 'health' } }\n",
|
|
245
|
+
)
|
|
246
|
+
const resolver = createMockResolver([{ id: 'demo', from: '@open-mercato/core' }])
|
|
247
|
+
const generatedPath = path.join(tmpDir, 'output', 'generated', 'openapi.generated.json')
|
|
248
|
+
await generateOpenApi({ resolver, quiet: true })
|
|
249
|
+
|
|
250
|
+
fs.unlinkSync(generatedPath)
|
|
251
|
+
const restored = await generateOpenApi({ resolver, quiet: true })
|
|
252
|
+
expect(restored.filesWritten).toEqual([generatedPath])
|
|
253
|
+
|
|
254
|
+
fs.writeFileSync(generatedPath, '{"tampered":true}')
|
|
255
|
+
const repaired = await generateOpenApi({ resolver, quiet: true })
|
|
256
|
+
expect(repaired.filesWritten).toEqual([generatedPath])
|
|
257
|
+
expect(JSON.parse(fs.readFileSync(generatedPath, 'utf8'))).toHaveProperty('openapi', '3.1.0')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('invalidates the early cache when the generated server URL changes', async () => {
|
|
261
|
+
const originalAppUrl = process.env.NEXT_PUBLIC_APP_URL
|
|
262
|
+
touchFile(
|
|
263
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'demo', 'api', 'route.ts'),
|
|
264
|
+
"export async function GET() {}\nexport const openApi = { GET: { summary: 'demo' } }\n",
|
|
265
|
+
)
|
|
266
|
+
const resolver = createMockResolver([{ id: 'demo', from: '@open-mercato/core' }])
|
|
267
|
+
const generatedPath = path.join(tmpDir, 'output', 'generated', 'openapi.generated.json')
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
process.env.NEXT_PUBLIC_APP_URL = 'https://first.example.test'
|
|
271
|
+
await generateOpenApi({ resolver, quiet: true })
|
|
272
|
+
process.env.NEXT_PUBLIC_APP_URL = 'https://second.example.test'
|
|
273
|
+
const result = await generateOpenApi({ resolver, quiet: true })
|
|
274
|
+
const openApiDoc = JSON.parse(fs.readFileSync(generatedPath, 'utf8')) as {
|
|
275
|
+
servers: Array<{ url: string }>
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
expect(result.filesWritten).toEqual([generatedPath])
|
|
279
|
+
expect(openApiDoc.servers[0]?.url).toBe('https://second.example.test')
|
|
280
|
+
} finally {
|
|
281
|
+
if (originalAppUrl === undefined) delete process.env.NEXT_PUBLIC_APP_URL
|
|
282
|
+
else process.env.NEXT_PUBLIC_APP_URL = originalAppUrl
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('does not cache a static fallback after a bundle failure', async () => {
|
|
287
|
+
touchFile(
|
|
288
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'demo', 'api', 'route.ts'),
|
|
289
|
+
"export async function GET() {}\nexport const openApi = { GET: { summary: 'demo' } }\n",
|
|
290
|
+
)
|
|
291
|
+
const resolver = createMockResolver([{ id: 'demo', from: '@open-mercato/core' }])
|
|
292
|
+
const generatorPath = path.join(tmpDir, 'packages', 'shared', 'src', 'lib', 'openapi', 'generator.ts')
|
|
293
|
+
const manifestPath = path.join(tmpDir, 'output', 'generated', 'openapi.generated.inputs.json')
|
|
294
|
+
await generateOpenApi({ resolver, quiet: true })
|
|
295
|
+
|
|
296
|
+
fs.unlinkSync(generatorPath)
|
|
297
|
+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
298
|
+
await generateOpenApi({ resolver, quiet: false })
|
|
299
|
+
await generateOpenApi({ resolver, quiet: false })
|
|
300
|
+
|
|
301
|
+
expect(consoleSpy.mock.calls.filter(([message]) =>
|
|
302
|
+
typeof message === 'string' && message.startsWith('[OpenAPI] Found'),
|
|
303
|
+
)).toHaveLength(2)
|
|
304
|
+
expect(fs.existsSync(manifestPath)).toBe(false)
|
|
305
|
+
consoleSpy.mockRestore()
|
|
306
|
+
})
|
|
133
307
|
})
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
toSnake,
|
|
14
14
|
rimrafDir,
|
|
15
15
|
logGenerationResult,
|
|
16
|
+
writeIfChanged,
|
|
16
17
|
type GeneratorResult,
|
|
17
18
|
createGeneratorResult,
|
|
18
19
|
} from '../utils'
|
|
@@ -240,7 +241,23 @@ function parseEntityFieldsFromFile(filePath: string, exportedClassNames: string[
|
|
|
240
241
|
return result
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
function
|
|
244
|
+
function writeTrackedFile(
|
|
245
|
+
filePath: string,
|
|
246
|
+
content: string,
|
|
247
|
+
result: GeneratorResult,
|
|
248
|
+
): void {
|
|
249
|
+
if (writeIfChanged(filePath, content)) {
|
|
250
|
+
result.filesWritten.push(filePath)
|
|
251
|
+
} else {
|
|
252
|
+
result.filesUnchanged.push(filePath)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function writePerEntityFieldFiles(
|
|
257
|
+
outRoot: string,
|
|
258
|
+
fieldsByEntity: EntityFieldMap,
|
|
259
|
+
result: GeneratorResult,
|
|
260
|
+
): void {
|
|
244
261
|
fs.mkdirSync(outRoot, { recursive: true })
|
|
245
262
|
const desiredEntities = new Set(Object.keys(fieldsByEntity))
|
|
246
263
|
for (const [entity, fields] of Object.entries(fieldsByEntity)) {
|
|
@@ -259,18 +276,24 @@ function writePerEntityFieldFiles(outRoot: string, fieldsByEntity: EntityFieldMa
|
|
|
259
276
|
],
|
|
260
277
|
})
|
|
261
278
|
}
|
|
262
|
-
|
|
279
|
+
writeTrackedFile(path.join(entDir, 'index.ts'), getSourceText(sourceFile), result)
|
|
263
280
|
}
|
|
264
281
|
|
|
265
282
|
const existingEntries = fs.existsSync(outRoot) ? fs.readdirSync(outRoot, { withFileTypes: true }) : []
|
|
266
283
|
for (const entry of existingEntries) {
|
|
267
284
|
if (!entry.isDirectory()) continue
|
|
268
285
|
if (desiredEntities.has(entry.name)) continue
|
|
269
|
-
|
|
286
|
+
const staleDirectory = path.join(outRoot, entry.name)
|
|
287
|
+
rimrafDir(staleDirectory)
|
|
288
|
+
result.filesWritten.push(staleDirectory)
|
|
270
289
|
}
|
|
271
290
|
}
|
|
272
291
|
|
|
273
|
-
function writeEntityFieldsRegistry(
|
|
292
|
+
function writeEntityFieldsRegistry(
|
|
293
|
+
generatedRoot: string,
|
|
294
|
+
fieldsByEntity: EntityFieldMap,
|
|
295
|
+
result: GeneratorResult,
|
|
296
|
+
): void {
|
|
274
297
|
const entities = Object.keys(fieldsByEntity).sort((a, b) => a.localeCompare(b))
|
|
275
298
|
const registry: Record<string, Record<string, string>> = {}
|
|
276
299
|
for (const entity of entities) {
|
|
@@ -302,8 +325,7 @@ function writeEntityFieldsRegistry(generatedRoot: string, fieldsByEntity: Entity
|
|
|
302
325
|
statements: [returnStatement(elementAccess(identifier('entityFieldsRegistry'), identifier('slug')))],
|
|
303
326
|
})
|
|
304
327
|
const outPath = path.join(generatedRoot, 'entity-fields-registry.ts')
|
|
305
|
-
|
|
306
|
-
fs.writeFileSync(outPath, getSourceText(sourceFile))
|
|
328
|
+
writeTrackedFile(outPath, getSourceText(sourceFile), result)
|
|
307
329
|
}
|
|
308
330
|
|
|
309
331
|
export async function generateEntityIds(options: EntityIdsOptions): Promise<GeneratorResult> {
|
|
@@ -472,9 +494,7 @@ export async function generateEntityIds(options: EntityIdsOptions): Promise<Gene
|
|
|
472
494
|
type: 'typeof E',
|
|
473
495
|
})
|
|
474
496
|
const src = getSourceText(groupSourceFile)
|
|
475
|
-
|
|
476
|
-
fs.writeFileSync(out, src)
|
|
477
|
-
result.filesWritten.push(out)
|
|
497
|
+
writeTrackedFile(out, src, result)
|
|
478
498
|
|
|
479
499
|
const fieldsRoot = path.join(pkgOutputDir, 'entities')
|
|
480
500
|
const fieldsByModule = fieldsByGroup[g] || {}
|
|
@@ -485,10 +505,10 @@ export async function generateEntityIds(options: EntityIdsOptions): Promise<Gene
|
|
|
485
505
|
combined[entity] = Array.from(new Set([...(combined[entity] || []), ...fields]))
|
|
486
506
|
}
|
|
487
507
|
}
|
|
488
|
-
writePerEntityFieldFiles(fieldsRoot, combined)
|
|
508
|
+
writePerEntityFieldFiles(fieldsRoot, combined, result)
|
|
489
509
|
|
|
490
510
|
// Generate static entity fields registry for Turbopack compatibility
|
|
491
|
-
writeEntityFieldsRegistry(pkgOutputDir, combined)
|
|
511
|
+
writeEntityFieldsRegistry(pkgOutputDir, combined, result)
|
|
492
512
|
}
|
|
493
513
|
|
|
494
514
|
// Write combined entity fields to root generated/ folder
|
|
@@ -500,8 +520,8 @@ export async function generateEntityIds(options: EntityIdsOptions): Promise<Gene
|
|
|
500
520
|
}
|
|
501
521
|
}
|
|
502
522
|
}
|
|
503
|
-
writePerEntityFieldFiles(path.join(outputDir, 'entities'), combinedAll)
|
|
504
|
-
writeEntityFieldsRegistry(outputDir, combinedAll)
|
|
523
|
+
writePerEntityFieldFiles(path.join(outputDir, 'entities'), combinedAll, result)
|
|
524
|
+
writeEntityFieldsRegistry(outputDir, combinedAll, result)
|
|
505
525
|
|
|
506
526
|
return result
|
|
507
527
|
}
|