@open-mercato/cli 0.6.7-develop.6552.1.5208a19369 → 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
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import * as fs from 'node:fs'
|
|
10
10
|
import * as path from 'node:path'
|
|
11
|
-
import type { PackageResolver } from '../resolver'
|
|
11
|
+
import type { ModuleEntry, PackageResolver } from '../resolver'
|
|
12
12
|
import { isModuleRouteFile } from './scanner'
|
|
13
13
|
import {
|
|
14
14
|
calculateChecksum,
|
|
15
|
+
calculateStructureChecksum,
|
|
15
16
|
readChecksumRecord,
|
|
16
17
|
writeChecksumRecord,
|
|
17
18
|
logGenerationResult,
|
|
@@ -32,6 +33,111 @@ interface ApiRouteInfo {
|
|
|
32
33
|
openApiPath: string
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
interface OpenApiInputManifest {
|
|
37
|
+
version: 1
|
|
38
|
+
inputPaths: string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface OpenApiInputContext {
|
|
42
|
+
enabled: ModuleEntry[]
|
|
43
|
+
apiRoots: string[]
|
|
44
|
+
moduleIdentity: Array<{
|
|
45
|
+
id: string
|
|
46
|
+
from: string
|
|
47
|
+
appBase: string
|
|
48
|
+
pkgBase: string
|
|
49
|
+
}>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface OpenApiBundleResult {
|
|
53
|
+
doc: Record<string, any> | null
|
|
54
|
+
inputPaths: string[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const OPENAPI_INPUT_MANIFEST_VERSION = 1
|
|
58
|
+
|
|
59
|
+
function createOpenApiInputContext(resolver: PackageResolver): OpenApiInputContext {
|
|
60
|
+
const enabled = resolver.loadEnabledModules()
|
|
61
|
+
const apiRoots: string[] = []
|
|
62
|
+
const moduleIdentity = enabled.map((entry) => {
|
|
63
|
+
const roots = resolver.getModulePaths(entry)
|
|
64
|
+
apiRoots.push(path.join(roots.appBase, 'api'), path.join(roots.pkgBase, 'api'))
|
|
65
|
+
return {
|
|
66
|
+
id: entry.id,
|
|
67
|
+
from: entry.from ?? '@open-mercato/core',
|
|
68
|
+
appBase: path.resolve(roots.appBase),
|
|
69
|
+
pkgBase: path.resolve(roots.pkgBase),
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
return { enabled, apiRoots, moduleIdentity }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readOpenApiInputManifest(filePath: string): OpenApiInputManifest | null {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Partial<OpenApiInputManifest>
|
|
78
|
+
if (
|
|
79
|
+
parsed.version !== OPENAPI_INPUT_MANIFEST_VERSION
|
|
80
|
+
|| !Array.isArray(parsed.inputPaths)
|
|
81
|
+
|| parsed.inputPaths.some((entry) => typeof entry !== 'string')
|
|
82
|
+
) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
version: OPENAPI_INPUT_MANIFEST_VERSION,
|
|
87
|
+
inputPaths: Array.from(new Set(parsed.inputPaths.map((entry) => path.resolve(entry))))
|
|
88
|
+
.sort((a, b) => a.localeCompare(b)),
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function trackedPathRecord(filePath: string): string {
|
|
96
|
+
try {
|
|
97
|
+
const stat = fs.statSync(filePath)
|
|
98
|
+
if (stat.isFile()) {
|
|
99
|
+
return `${path.resolve(filePath)}:${calculateChecksum(fs.readFileSync(filePath, 'utf8'))}`
|
|
100
|
+
}
|
|
101
|
+
return `${path.resolve(filePath)}:${stat.size}:${stat.mtimeMs}`
|
|
102
|
+
} catch {
|
|
103
|
+
return `missing:${path.resolve(filePath)}`
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function calculateOpenApiInputFingerprint(
|
|
108
|
+
resolver: PackageResolver,
|
|
109
|
+
context: OpenApiInputContext,
|
|
110
|
+
manifest: OpenApiInputManifest,
|
|
111
|
+
): string {
|
|
112
|
+
const rootDir = resolver.getRootDir()
|
|
113
|
+
const appDir = resolver.getAppDir()
|
|
114
|
+
const configPaths = [
|
|
115
|
+
resolver.getModulesConfigPath(),
|
|
116
|
+
path.join(rootDir, 'package.json'),
|
|
117
|
+
path.join(rootDir, 'yarn.lock'),
|
|
118
|
+
path.join(rootDir, 'package-lock.json'),
|
|
119
|
+
path.join(rootDir, 'pnpm-lock.yaml'),
|
|
120
|
+
path.join(rootDir, 'tsconfig.json'),
|
|
121
|
+
path.join(rootDir, 'tsconfig.base.json'),
|
|
122
|
+
path.join(appDir, 'package.json'),
|
|
123
|
+
path.join(appDir, 'tsconfig.json'),
|
|
124
|
+
]
|
|
125
|
+
const trackedRecords = [...manifest.inputPaths, ...configPaths]
|
|
126
|
+
.map(trackedPathRecord)
|
|
127
|
+
.sort((a, b) => a.localeCompare(b))
|
|
128
|
+
return calculateChecksum(JSON.stringify({
|
|
129
|
+
version: OPENAPI_INPUT_MANIFEST_VERSION,
|
|
130
|
+
modules: context.moduleIdentity,
|
|
131
|
+
routeStructure: calculateStructureChecksum(context.apiRoots),
|
|
132
|
+
trackedRecords,
|
|
133
|
+
environment: {
|
|
134
|
+
node: process.version,
|
|
135
|
+
nodeEnv: process.env.NODE_ENV ?? '',
|
|
136
|
+
publicAppUrl: process.env.NEXT_PUBLIC_APP_URL ?? '',
|
|
137
|
+
},
|
|
138
|
+
}))
|
|
139
|
+
}
|
|
140
|
+
|
|
35
141
|
function resolveExistingPath(paths: string[]): string | null {
|
|
36
142
|
for (const candidate of paths) {
|
|
37
143
|
if (fs.existsSync(candidate)) return candidate
|
|
@@ -42,9 +148,11 @@ function resolveExistingPath(paths: string[]): string | null {
|
|
|
42
148
|
/**
|
|
43
149
|
* Find all API route files and extract their OpenAPI specs.
|
|
44
150
|
*/
|
|
45
|
-
async function findApiRoutes(
|
|
151
|
+
async function findApiRoutes(
|
|
152
|
+
resolver: PackageResolver,
|
|
153
|
+
enabled: ModuleEntry[],
|
|
154
|
+
): Promise<ApiRouteInfo[]> {
|
|
46
155
|
const routes: ApiRouteInfo[] = []
|
|
47
|
-
const enabled = resolver.loadEnabledModules()
|
|
48
156
|
|
|
49
157
|
for (const entry of enabled) {
|
|
50
158
|
const modId = entry.id
|
|
@@ -198,13 +306,13 @@ async function generateOpenApiViaBundle(
|
|
|
198
306
|
routes: ApiRouteInfo[],
|
|
199
307
|
resolver: PackageResolver,
|
|
200
308
|
quiet: boolean
|
|
201
|
-
): Promise<
|
|
309
|
+
): Promise<OpenApiBundleResult> {
|
|
202
310
|
let esbuild: typeof import('esbuild')
|
|
203
311
|
try {
|
|
204
312
|
esbuild = await import('esbuild')
|
|
205
313
|
} catch {
|
|
206
314
|
if (!quiet) console.log('[OpenAPI] esbuild not available, skipping bundle approach')
|
|
207
|
-
return null
|
|
315
|
+
return { doc: null, inputPaths: [] }
|
|
208
316
|
}
|
|
209
317
|
|
|
210
318
|
const { execFileSync } = await import('node:child_process')
|
|
@@ -225,13 +333,14 @@ async function generateOpenApiViaBundle(
|
|
|
225
333
|
if (!quiet) {
|
|
226
334
|
console.log(`[OpenAPI] Generator source not found at ${generatorPath}, skipping bundle approach`)
|
|
227
335
|
}
|
|
228
|
-
return null
|
|
336
|
+
return { doc: null, inputPaths: [] }
|
|
229
337
|
}
|
|
230
338
|
|
|
231
339
|
const cacheDir = path.join(rootDir, 'node_modules', '.cache')
|
|
232
340
|
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })
|
|
233
341
|
|
|
234
342
|
const bundlePath = path.join(cacheDir, '_openapi-bundle.mjs')
|
|
343
|
+
let inputPaths: string[] = []
|
|
235
344
|
|
|
236
345
|
// Build the entry script that imports all routes and calls buildOpenApiDocument
|
|
237
346
|
const importLines: string[] = [
|
|
@@ -418,7 +527,7 @@ process.stdout.write(JSON.stringify(deepClone(doc), (_, v) =>
|
|
|
418
527
|
}
|
|
419
528
|
|
|
420
529
|
try {
|
|
421
|
-
await esbuild.build({
|
|
530
|
+
const buildResult = await esbuild.build({
|
|
422
531
|
stdin: {
|
|
423
532
|
contents: entryScript,
|
|
424
533
|
resolveDir: appDir,
|
|
@@ -426,6 +535,8 @@ process.stdout.write(JSON.stringify(deepClone(doc), (_, v) =>
|
|
|
426
535
|
loader: 'ts',
|
|
427
536
|
},
|
|
428
537
|
bundle: true,
|
|
538
|
+
metafile: true,
|
|
539
|
+
absWorkingDir: rootDir,
|
|
429
540
|
format: 'esm',
|
|
430
541
|
platform: 'node',
|
|
431
542
|
target: 'node18',
|
|
@@ -437,6 +548,12 @@ process.stdout.write(JSON.stringify(deepClone(doc), (_, v) =>
|
|
|
437
548
|
plugins: [stubNextPlugin, resolveProjectImportsPlugin, externalNonWorkspacePlugin],
|
|
438
549
|
})
|
|
439
550
|
|
|
551
|
+
inputPaths = Object.keys(buildResult.metafile?.inputs ?? {})
|
|
552
|
+
.filter((inputPath) => !inputPath.startsWith('<'))
|
|
553
|
+
.map((inputPath) => path.resolve(rootDir, inputPath))
|
|
554
|
+
.filter((inputPath) => fs.existsSync(inputPath))
|
|
555
|
+
.sort((a, b) => a.localeCompare(b))
|
|
556
|
+
|
|
440
557
|
const stdout = execFileSync(process.execPath, [bundlePath], {
|
|
441
558
|
timeout: 60_000,
|
|
442
559
|
maxBuffer: 20 * 1024 * 1024,
|
|
@@ -459,7 +576,7 @@ process.stdout.write(JSON.stringify(deepClone(doc), (_, v) =>
|
|
|
459
576
|
console.log(`[OpenAPI] Bundle approach: ${pathCount} paths, ${withBody} with requestBody schemas`)
|
|
460
577
|
}
|
|
461
578
|
|
|
462
|
-
return doc
|
|
579
|
+
return { doc, inputPaths }
|
|
463
580
|
} catch (err) {
|
|
464
581
|
const errMsg = err instanceof Error ? err.message : String(err)
|
|
465
582
|
const stderr = (err as any)?.stderr
|
|
@@ -483,7 +600,7 @@ process.stdout.write(JSON.stringify(deepClone(doc), (_, v) =>
|
|
|
483
600
|
}
|
|
484
601
|
}
|
|
485
602
|
}
|
|
486
|
-
return null
|
|
603
|
+
return { doc: null, inputPaths }
|
|
487
604
|
} finally {
|
|
488
605
|
// Shut down esbuild's persistent Go service so it does not deadlock at
|
|
489
606
|
// process exit when a plugin request is still in flight.
|
|
@@ -552,14 +669,33 @@ export async function generateOpenApi(options: GenerateOpenApiOptions): Promise<
|
|
|
552
669
|
const outputDir = resolver.getOutputDir()
|
|
553
670
|
const outFile = path.join(outputDir, 'openapi.generated.json')
|
|
554
671
|
const checksumFile = path.join(outputDir, 'openapi.generated.checksum')
|
|
672
|
+
const inputManifestFile = path.join(outputDir, 'openapi.generated.inputs.json')
|
|
555
673
|
|
|
556
674
|
// Ensure output directory exists
|
|
557
675
|
if (!fs.existsSync(outputDir)) {
|
|
558
676
|
fs.mkdirSync(outputDir, { recursive: true })
|
|
559
677
|
}
|
|
560
678
|
|
|
679
|
+
const inputContext = createOpenApiInputContext(resolver)
|
|
680
|
+
const existingChecksums = readChecksumRecord(checksumFile)
|
|
681
|
+
const existingManifest = readOpenApiInputManifest(inputManifestFile)
|
|
682
|
+
|
|
683
|
+
if (
|
|
684
|
+
existingChecksums?.structure
|
|
685
|
+
&& existingManifest
|
|
686
|
+
&& fs.existsSync(outFile)
|
|
687
|
+
&& calculateChecksum(fs.readFileSync(outFile, 'utf8')) === existingChecksums.content
|
|
688
|
+
&& calculateOpenApiInputFingerprint(resolver, inputContext, existingManifest) === existingChecksums.structure
|
|
689
|
+
) {
|
|
690
|
+
result.filesUnchanged.push(outFile)
|
|
691
|
+
if (!quiet) {
|
|
692
|
+
console.log(`[OpenAPI] Skipped (inputs unchanged): ${outFile}`)
|
|
693
|
+
}
|
|
694
|
+
return result
|
|
695
|
+
}
|
|
696
|
+
|
|
561
697
|
// Find all API routes
|
|
562
|
-
const routes = await findApiRoutes(resolver)
|
|
698
|
+
const routes = await findApiRoutes(resolver, inputContext.enabled)
|
|
563
699
|
|
|
564
700
|
if (!quiet) {
|
|
565
701
|
console.log(`[OpenAPI] Found ${routes.length} API route files`)
|
|
@@ -567,7 +703,8 @@ export async function generateOpenApi(options: GenerateOpenApiOptions): Promise<
|
|
|
567
703
|
|
|
568
704
|
// Determine project root (cli package is at packages/cli/src/lib/generators/)
|
|
569
705
|
// Try esbuild bundle approach first — produces full requestBody/response schemas
|
|
570
|
-
|
|
706
|
+
const bundleResult = await generateOpenApiViaBundle(routes, resolver, quiet)
|
|
707
|
+
let doc: Record<string, any> | null = bundleResult.doc
|
|
571
708
|
|
|
572
709
|
// Fallback to static regex approach (extracts operationId/summary/tags but no schemas)
|
|
573
710
|
if (!doc) {
|
|
@@ -601,10 +738,26 @@ export async function generateOpenApi(options: GenerateOpenApiOptions): Promise<
|
|
|
601
738
|
|
|
602
739
|
const output = JSON.stringify(doc, null, 2)
|
|
603
740
|
const checksum = calculateChecksum(output)
|
|
741
|
+
let structureChecksum = ''
|
|
742
|
+
|
|
743
|
+
if (bundleResult.doc && bundleResult.inputPaths.length > 0) {
|
|
744
|
+
const manifest: OpenApiInputManifest = {
|
|
745
|
+
version: OPENAPI_INPUT_MANIFEST_VERSION,
|
|
746
|
+
inputPaths: Array.from(new Set(bundleResult.inputPaths)).sort((a, b) => a.localeCompare(b)),
|
|
747
|
+
}
|
|
748
|
+
fs.writeFileSync(inputManifestFile, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
749
|
+
structureChecksum = calculateOpenApiInputFingerprint(resolver, inputContext, manifest)
|
|
750
|
+
} else {
|
|
751
|
+
try { fs.unlinkSync(inputManifestFile) } catch {}
|
|
752
|
+
}
|
|
604
753
|
|
|
605
754
|
// Check if unchanged
|
|
606
|
-
const
|
|
607
|
-
|
|
755
|
+
const existingOutputMatches = fs.existsSync(outFile)
|
|
756
|
+
&& calculateChecksum(fs.readFileSync(outFile, 'utf8')) === checksum
|
|
757
|
+
if (existingChecksums && existingChecksums.content === checksum && existingOutputMatches) {
|
|
758
|
+
if (existingChecksums.structure !== structureChecksum) {
|
|
759
|
+
writeChecksumRecord(checksumFile, { content: checksum, structure: structureChecksum })
|
|
760
|
+
}
|
|
608
761
|
result.filesUnchanged.push(outFile)
|
|
609
762
|
if (!quiet) {
|
|
610
763
|
console.log(`[OpenAPI] Skipped (unchanged): ${outFile}`)
|
|
@@ -614,7 +767,7 @@ export async function generateOpenApi(options: GenerateOpenApiOptions): Promise<
|
|
|
614
767
|
|
|
615
768
|
// Write the file
|
|
616
769
|
fs.writeFileSync(outFile, output)
|
|
617
|
-
writeChecksumRecord(checksumFile, { content: checksum, structure:
|
|
770
|
+
writeChecksumRecord(checksumFile, { content: checksum, structure: structureChecksum })
|
|
618
771
|
|
|
619
772
|
result.filesWritten.push(outFile)
|
|
620
773
|
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* In-process generate watcher.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Coalesces filesystem events on a configurable interval, then verifies them
|
|
5
|
+
* with the existing structural fingerprint before invoking the generator.
|
|
6
|
+
* If filesystem watching is unavailable, it falls back to fingerprint polling.
|
|
6
7
|
* The polling loop is identical to the legacy standalone watcher; the only
|
|
7
8
|
* difference is that it now runs inside whatever process calls it
|
|
8
9
|
* (typically `mercato server dev`), so the dev runtime no longer needs a
|
|
9
10
|
* sidecar `mercato generate watch` Node process.
|
|
10
11
|
*
|
|
11
|
-
* Contract preserved
|
|
12
|
-
* - Default poll interval 1000 ms (minimum 250 ms).
|
|
12
|
+
* Contract preserved from the prior standalone watcher:
|
|
13
|
+
* - Default debounce/fallback poll interval 1000 ms (minimum 250 ms).
|
|
13
14
|
* - One initial generator run unless `skipInitial` is true.
|
|
14
15
|
* - Concurrent regeneration requests are coalesced (running + pending).
|
|
15
16
|
* - Generator errors are logged but never crash the watcher.
|
|
@@ -18,12 +19,25 @@
|
|
|
18
19
|
|
|
19
20
|
export type GenerateWatcherLogger = Pick<Console, 'log' | 'error'>
|
|
20
21
|
|
|
22
|
+
export type GenerateWatcherChangeSignal = {
|
|
23
|
+
/** Monotonic event generation. Changes indicate that a checksum may be stale. */
|
|
24
|
+
currentVersion(): number
|
|
25
|
+
/** Refresh filesystem subscriptions after module configuration changes. */
|
|
26
|
+
refresh(): Promise<void> | void
|
|
27
|
+
/** When true, preserve the legacy full-checksum-on-every-poll behavior. */
|
|
28
|
+
usesPollingFallback(): boolean
|
|
29
|
+
/** Close filesystem subscriptions. Idempotent. */
|
|
30
|
+
close(): Promise<void> | void
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
export type GenerateWatcherOptions = {
|
|
22
34
|
/**
|
|
23
35
|
* Function that returns the current structural fingerprint of the module
|
|
24
36
|
* tree. The watcher re-runs `runGenerators` whenever this value changes.
|
|
25
37
|
*/
|
|
26
38
|
computeStructureChecksum: () => Promise<string> | string
|
|
39
|
+
/** Optional event signal that avoids full checksum work during idle polls. */
|
|
40
|
+
changeSignal?: GenerateWatcherChangeSignal
|
|
27
41
|
/**
|
|
28
42
|
* Function that performs the actual regeneration work. Called once on
|
|
29
43
|
* startup (unless `skipInitial`) and again whenever the checksum changes.
|
|
@@ -69,13 +83,14 @@ export function startInProcessGenerateWatcher(
|
|
|
69
83
|
const quiet = options.quiet === true
|
|
70
84
|
const pollMs = resolvePollMs(options.pollMs)
|
|
71
85
|
const skipInitial = options.skipInitial === true
|
|
72
|
-
const { computeStructureChecksum, runGenerators } = options
|
|
86
|
+
const { changeSignal, computeStructureChecksum, runGenerators } = options
|
|
73
87
|
|
|
74
88
|
let stopping = false
|
|
75
89
|
let pollTimer: ReturnType<typeof setTimeout> | null = null
|
|
76
90
|
let running = false
|
|
77
91
|
let pendingReason: string | null = null
|
|
78
92
|
let previousChecksum = ''
|
|
93
|
+
let observedChangeVersion = -1
|
|
79
94
|
let doneResolve: (() => void) | null = null
|
|
80
95
|
const done = new Promise<void>((resolve) => {
|
|
81
96
|
doneResolve = resolve
|
|
@@ -114,7 +129,17 @@ export function startInProcessGenerateWatcher(
|
|
|
114
129
|
void (async () => {
|
|
115
130
|
if (stopping) return
|
|
116
131
|
try {
|
|
132
|
+
const changeVersion = changeSignal?.currentVersion() ?? 0
|
|
133
|
+
if (
|
|
134
|
+
changeSignal
|
|
135
|
+
&& !changeSignal.usesPollingFallback()
|
|
136
|
+
&& changeVersion === observedChangeVersion
|
|
137
|
+
) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
117
140
|
const nextChecksum = await computeStructureChecksum()
|
|
141
|
+
observedChangeVersion = changeVersion
|
|
142
|
+
await changeSignal?.refresh()
|
|
118
143
|
if (nextChecksum !== previousChecksum) {
|
|
119
144
|
previousChecksum = nextChecksum
|
|
120
145
|
await runOnce('structure change')
|
|
@@ -132,15 +157,18 @@ export function startInProcessGenerateWatcher(
|
|
|
132
157
|
|
|
133
158
|
void (async () => {
|
|
134
159
|
try {
|
|
160
|
+
await changeSignal?.refresh()
|
|
161
|
+
const initialChangeVersion = changeSignal?.currentVersion() ?? 0
|
|
135
162
|
if (!skipInitial) {
|
|
136
163
|
await runOnce('initial')
|
|
137
164
|
}
|
|
138
165
|
previousChecksum = await computeStructureChecksum()
|
|
166
|
+
observedChangeVersion = initialChangeVersion
|
|
139
167
|
if (!quiet) {
|
|
140
168
|
if (skipInitial) {
|
|
141
169
|
logger.log('[generate:watch] Skipping initial regeneration and watching the current generated state.')
|
|
142
170
|
}
|
|
143
|
-
logger.log(`[generate:watch] Watching structural module files
|
|
171
|
+
logger.log(`[generate:watch] Watching structural module files with a ${pollMs}ms debounce (in-process).`)
|
|
144
172
|
}
|
|
145
173
|
scheduleNext()
|
|
146
174
|
} catch (error) {
|
|
@@ -162,6 +190,7 @@ export function startInProcessGenerateWatcher(
|
|
|
162
190
|
clearTimeout(pollTimer)
|
|
163
191
|
pollTimer = null
|
|
164
192
|
}
|
|
193
|
+
await changeSignal?.close()
|
|
165
194
|
if (doneResolve) {
|
|
166
195
|
doneResolve()
|
|
167
196
|
doneResolve = null
|
package/src/mercato.ts
CHANGED
|
@@ -750,7 +750,7 @@ async function runPostGenerateStructuralCachePurge(quiet: boolean): Promise<void
|
|
|
750
750
|
* so the watcher embedded in the server lifecycle can reuse the same closure
|
|
751
751
|
* without re-importing the closure-scoped version inside `buildBaseModules`.
|
|
752
752
|
*/
|
|
753
|
-
async function runGeneratorSuite(quiet: boolean): Promise<
|
|
753
|
+
async function runGeneratorSuite(quiet: boolean): Promise<boolean> {
|
|
754
754
|
const { createResolver } = await import('./lib/resolver')
|
|
755
755
|
const {
|
|
756
756
|
generateEntityIds,
|
|
@@ -763,35 +763,84 @@ async function runGeneratorSuite(quiet: boolean): Promise<void> {
|
|
|
763
763
|
generateOpenApi,
|
|
764
764
|
} = await import('./lib/generators')
|
|
765
765
|
const resolver = createResolver()
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
766
|
+
const results = [
|
|
767
|
+
await generateEntityIds({ resolver, quiet }),
|
|
768
|
+
await generateModuleRegistry({ resolver, quiet }),
|
|
769
|
+
await generateModuleRegistryApp({ resolver, quiet }),
|
|
770
|
+
await generateModuleRegistryCli({ resolver, quiet }),
|
|
771
|
+
await generateModuleEntities({ resolver, quiet }),
|
|
772
|
+
await generateModuleDi({ resolver, quiet }),
|
|
773
|
+
await generateModulePackageSources({ resolver, quiet }),
|
|
774
|
+
await generateOpenApi({ resolver, quiet }),
|
|
775
|
+
]
|
|
776
|
+
return results.some((result) => (result?.filesWritten.length ?? 0) > 0)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function runGeneratorSuiteWithStructuralInvalidation(quiet: boolean): Promise<void> {
|
|
780
|
+
const generatedFilesChanged = await runGeneratorSuite(quiet)
|
|
781
|
+
if (!generatedFilesChanged) {
|
|
782
|
+
if (!quiet) {
|
|
783
|
+
console.log('[generate] Generated outputs unchanged; skipping structural cache purge.')
|
|
784
|
+
}
|
|
785
|
+
return
|
|
786
|
+
}
|
|
787
|
+
await runPostGenerateStructuralCachePurge(quiet)
|
|
774
788
|
}
|
|
775
789
|
|
|
776
790
|
/**
|
|
777
|
-
* Builds the structural
|
|
778
|
-
*
|
|
779
|
-
*
|
|
791
|
+
* Builds the event-gated structural fingerprint runtime used by generate
|
|
792
|
+
* watchers. Filesystem events mark the tree dirty; the existing full content
|
|
793
|
+
* checksum remains the final authority and the fallback when watching fails.
|
|
780
794
|
*/
|
|
781
|
-
function
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
795
|
+
async function createGenerateWatchRuntime() {
|
|
796
|
+
const [
|
|
797
|
+
{ createResolver },
|
|
798
|
+
{ calculateGenerateWatchStructureChecksum },
|
|
799
|
+
{ createGenerateWatchChangeSignal },
|
|
800
|
+
{ resolveStandaloneSourceMirrorBase },
|
|
801
|
+
] = await Promise.all([
|
|
802
|
+
import('./lib/resolver'),
|
|
803
|
+
import('./lib/generate-watch-structure'),
|
|
804
|
+
import('./lib/generate-watch-events'),
|
|
805
|
+
import('./lib/generators/scanner'),
|
|
806
|
+
])
|
|
807
|
+
|
|
808
|
+
const collectWatchState = () => {
|
|
785
809
|
const resolver = createResolver()
|
|
786
810
|
const moduleRoots = []
|
|
787
811
|
for (const entry of resolver.loadEnabledModules()) {
|
|
788
812
|
const roots = resolver.getModulePaths(entry)
|
|
789
813
|
moduleRoots.push({ appBase: roots.appBase, pkgBase: roots.pkgBase })
|
|
790
814
|
}
|
|
791
|
-
return
|
|
792
|
-
modulesFile:
|
|
815
|
+
return {
|
|
816
|
+
modulesFile: resolver.getModulesConfigPath(),
|
|
793
817
|
moduleRoots,
|
|
794
|
-
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return {
|
|
822
|
+
computeStructureChecksum: async () => {
|
|
823
|
+
return calculateGenerateWatchStructureChecksum(collectWatchState())
|
|
824
|
+
},
|
|
825
|
+
changeSignal: createGenerateWatchChangeSignal({
|
|
826
|
+
getWatchTargets: () => {
|
|
827
|
+
const state = collectWatchState()
|
|
828
|
+
const targets: Array<{ directory: string; recursive: boolean; fileName?: string }> = [{
|
|
829
|
+
directory: path.dirname(state.modulesFile),
|
|
830
|
+
recursive: false,
|
|
831
|
+
fileName: path.basename(state.modulesFile),
|
|
832
|
+
}]
|
|
833
|
+
for (const roots of state.moduleRoots) {
|
|
834
|
+
targets.push({ directory: path.dirname(roots.appBase), recursive: true })
|
|
835
|
+
targets.push({ directory: path.dirname(roots.pkgBase), recursive: true })
|
|
836
|
+
const sourceMirror = resolveStandaloneSourceMirrorBase(roots.pkgBase)
|
|
837
|
+
if (sourceMirror) {
|
|
838
|
+
targets.push({ directory: path.dirname(sourceMirror), recursive: true })
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return targets
|
|
842
|
+
},
|
|
843
|
+
}),
|
|
795
844
|
}
|
|
796
845
|
}
|
|
797
846
|
|
|
@@ -1777,8 +1826,7 @@ export async function run(argv = process.argv) {
|
|
|
1777
1826
|
const quiet = args.includes('--quiet') || args.includes('-q')
|
|
1778
1827
|
|
|
1779
1828
|
console.log('Running all generators...')
|
|
1780
|
-
await
|
|
1781
|
-
await runPostGenerateStructuralCachePurge(quiet)
|
|
1829
|
+
await runGeneratorSuiteWithStructuralInvalidation(quiet)
|
|
1782
1830
|
console.log('All generators completed.')
|
|
1783
1831
|
},
|
|
1784
1832
|
},
|
|
@@ -1791,14 +1839,14 @@ export async function run(argv = process.argv) {
|
|
|
1791
1839
|
const parsedInterval = intervalArg ? Number.parseInt(intervalArg.split('=')[1] ?? '', 10) : NaN
|
|
1792
1840
|
const intervalMs = Number.isFinite(parsedInterval) && parsedInterval >= 250 ? parsedInterval : 1000
|
|
1793
1841
|
|
|
1842
|
+
const generateWatchRuntime = await createGenerateWatchRuntime()
|
|
1794
1843
|
const watcher = startInProcessGenerateWatcher({
|
|
1795
1844
|
pollMs: intervalMs,
|
|
1796
1845
|
skipInitial,
|
|
1797
1846
|
quiet,
|
|
1798
|
-
|
|
1847
|
+
...generateWatchRuntime,
|
|
1799
1848
|
runGenerators: async () => {
|
|
1800
|
-
await
|
|
1801
|
-
await runPostGenerateStructuralCachePurge(true)
|
|
1849
|
+
await runGeneratorSuiteWithStructuralInvalidation(true)
|
|
1802
1850
|
},
|
|
1803
1851
|
})
|
|
1804
1852
|
|
|
@@ -2238,6 +2286,7 @@ export async function run(argv = process.argv) {
|
|
|
2238
2286
|
// (measured against the legacy sidecar). Opt back into the
|
|
2239
2287
|
// sidecar with `OM_DEV_GENERATE_WATCH_MODE=legacy` if needed.
|
|
2240
2288
|
console.log('[server] In-process generate watcher enabled — structural changes will regenerate without a sidecar process.')
|
|
2289
|
+
const generateWatchRuntime = await createGenerateWatchRuntime()
|
|
2241
2290
|
activeGenerateWatcher = startInProcessGenerateWatcher({
|
|
2242
2291
|
// `--skip-initial` equivalent: `yarn dev` always runs an
|
|
2243
2292
|
// initial `mercato generate` before reaching the server
|
|
@@ -2246,10 +2295,9 @@ export async function run(argv = process.argv) {
|
|
|
2246
2295
|
// twice in a row.
|
|
2247
2296
|
skipInitial: true,
|
|
2248
2297
|
quiet: false,
|
|
2249
|
-
|
|
2298
|
+
...generateWatchRuntime,
|
|
2250
2299
|
runGenerators: async () => {
|
|
2251
|
-
await
|
|
2252
|
-
await runPostGenerateStructuralCachePurge(true)
|
|
2300
|
+
await runGeneratorSuiteWithStructuralInvalidation(true)
|
|
2253
2301
|
},
|
|
2254
2302
|
})
|
|
2255
2303
|
} else {
|