create-mercato-app 0.6.2-develop.3467.1.2a1818709d → 0.6.2
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 +6 -7
- package/template/.env.example +53 -0
- package/template/AGENTS.md +125 -1
- package/template/package.json.template +4 -4
- package/template/scripts/dev-cache-purge.mjs +33 -0
- package/template/scripts/dev.mjs +3 -0
- 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.2
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Create a new Open Mercato application",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "node build.mjs",
|
|
16
|
-
"test": "node --import tsx --test src/**/*.test.ts",
|
|
16
|
+
"test": "node --import tsx --test --test-timeout=120000 src/**/*.test.ts",
|
|
17
17
|
"typecheck": "tsc --noEmit"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
@@ -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"
|
|
@@ -41,6 +41,5 @@
|
|
|
41
41
|
"scaffolding",
|
|
42
42
|
"cli"
|
|
43
43
|
],
|
|
44
|
-
"license": "MIT"
|
|
45
|
-
"stableVersion": "0.6.1"
|
|
44
|
+
"license": "MIT"
|
|
46
45
|
}
|
package/template/.env.example
CHANGED
|
@@ -55,6 +55,13 @@ OM_ENABLE_ENTERPRISE_MODULES_SSO=false
|
|
|
55
55
|
# Requires OM_ENABLE_ENTERPRISE_MODULES=true
|
|
56
56
|
OM_ENABLE_ENTERPRISE_MODULES_SECURITY=false
|
|
57
57
|
|
|
58
|
+
# Enable the S3-compatible storage provider module (default: false).
|
|
59
|
+
# When true, `storage_s3` is added to enabledModules in modules.ts and the
|
|
60
|
+
# Integration Marketplace exposes the "S3 Object Storage" card.
|
|
61
|
+
# The provider can also be preconfigured from env vars — see the
|
|
62
|
+
# "S3 Storage Preconfiguration" block at the bottom of this file.
|
|
63
|
+
OM_ENABLE_STORAGE_S3=false
|
|
64
|
+
|
|
58
65
|
# OM_SECURITY_MFA_SETUP_SECRET=change-me-mfa-setup-secret
|
|
59
66
|
OM_SECURITY_TOTP_ISSUER=Open Mercato
|
|
60
67
|
OM_SECURITY_TOTP_WINDOW=1
|
|
@@ -451,6 +458,52 @@ OCR_MODEL=gpt-4o
|
|
|
451
458
|
# OM_INTEGRATION_AKENEO_CATEGORIES_SETTINGS_JSON=
|
|
452
459
|
# OM_INTEGRATION_AKENEO_ATTRIBUTES_SETTINGS_JSON=
|
|
453
460
|
|
|
461
|
+
# ============================================================================
|
|
462
|
+
# S3 Storage Preconfiguration
|
|
463
|
+
# ============================================================================
|
|
464
|
+
# If these credentials are set, the storage_s3 provider preconfigures itself
|
|
465
|
+
# during tenant setup and can be rerun later (for existing tenants) with:
|
|
466
|
+
# yarn mercato storage_s3 configure-from-env --tenant <tenantId> --org <organizationId>
|
|
467
|
+
#
|
|
468
|
+
# For unattended deploys (Dokploy, Coolify, Kamal, etc.), use --all-tenants
|
|
469
|
+
# as a post-deploy hook to apply the preset to every active organization
|
|
470
|
+
# without tracking UUIDs in your deploy config:
|
|
471
|
+
# yarn mercato storage_s3 configure-from-env --all-tenants
|
|
472
|
+
#
|
|
473
|
+
# The CLI exits non-zero on an incomplete preset so CI/automation can detect
|
|
474
|
+
# misconfiguration. Pass --force (or set OM_INTEGRATION_STORAGE_S3_FORCE_PRECONFIGURE=true)
|
|
475
|
+
# to overwrite credentials that have already been stored for the tenant.
|
|
476
|
+
#
|
|
477
|
+
# The module itself is only loaded when OM_ENABLE_STORAGE_S3=true (see above)
|
|
478
|
+
# — without that flag the preconfiguration block below is ignored.
|
|
479
|
+
#
|
|
480
|
+
# Required for auto-preconfiguration:
|
|
481
|
+
# OM_INTEGRATION_STORAGE_S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
|
|
482
|
+
# OM_INTEGRATION_STORAGE_S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
|
483
|
+
# OM_INTEGRATION_STORAGE_S3_REGION=eu-central-1
|
|
484
|
+
# OM_INTEGRATION_STORAGE_S3_BUCKET=my-mercato-attachments
|
|
485
|
+
#
|
|
486
|
+
# Optional behavior:
|
|
487
|
+
# OM_INTEGRATION_STORAGE_S3_SESSION_TOKEN=
|
|
488
|
+
# OM_INTEGRATION_STORAGE_S3_ENDPOINT=https://fra1.digitaloceanspaces.com
|
|
489
|
+
# OM_INTEGRATION_STORAGE_S3_FORCE_PATH_STYLE=false
|
|
490
|
+
# OM_INTEGRATION_STORAGE_S3_FORCE_PRECONFIGURE=false
|
|
491
|
+
#
|
|
492
|
+
# Notes:
|
|
493
|
+
# - SESSION_TOKEN is only needed for short-lived STS credentials.
|
|
494
|
+
# - Leave ENDPOINT blank for AWS S3. Set it for MinIO, DigitalOcean Spaces,
|
|
495
|
+
# Cloudflare R2, Backblaze B2, etc.
|
|
496
|
+
# - FORCE_PATH_STYLE must be true for MinIO and most self-hosted S3 services.
|
|
497
|
+
# - Leave the block commented out when S3 should not be preconfigured.
|
|
498
|
+
# OM_INTEGRATION_STORAGE_S3_ACCESS_KEY_ID=
|
|
499
|
+
# OM_INTEGRATION_STORAGE_S3_SECRET_ACCESS_KEY=
|
|
500
|
+
# OM_INTEGRATION_STORAGE_S3_REGION=
|
|
501
|
+
# OM_INTEGRATION_STORAGE_S3_BUCKET=
|
|
502
|
+
# OM_INTEGRATION_STORAGE_S3_SESSION_TOKEN=
|
|
503
|
+
# OM_INTEGRATION_STORAGE_S3_ENDPOINT=
|
|
504
|
+
# OM_INTEGRATION_STORAGE_S3_FORCE_PATH_STYLE=false
|
|
505
|
+
# OM_INTEGRATION_STORAGE_S3_FORCE_PRECONFIGURE=false
|
|
506
|
+
|
|
454
507
|
# ============================================================================
|
|
455
508
|
# InboxOps Configuration (email-to-ERP agent)
|
|
456
509
|
# ============================================================================
|
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
|
},
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
// Greenfield must not inherit the prior run's compiler state. Wiping the
|
|
5
|
+
// configured Next.js distDir (`.mercato/next`) plus the legacy `.next` location guarantees Turbopack rebuilds the route table
|
|
6
|
+
// and middleware manifest from scratch on the next launch.
|
|
7
|
+
export const GREENFIELD_PURGE_TARGETS = Object.freeze([
|
|
8
|
+
Object.freeze(['.mercato', 'next']),
|
|
9
|
+
Object.freeze(['.next']),
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
export function purgeAppBuildCaches({
|
|
13
|
+
rootDir = process.cwd(),
|
|
14
|
+
fsImpl = fs,
|
|
15
|
+
logger = console,
|
|
16
|
+
targets = GREENFIELD_PURGE_TARGETS,
|
|
17
|
+
} = {}) {
|
|
18
|
+
const removed = []
|
|
19
|
+
for (const segments of targets) {
|
|
20
|
+
const target = path.join(rootDir, ...segments)
|
|
21
|
+
if (!fsImpl.existsSync(target)) continue
|
|
22
|
+
fsImpl.rmSync(target, { recursive: true, force: true })
|
|
23
|
+
removed.push(segments.join('/'))
|
|
24
|
+
}
|
|
25
|
+
if (removed.length === 0) {
|
|
26
|
+
logger.log('🧹 [dev:greenfield] no stale Next/Turbopack build directories to purge')
|
|
27
|
+
} else {
|
|
28
|
+
for (const relPath of removed) {
|
|
29
|
+
logger.log(`🧹 [dev:greenfield] removed ${relPath}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { removed }
|
|
33
|
+
}
|
package/template/scripts/dev.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
resolveProgressPercent,
|
|
23
23
|
stripAnsi,
|
|
24
24
|
} from './dev-splash-helpers.mjs'
|
|
25
|
+
import { purgeAppBuildCaches } from './dev-cache-purge.mjs'
|
|
25
26
|
import { resolveSpawnCommand } from './dev-spawn-utils.mjs'
|
|
26
27
|
import { createDevSplashCodingFlow } from './dev-splash-coding-flow.mjs'
|
|
27
28
|
import { createDevSplashGitRepoFlow } from './dev-splash-git-repo-flow.mjs'
|
|
@@ -1647,6 +1648,7 @@ async function runClassicStandardDev() {
|
|
|
1647
1648
|
}
|
|
1648
1649
|
|
|
1649
1650
|
async function runGreenfieldDev() {
|
|
1651
|
+
purgeAppBuildCaches()
|
|
1650
1652
|
await runStage('🧱 Greenfield build packages', ['build:packages'], { stageCurrent: 1, stageTotal: 5 })
|
|
1651
1653
|
await runStage('🧬 Greenfield generate artifacts', ['generate'], { stageCurrent: 2, stageTotal: 5 })
|
|
1652
1654
|
await runStage('🧱 Greenfield rebuild packages', ['build:packages'], { stageCurrent: 3, stageTotal: 5 })
|
|
@@ -1657,6 +1659,7 @@ async function runGreenfieldDev() {
|
|
|
1657
1659
|
}
|
|
1658
1660
|
|
|
1659
1661
|
async function runClassicGreenfieldDev() {
|
|
1662
|
+
purgeAppBuildCaches()
|
|
1660
1663
|
await runRawYarnCommand(['build:packages'])
|
|
1661
1664
|
await runRawYarnCommand(['generate'])
|
|
1662
1665
|
await runRawYarnCommand(['build:packages'])
|
|
@@ -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
|
+
]
|