@open-mercato/cli 0.7.0 → 0.7.1-develop.7102.1.b41f7e3e51
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
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-implementation parity between the two module registry variants.
|
|
3
|
+
*
|
|
4
|
+
* `generateModuleRegistry` (modules.generated.ts / modules.runtime.generated.ts) and
|
|
5
|
+
* `generateModuleRegistryApp` (modules.app.generated.ts) describe the same modules from
|
|
6
|
+
* the same discovery input. Nothing asserted that they agree, which is what let the two
|
|
7
|
+
* emitters drift far enough apart to need this migration in the first place.
|
|
8
|
+
*
|
|
9
|
+
* These tests generate every variant from one fixture, parse the emitted module entries,
|
|
10
|
+
* and assert that for each module id the properties both variants are expected to carry
|
|
11
|
+
* are present or absent identically. Properties only one variant is supposed to carry are
|
|
12
|
+
* asserted explicitly as known, intentional divergences, so a future change that erases or
|
|
13
|
+
* widens one of them fails here rather than silently.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
import os from 'node:os'
|
|
18
|
+
import ts from 'typescript-js'
|
|
19
|
+
import type { PackageResolver, ModuleEntry } from '../../resolver'
|
|
20
|
+
import { generateModuleRegistry, generateModuleRegistryApp } from '../module-registry'
|
|
21
|
+
|
|
22
|
+
/** Properties both registry variants must agree on, per module. */
|
|
23
|
+
const SHARED_PROPERTIES = [
|
|
24
|
+
'id',
|
|
25
|
+
'info',
|
|
26
|
+
'frontendRoutes',
|
|
27
|
+
'backendRoutes',
|
|
28
|
+
'translations',
|
|
29
|
+
'subscribers',
|
|
30
|
+
'workers',
|
|
31
|
+
'entityExtensions',
|
|
32
|
+
'customFieldSets',
|
|
33
|
+
'features',
|
|
34
|
+
'customEntities',
|
|
35
|
+
'setup',
|
|
36
|
+
'defaultEncryptionMaps',
|
|
37
|
+
'integrations',
|
|
38
|
+
'bundles',
|
|
39
|
+
] as const
|
|
40
|
+
|
|
41
|
+
let tmpDir: string
|
|
42
|
+
let outputDir: string
|
|
43
|
+
let fileMtimeNonce = 0
|
|
44
|
+
|
|
45
|
+
function touchFile(filePath: string, content = ''): void {
|
|
46
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
47
|
+
fs.writeFileSync(filePath, content)
|
|
48
|
+
const mtime = new Date(Date.now() + fileMtimeNonce)
|
|
49
|
+
fileMtimeNonce += 1
|
|
50
|
+
fs.utimesSync(filePath, mtime, mtime)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pkgModulePath(modId: string, ...segments: string[]): string {
|
|
54
|
+
return path.join(tmpDir, 'packages', 'core', 'src', 'modules', modId, ...segments)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createMockResolver(enabled: ModuleEntry[]): PackageResolver {
|
|
58
|
+
return {
|
|
59
|
+
isMonorepo: () => true,
|
|
60
|
+
getRootDir: () => tmpDir,
|
|
61
|
+
getAppDir: () => path.join(tmpDir, 'app'),
|
|
62
|
+
getOutputDir: () => outputDir,
|
|
63
|
+
getModulesConfigPath: () => path.join(tmpDir, 'app', 'src', 'modules.ts'),
|
|
64
|
+
discoverPackages: () => [],
|
|
65
|
+
loadEnabledModules: () => enabled,
|
|
66
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
67
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
68
|
+
pkgBase: path.join(tmpDir, 'packages', 'core', 'src', 'modules', entry.id),
|
|
69
|
+
}),
|
|
70
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
71
|
+
appBase: `@/modules/${entry.id}`,
|
|
72
|
+
pkgBase: `@open-mercato/core/modules/${entry.id}`,
|
|
73
|
+
}),
|
|
74
|
+
getPackageOutputDir: () => outputDir,
|
|
75
|
+
getPackageRoot: () => path.join(tmpDir, 'packages', 'core'),
|
|
76
|
+
} as unknown as PackageResolver
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A module carrying every optional property, plus a deliberately bare one, so the
|
|
81
|
+
* parity assertions cover both the "present in both" and "absent in both" directions.
|
|
82
|
+
*/
|
|
83
|
+
function scaffoldFixture(): ModuleEntry[] {
|
|
84
|
+
touchFile(pkgModulePath('orders', 'index.ts'), "export const metadata = { id: 'orders', label: 'Orders' }\n")
|
|
85
|
+
touchFile(pkgModulePath('orders', 'frontend', 'page.tsx'), 'export default function OrdersPage() { return null }\n')
|
|
86
|
+
touchFile(pkgModulePath('orders', 'backend', 'page.tsx'), 'export default function OrdersBackendPage() { return null }\n')
|
|
87
|
+
touchFile(
|
|
88
|
+
pkgModulePath('orders', 'api', 'orders', 'route.ts'),
|
|
89
|
+
"export const metadata = { path: '/orders' }\nexport function GET() { return new Response('ok') }\n",
|
|
90
|
+
)
|
|
91
|
+
touchFile(
|
|
92
|
+
pkgModulePath('orders', 'subscribers', 'on-created.ts'),
|
|
93
|
+
"export const metadata = { event: 'orders.order.created', persistent: true }\nexport default async function handler() {}\n",
|
|
94
|
+
)
|
|
95
|
+
touchFile(
|
|
96
|
+
pkgModulePath('orders', 'workers', 'sync-job.ts'),
|
|
97
|
+
"export const metadata = { queue: 'orders.sync', concurrency: 2 }\nexport default async function handler() {}\n",
|
|
98
|
+
)
|
|
99
|
+
touchFile(
|
|
100
|
+
pkgModulePath('orders', 'data', 'extensions.ts'),
|
|
101
|
+
"export const extensions = [{ sourceEntity: 'orders:sales_order', targetEntity: 'customers:person', foreignKey: 'customer_id' }]\n",
|
|
102
|
+
)
|
|
103
|
+
touchFile(pkgModulePath('orders', 'acl.ts'), "export const features = ['orders.view', 'orders.create']\n")
|
|
104
|
+
touchFile(pkgModulePath('orders', 'setup.ts'), "export const setup = { defaultRoleFeatures: ['orders.view'] }\n")
|
|
105
|
+
touchFile(
|
|
106
|
+
pkgModulePath('orders', 'encryption.ts'),
|
|
107
|
+
"export const defaultEncryptionMaps = [{ entityId: 'orders:sales_order', fields: [{ field: 'customer_email' }] }]\nexport default defaultEncryptionMaps\n",
|
|
108
|
+
)
|
|
109
|
+
touchFile(
|
|
110
|
+
pkgModulePath('orders', 'integration.ts'),
|
|
111
|
+
"export const integration = { id: 'orders.shipping', label: 'Shipping' }\nexport const bundle = { id: 'orders.bundle', label: 'Bundle' }\n",
|
|
112
|
+
)
|
|
113
|
+
touchFile(pkgModulePath('orders', 'ce.ts'), "export const entities = [{ id: 'orders:custom', fields: [{ id: 'note', kind: 'text' }] }]\n")
|
|
114
|
+
touchFile(pkgModulePath('orders', 'i18n', 'en.json'), JSON.stringify({ orders: { title: 'Orders' } }))
|
|
115
|
+
touchFile(pkgModulePath('orders', 'cli.ts'), 'export default { command: "orders", run: async () => {} }\n')
|
|
116
|
+
|
|
117
|
+
touchFile(pkgModulePath('bare', 'index.ts'), "export const metadata = { id: 'bare', label: 'Bare' }\n")
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
{ id: 'orders', from: '@open-mercato/core' },
|
|
121
|
+
{ id: 'bare', from: '@open-mercato/core' },
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readGenerated(fileName: string): string {
|
|
126
|
+
const filePath = path.join(outputDir, fileName)
|
|
127
|
+
if (!fs.existsSync(filePath)) throw new Error(`Expected generated file not found: ${fileName}`)
|
|
128
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Maps module id → the set of property names that module's entry declares. */
|
|
132
|
+
function parseModulePropertiesById(content: string, fileName: string): Map<string, Set<string>> {
|
|
133
|
+
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
134
|
+
|
|
135
|
+
let modulesArray: ts.ArrayLiteralExpression | null = null
|
|
136
|
+
const visit = (node: ts.Node): void => {
|
|
137
|
+
if (
|
|
138
|
+
!modulesArray
|
|
139
|
+
&& ts.isVariableDeclaration(node)
|
|
140
|
+
&& ts.isIdentifier(node.name)
|
|
141
|
+
&& node.name.text === 'modules'
|
|
142
|
+
&& node.initializer
|
|
143
|
+
&& ts.isArrayLiteralExpression(node.initializer)
|
|
144
|
+
) {
|
|
145
|
+
modulesArray = node.initializer
|
|
146
|
+
}
|
|
147
|
+
ts.forEachChild(node, visit)
|
|
148
|
+
}
|
|
149
|
+
visit(sourceFile)
|
|
150
|
+
if (!modulesArray) throw new Error(`No exported "modules" array literal found in ${fileName}`)
|
|
151
|
+
|
|
152
|
+
const byId = new Map<string, Set<string>>()
|
|
153
|
+
for (const element of (modulesArray as ts.ArrayLiteralExpression).elements) {
|
|
154
|
+
if (!ts.isObjectLiteralExpression(element)) {
|
|
155
|
+
throw new Error(`Non-object module entry in ${fileName}`)
|
|
156
|
+
}
|
|
157
|
+
const names = new Set<string>()
|
|
158
|
+
let moduleId: string | null = null
|
|
159
|
+
for (const property of element.properties) {
|
|
160
|
+
if (!ts.isPropertyAssignment(property)) continue
|
|
161
|
+
const name = ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)
|
|
162
|
+
? property.name.text
|
|
163
|
+
: null
|
|
164
|
+
if (!name) continue
|
|
165
|
+
names.add(name)
|
|
166
|
+
if (name === 'id' && ts.isStringLiteralLike(property.initializer)) {
|
|
167
|
+
moduleId = property.initializer.text
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!moduleId) throw new Error(`Module entry without a literal id in ${fileName}`)
|
|
171
|
+
byId.set(moduleId, names)
|
|
172
|
+
}
|
|
173
|
+
return byId
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function restrictTo(names: Set<string>, allowed: readonly string[]): string[] {
|
|
177
|
+
return allowed.filter((name) => names.has(name)).sort()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
describe('module registry variant parity', () => {
|
|
181
|
+
let mainVariant: Map<string, Set<string>>
|
|
182
|
+
let runtimeVariant: Map<string, Set<string>>
|
|
183
|
+
let appVariant: Map<string, Set<string>>
|
|
184
|
+
|
|
185
|
+
beforeAll(async () => {
|
|
186
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'registry-parity-'))
|
|
187
|
+
outputDir = path.join(tmpDir, 'app', '.mercato', 'generated')
|
|
188
|
+
fs.mkdirSync(outputDir, { recursive: true })
|
|
189
|
+
|
|
190
|
+
const resolver = createMockResolver(scaffoldFixture())
|
|
191
|
+
const mainResult = await generateModuleRegistry({ resolver, quiet: true })
|
|
192
|
+
const appResult = await generateModuleRegistryApp({ resolver, quiet: true })
|
|
193
|
+
expect(mainResult.errors).toEqual([])
|
|
194
|
+
expect(appResult.errors).toEqual([])
|
|
195
|
+
|
|
196
|
+
mainVariant = parseModulePropertiesById(readGenerated('modules.generated.ts'), 'modules.generated.ts')
|
|
197
|
+
runtimeVariant = parseModulePropertiesById(
|
|
198
|
+
readGenerated('modules.runtime.generated.ts'),
|
|
199
|
+
'modules.runtime.generated.ts',
|
|
200
|
+
)
|
|
201
|
+
appVariant = parseModulePropertiesById(readGenerated('modules.app.generated.ts'), 'modules.app.generated.ts')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
afterAll(() => {
|
|
205
|
+
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('discovers the same module ids in every variant', () => {
|
|
209
|
+
const ids = [...mainVariant.keys()].sort()
|
|
210
|
+
expect(ids).toEqual(['bare', 'orders'])
|
|
211
|
+
expect([...runtimeVariant.keys()].sort()).toEqual(ids)
|
|
212
|
+
expect([...appVariant.keys()].sort()).toEqual(ids)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it.each(['orders', 'bare'])('main and app variants agree on the shared properties of "%s"', (moduleId) => {
|
|
216
|
+
const main = mainVariant.get(moduleId)
|
|
217
|
+
const app = appVariant.get(moduleId)
|
|
218
|
+
expect(main).toBeDefined()
|
|
219
|
+
expect(app).toBeDefined()
|
|
220
|
+
expect(restrictTo(main!, SHARED_PROPERTIES)).toEqual(restrictTo(app!, SHARED_PROPERTIES))
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it.each(['orders', 'bare'])('main and runtime variants agree on the shared properties of "%s"', (moduleId) => {
|
|
224
|
+
const main = mainVariant.get(moduleId)
|
|
225
|
+
const runtime = runtimeVariant.get(moduleId)
|
|
226
|
+
expect(main).toBeDefined()
|
|
227
|
+
expect(runtime).toBeDefined()
|
|
228
|
+
expect(restrictTo(main!, SHARED_PROPERTIES)).toEqual(restrictTo(runtime!, SHARED_PROPERTIES))
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('carries every optional property for the fully-featured module', () => {
|
|
232
|
+
expect(restrictTo(mainVariant.get('orders')!, SHARED_PROPERTIES)).toEqual([...SHARED_PROPERTIES].sort())
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('omits every optional property for the bare module', () => {
|
|
236
|
+
expect(restrictTo(mainVariant.get('bare')!, SHARED_PROPERTIES)).toEqual(['customFieldSets', 'id', 'info'])
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('known, intentional divergences', () => {
|
|
240
|
+
it('only the main variant carries eager api handlers and the cli entry point', () => {
|
|
241
|
+
expect(mainVariant.get('orders')!.has('apis')).toBe(true)
|
|
242
|
+
expect(mainVariant.get('orders')!.has('cli')).toBe(true)
|
|
243
|
+
expect(appVariant.get('orders')!.has('apis')).toBe(false)
|
|
244
|
+
expect(appVariant.get('orders')!.has('cli')).toBe(false)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('the runtime variant carries api handlers but not the cli entry point', () => {
|
|
248
|
+
expect(runtimeVariant.get('orders')!.has('apis')).toBe(true)
|
|
249
|
+
expect(runtimeVariant.get('orders')!.has('cli')).toBe(false)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('only the main and app variants carry dashboardWidgets, and only the app variant emits it unconditionally', () => {
|
|
253
|
+
expect(mainVariant.get('bare')!.has('dashboardWidgets')).toBe(false)
|
|
254
|
+
expect(appVariant.get('bare')!.has('dashboardWidgets')).toBe(true)
|
|
255
|
+
expect(runtimeVariant.get('orders')!.has('dashboardWidgets')).toBe(false)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
})
|
|
@@ -47,6 +47,21 @@ describe('route manifest shards', () => {
|
|
|
47
47
|
expect(loader).toContain(`const ROUTE_SHARD_KEYS = ["customers","__dynamic","customers","__root"]`)
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
+
it('indents every line of a multi-line declaration inside the shard array', () => {
|
|
51
|
+
const declaration = ['{', ' moduleId: "customers",', ' path: "/customers"', '} as any'].join('\n')
|
|
52
|
+
const outputs = renderRouteManifestShardOutputs('api', [{ path: '/customers', declaration }])
|
|
53
|
+
const shard = outputs.find((entry) => entry.fileName.includes('route-shard.'))?.content ?? ''
|
|
54
|
+
|
|
55
|
+
expect(shard).toContain([
|
|
56
|
+
'export const routes: ApiRouteManifestEntry[] = [',
|
|
57
|
+
' {',
|
|
58
|
+
' moduleId: "customers",',
|
|
59
|
+
' path: "/customers"',
|
|
60
|
+
' } as any',
|
|
61
|
+
']',
|
|
62
|
+
].join('\n'))
|
|
63
|
+
})
|
|
64
|
+
|
|
50
65
|
it('renders metadata without load functions while retaining metadata imports', () => {
|
|
51
66
|
const output = renderRouteMetadataOutput('backend', [{
|
|
52
67
|
path: '/backend/settings',
|
|
@@ -186,6 +186,7 @@ function scaffoldFixture(): ModuleEntry[] {
|
|
|
186
186
|
touchFile(pkgModulePath('orders', 'subscribers', 'on-created.ts'), "export const metadata = { event: 'orders.order.created', persistent: true }\nexport default async function handler() {}\n")
|
|
187
187
|
touchFile(pkgModulePath('orders', 'subscribers', 'on-payment.ts'), "export const metadata = { event: 'payments.payment.completed', persistent: true }\nexport default async function handler() {}\n")
|
|
188
188
|
touchFile(pkgModulePath('orders', 'workers', 'sync-job.ts'), "export const metadata = { queue: 'orders.sync', concurrency: 2 }\nexport default async function handler() {}\n")
|
|
189
|
+
touchFile(pkgModulePath('orders', 'workers', 'safe-job.ts'), "export const metadata = { queue: 'orders.safe', schedulerSafe: true }\nexport default async function handler() {}\n")
|
|
189
190
|
touchFile(pkgModulePath('orders', 'widgets', 'dashboard', 'revenue', 'widget.tsx'), 'export default function RevenueWidget() { return null }\n')
|
|
190
191
|
touchFile(pkgModulePath('orders', 'widgets', 'injection', 'sidebar', 'widget.tsx'), 'export default function SidebarWidget() { return null }\n')
|
|
191
192
|
touchFile(pkgModulePath('orders', 'widgets', 'injection-table.ts'), `export const injectionTable = {\n 'crud-form:orders:sales_order:fields': [{ widgetId: 'orders.sidebar', kind: 'section', priority: 50 }],\n}\nexport default injectionTable\n`)
|
|
@@ -507,8 +508,8 @@ describe('modules.generated.ts', () => {
|
|
|
507
508
|
})
|
|
508
509
|
|
|
509
510
|
it('modules include translation locale keys', () => {
|
|
510
|
-
expect(content).
|
|
511
|
-
expect(content).
|
|
511
|
+
expect(content).toMatch(/['"]en['"]:/)
|
|
512
|
+
expect(content).toMatch(/['"]pl['"]:/)
|
|
512
513
|
})
|
|
513
514
|
|
|
514
515
|
it('modules include customFieldSets reference from ce.ts', () => {
|
|
@@ -635,7 +636,7 @@ describe('api-routes.generated.ts', () => {
|
|
|
635
636
|
|
|
636
637
|
it('products API route has all 4 methods', () => {
|
|
637
638
|
expect(content).toContain('path: "/products"')
|
|
638
|
-
expect(content).toMatch(/methods
|
|
639
|
+
expect(content).toMatch(/methods:\s*\[\s*"GET",\s*"POST",\s*"PUT",\s*"DELETE",?\s*\]/)
|
|
639
640
|
})
|
|
640
641
|
|
|
641
642
|
it('keeps API handler imports lazy in route entries', () => {
|
|
@@ -1265,6 +1266,9 @@ describe('modules.bootstrap.generated.ts', () => {
|
|
|
1265
1266
|
expect(content).toContain('orders.order.created')
|
|
1266
1267
|
expect(content).toContain('setup:')
|
|
1267
1268
|
expect(content).toContain('features:')
|
|
1269
|
+
// worker metadata flags must survive into the registry the server registers
|
|
1270
|
+
expect(content).toContain("queue: \"orders.safe\"")
|
|
1271
|
+
expect(content).toContain('schedulerSafe: true')
|
|
1268
1272
|
|
|
1269
1273
|
expect(content).not.toContain('frontendRoutes:')
|
|
1270
1274
|
expect(content).not.toContain('backendRoutes:')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { addImportSpec, addImportSpecs, addImportStatement, addImportStatements, type GeneratedImportSpec } from './imports'
|
|
2
|
-
export { addAutoGeneratedComment, createGeneratedSourceFile, getSourceText } from './source-file'
|
|
2
|
+
export { addAutoGeneratedComment, createGeneratedSourceFile, getSourceText, renderGeneratedValue } from './source-file'
|
|
3
3
|
export {
|
|
4
4
|
arrayLiteral,
|
|
5
5
|
assignmentStatement,
|
|
@@ -11,6 +11,7 @@ export {
|
|
|
11
11
|
callExpression,
|
|
12
12
|
conditionalExpression,
|
|
13
13
|
elementAccess,
|
|
14
|
+
escapeGeneratedJsonLiteral,
|
|
14
15
|
expressionStatement,
|
|
15
16
|
forOfStatement,
|
|
16
17
|
identifier,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IndentationText, Project, type SourceFile } from 'ts-morph'
|
|
2
|
+
import { writeValue, type GeneratedValue } from './writers'
|
|
2
3
|
|
|
3
4
|
let sharedProject: Project | null = null
|
|
4
5
|
|
|
@@ -26,6 +27,18 @@ export function createGeneratedSourceFile(fileName: string): SourceFile {
|
|
|
26
27
|
return project.createSourceFile(fileName, '', { overwrite: true })
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Renders a single generated value to source text, outside of any source file. Generators
|
|
32
|
+
* that hand a pre-rendered expression to a consumer expecting a string (route manifest
|
|
33
|
+
* shards) use this instead of concatenating the expression by hand, so the AST writers stay
|
|
34
|
+
* the one place an expression is constructed.
|
|
35
|
+
*/
|
|
36
|
+
export function renderGeneratedValue(value: GeneratedValue): string {
|
|
37
|
+
const writer = getProject().createWriter()
|
|
38
|
+
writeValue(writer, value)
|
|
39
|
+
return writer.toString()
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
export function addAutoGeneratedComment(sourceFile: SourceFile, generator: string): void {
|
|
30
43
|
sourceFile.insertText(0, `// AUTO-GENERATED by mercato generate ${generator}\n`)
|
|
31
44
|
}
|
|
@@ -22,6 +22,31 @@ export interface SpreadPropertyWriter {
|
|
|
22
22
|
export type GeneratedObjectEntry = ObjectPropertyWriter | SpreadPropertyWriter
|
|
23
23
|
export type StatementWriter = WriterFunction
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Escapes the characters that are legal inside a JSON string but hazardous inside a
|
|
27
|
+
* generated JavaScript source file: `<` and `>` so an emitted value can never close a
|
|
28
|
+
* surrounding script context, and U+2028 / U+2029 which older JavaScript parsers reject
|
|
29
|
+
* inside a string literal. The escape sequences denote the same runtime string, so this
|
|
30
|
+
* only ever changes the emitted text, never the emitted value.
|
|
31
|
+
*/
|
|
32
|
+
export function escapeGeneratedJsonLiteral(json: string): string {
|
|
33
|
+
return json
|
|
34
|
+
.replace(/</g, '\\u003c')
|
|
35
|
+
.replace(/>/g, '\\u003e')
|
|
36
|
+
.replace(/\u2028/g, '\\u2028')
|
|
37
|
+
.replace(/\u2029/g, '\\u2029')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The single point where a string value becomes source text on the AST path. The string
|
|
42
|
+
* paths that are still emitted as templates share the escaping step above rather than
|
|
43
|
+
* this function, because they also serialize objects, whose nested strings need the same
|
|
44
|
+
* hardening.
|
|
45
|
+
*/
|
|
46
|
+
export function stringLiteral(value: string): string {
|
|
47
|
+
return escapeGeneratedJsonLiteral(JSON.stringify(value))
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
export function writeLiteral(writer: CodeBlockWriter, value: Exclude<GeneratedValue, WriterFunction>): void {
|
|
26
51
|
if (value === undefined) {
|
|
27
52
|
writer.write('undefined')
|
|
@@ -34,7 +59,7 @@ export function writeLiteral(writer: CodeBlockWriter, value: Exclude<GeneratedVa
|
|
|
34
59
|
}
|
|
35
60
|
|
|
36
61
|
if (typeof value === 'string') {
|
|
37
|
-
writer.write(
|
|
62
|
+
writer.write(stringLiteral(value))
|
|
38
63
|
return
|
|
39
64
|
}
|
|
40
65
|
|