@open-mercato/cli 0.7.0 → 0.7.1-develop.7103.1.41ff100d93
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 +7 -1
- package/dist/agentic/guides/module-facts.json +5509 -599
- package/dist/agentic/guides/module-facts.v2.json +5595 -599
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/di-registrations-rich.md +11 -0
- package/dist/agentic/guides/modules/api_keys/di-service-tokens.md +11 -0
- package/dist/agentic/guides/modules/api_keys/exact-override-targets.md +1 -0
- package/dist/agentic/guides/modules/api_keys/index.md +4 -2
- package/dist/agentic/guides/modules/attachments/di-registrations-rich.md +6 -5
- package/dist/agentic/guides/modules/attachments/di-service-tokens.md +5 -4
- package/dist/agentic/guides/modules/attachments/exact-override-targets.md +6 -5
- package/dist/agentic/guides/modules/attachments/index.md +2 -2
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/di-registrations-rich.md +3 -2
- package/dist/agentic/guides/modules/auth/di-service-tokens.md +3 -2
- package/dist/agentic/guides/modules/auth/exact-override-targets.md +3 -2
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/incoming-installed-contributions.md +3 -0
- package/dist/agentic/guides/modules/catalog/index.md +2 -2
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/acl-features.md +13 -0
- package/dist/agentic/guides/modules/channel_discord/ai-agents.md +11 -0
- package/dist/agentic/guides/modules/channel_discord/backend-pages.md +11 -0
- package/dist/agentic/guides/modules/channel_discord/cli-commands.md +13 -0
- package/dist/agentic/guides/modules/channel_discord/contribution-resolutions.md +14 -0
- package/dist/agentic/guides/modules/channel_discord/di-registrations-rich.md +12 -0
- package/dist/agentic/guides/modules/channel_discord/exact-override-targets.md +26 -0
- package/dist/agentic/guides/modules/channel_discord/index.md +22 -0
- package/dist/agentic/guides/modules/channel_discord/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/channel_discord/setup.md +11 -0
- package/dist/agentic/guides/modules/channel_discord/umes-contributions.md +14 -0
- package/dist/agentic/guides/modules/channel_discord/workers.md +12 -0
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/di-registrations-rich.md +9 -8
- package/dist/agentic/guides/modules/communication_channels/exact-override-targets.md +9 -8
- package/dist/agentic/guides/modules/communication_channels/incoming-installed-contributions.md +2 -0
- package/dist/agentic/guides/modules/communication_channels/index.md +3 -3
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
- package/dist/agentic/guides/modules/customers/active-extension-bindings.md +6 -3
- package/dist/agentic/guides/modules/customers/contribution-resolutions.md +1 -0
- package/dist/agentic/guides/modules/customers/events.md +2 -1
- package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +7 -0
- package/dist/agentic/guides/modules/customers/index.md +4 -4
- package/dist/agentic/guides/modules/customers/umes-contributions.md +1 -0
- package/dist/agentic/guides/modules/customers/umes-hosts.md +1 -0
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/exact-override-targets.md +2 -2
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/workers.md +2 -2
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/di-registrations-rich.md +12 -0
- package/dist/agentic/guides/modules/directory/di-service-tokens.md +12 -0
- package/dist/agentic/guides/modules/directory/exact-override-targets.md +2 -0
- package/dist/agentic/guides/modules/directory/index.md +5 -3
- package/dist/agentic/guides/modules/documents/acl-features.md +17 -0
- package/dist/agentic/guides/modules/documents/backend-pages.md +13 -0
- package/dist/agentic/guides/modules/documents/contribution-resolutions.md +27 -0
- package/dist/agentic/guides/modules/documents/di-registrations-rich.md +21 -0
- package/dist/agentic/guides/modules/documents/domain-commands.md +37 -0
- package/dist/agentic/guides/modules/documents/encryption.md +11 -0
- package/dist/agentic/guides/modules/documents/entities.md +21 -0
- package/dist/agentic/guides/modules/documents/events.md +25 -0
- package/dist/agentic/guides/modules/documents/exact-override-targets.md +39 -0
- package/dist/agentic/guides/modules/documents/index.md +25 -0
- package/dist/agentic/guides/modules/documents/notifications.md +13 -0
- package/dist/agentic/guides/modules/documents/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/documents/setup.md +11 -0
- package/dist/agentic/guides/modules/documents/umes-contributions.md +27 -0
- package/dist/agentic/guides/modules/documents/umes-hosts.md +36 -0
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/exact-override-targets.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/workers.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/active-extension-bindings.md +1 -0
- package/dist/agentic/guides/modules/sales/contribution-resolutions.md +1 -0
- package/dist/agentic/guides/modules/sales/exact-override-targets.md +5 -4
- package/dist/agentic/guides/modules/sales/host-extension-points.md +1 -1
- package/dist/agentic/guides/modules/sales/incoming-installed-contributions.md +2 -0
- package/dist/agentic/guides/modules/sales/index.md +3 -3
- package/dist/agentic/guides/modules/sales/setup.md +1 -1
- package/dist/agentic/guides/modules/sales/umes-contributions.md +1 -0
- package/dist/agentic/guides/modules/sales/umes-diagnostics.md +1 -0
- package/dist/agentic/guides/modules/sales/umes-hosts.md +9 -0
- package/dist/agentic/guides/modules/scheduler/cli-commands.md +1 -0
- package/dist/agentic/guides/modules/scheduler/exact-override-targets.md +3 -1
- package/dist/agentic/guides/modules/scheduler/index.md +2 -2
- package/dist/agentic/guides/modules/scheduler/workers.md +2 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/security/owned-contract-module-metadata.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/index.md +1 -1
- package/dist/agentic/guides/reference-module-facts.json +2 -2
- package/dist/agentic/guides/reference-modules/example/index.md +1 -1
- package/dist/agentic/guides/upstream/manifest.json +1 -1
- package/dist/agentic/shared/AGENTS.md.template +3 -3
- package/dist/agentic/shared/ai/harness/README.md +2 -2
- package/dist/agentic/shared/ai/harness/RELEASE.md +8 -6
- package/dist/agentic/shared/ai/harness/cases.json +83 -0
- package/dist/agentic/shared/ai/harness/cases.schema.json +4 -4
- package/dist/agentic/shared/ai/harness/validators.json +1 -1
- package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +37 -11
- package/dist/lib/generators/ast/index.js +4 -1
- package/dist/lib/generators/ast/index.js.map +2 -2
- package/dist/lib/generators/ast/source-file.js +8 -1
- package/dist/lib/generators/ast/source-file.js.map +2 -2
- package/dist/lib/generators/ast/writers.js +9 -1
- package/dist/lib/generators/ast/writers.js.map +2 -2
- package/dist/lib/generators/module-registry.js +424 -189
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/generators/route-manifest-shards.js +4 -1
- package/dist/lib/generators/route-manifest-shards.js.map +2 -2
- package/dist/lib/spawn.js +3 -2
- package/dist/lib/spawn.js.map +2 -2
- package/dist/lib/testing/integration.js +162 -24
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +14 -0
- package/dist/mercato.js.map +2 -2
- package/package.json +8 -7
- package/src/__tests__/resolve-queue-abandon-hook.test.ts +39 -0
- package/src/lib/__tests__/spawn.test.ts +23 -0
- package/src/lib/generators/__tests__/__snapshots__/bootstrap-registrations.test.ts.snap +14 -0
- package/src/lib/generators/__tests__/__snapshots__/command-loaders.test.ts.snap +23 -0
- package/src/lib/generators/__tests__/ast-writers-escaping.test.ts +64 -0
- package/src/lib/generators/__tests__/bootstrap-registrations.test.ts +176 -0
- package/src/lib/generators/__tests__/command-loaders.test.ts +239 -0
- package/src/lib/generators/__tests__/generated-registry-imports.test.ts +128 -0
- package/src/lib/generators/__tests__/metadata-extraction.test.ts +44 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +32 -8
- package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +4 -1
- package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +1 -1
- package/src/lib/generators/__tests__/module-subset.test.ts +96 -72
- package/src/lib/generators/__tests__/output-snapshots.test.ts +1 -1
- package/src/lib/generators/__tests__/registry-variant-parity.test.ts +258 -0
- package/src/lib/generators/__tests__/route-manifest-shards.test.ts +15 -0
- package/src/lib/generators/__tests__/structural-contracts.test.ts +7 -3
- package/src/lib/generators/ast/index.ts +2 -1
- package/src/lib/generators/ast/source-file.ts +13 -0
- package/src/lib/generators/ast/writers.ts +26 -1
- package/src/lib/generators/module-registry.ts +534 -227
- package/src/lib/generators/route-manifest-shards.ts +8 -1
- package/src/lib/spawn.ts +5 -4
- package/src/lib/testing/__tests__/integration.test.ts +435 -0
- package/src/lib/testing/integration.ts +254 -32
- package/src/mercato.ts +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1-develop.7103.1.41ff100d93",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"@mikro-orm/decorators": "^7.1.8",
|
|
61
61
|
"@mikro-orm/migrations": "^7.1.8",
|
|
62
62
|
"@mikro-orm/postgresql": "^7.1.8",
|
|
63
|
-
"@open-mercato/queue": "0.7.
|
|
64
|
-
"@open-mercato/shared": "0.7.
|
|
65
|
-
"@open-mercato/telemetry": "0.7.
|
|
63
|
+
"@open-mercato/queue": "0.7.1-develop.7103.1.41ff100d93",
|
|
64
|
+
"@open-mercato/shared": "0.7.1-develop.7103.1.41ff100d93",
|
|
65
|
+
"@open-mercato/telemetry": "0.7.1-develop.7103.1.41ff100d93",
|
|
66
66
|
"cross-spawn": "^7.0.6",
|
|
67
67
|
"pg": "8.22.0",
|
|
68
68
|
"semver": "^7.8.5",
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
"typescript-js": "npm:typescript@6.0.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.7.
|
|
76
|
+
"@open-mercato/shared": "0.7.1-develop.7103.1.41ff100d93"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@open-mercato/shared": "0.7.
|
|
79
|
+
"@open-mercato/shared": "0.7.1-develop.7103.1.41ff100d93",
|
|
80
80
|
"@types/jest": "^30.0.0",
|
|
81
81
|
"jest": "^30.4.2",
|
|
82
82
|
"ts-jest": "^29.4.12"
|
|
@@ -88,5 +88,6 @@
|
|
|
88
88
|
"type": "git",
|
|
89
89
|
"url": "https://github.com/open-mercato/open-mercato",
|
|
90
90
|
"directory": "packages/cli"
|
|
91
|
-
}
|
|
91
|
+
},
|
|
92
|
+
"stableVersion": "0.7.0"
|
|
92
93
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { resolveQueueAbandonHook } from '../mercato'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Covers the CLI's per-queue pick, which is the last link between worker metadata and the queue the
|
|
5
|
+
* worker runs on. The warning exists because two handlers on one queue each expecting to report is a
|
|
6
|
+
* wiring mistake, and a silent first-wins would hide it.
|
|
7
|
+
*/
|
|
8
|
+
describe('resolveQueueAbandonHook', () => {
|
|
9
|
+
const hookA = jest.fn(async () => {})
|
|
10
|
+
const hookB = jest.fn(async () => {})
|
|
11
|
+
|
|
12
|
+
it('returns undefined when no worker declares one', () => {
|
|
13
|
+
const warn = jest.fn()
|
|
14
|
+
expect(resolveQueueAbandonHook('q', [{ id: 'a' }, { id: 'b' }], warn)).toBeUndefined()
|
|
15
|
+
expect(warn).not.toHaveBeenCalled()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns the single declared callback without warning', () => {
|
|
19
|
+
const warn = jest.fn()
|
|
20
|
+
expect(resolveQueueAbandonHook('q', [{ id: 'a' }, { id: 'b', onJobAbandoned: hookA }], warn)).toBe(hookA)
|
|
21
|
+
expect(warn).not.toHaveBeenCalled()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('keeps the first and warns, naming the queue and every declaring worker', () => {
|
|
25
|
+
const warn = jest.fn()
|
|
26
|
+
const picked = resolveQueueAbandonHook(
|
|
27
|
+
'data-sync-import',
|
|
28
|
+
[{ id: 'first', onJobAbandoned: hookA }, { id: 'second', onJobAbandoned: hookB }],
|
|
29
|
+
warn,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
expect(picked).toBe(hookA)
|
|
33
|
+
expect(warn).toHaveBeenCalledTimes(1)
|
|
34
|
+
const message = warn.mock.calls[0][0] as string
|
|
35
|
+
expect(message).toContain('data-sync-import')
|
|
36
|
+
expect(message).toContain('first')
|
|
37
|
+
expect(message).toContain('second')
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -39,3 +39,26 @@ describe('resolveSpawnCommand', () => {
|
|
|
39
39
|
)
|
|
40
40
|
})
|
|
41
41
|
})
|
|
42
|
+
|
|
43
|
+
describe('resolveSpawnCommand detached passthrough', () => {
|
|
44
|
+
// The whole process-group teardown in the ephemeral harness rests on this flag reaching `spawn`:
|
|
45
|
+
// without it the app tree stays in the runner's group and the negated-pid kill has nothing of its
|
|
46
|
+
// own to signal.
|
|
47
|
+
it('passes detached through on the POSIX branch', () => {
|
|
48
|
+
const result = resolveSpawnCommand('yarn', ['start'], { platform: 'linux', detached: true })
|
|
49
|
+
|
|
50
|
+
expect(result.spawnOptions).toEqual({ detached: true })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('passes detached through on the Windows cmd branch', () => {
|
|
54
|
+
const result = resolveSpawnCommand('yarn.cmd', ['start'], { platform: 'win32', detached: true })
|
|
55
|
+
|
|
56
|
+
expect(result.spawnOptions).toEqual({ detached: true })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('omits detached entirely when the caller does not ask for it', () => {
|
|
60
|
+
expect(resolveSpawnCommand('yarn', ['start'], { platform: 'linux' }).spawnOptions).toEqual({})
|
|
61
|
+
expect(resolveSpawnCommand('yarn', ['start']).spawnOptions).not.toHaveProperty('detached')
|
|
62
|
+
expect(resolveSpawnCommand('yarn', ['start'], { platform: 'linux', detached: false }).spawnOptions).toEqual({})
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
|
2
|
+
|
|
3
|
+
exports[`renderBootstrapRegistrationsFile pins the emitted text so emitter changes surface as a reviewable diff 1`] = `
|
|
4
|
+
"// AUTO-GENERATED by mercato generate registry
|
|
5
|
+
import { backendRoutes } from "./backend-routes.generated";
|
|
6
|
+
import { frontendRoutes } from "./frontend-routes.generated";
|
|
7
|
+
import { registerBackendRouteManifests, registerFrontendRouteManifests } from "@open-mercato/shared/modules/registry";
|
|
8
|
+
|
|
9
|
+
export function runBootstrapRegistrations(): void {
|
|
10
|
+
registerBackendRouteManifests(backendRoutes)
|
|
11
|
+
registerFrontendRouteManifests(frontendRoutes)
|
|
12
|
+
}
|
|
13
|
+
"
|
|
14
|
+
`;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
|
2
|
+
|
|
3
|
+
exports[`renderCommandLoadersFile pins the emitted text so emitter changes surface as a reviewable diff 1`] = `
|
|
4
|
+
"// AUTO-GENERATED by mercato generate command-loaders
|
|
5
|
+
import type { CommandLoader } from "@open-mercato/shared/lib/commands";
|
|
6
|
+
|
|
7
|
+
export const commandLoaderEntries: CommandLoader[] = [
|
|
8
|
+
{
|
|
9
|
+
moduleId: "catalog",
|
|
10
|
+
id: "catalog.products.create",
|
|
11
|
+
key: "catalog:commands:products",
|
|
12
|
+
load: () => import("@open-mercato/core/modules/catalog/commands/products")
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
moduleId: "catalog",
|
|
16
|
+
key: "catalog:commands:products",
|
|
17
|
+
load: () => import("@open-mercato/core/modules/catalog/commands/products")
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export default commandLoaderEntries;
|
|
22
|
+
"
|
|
23
|
+
`;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `stringLiteral` is the only place a JavaScript string value becomes source text in the
|
|
3
|
+
* AST generator stack. It must escape the characters that are legal inside a JSON string
|
|
4
|
+
* but hazardous inside an emitted source file, matching the escaping the string emitters
|
|
5
|
+
* applied before they were retired — otherwise the migration would have silently dropped
|
|
6
|
+
* that hardening from every generated file.
|
|
7
|
+
*/
|
|
8
|
+
import { escapeGeneratedJsonLiteral, stringLiteral, writeValue } from '../ast/writers'
|
|
9
|
+
import type { CodeBlockWriter } from 'ts-morph'
|
|
10
|
+
|
|
11
|
+
function render(value: unknown): string {
|
|
12
|
+
const chunks: string[] = []
|
|
13
|
+
const writer = { write: (text: string) => { chunks.push(text) } } as unknown as CodeBlockWriter
|
|
14
|
+
writeValue(writer, value as never)
|
|
15
|
+
return chunks.join('')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('stringLiteral', () => {
|
|
19
|
+
it('escapes angle brackets so an emitted value cannot close a surrounding script context', () => {
|
|
20
|
+
expect(stringLiteral('a</script>b')).toBe('"a\\u003c/script\\u003eb"')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('escapes the line and paragraph separators older parsers reject inside a string literal', () => {
|
|
24
|
+
expect(stringLiteral('a
b
c')).toBe('"a\\u2028b\\u2029c"')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('leaves an ordinary string as plain JSON', () => {
|
|
28
|
+
expect(stringLiteral('catalog:commands:products')).toBe('"catalog:commands:products"')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('still escapes what JSON escapes', () => {
|
|
32
|
+
expect(stringLiteral('a"b\\c\nd')).toBe('"a\\"b\\\\c\\nd"')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('denotes the same runtime string after escaping', () => {
|
|
36
|
+
const original = 'a</script>b
c'
|
|
37
|
+
expect(JSON.parse(stringLiteral(original))).toBe(original)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('escapeGeneratedJsonLiteral', () => {
|
|
42
|
+
it('hardens strings nested inside a serialized object', () => {
|
|
43
|
+
const literal = escapeGeneratedJsonLiteral(JSON.stringify({ GET: { title: 'a</script>b' } }))
|
|
44
|
+
expect(literal).toBe('{"GET":{"title":"a\\u003c/script\\u003eb"}}')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('keeps the object literal parseable back to the same value', () => {
|
|
48
|
+
const value = { GET: { title: 'a</script>b
c
d' } }
|
|
49
|
+
expect(JSON.parse(escapeGeneratedJsonLiteral(JSON.stringify(value)))).toEqual(value)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('writeValue', () => {
|
|
54
|
+
it('routes string values through the escaping helper', () => {
|
|
55
|
+
expect(render('a<b>c')).toBe('"a\\u003cb\\u003ec"')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('leaves non-string primitives alone', () => {
|
|
59
|
+
expect(render(42)).toBe('42')
|
|
60
|
+
expect(render(true)).toBe('true')
|
|
61
|
+
expect(render(null)).toBe('null')
|
|
62
|
+
expect(render(undefined)).toBe('undefined')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract tests for `renderBootstrapRegistrationsFile`, the emitter behind
|
|
3
|
+
* `bootstrap-registrations.generated.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The file is always written — even when no generator plugin contributes a bootstrap
|
|
6
|
+
* registration — so `bootstrap.ts` can import it unconditionally. These tests cover
|
|
7
|
+
* that core-only case, one plugin, two plugins whose registration imports overlap
|
|
8
|
+
* (de-duplication), and a malformed import statement, which must fail at generation
|
|
9
|
+
* time with the offending statement named rather than emit a broken file.
|
|
10
|
+
*/
|
|
11
|
+
import ts from 'typescript-js'
|
|
12
|
+
import { renderBootstrapRegistrationsFile } from '../module-registry'
|
|
13
|
+
|
|
14
|
+
const CORE_ENTRY_IMPORTS = [
|
|
15
|
+
'import { backendRoutes } from "./backend-routes.generated"',
|
|
16
|
+
'import { frontendRoutes } from "./frontend-routes.generated"',
|
|
17
|
+
]
|
|
18
|
+
const CORE_REGISTRATION_IMPORTS = [
|
|
19
|
+
"import { registerBackendRouteManifests, registerFrontendRouteManifests } from '@open-mercato/shared/modules/registry'",
|
|
20
|
+
]
|
|
21
|
+
const CORE_CALLS = [
|
|
22
|
+
'registerBackendRouteManifests(backendRoutes)',
|
|
23
|
+
'registerFrontendRouteManifests(frontendRoutes)',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
function parseFile(content: string): ts.SourceFile {
|
|
27
|
+
return ts.createSourceFile('bootstrap-registrations.generated.ts', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function expectNoSyntaxErrors(content: string): void {
|
|
31
|
+
const sourceFile = parseFile(content)
|
|
32
|
+
const diagnostics = (sourceFile as unknown as { parseDiagnostics?: ts.Diagnostic[] }).parseDiagnostics ?? []
|
|
33
|
+
expect(diagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, ' '))).toEqual([])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseImports(content: string): Array<{ moduleSpecifier: string; names: string[] }> {
|
|
37
|
+
return parseFile(content)
|
|
38
|
+
.statements.filter(ts.isImportDeclaration)
|
|
39
|
+
.map((declaration) => {
|
|
40
|
+
const bindings = declaration.importClause?.namedBindings
|
|
41
|
+
return {
|
|
42
|
+
moduleSpecifier: (declaration.moduleSpecifier as ts.StringLiteral).text,
|
|
43
|
+
names: bindings && ts.isNamedImports(bindings) ? bindings.elements.map((element) => element.name.text) : [],
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseRegistrationCalls(content: string): string[] {
|
|
49
|
+
const sourceFile = parseFile(content)
|
|
50
|
+
const declaration = sourceFile.statements.find(
|
|
51
|
+
(statement): statement is ts.FunctionDeclaration =>
|
|
52
|
+
ts.isFunctionDeclaration(statement) && statement.name?.text === 'runBootstrapRegistrations',
|
|
53
|
+
)
|
|
54
|
+
if (!declaration) throw new Error('runBootstrapRegistrations declaration not found')
|
|
55
|
+
return (declaration.body?.statements ?? []).map((statement) => statement.getText(sourceFile).replace(/;$/, ''))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isExportedVoidFunction(content: string): boolean {
|
|
59
|
+
const declaration = parseFile(content).statements.find(
|
|
60
|
+
(statement): statement is ts.FunctionDeclaration =>
|
|
61
|
+
ts.isFunctionDeclaration(statement) && statement.name?.text === 'runBootstrapRegistrations',
|
|
62
|
+
)
|
|
63
|
+
if (!declaration) return false
|
|
64
|
+
const isExported = declaration.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
65
|
+
return isExported && declaration.type?.kind === ts.SyntaxKind.VoidKeyword
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('renderBootstrapRegistrationsFile', () => {
|
|
69
|
+
it('emits the core route registrations when no plugin contributes one', () => {
|
|
70
|
+
const content = renderBootstrapRegistrationsFile({
|
|
71
|
+
entryImports: CORE_ENTRY_IMPORTS,
|
|
72
|
+
registrationImports: CORE_REGISTRATION_IMPORTS,
|
|
73
|
+
calls: CORE_CALLS,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expectNoSyntaxErrors(content)
|
|
77
|
+
expect(content.startsWith('// AUTO-GENERATED by mercato generate registry\n')).toBe(true)
|
|
78
|
+
expect(isExportedVoidFunction(content)).toBe(true)
|
|
79
|
+
expect(parseImports(content)).toEqual([
|
|
80
|
+
{ moduleSpecifier: './backend-routes.generated', names: ['backendRoutes'] },
|
|
81
|
+
{ moduleSpecifier: './frontend-routes.generated', names: ['frontendRoutes'] },
|
|
82
|
+
{
|
|
83
|
+
moduleSpecifier: '@open-mercato/shared/modules/registry',
|
|
84
|
+
names: ['registerBackendRouteManifests', 'registerFrontendRouteManifests'],
|
|
85
|
+
},
|
|
86
|
+
])
|
|
87
|
+
expect(parseRegistrationCalls(content)).toEqual(CORE_CALLS)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('appends a single plugin registration after the core ones', () => {
|
|
91
|
+
const content = renderBootstrapRegistrationsFile({
|
|
92
|
+
entryImports: [
|
|
93
|
+
...CORE_ENTRY_IMPORTS,
|
|
94
|
+
'import { securityMfaProviderEntries } from "./security-mfa-providers.generated"',
|
|
95
|
+
],
|
|
96
|
+
registrationImports: [
|
|
97
|
+
...CORE_REGISTRATION_IMPORTS,
|
|
98
|
+
"import { registerMfaProviders } from '@open-mercato/core/modules/security/mfa'",
|
|
99
|
+
],
|
|
100
|
+
calls: [...CORE_CALLS, 'registerMfaProviders(securityMfaProviderEntries)'],
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expectNoSyntaxErrors(content)
|
|
104
|
+
expect(parseImports(content).map((parsed) => parsed.moduleSpecifier)).toEqual([
|
|
105
|
+
'./backend-routes.generated',
|
|
106
|
+
'./frontend-routes.generated',
|
|
107
|
+
'./security-mfa-providers.generated',
|
|
108
|
+
'@open-mercato/shared/modules/registry',
|
|
109
|
+
'@open-mercato/core/modules/security/mfa',
|
|
110
|
+
])
|
|
111
|
+
expect(parseRegistrationCalls(content)).toEqual([
|
|
112
|
+
...CORE_CALLS,
|
|
113
|
+
'registerMfaProviders(securityMfaProviderEntries)',
|
|
114
|
+
])
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('de-duplicates an identical registration import contributed by two plugins', () => {
|
|
118
|
+
const sharedRegistrationImport = "import { registerEntries } from '@open-mercato/core/modules/shared/registrations'"
|
|
119
|
+
const content = renderBootstrapRegistrationsFile({
|
|
120
|
+
entryImports: [
|
|
121
|
+
...CORE_ENTRY_IMPORTS,
|
|
122
|
+
'import { firstPluginEntries } from "./first-plugin.generated"',
|
|
123
|
+
'import { secondPluginEntries } from "./second-plugin.generated"',
|
|
124
|
+
],
|
|
125
|
+
registrationImports: [...CORE_REGISTRATION_IMPORTS, sharedRegistrationImport, sharedRegistrationImport],
|
|
126
|
+
calls: [...CORE_CALLS, 'registerEntries(firstPluginEntries)', 'registerEntries(secondPluginEntries)'],
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
expectNoSyntaxErrors(content)
|
|
130
|
+
expect(parseImports(content).filter((parsed) => parsed.names.includes('registerEntries'))).toHaveLength(1)
|
|
131
|
+
expect(parseRegistrationCalls(content)).toEqual([
|
|
132
|
+
...CORE_CALLS,
|
|
133
|
+
'registerEntries(firstPluginEntries)',
|
|
134
|
+
'registerEntries(secondPluginEntries)',
|
|
135
|
+
])
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('emits a valid module with an empty body when nothing registers at all', () => {
|
|
139
|
+
const content = renderBootstrapRegistrationsFile({ entryImports: [], registrationImports: [], calls: [] })
|
|
140
|
+
|
|
141
|
+
expectNoSyntaxErrors(content)
|
|
142
|
+
expect(isExportedVoidFunction(content)).toBe(true)
|
|
143
|
+
expect(parseImports(content)).toEqual([])
|
|
144
|
+
expect(parseRegistrationCalls(content)).toEqual([])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('fails at generation time and names the statement when a plugin import is malformed', () => {
|
|
148
|
+
expect(() =>
|
|
149
|
+
renderBootstrapRegistrationsFile({
|
|
150
|
+
entryImports: CORE_ENTRY_IMPORTS,
|
|
151
|
+
registrationImports: [...CORE_REGISTRATION_IMPORTS, 'registerSomething from "./not-an-import"'],
|
|
152
|
+
calls: CORE_CALLS,
|
|
153
|
+
}),
|
|
154
|
+
).toThrow('Expected import declaration, received: registerSomething from "./not-an-import"')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('fails when a plugin import uses a non-literal module specifier', () => {
|
|
158
|
+
expect(() =>
|
|
159
|
+
renderBootstrapRegistrationsFile({
|
|
160
|
+
entryImports: CORE_ENTRY_IMPORTS,
|
|
161
|
+
registrationImports: [...CORE_REGISTRATION_IMPORTS, 'import { registerSomething } from `./templated`'],
|
|
162
|
+
calls: CORE_CALLS,
|
|
163
|
+
}),
|
|
164
|
+
).toThrow('Unsupported import declaration: import { registerSomething } from `./templated`')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('pins the emitted text so emitter changes surface as a reviewable diff', () => {
|
|
168
|
+
expect(
|
|
169
|
+
renderBootstrapRegistrationsFile({
|
|
170
|
+
entryImports: CORE_ENTRY_IMPORTS,
|
|
171
|
+
registrationImports: CORE_REGISTRATION_IMPORTS,
|
|
172
|
+
calls: CORE_CALLS,
|
|
173
|
+
}),
|
|
174
|
+
).toMatchSnapshot()
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract tests for `renderCommandLoadersFile`, the emitter behind
|
|
3
|
+
* `command-loaders.generated.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The emitter is exercised directly rather than through a full generate run so the
|
|
6
|
+
* assertions stay readable and fast. Expectations are structural — the file is parsed
|
|
7
|
+
* and its exported array is inspected — so they survive the formatting differences a
|
|
8
|
+
* ts-morph emitter introduces. One inline snapshot pins the emitted text as well, so a
|
|
9
|
+
* reviewer can see the formatting delta of any future emitter change in the diff.
|
|
10
|
+
*/
|
|
11
|
+
import ts from 'typescript-js'
|
|
12
|
+
import { renderCommandLoadersFile, type CommandLoaderGenerationEntry } from '../module-registry'
|
|
13
|
+
|
|
14
|
+
type ParsedLoaderEntry = {
|
|
15
|
+
moduleId?: string
|
|
16
|
+
id?: string
|
|
17
|
+
key?: string
|
|
18
|
+
loadImportPath?: string
|
|
19
|
+
propertyOrder: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseFile(content: string): ts.SourceFile {
|
|
23
|
+
return ts.createSourceFile('command-loaders.generated.ts', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function expectNoSyntaxErrors(content: string): void {
|
|
27
|
+
const sourceFile = parseFile(content)
|
|
28
|
+
const diagnostics = (sourceFile as unknown as { parseDiagnostics?: ts.Diagnostic[] }).parseDiagnostics ?? []
|
|
29
|
+
expect(diagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, ' '))).toEqual([])
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findExportedArray(sourceFile: ts.SourceFile, exportName: string): ts.ArrayLiteralExpression {
|
|
33
|
+
let found: ts.ArrayLiteralExpression | null = null
|
|
34
|
+
const visit = (node: ts.Node): void => {
|
|
35
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === exportName) {
|
|
36
|
+
if (node.initializer && ts.isArrayLiteralExpression(node.initializer)) {
|
|
37
|
+
found = node.initializer
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
ts.forEachChild(node, visit)
|
|
41
|
+
}
|
|
42
|
+
visit(sourceFile)
|
|
43
|
+
if (!found) throw new Error(`Exported array "${exportName}" not found`)
|
|
44
|
+
return found
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readStringLiteral(node: ts.Expression): string | undefined {
|
|
48
|
+
return ts.isStringLiteralLike(node) ? node.text : undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseTypeOnlyImports(sourceFile: ts.SourceFile): Array<{ moduleSpecifier: string; names: string[] }> {
|
|
52
|
+
return sourceFile.statements
|
|
53
|
+
.filter(ts.isImportDeclaration)
|
|
54
|
+
.filter((declaration) => declaration.importClause?.isTypeOnly === true)
|
|
55
|
+
.map((declaration) => {
|
|
56
|
+
const bindings = declaration.importClause?.namedBindings
|
|
57
|
+
return {
|
|
58
|
+
moduleSpecifier: (declaration.moduleSpecifier as ts.StringLiteral).text,
|
|
59
|
+
names: bindings && ts.isNamedImports(bindings) ? bindings.elements.map((element) => element.name.text) : [],
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readExportedArrayTypeAnnotation(sourceFile: ts.SourceFile, exportName: string): string | undefined {
|
|
65
|
+
let annotation: string | undefined
|
|
66
|
+
const visit = (node: ts.Node): void => {
|
|
67
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === exportName) {
|
|
68
|
+
annotation = node.type?.getText(sourceFile)
|
|
69
|
+
}
|
|
70
|
+
ts.forEachChild(node, visit)
|
|
71
|
+
}
|
|
72
|
+
visit(sourceFile)
|
|
73
|
+
return annotation
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readDefaultExportedIdentifier(sourceFile: ts.SourceFile): string | undefined {
|
|
77
|
+
const assignment = sourceFile.statements.find(ts.isExportAssignment)
|
|
78
|
+
if (!assignment || assignment.isExportEquals) return undefined
|
|
79
|
+
return ts.isIdentifier(assignment.expression) ? assignment.expression.text : undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readLoadImportPath(node: ts.Expression): string | undefined {
|
|
83
|
+
if (!ts.isArrowFunction(node)) return undefined
|
|
84
|
+
const body = node.body
|
|
85
|
+
if (ts.isBlock(body)) return undefined
|
|
86
|
+
if (!ts.isCallExpression(body) || body.expression.kind !== ts.SyntaxKind.ImportKeyword) return undefined
|
|
87
|
+
const [specifier] = body.arguments
|
|
88
|
+
return specifier ? readStringLiteral(specifier) : undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseLoaderEntries(content: string): ParsedLoaderEntry[] {
|
|
92
|
+
const sourceFile = parseFile(content)
|
|
93
|
+
const array = findExportedArray(sourceFile, 'commandLoaderEntries')
|
|
94
|
+
return array.elements.map((element) => {
|
|
95
|
+
if (!ts.isObjectLiteralExpression(element)) throw new Error('Expected an object literal entry')
|
|
96
|
+
const parsed: ParsedLoaderEntry = { propertyOrder: [] }
|
|
97
|
+
for (const property of element.properties) {
|
|
98
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) continue
|
|
99
|
+
const name = property.name.text
|
|
100
|
+
parsed.propertyOrder.push(name)
|
|
101
|
+
if (name === 'moduleId') parsed.moduleId = readStringLiteral(property.initializer)
|
|
102
|
+
if (name === 'id') parsed.id = readStringLiteral(property.initializer)
|
|
103
|
+
if (name === 'key') parsed.key = readStringLiteral(property.initializer)
|
|
104
|
+
if (name === 'load') parsed.loadImportPath = readLoadImportPath(property.initializer)
|
|
105
|
+
}
|
|
106
|
+
return parsed
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function entry(overrides: Partial<CommandLoaderGenerationEntry> = {}): CommandLoaderGenerationEntry {
|
|
111
|
+
return {
|
|
112
|
+
moduleId: 'catalog',
|
|
113
|
+
key: 'catalog:commands:products',
|
|
114
|
+
importPath: '@open-mercato/core/modules/catalog/commands/products',
|
|
115
|
+
ids: ['catalog.products.create'],
|
|
116
|
+
...overrides,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('renderCommandLoadersFile', () => {
|
|
121
|
+
it('emits a parseable module with the documented exports', () => {
|
|
122
|
+
const content = renderCommandLoadersFile([entry()])
|
|
123
|
+
|
|
124
|
+
expectNoSyntaxErrors(content)
|
|
125
|
+
expect(content.startsWith('// AUTO-GENERATED by mercato generate command-loaders\n')).toBe(true)
|
|
126
|
+
expect(content.endsWith('\n')).toBe(true)
|
|
127
|
+
|
|
128
|
+
const sourceFile = parseFile(content)
|
|
129
|
+
expect(parseTypeOnlyImports(sourceFile)).toEqual([
|
|
130
|
+
{ moduleSpecifier: '@open-mercato/shared/lib/commands', names: ['CommandLoader'] },
|
|
131
|
+
])
|
|
132
|
+
expect(readExportedArrayTypeAnnotation(sourceFile, 'commandLoaderEntries')).toBe('CommandLoader[]')
|
|
133
|
+
expect(readDefaultExportedIdentifier(sourceFile)).toBe('commandLoaderEntries')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('emits one id-scoped entry per command id followed by the module-scoped fallback entry', () => {
|
|
137
|
+
const content = renderCommandLoadersFile([
|
|
138
|
+
entry({ ids: ['catalog.products.create', 'catalog.products.delete'] }),
|
|
139
|
+
])
|
|
140
|
+
|
|
141
|
+
expect(parseLoaderEntries(content)).toEqual([
|
|
142
|
+
{
|
|
143
|
+
moduleId: 'catalog',
|
|
144
|
+
id: 'catalog.products.create',
|
|
145
|
+
key: 'catalog:commands:products',
|
|
146
|
+
loadImportPath: '@open-mercato/core/modules/catalog/commands/products',
|
|
147
|
+
propertyOrder: ['moduleId', 'id', 'key', 'load'],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
moduleId: 'catalog',
|
|
151
|
+
id: 'catalog.products.delete',
|
|
152
|
+
key: 'catalog:commands:products',
|
|
153
|
+
loadImportPath: '@open-mercato/core/modules/catalog/commands/products',
|
|
154
|
+
propertyOrder: ['moduleId', 'id', 'key', 'load'],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
moduleId: 'catalog',
|
|
158
|
+
id: undefined,
|
|
159
|
+
key: 'catalog:commands:products',
|
|
160
|
+
loadImportPath: '@open-mercato/core/modules/catalog/commands/products',
|
|
161
|
+
propertyOrder: ['moduleId', 'key', 'load'],
|
|
162
|
+
},
|
|
163
|
+
])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('keeps entries from several modules in input order', () => {
|
|
167
|
+
const content = renderCommandLoadersFile([
|
|
168
|
+
entry(),
|
|
169
|
+
entry({
|
|
170
|
+
moduleId: 'sales',
|
|
171
|
+
key: 'sales:commands:orders',
|
|
172
|
+
importPath: '@open-mercato/core/modules/sales/commands/orders',
|
|
173
|
+
ids: ['sales.orders.create'],
|
|
174
|
+
}),
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
expect(parseLoaderEntries(content).map((parsed) => [parsed.key, parsed.id])).toEqual([
|
|
178
|
+
['catalog:commands:products', 'catalog.products.create'],
|
|
179
|
+
['catalog:commands:products', undefined],
|
|
180
|
+
['sales:commands:orders', 'sales.orders.create'],
|
|
181
|
+
['sales:commands:orders', undefined],
|
|
182
|
+
])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('emits the module-scoped entry only when a module declares no command ids', () => {
|
|
186
|
+
const content = renderCommandLoadersFile([entry({ ids: [] })])
|
|
187
|
+
|
|
188
|
+
expectNoSyntaxErrors(content)
|
|
189
|
+
expect(parseLoaderEntries(content)).toEqual([
|
|
190
|
+
{
|
|
191
|
+
moduleId: 'catalog',
|
|
192
|
+
id: undefined,
|
|
193
|
+
key: 'catalog:commands:products',
|
|
194
|
+
loadImportPath: '@open-mercato/core/modules/catalog/commands/products',
|
|
195
|
+
propertyOrder: ['moduleId', 'key', 'load'],
|
|
196
|
+
},
|
|
197
|
+
])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('emits a valid module with an empty array when no module contributes commands', () => {
|
|
201
|
+
const content = renderCommandLoadersFile([])
|
|
202
|
+
|
|
203
|
+
expectNoSyntaxErrors(content)
|
|
204
|
+
expect(parseLoaderEntries(content)).toEqual([])
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('rejects the same command id contributed by two different modules', () => {
|
|
208
|
+
expect(() =>
|
|
209
|
+
renderCommandLoadersFile([
|
|
210
|
+
entry(),
|
|
211
|
+
entry({
|
|
212
|
+
moduleId: 'sales',
|
|
213
|
+
key: 'sales:commands:products',
|
|
214
|
+
importPath: '@open-mercato/core/modules/sales/commands/products',
|
|
215
|
+
}),
|
|
216
|
+
]),
|
|
217
|
+
).toThrow(
|
|
218
|
+
'[generate] Duplicate command id "catalog.products.create" discovered in "catalog:commands:products" and "sales:commands:products"',
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('tolerates the same command id repeated within one module key', () => {
|
|
223
|
+
const content = renderCommandLoadersFile([
|
|
224
|
+
entry({ ids: ['catalog.products.create', 'catalog.products.create'] }),
|
|
225
|
+
])
|
|
226
|
+
|
|
227
|
+
expect(parseLoaderEntries(content)).toHaveLength(3)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('rejects an unsafe import path instead of emitting it', () => {
|
|
231
|
+
expect(() => renderCommandLoadersFile([entry({ importPath: '' })])).toThrow(
|
|
232
|
+
'Unsafe generated module specifier',
|
|
233
|
+
)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('pins the emitted text so emitter changes surface as a reviewable diff', () => {
|
|
237
|
+
expect(renderCommandLoadersFile([entry()])).toMatchSnapshot()
|
|
238
|
+
})
|
|
239
|
+
})
|