create-mercato-app 0.6.2-develop.3467.1.2a1818709d → 0.6.3-develop.3484.1.1427e58147
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/package.json +5 -5
- package/template/AGENTS.md +125 -1
- package/template/package.json.template +4 -4
- package/template/src/app/(backend)/backend/[...slug]/page.tsx +7 -3
- package/template/src/app/(backend)/backend/__tests__/backend-require-features.test.tsx +7 -0
- package/template/src/app/(frontend)/[...slug]/page.tsx +7 -5
- package/template/src/app/api/[...slug]/route.ts +6 -10
- package/template/src/bootstrap.ts +1 -1
- package/template/src/modules/example/__integration__/TC-UMES-022-overrides.spec.ts +21 -0
- package/template/src/modules/example/api/override-probe/route.ts +38 -0
- package/template/src/modules.ts +74 -4
- package/template/src/official-modules.generated.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-mercato-app",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3-develop.3484.1.1427e58147",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create a new Open Mercato application",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"tar": "^7.5.15"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@types/node": "^25.
|
|
24
|
+
"@types/node": "^25.9.0",
|
|
25
25
|
"esbuild": "^0.28.0",
|
|
26
|
-
"tsx": "^4.
|
|
27
|
-
"typescript": "^
|
|
26
|
+
"tsx": "^4.22.2",
|
|
27
|
+
"typescript": "^6.0.3"
|
|
28
28
|
},
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"cli"
|
|
43
43
|
],
|
|
44
44
|
"license": "MIT",
|
|
45
|
-
"stableVersion": "0.6.
|
|
45
|
+
"stableVersion": "0.6.2"
|
|
46
46
|
}
|
package/template/AGENTS.md
CHANGED
|
@@ -147,6 +147,17 @@ This is a Next.js 16 application built on the **Open Mercato** modular ERP frame
|
|
|
147
147
|
- `src/bootstrap.ts` - Application initialization (imports generated files, registers i18n)
|
|
148
148
|
- `.mercato/generated/` - Auto-generated files from `yarn generate` (do not edit manually)
|
|
149
149
|
|
|
150
|
+
### Module Overrides
|
|
151
|
+
|
|
152
|
+
Use the unified `entry.overrides` field in `src/modules.ts` when this app needs to replace or disable a contract from a package-backed module without forking it. The template ships a non-applied `moduleOverrideExamples` object covering AI, routes, events, workers, widgets, notifications, interceptors, setup, ACL, DI, and encryption. Copy only the specific domains you need into the target module entry's `overrides` field.
|
|
153
|
+
|
|
154
|
+
Rules:
|
|
155
|
+
|
|
156
|
+
- `null` disables a matching contract; an object/function definition replaces it.
|
|
157
|
+
- API route keys are `'METHOD /api/path'`; page route keys are `'/backend/path'` or `'/frontend/path'`.
|
|
158
|
+
- `setup` overrides apply to the module entry carrying them, not to a separate setup id map.
|
|
159
|
+
- The standard `src/bootstrap.ts` already calls `applyModuleOverridesFromEnabledModules(enabledModules)` before registries load.
|
|
160
|
+
|
|
150
161
|
### Routing Structure
|
|
151
162
|
|
|
152
163
|
- `/backend/*` - Admin panel routes (AppShell with sidebar navigation)
|
|
@@ -248,7 +259,7 @@ export const aiAgentOverrides: AiAgentOverridesMap = {
|
|
|
248
259
|
}
|
|
249
260
|
```
|
|
250
261
|
|
|
251
|
-
Example `modules.ts` inline override (preferred for app-level decisions that
|
|
262
|
+
Example `modules.ts` inline override (preferred for app-level decisions that do not deserve a fake module). All module contract domains live under the same `entry.overrides` umbrella per the [unified spec](https://github.com/open-mercato/open-mercato/blob/main/.ai/specs/2026-05-04-modules-ts-unified-overrides.md):
|
|
252
263
|
|
|
253
264
|
```ts
|
|
254
265
|
// src/modules.ts
|
|
@@ -260,6 +271,17 @@ Example `modules.ts` inline override (preferred for app-level decisions that don
|
|
|
260
271
|
agents: { 'catalog.catalog_assistant': null },
|
|
261
272
|
tools: { 'inbox_ops_accept_action': null },
|
|
262
273
|
},
|
|
274
|
+
routes: {
|
|
275
|
+
api: {
|
|
276
|
+
'GET /api/example/override-probe': {
|
|
277
|
+
handler: async () => Response.json({ ok: true, source: 'override' }),
|
|
278
|
+
metadata: { requireAuth: false },
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
pages: {
|
|
282
|
+
'/backend/example/reports': null,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
263
285
|
},
|
|
264
286
|
},
|
|
265
287
|
```
|
|
@@ -290,6 +312,108 @@ yarn mercato configs cache structural --all-tenants
|
|
|
290
312
|
|
|
291
313
|
Refer to the `create-ai-agent` skill (`.ai/skills/create-ai-agent/SKILL.md`) and the public docs at `framework/ai-assistant/overrides` for the full contract, MUST rules, and the resolution order.
|
|
292
314
|
|
|
315
|
+
### Unified module contract overrides
|
|
316
|
+
|
|
317
|
+
The same `entry.overrides` surface that disables/replaces AI agents also wires routes, subscribers, workers, widgets, notifications, interceptors, enrichers, guards, CLI commands, setup hooks, ACL features, DI bindings, and encryption maps. Use it when you want to replace a contract shipped by an upstream module without forking the source.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
// src/modules.ts — representative examples across override phases
|
|
321
|
+
{
|
|
322
|
+
id: 'example',
|
|
323
|
+
from: '@app',
|
|
324
|
+
overrides: {
|
|
325
|
+
routes: {
|
|
326
|
+
api: {
|
|
327
|
+
// disable
|
|
328
|
+
'DELETE /api/example/items': null,
|
|
329
|
+
// replace
|
|
330
|
+
'POST /api/example/items': {
|
|
331
|
+
handler: async (req) => new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
|
332
|
+
metadata: { requireAuth: true, requireFeatures: ['example.manage'] },
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
pages: {
|
|
336
|
+
'/backend/example/items': null,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
events: {
|
|
340
|
+
subscribers: {
|
|
341
|
+
'example.todo.created.notify': null,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
workers: {
|
|
345
|
+
'example:sync': null,
|
|
346
|
+
},
|
|
347
|
+
widgets: {
|
|
348
|
+
injection: { 'example.toolbar': null },
|
|
349
|
+
dashboard: { 'example.kpi': null },
|
|
350
|
+
components: {
|
|
351
|
+
'page:/backend/example': {
|
|
352
|
+
target: { componentId: 'page:/backend/example' },
|
|
353
|
+
priority: 10,
|
|
354
|
+
propsTransform: (props) => props,
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
notifications: {
|
|
359
|
+
types: { 'example.notice': null },
|
|
360
|
+
handlers: { 'example.notice.toast': null },
|
|
361
|
+
},
|
|
362
|
+
interceptors: { 'example.items.audit': null },
|
|
363
|
+
commandInterceptors: { 'example.command.audit': null },
|
|
364
|
+
enrichers: { 'example.items.enricher': null },
|
|
365
|
+
guards: { 'example.backend.guard': null },
|
|
366
|
+
cli: { 'example seed': null },
|
|
367
|
+
setup: {
|
|
368
|
+
defaultRoleFeatures: { admin: ['example.view'] },
|
|
369
|
+
seedExamples: false,
|
|
370
|
+
},
|
|
371
|
+
acl: {
|
|
372
|
+
features: { 'example.manage': null },
|
|
373
|
+
},
|
|
374
|
+
di: {
|
|
375
|
+
exampleService: {
|
|
376
|
+
register: (container, key) => container.register({ [key]: { mode: 'replacement' } }),
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
encryption: {
|
|
380
|
+
maps: { 'example:item': null },
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Programmatic equivalent (boot-time, env-driven, or test scaffold):
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
// src/bootstrap.ts (extra)
|
|
390
|
+
import {
|
|
391
|
+
applyApiRouteOverrides,
|
|
392
|
+
applyPageRouteOverrides,
|
|
393
|
+
applyWorkerOverrides,
|
|
394
|
+
} from '@open-mercato/shared/modules/overrides'
|
|
395
|
+
|
|
396
|
+
applyApiRouteOverrides({
|
|
397
|
+
'GET /api/example/items': null,
|
|
398
|
+
})
|
|
399
|
+
applyPageRouteOverrides({
|
|
400
|
+
'/backend/example/items': null,
|
|
401
|
+
})
|
|
402
|
+
applyWorkerOverrides({
|
|
403
|
+
'example:sync': null,
|
|
404
|
+
})
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Key rules:
|
|
408
|
+
|
|
409
|
+
- Keys are `'METHOD /api/path'`; method is normalized, path leading slash optional, trailing slashes stripped.
|
|
410
|
+
- Page route keys are `'/backend/path'` or `'/frontend/path'`.
|
|
411
|
+
- `null` disables the matching contract; a definition replaces it.
|
|
412
|
+
- Programmatic > `modules.ts` > file-based where supported > base. The dispatcher MUST run before registries first-load — the template's `bootstrap.ts` already does this.
|
|
413
|
+
- Stale override keys log a warning so operators notice renamed or removed upstream contracts.
|
|
414
|
+
|
|
415
|
+
Full reference: `framework/modules/overrides` and `framework/modules/routes-and-pages`.
|
|
416
|
+
|
|
293
417
|
## Disabling the Dashboards Module: Update /backend
|
|
294
418
|
|
|
295
419
|
The default `/backend` page (`src/app/(backend)/backend/page.tsx`) renders `<DashboardScreen />` from `@open-mercato/ui/backend/dashboard`. That component's data flow depends on the `dashboards` module being enabled — widgets, layouts, and the dashboard API routes all live there.
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"@uiw/react-markdown-preview": "^5.2.0",
|
|
79
79
|
"@uiw/react-md-editor": "^4.1.0",
|
|
80
80
|
"@xyflow/react": "^12.6.0",
|
|
81
|
-
"ai": "^6.0.
|
|
81
|
+
"ai": "^6.0.185",
|
|
82
82
|
"awilix": "^12.0.5",
|
|
83
83
|
"bcryptjs": "^3.0.3",
|
|
84
84
|
"class-variance-authority": "^0.7.1",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"mammoth": "^1.9.0",
|
|
91
91
|
"newrelic": "^13.19.1",
|
|
92
92
|
"next": "16.2.6",
|
|
93
|
-
"pg": "8.
|
|
93
|
+
"pg": "8.21.0",
|
|
94
94
|
"pdfjs-dist": "^5.4.149",
|
|
95
95
|
"react": "19.2.5",
|
|
96
96
|
"react-big-calendar": "^1.19.4",
|
|
@@ -103,8 +103,8 @@
|
|
|
103
103
|
"semver": "^7.7.4",
|
|
104
104
|
"tailwind-merge": "^3.5.0",
|
|
105
105
|
"zod": "4.3.6",
|
|
106
|
-
"@stripe/react-stripe-js": "^6.
|
|
107
|
-
"@stripe/stripe-js": "^9.
|
|
106
|
+
"@stripe/react-stripe-js": "^6.4.0",
|
|
107
|
+
"@stripe/stripe-js": "^9.6.0",
|
|
108
108
|
"@open-mercato/gateway-stripe": "{{PACKAGE_VERSION}}",
|
|
109
109
|
"@open-mercato/sync-akeneo": "{{PACKAGE_VERSION}}"
|
|
110
110
|
},
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { notFound, redirect } from 'next/navigation'
|
|
2
2
|
import { cookies } from 'next/headers'
|
|
3
3
|
import Link from 'next/link'
|
|
4
|
-
import { findRouteManifestMatch } from '@open-mercato/shared/modules/registry'
|
|
4
|
+
import { findRouteManifestMatch, getBackendRouteManifests, registerBackendRouteManifests } from '@open-mercato/shared/modules/registry'
|
|
5
5
|
import { backendRoutes } from '@/.mercato/generated/backend-routes.generated'
|
|
6
|
+
import { bootstrap } from '@/bootstrap'
|
|
6
7
|
import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
|
|
7
8
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
8
9
|
import { ApplyBreadcrumb } from '@open-mercato/ui/backend/AppShell'
|
|
@@ -17,6 +18,9 @@ import { resolveLocalizedTitleMetadata } from '@/lib/metadata'
|
|
|
17
18
|
import { resolvePageMiddlewareRedirect } from '@open-mercato/shared/lib/middleware/page-executor'
|
|
18
19
|
import { backendMiddlewareEntries } from '@/.mercato/generated/backend-middleware.generated'
|
|
19
20
|
|
|
21
|
+
bootstrap()
|
|
22
|
+
registerBackendRouteManifests(backendRoutes)
|
|
23
|
+
|
|
20
24
|
type Awaitable<T> = T | Promise<T>
|
|
21
25
|
|
|
22
26
|
type BackendParams = { params: Awaitable<{ slug?: string[] }> }
|
|
@@ -39,7 +43,7 @@ async function renderAccessDenied() {
|
|
|
39
43
|
export async function generateMetadata(props: BackendParams): Promise<Metadata> {
|
|
40
44
|
const params = await props.params
|
|
41
45
|
const pathname = '/backend/' + (params.slug?.join('/') ?? '')
|
|
42
|
-
const match = findRouteManifestMatch(
|
|
46
|
+
const match = findRouteManifestMatch(getBackendRouteManifests(), pathname)
|
|
43
47
|
if (!match) {
|
|
44
48
|
return {}
|
|
45
49
|
}
|
|
@@ -53,7 +57,7 @@ export async function generateMetadata(props: BackendParams): Promise<Metadata>
|
|
|
53
57
|
export default async function BackendCatchAll(props: BackendParams) {
|
|
54
58
|
const params = await props.params
|
|
55
59
|
const pathname = '/backend/' + (params.slug?.join('/') ?? '')
|
|
56
|
-
const match = findRouteManifestMatch(
|
|
60
|
+
const match = findRouteManifestMatch(getBackendRouteManifests(), pathname)
|
|
57
61
|
if (!match) return notFound()
|
|
58
62
|
let auth: AuthContext = null
|
|
59
63
|
let container: Awaited<ReturnType<typeof createRequestContainer>> | null = null
|
|
@@ -8,6 +8,11 @@ jest.mock('@/.mercato/generated/backend-routes.generated', () => ({
|
|
|
8
8
|
backendRoutes: [],
|
|
9
9
|
}))
|
|
10
10
|
|
|
11
|
+
jest.mock('@/bootstrap', () => ({
|
|
12
|
+
bootstrap: jest.fn(),
|
|
13
|
+
isBootstrapped: jest.fn(() => true),
|
|
14
|
+
}))
|
|
15
|
+
|
|
11
16
|
import BackendCatchAll from '@/app/(backend)/backend/[...slug]/page'
|
|
12
17
|
|
|
13
18
|
// Mock UI breadcrumb component to avoid UI package dependency
|
|
@@ -39,6 +44,8 @@ jest.mock('@open-mercato/shared/modules/registry', () => ({
|
|
|
39
44
|
},
|
|
40
45
|
params: {},
|
|
41
46
|
})),
|
|
47
|
+
getBackendRouteManifests: jest.fn(() => []),
|
|
48
|
+
registerBackendRouteManifests: jest.fn(),
|
|
42
49
|
}))
|
|
43
50
|
|
|
44
51
|
jest.mock('@/.mercato/generated/backend-middleware.generated', () => ({
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { notFound, redirect } from 'next/navigation'
|
|
2
2
|
import Link from 'next/link'
|
|
3
|
-
import { findRouteManifestMatch, registerFrontendRouteManifests } from '@open-mercato/shared/modules/registry'
|
|
3
|
+
import { findRouteManifestMatch, getFrontendRouteManifests, registerFrontendRouteManifests } from '@open-mercato/shared/modules/registry'
|
|
4
|
+
import { bootstrap } from '@/bootstrap'
|
|
4
5
|
import { frontendRoutes } from '@/.mercato/generated/frontend-routes.generated'
|
|
5
|
-
|
|
6
|
-
registerFrontendRouteManifests(frontendRoutes)
|
|
7
6
|
import { getAuthFromCookies } from '@open-mercato/shared/lib/auth/server'
|
|
8
7
|
import { AccessDeniedMessage } from '@open-mercato/ui/backend/detail'
|
|
9
8
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
@@ -16,6 +15,9 @@ import { resolveLocalizedTitleMetadata } from '@/lib/metadata'
|
|
|
16
15
|
import { resolvePageMiddlewareRedirect } from '@open-mercato/shared/lib/middleware/page-executor'
|
|
17
16
|
import { frontendMiddlewareEntries } from '@/.mercato/generated/frontend-middleware.generated'
|
|
18
17
|
|
|
18
|
+
bootstrap()
|
|
19
|
+
registerFrontendRouteManifests(frontendRoutes)
|
|
20
|
+
|
|
19
21
|
type FrontendParams = { params: Promise<{ slug: string[] }> }
|
|
20
22
|
|
|
21
23
|
async function renderAccessDenied() {
|
|
@@ -36,7 +38,7 @@ async function renderAccessDenied() {
|
|
|
36
38
|
export async function generateMetadata({ params }: FrontendParams): Promise<Metadata> {
|
|
37
39
|
const p = await params
|
|
38
40
|
const pathname = '/' + (p.slug?.join('/') ?? '')
|
|
39
|
-
const match = findRouteManifestMatch(
|
|
41
|
+
const match = findRouteManifestMatch(getFrontendRouteManifests(), pathname)
|
|
40
42
|
if (!match) {
|
|
41
43
|
return {}
|
|
42
44
|
}
|
|
@@ -50,7 +52,7 @@ export async function generateMetadata({ params }: FrontendParams): Promise<Meta
|
|
|
50
52
|
export default async function SiteCatchAll({ params }: FrontendParams) {
|
|
51
53
|
const p = await params
|
|
52
54
|
const pathname = '/' + (p.slug?.join('/') ?? '')
|
|
53
|
-
const match = findRouteManifestMatch(
|
|
55
|
+
const match = findRouteManifestMatch(getFrontendRouteManifests(), pathname)
|
|
54
56
|
if (!match) return notFound()
|
|
55
57
|
|
|
56
58
|
// Customer portal auth gate — separate from staff auth
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { NextResponse, type NextRequest } from 'next/server'
|
|
2
|
-
import { findApiRouteManifestMatch,
|
|
2
|
+
import { findApiRouteManifestMatch, getApiRouteManifests, registerApiRouteManifests, type HttpMethod } from '@open-mercato/shared/modules/registry'
|
|
3
3
|
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
4
4
|
import { apiRoutes } from '@/.mercato/generated/api-routes.generated'
|
|
5
|
-
import { backendRoutes } from '@/.mercato/generated/backend-routes.generated'
|
|
6
|
-
import { frontendRoutes } from '@/.mercato/generated/frontend-routes.generated'
|
|
7
5
|
import { resolveAuthFromRequestDetailed } from '@open-mercato/shared/lib/auth/server'
|
|
8
6
|
import { bootstrap } from '@/bootstrap'
|
|
9
|
-
|
|
10
|
-
// Ensure all package registrations are initialized for API routes
|
|
11
|
-
bootstrap()
|
|
12
|
-
registerBackendRouteManifests(backendRoutes)
|
|
13
|
-
registerFrontendRouteManifests(frontendRoutes)
|
|
14
|
-
registerApiRouteManifests(apiRoutes)
|
|
15
7
|
import type { AuthContext } from '@open-mercato/shared/lib/auth/server'
|
|
16
8
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
17
9
|
import { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
@@ -25,6 +17,10 @@ import { checkRateLimit, getClientIp, RATE_LIMIT_ERROR_KEY, RATE_LIMIT_ERROR_FAL
|
|
|
25
17
|
import { getGlobalEventBus } from '@open-mercato/shared/modules/events'
|
|
26
18
|
import { applicationLifecycleEvents, type ApplicationLifecycleEventId } from '@open-mercato/shared/lib/runtime/events'
|
|
27
19
|
|
|
20
|
+
// Ensure all package registrations are initialized for API routes.
|
|
21
|
+
bootstrap()
|
|
22
|
+
registerApiRouteManifests(apiRoutes)
|
|
23
|
+
|
|
28
24
|
type MethodMetadata = {
|
|
29
25
|
requireAuth?: boolean
|
|
30
26
|
/** @deprecated Use `requireFeatures` instead — role names are mutable and can be spoofed */
|
|
@@ -293,7 +289,7 @@ async function handleRequest(
|
|
|
293
289
|
receivedAt: new Date().toISOString(),
|
|
294
290
|
}
|
|
295
291
|
await emitLifecycleEvent(applicationLifecycleEvents.requestReceived, receivedPayload)
|
|
296
|
-
const match = findApiRouteManifestMatch(
|
|
292
|
+
const match = findApiRouteManifestMatch(getApiRouteManifests(), method, pathname)
|
|
297
293
|
if (!match) {
|
|
298
294
|
const response = NextResponse.json({ error: t('api.errors.notFound', 'Not Found') }, { status: 404 })
|
|
299
295
|
await emitLifecycleEvent(applicationLifecycleEvents.requestNotFound, {
|
|
@@ -29,7 +29,7 @@ registerAppDictionaryLoader(async (locale: Locale): Promise<Record<string, unkno
|
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
// modules.ts inline overrides (replace/disable any contract a module
|
|
32
|
-
// presents
|
|
32
|
+
// presents through the unified modules.ts override surface).
|
|
33
33
|
// Importing @open-mercato/ai-assistant here also runs the side-effect
|
|
34
34
|
// that registers the AI domain applier with the umbrella dispatcher.
|
|
35
35
|
import { enabledModules } from '@/modules'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
function resolveUrl(path: string): string {
|
|
4
|
+
const baseUrl = process.env.BASE_URL?.trim()
|
|
5
|
+
return baseUrl ? `${baseUrl}${path}` : path
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
test.describe('TC-UMES-022: modules.ts overrides', () => {
|
|
9
|
+
test('routes.api override replaces the example override probe handler', async ({ request }) => {
|
|
10
|
+
const response = await request.get(resolveUrl('/api/example/override-probe'))
|
|
11
|
+
|
|
12
|
+
expect(response.ok(), `GET /api/example/override-probe returned ${response.status()}`).toBeTruthy()
|
|
13
|
+
const body = await response.json()
|
|
14
|
+
expect(body).toMatchObject({
|
|
15
|
+
ok: true,
|
|
16
|
+
source: 'modules.ts override',
|
|
17
|
+
route: 'example.override-probe',
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
3
|
+
import { exampleTag } from '../openapi'
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
GET: { requireAuth: false },
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
return Response.json({
|
|
11
|
+
ok: true,
|
|
12
|
+
source: 'base',
|
|
13
|
+
route: 'example.override-probe',
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const openApi: OpenApiRouteDoc = {
|
|
18
|
+
tag: exampleTag,
|
|
19
|
+
methods: {
|
|
20
|
+
GET: {
|
|
21
|
+
summary: 'Example override probe',
|
|
22
|
+
description: 'Returns a small payload used by integration tests to verify modules.ts API route overrides.',
|
|
23
|
+
tags: [exampleTag],
|
|
24
|
+
responses: [
|
|
25
|
+
{
|
|
26
|
+
status: 200,
|
|
27
|
+
description: 'Probe payload',
|
|
28
|
+
schema: z.object({
|
|
29
|
+
ok: z.boolean(),
|
|
30
|
+
source: z.string(),
|
|
31
|
+
route: z.string(),
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
|
package/template/src/modules.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// - id: module id (plural snake_case; special cases: 'auth')
|
|
3
3
|
// - from: '@open-mercato/core' | '@app' | custom alias/path in future
|
|
4
4
|
// - overrides: optional unified per-app override surface — replace or
|
|
5
|
-
// disable any contract a module presents
|
|
6
|
-
//
|
|
5
|
+
// disable any contract a module presents: AI, routes, events, workers,
|
|
6
|
+
// widgets, notifications, interceptors, setup, ACL, DI, encryption, etc.
|
|
7
7
|
// See `.ai/specs/2026-05-04-modules-ts-unified-overrides.md` and
|
|
8
|
-
// `apps/docs/docs/framework/
|
|
8
|
+
// `apps/docs/docs/framework/modules/overrides.mdx`.
|
|
9
9
|
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
10
10
|
import type { ModuleOverrides } from '@open-mercato/shared/modules/overrides'
|
|
11
|
+
import { officialModuleEntries } from './official-modules.generated'
|
|
11
12
|
|
|
12
13
|
export type ModuleEntry = {
|
|
13
14
|
id: string
|
|
@@ -15,6 +16,52 @@ export type ModuleEntry = {
|
|
|
15
16
|
overrides?: ModuleOverrides
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Copyable examples for every wired `entry.overrides` domain.
|
|
21
|
+
*
|
|
22
|
+
* This object is intentionally not assigned to any enabled module. Use it as
|
|
23
|
+
* a reference when a downstream app needs to disable or replace contracts
|
|
24
|
+
* from a package-backed module without editing that module's source.
|
|
25
|
+
*/
|
|
26
|
+
export const moduleOverrideExamples: ModuleOverrides = {
|
|
27
|
+
ai: {
|
|
28
|
+
agents: { 'catalog.catalog_assistant': null },
|
|
29
|
+
tools: { inbox_ops_accept_action: null },
|
|
30
|
+
},
|
|
31
|
+
routes: {
|
|
32
|
+
api: { 'DELETE /api/example/items': null },
|
|
33
|
+
pages: { '/backend/example/reports': null },
|
|
34
|
+
},
|
|
35
|
+
events: {
|
|
36
|
+
subscribers: { 'example.todo.audit': null },
|
|
37
|
+
},
|
|
38
|
+
workers: { 'example:sync': null },
|
|
39
|
+
widgets: {
|
|
40
|
+
injection: { 'example.sidebar': null },
|
|
41
|
+
components: { 'page:/backend/example': null },
|
|
42
|
+
dashboard: { 'example.kpi': null },
|
|
43
|
+
},
|
|
44
|
+
notifications: {
|
|
45
|
+
types: { 'example.notice': null },
|
|
46
|
+
handlers: { 'example.notice.toast': null },
|
|
47
|
+
},
|
|
48
|
+
interceptors: { 'example.items.interceptor': null },
|
|
49
|
+
commandInterceptors: { 'example.command.interceptor': null },
|
|
50
|
+
enrichers: { 'example.items.enricher': null },
|
|
51
|
+
guards: { 'example.backend.guard': null },
|
|
52
|
+
cli: { 'example seed': null },
|
|
53
|
+
setup: {
|
|
54
|
+
seedExamples: false,
|
|
55
|
+
},
|
|
56
|
+
acl: {
|
|
57
|
+
features: { 'example.manage': null },
|
|
58
|
+
},
|
|
59
|
+
di: { exampleService: null },
|
|
60
|
+
encryption: {
|
|
61
|
+
maps: { 'example:item': null },
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
18
65
|
export const enabledModules: ModuleEntry[] = [
|
|
19
66
|
{ id: 'dashboards', from: '@open-mercato/core' },
|
|
20
67
|
{ id: 'auth', from: '@open-mercato/core' },
|
|
@@ -60,10 +107,33 @@ export const enabledModules: ModuleEntry[] = [
|
|
|
60
107
|
{ id: 'webhooks', from: '@open-mercato/webhooks' },
|
|
61
108
|
{ id: 'customer_accounts', from: '@open-mercato/core' },
|
|
62
109
|
{ id: 'portal', from: '@open-mercato/core' },
|
|
63
|
-
{
|
|
110
|
+
{
|
|
111
|
+
id: 'example',
|
|
112
|
+
from: '@app',
|
|
113
|
+
overrides: {
|
|
114
|
+
routes: {
|
|
115
|
+
api: {
|
|
116
|
+
'GET /api/example/override-probe': {
|
|
117
|
+
handler: async () => Response.json({
|
|
118
|
+
ok: true,
|
|
119
|
+
source: 'modules.ts override',
|
|
120
|
+
route: 'example.override-probe',
|
|
121
|
+
}),
|
|
122
|
+
metadata: { requireAuth: false },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
64
128
|
{ id: 'ratelimit_probe', from: '@app' },
|
|
65
129
|
]
|
|
66
130
|
|
|
131
|
+
// Official modules activated via official-modules.json / official-modules.local.json
|
|
132
|
+
// (managed by `yarn official-modules`; backed by the external/official-modules submodule).
|
|
133
|
+
for (const entry of officialModuleEntries) {
|
|
134
|
+
if (!enabledModules.some((existing) => existing.id === entry.id)) enabledModules.push(entry)
|
|
135
|
+
}
|
|
136
|
+
|
|
67
137
|
if (enabledModules.some((entry) => entry.id === 'example')) {
|
|
68
138
|
enabledModules.push({ id: 'example_customers_sync', from: '@app' })
|
|
69
139
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// AUTO-GENERATED — do not edit by hand.
|
|
2
|
+
// Source: official-modules.json (+ official-modules.local.json override).
|
|
3
|
+
// Regenerate with: yarn official-modules
|
|
4
|
+
//
|
|
5
|
+
// Why is this file in src/ and not under .mercato/generated/?
|
|
6
|
+
// It is a *versioned* generated registry — the activation set must travel
|
|
7
|
+
// with the repo and survive `yarn clean-generated`. Moving it under any
|
|
8
|
+
// `generated/` folder would gitignore-and-wipe it. See
|
|
9
|
+
// .ai/specs/2026-05-19-official-modules-generated-location-decision.md
|
|
10
|
+
// and the "Generated Files: versioned vs ephemeral" section in AGENTS.md.
|
|
11
|
+
import type { ModuleEntry } from './modules'
|
|
12
|
+
|
|
13
|
+
export const officialModuleEntries: ModuleEntry[] = [
|
|
14
|
+
]
|