@open-mercato/cli 0.6.7-develop.6686.1.bdda5ed397 → 0.6.7-develop.6688.1.4426ef7fa7
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/agentic/guides/module-facts.json +108 -108
- 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/wms.md +1 -1
- package/dist/agentic/guides/modules/workflows.md +1 -1
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/lib/generators/extensions/injection-widgets.js +20 -0
- package/dist/lib/generators/extensions/injection-widgets.js.map +2 -2
- package/package.json +5 -5
- package/src/lib/__tests__/injection-widget-id-validation.test.ts +184 -0
- package/src/lib/generators/extensions/injection-widgets.ts +51 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6688.1.4426ef7fa7",
|
|
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.5",
|
|
61
61
|
"@mikro-orm/migrations": "^7.1.5",
|
|
62
62
|
"@mikro-orm/postgresql": "^7.1.5",
|
|
63
|
-
"@open-mercato/queue": "0.6.7-develop.
|
|
64
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
63
|
+
"@open-mercato/queue": "0.6.7-develop.6688.1.4426ef7fa7",
|
|
64
|
+
"@open-mercato/shared": "0.6.7-develop.6688.1.4426ef7fa7",
|
|
65
65
|
"cross-spawn": "^7.0.6",
|
|
66
66
|
"pg": "8.22.0",
|
|
67
67
|
"semver": "^7.8.5",
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"typescript-js": "npm:typescript@6.0.3"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
75
|
+
"@open-mercato/shared": "0.6.7-develop.6688.1.4426ef7fa7"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
78
|
+
"@open-mercato/shared": "0.6.7-develop.6688.1.4426ef7fa7",
|
|
79
79
|
"@types/jest": "^30.0.0",
|
|
80
80
|
"jest": "^30.4.2",
|
|
81
81
|
"ts-jest": "^29.4.11"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { createInjectionWidgetsExtension } from '../generators/extensions/injection-widgets'
|
|
5
|
+
import type { ModuleScanContext } from '../generators/extension'
|
|
6
|
+
|
|
7
|
+
// A widget's `metadata.id` and the `widgetId` that mounts it are two hand-typed literals in two
|
|
8
|
+
// different files. Nothing linked them, so a typo in either failed silently — the widget was never
|
|
9
|
+
// mounted, or the table entry resolved to nothing. These cases drive the real scan → generate seam
|
|
10
|
+
// (real filesystem fixtures, real AST extraction) and assert the reconciliation warnings.
|
|
11
|
+
|
|
12
|
+
let tmpRoot: string
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'om-injection-widgets-'))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true })
|
|
20
|
+
jest.restoreAllMocks()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function widgetSource(id: string): string {
|
|
24
|
+
return [
|
|
25
|
+
"import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'",
|
|
26
|
+
"import Impl from './widget.client'",
|
|
27
|
+
'',
|
|
28
|
+
'const widget: InjectionWidgetModule = {',
|
|
29
|
+
' metadata: {',
|
|
30
|
+
` id: '${id}',`,
|
|
31
|
+
" title: 'Fixture widget',",
|
|
32
|
+
' },',
|
|
33
|
+
' Widget: Impl,',
|
|
34
|
+
'}',
|
|
35
|
+
'',
|
|
36
|
+
'export default widget',
|
|
37
|
+
].join('\n')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function tableSource(widgetIds: string[]): string {
|
|
41
|
+
const entries = widgetIds
|
|
42
|
+
.map((id, index) => ` 'detail:fixture:tab${index}': [{ widgetId: '${id}', kind: 'tab' }],`)
|
|
43
|
+
.join('\n')
|
|
44
|
+
return [
|
|
45
|
+
"import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'",
|
|
46
|
+
'',
|
|
47
|
+
'export const injectionTable: ModuleInjectionTable = {',
|
|
48
|
+
entries,
|
|
49
|
+
'}',
|
|
50
|
+
'',
|
|
51
|
+
'export default injectionTable',
|
|
52
|
+
].join('\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Lays out one module on disk in the shape the scanner expects and returns a scan context for it.
|
|
57
|
+
* `declaredIds` each get `widgets/injection/<name>/widget.ts`; `referencedIds` go into the module's
|
|
58
|
+
* `widgets/injection-table.ts` (omitted entirely when the list is empty).
|
|
59
|
+
*/
|
|
60
|
+
function makeModule(moduleId: string, declaredIds: string[], referencedIds: string[]): ModuleScanContext {
|
|
61
|
+
const base = path.join(tmpRoot, moduleId)
|
|
62
|
+
const injectionDir = path.join(base, 'widgets', 'injection')
|
|
63
|
+
fs.mkdirSync(injectionDir, { recursive: true })
|
|
64
|
+
|
|
65
|
+
declaredIds.forEach((id, index) => {
|
|
66
|
+
const dir = path.join(injectionDir, `w${index}`)
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
68
|
+
fs.writeFileSync(path.join(dir, 'widget.ts'), widgetSource(id))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const tablePath = path.join(base, 'widgets', 'injection-table.ts')
|
|
72
|
+
if (referencedIds.length > 0) fs.writeFileSync(tablePath, tableSource(referencedIds))
|
|
73
|
+
|
|
74
|
+
const roots = { appBase: base, pkgBase: base }
|
|
75
|
+
return {
|
|
76
|
+
moduleId,
|
|
77
|
+
roots,
|
|
78
|
+
imps: { appBase: `@fixture/${moduleId}`, pkgBase: `@fixture/${moduleId}` },
|
|
79
|
+
importIdRef: { value: 0 },
|
|
80
|
+
sharedImports: [],
|
|
81
|
+
resolveModuleFile: (() =>
|
|
82
|
+
referencedIds.length > 0
|
|
83
|
+
? { absolutePath: tablePath, importPath: `@fixture/${moduleId}/widgets/injection-table` }
|
|
84
|
+
: null) as unknown as ModuleScanContext['resolveModuleFile'],
|
|
85
|
+
resolveFirstModuleFile: (() => null) as unknown as ModuleScanContext['resolveFirstModuleFile'],
|
|
86
|
+
processStandaloneConfig: () => null,
|
|
87
|
+
sanitizeGeneratedModuleSpecifier: (importPath: string) => importPath,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function runGenerate(contexts: ModuleScanContext[]): string[] {
|
|
92
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
93
|
+
const extension = createInjectionWidgetsExtension()
|
|
94
|
+
for (const ctx of contexts) extension.scanModule(ctx)
|
|
95
|
+
extension.generateOutput()
|
|
96
|
+
return warn.mock.calls.map((call) => String(call[0]))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('injection widget id reconciliation at generate time', () => {
|
|
100
|
+
it('stays silent when every declared widget is referenced and vice versa', () => {
|
|
101
|
+
const warnings = runGenerate([makeModule('alpha', ['alpha.one', 'alpha.two'], ['alpha.one', 'alpha.two'])])
|
|
102
|
+
|
|
103
|
+
expect(warnings).toEqual([])
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('warns when an injection table references a widget id nothing declares', () => {
|
|
107
|
+
const warnings = runGenerate([makeModule('alpha', ['alpha.one'], ['alpha.one', 'alpha.typo'])])
|
|
108
|
+
|
|
109
|
+
expect(warnings).toHaveLength(1)
|
|
110
|
+
expect(warnings[0]).toContain('alpha.typo')
|
|
111
|
+
expect(warnings[0]).toContain('references widget id')
|
|
112
|
+
expect(warnings[0]).toContain('alpha')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('stays silent about a declared widget that no table references', () => {
|
|
116
|
+
// The mirror check is deliberately not implemented: `extractInjectionTableWidgetIds` reads only
|
|
117
|
+
// statically-analyzable literals, so a table built by composition or computation exposes no ids
|
|
118
|
+
// and every widget in such a module would be reported as unreferenced. Pinned so the omission is
|
|
119
|
+
// a decision rather than an oversight.
|
|
120
|
+
const warnings = runGenerate([makeModule('alpha', ['alpha.one', 'alpha.orphan'], ['alpha.one'])])
|
|
121
|
+
|
|
122
|
+
expect(warnings).toEqual([])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('reports only the unresolvable reference when the two sides disagree entirely', () => {
|
|
126
|
+
const warnings = runGenerate([makeModule('alpha', ['alpha.declared'], ['alpha.referenced'])])
|
|
127
|
+
|
|
128
|
+
expect(warnings).toHaveLength(1)
|
|
129
|
+
expect(warnings[0]).toContain('alpha.referenced')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('does not warn about a table assembled by composition, whose ids are invisible statically', () => {
|
|
133
|
+
// Mirrors the real `example` module: `cond ? { ...a, ...b } : a`. The extractor sees no ids, so
|
|
134
|
+
// there is nothing to reconcile — and critically, no false warning.
|
|
135
|
+
const base = path.join(tmpRoot, 'composed')
|
|
136
|
+
fs.mkdirSync(path.join(base, 'widgets', 'injection', 'w0'), { recursive: true })
|
|
137
|
+
fs.writeFileSync(path.join(base, 'widgets', 'injection', 'w0', 'widget.ts'), widgetSource('composed.one'))
|
|
138
|
+
const tablePath = path.join(base, 'widgets', 'injection-table.ts')
|
|
139
|
+
fs.writeFileSync(
|
|
140
|
+
tablePath,
|
|
141
|
+
[
|
|
142
|
+
"import type { ModuleInjectionTable } from '@open-mercato/shared/modules/widgets/injection'",
|
|
143
|
+
"const a: ModuleInjectionTable = { 'detail:x:tab': [{ widgetId: 'composed.one' }] }",
|
|
144
|
+
"const b: ModuleInjectionTable = { 'detail:y:tab': [{ widgetId: 'composed.two' }] }",
|
|
145
|
+
'export const injectionTable: ModuleInjectionTable = process.env.FLAG ? { ...a, ...b } : a',
|
|
146
|
+
'export default injectionTable',
|
|
147
|
+
].join('\n'),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
const ctx: ModuleScanContext = {
|
|
151
|
+
...makeModule('composed', [], []),
|
|
152
|
+
resolveModuleFile: (() => ({
|
|
153
|
+
absolutePath: tablePath,
|
|
154
|
+
importPath: '@fixture/composed/widgets/injection-table',
|
|
155
|
+
})) as unknown as ModuleScanContext['resolveModuleFile'],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
expect(runGenerate([ctx])).toEqual([])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('accepts a table in one module referencing a widget declared in another', () => {
|
|
162
|
+
const warnings = runGenerate([
|
|
163
|
+
makeModule('alpha', ['alpha.shared'], []),
|
|
164
|
+
makeModule('beta', [], ['alpha.shared']),
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
expect(warnings).toEqual([])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('does not warn for the single-widget id inference', () => {
|
|
171
|
+
// A lone widget whose metadata.id cannot be extracted adopts the table's single id; that
|
|
172
|
+
// convenience path must not then report itself as a mismatch.
|
|
173
|
+
const ctx = makeModule('gamma', [], ['gamma.inferred'])
|
|
174
|
+
const dir = path.join(tmpRoot, 'gamma', 'widgets', 'injection', 'solo')
|
|
175
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
176
|
+
fs.writeFileSync(path.join(dir, 'widget.ts'), 'export default { Widget: () => null }\n')
|
|
177
|
+
|
|
178
|
+
expect(runGenerate([ctx])).toEqual([])
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('stays silent for a module with no injection widgets and no table', () => {
|
|
182
|
+
expect(runGenerate([makeModule('delta', [], [])])).toEqual([])
|
|
183
|
+
})
|
|
184
|
+
})
|
|
@@ -183,9 +183,52 @@ function resolveScannedWidgetPath(roots: ModuleRoots, fromApp: boolean, relPath:
|
|
|
183
183
|
return path.join(base, 'widgets', 'injection', ...relPath.split('/'))
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* A widget's `metadata.id` and the injection-table entry that mounts it are two hand-typed string
|
|
188
|
+
* literals in two different files, and nothing links them. A typo in either fails silently: the spot
|
|
189
|
+
* resolves to no widget and nothing is logged, so it only surfaces as "why isn't my widget
|
|
190
|
+
* rendering" during manual QA.
|
|
191
|
+
*
|
|
192
|
+
* Reports table entries whose `widgetId` no scanned widget declares. Diagnostics only — generated
|
|
193
|
+
* output is unchanged and the build is not failed, since a table entry may legitimately point at a
|
|
194
|
+
* widget supplied by a module that is not enabled in this app.
|
|
195
|
+
*
|
|
196
|
+
* Deliberately one-directional. The mirror check ("a widget nothing references is never mounted")
|
|
197
|
+
* cannot be done reliably yet: `extractInjectionTableWidgetIds` only reads statically-analyzable
|
|
198
|
+
* object literals, so a table assembled by composition or computation contributes **no** visible
|
|
199
|
+
* ids — `example` exports `cond ? { ...a, ...b } : a` and `translations` exports
|
|
200
|
+
* `buildInjectionTable()`, and both would have every one of their widgets reported as unreferenced.
|
|
201
|
+
* Under-reporting is harmless in the direction implemented here (a reference the extractor never saw
|
|
202
|
+
* cannot produce a warning) but fatal in the other. Teaching the extractor to resolve spreads,
|
|
203
|
+
* conditionals and call expressions is the prerequisite for adding it.
|
|
204
|
+
*/
|
|
205
|
+
function reportUnresolvableWidgetReferences(
|
|
206
|
+
widgetEntries: Map<string, InjectionWidgetEntry>,
|
|
207
|
+
tableWidgetRefs: Map<string, Set<string>>,
|
|
208
|
+
): void {
|
|
209
|
+
const declaredIds = new Set<string>()
|
|
210
|
+
for (const entry of widgetEntries.values()) {
|
|
211
|
+
if (entry.widgetId) declaredIds.add(entry.widgetId)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const [widgetId, modules] of Array.from(tableWidgetRefs.entries()).sort(([a], [b]) => a.localeCompare(b))) {
|
|
215
|
+
if (declaredIds.has(widgetId)) continue
|
|
216
|
+
const owners = Array.from(modules).sort((left, right) => left.localeCompare(right)).join(', ')
|
|
217
|
+
console.warn(
|
|
218
|
+
`[generate] ⚠ Injection table in "${owners}" references widget id "${widgetId}", but no scanned ` +
|
|
219
|
+
'widget declares it — that spot will render nothing. Check the id against the widget\'s ' +
|
|
220
|
+
'metadata.id, and that the widget lives in a file the scanner reads ' +
|
|
221
|
+
'(widgets/injection/**/widget.ts|tsx — note that widget.client.tsx alone is not scanned).',
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
186
226
|
export function createInjectionWidgetsExtension(): GeneratorExtension {
|
|
187
227
|
const widgetEntries = new Map<string, InjectionWidgetEntry>()
|
|
188
228
|
const tableEntries: InjectionTableEntry[] = []
|
|
229
|
+
// widgetId -> module ids whose injection table references it. Collected across every module so a
|
|
230
|
+
// table may legitimately reference a widget another module owns.
|
|
231
|
+
const tableWidgetRefs = new Map<string, Set<string>>()
|
|
189
232
|
|
|
190
233
|
return {
|
|
191
234
|
id: 'registry.injection-widgets',
|
|
@@ -217,6 +260,12 @@ export function createInjectionWidgetsExtension(): GeneratorExtension {
|
|
|
217
260
|
moduleWidgetEntries[0].widgetId = tableWidgetIds[0]
|
|
218
261
|
}
|
|
219
262
|
|
|
263
|
+
for (const widgetId of tableWidgetIds) {
|
|
264
|
+
const modules = tableWidgetRefs.get(widgetId) ?? new Set<string>()
|
|
265
|
+
modules.add(ctx.moduleId)
|
|
266
|
+
tableWidgetRefs.set(widgetId, modules)
|
|
267
|
+
}
|
|
268
|
+
|
|
220
269
|
for (const entry of moduleWidgetEntries) {
|
|
221
270
|
const existing = widgetEntries.get(entry.key)
|
|
222
271
|
if (!existing || (existing.source !== 'app' && entry.source === 'app')) {
|
|
@@ -233,6 +282,8 @@ export function createInjectionWidgetsExtension(): GeneratorExtension {
|
|
|
233
282
|
}
|
|
234
283
|
},
|
|
235
284
|
generateOutput() {
|
|
285
|
+
reportUnresolvableWidgetReferences(widgetEntries, tableWidgetRefs)
|
|
286
|
+
|
|
236
287
|
const widgetDecls = Array.from(widgetEntries.entries())
|
|
237
288
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
238
289
|
.map(([, entry]) =>
|