@open-mercato/cli 0.10.1-develop.7990.1.82d0a183c5 → 0.10.1-develop.8004.1.a8a79ee43f
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 +1 -1
- package/build.mjs +1 -0
- package/dist/agentic/guides/module-facts.json +162 -162
- package/dist/agentic/guides/module-facts.v2.json +162 -162
- package/dist/agentic/guides/module-tool.md +21 -0
- package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
- 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/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/availability/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/carrier_inpost/index.md +1 -1
- package/dist/agentic/guides/modules/cart/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
- 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/channel_resend/index.md +1 -1
- package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
- package/dist/agentic/guides/modules/channel_smtp/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- 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/customer_groups/index.md +1 -1
- package/dist/agentic/guides/modules/customers/index.md +1 -1
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- 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/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- package/dist/agentic/guides/modules/ecommerce/index.md +1 -1
- 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/forms/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_autopay/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_tpay/index.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/phone_calls/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/promotions/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/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -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/seeds/index.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/telemetry/index.md +1 -1
- package/dist/agentic/guides/modules/tillio/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 +1 -1
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +4 -0
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/agentic/shared/ai/harness/cases.json +23 -9
- package/dist/agentic/shared/ai/harness/source-link-inventory.json +90 -4
- package/dist/i18n/de.json +4 -0
- package/dist/i18n/en.json +4 -0
- package/dist/i18n/es.json +4 -0
- package/dist/i18n/ko.json +4 -0
- package/dist/i18n/pl.json +4 -0
- package/dist/lib/__fixtures__/module-search/built-in.json +235 -0
- package/dist/lib/__fixtures__/module-search/catalog-parity.json +4076 -0
- package/dist/lib/__fixtures__/module-search/modules-index.json +1980 -0
- package/dist/lib/__fixtures__/official-module-package/package.json +33 -0
- package/dist/lib/__integration__/TC-INT-007.spec.js +207 -19
- package/dist/lib/__integration__/TC-INT-007.spec.js.map +2 -2
- package/dist/lib/__integration__/TC-INT-009.spec.js +267 -0
- package/dist/lib/__integration__/TC-INT-009.spec.js.map +7 -0
- package/dist/lib/eject.js +25 -9
- package/dist/lib/eject.js.map +2 -2
- package/dist/lib/generators/entity-ids.js +2 -0
- package/dist/lib/generators/entity-ids.js.map +2 -2
- package/dist/lib/generators/module-di.js +2 -0
- package/dist/lib/generators/module-di.js.map +2 -2
- package/dist/lib/generators/module-entities.js +2 -0
- package/dist/lib/generators/module-entities.js.map +2 -2
- package/dist/lib/generators/module-package-sources.js +6 -4
- package/dist/lib/generators/module-package-sources.js.map +2 -2
- package/dist/lib/generators/module-registry.js +32 -86
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/generators/openapi.js +2 -0
- package/dist/lib/generators/openapi.js.map +2 -2
- package/dist/lib/generators/source-exports.js +73 -0
- package/dist/lib/generators/source-exports.js.map +7 -0
- package/dist/lib/init-secrets.js +33 -1
- package/dist/lib/init-secrets.js.map +2 -2
- package/dist/lib/init-summary.js +44 -0
- package/dist/lib/init-summary.js.map +7 -0
- package/dist/lib/module-github.js +98 -0
- package/dist/lib/module-github.js.map +7 -0
- package/dist/lib/module-identity.js +131 -0
- package/dist/lib/module-identity.js.map +7 -0
- package/dist/lib/module-inspection.js +124 -0
- package/dist/lib/module-inspection.js.map +7 -0
- package/dist/lib/module-install.js +150 -21
- package/dist/lib/module-install.js.map +3 -3
- package/dist/lib/module-package.js +205 -15
- package/dist/lib/module-package.js.map +2 -2
- package/dist/lib/modules-config.js +69 -30
- package/dist/lib/modules-config.js.map +2 -2
- package/dist/lib/resolver.js +33 -34
- package/dist/lib/resolver.js.map +2 -2
- package/dist/mercato.js +17 -37
- package/dist/mercato.js.map +2 -2
- package/package.json +7 -7
- package/src/__tests__/mercato.test.ts +71 -4
- package/src/i18n/de.json +4 -0
- package/src/i18n/en.json +4 -0
- package/src/i18n/es.json +4 -0
- package/src/i18n/ko.json +4 -0
- package/src/i18n/pl.json +4 -0
- package/src/lib/__fixtures__/official-module-package/dist/modules/test_package/widgets/injection/test/widget.js +1 -0
- package/src/lib/__integration__/TC-INT-007.spec.ts +219 -19
- package/src/lib/__integration__/TC-INT-009.spec.ts +279 -0
- package/src/lib/__tests__/eject.test.ts +98 -4
- package/src/lib/__tests__/init-secrets.test.ts +85 -1
- package/src/lib/__tests__/init-summary.test.ts +50 -0
- package/src/lib/__tests__/module-github.test.ts +108 -0
- package/src/lib/__tests__/module-identity.test.ts +144 -0
- package/src/lib/__tests__/module-inspection.test.ts +195 -0
- package/src/lib/__tests__/module-install.test.ts +399 -10
- package/src/lib/__tests__/module-package.test.ts +154 -0
- package/src/lib/__tests__/modules-config.test.ts +55 -0
- package/src/lib/__tests__/resolver.packages.test.ts +29 -0
- package/src/lib/eject.ts +28 -10
- package/src/lib/generators/__tests__/module-id-collisions.test.ts +146 -0
- package/src/lib/generators/__tests__/module-package-sources.test.ts +19 -2
- package/src/lib/generators/__tests__/module-requirements.test.ts +56 -0
- package/src/lib/generators/entity-ids.ts +2 -0
- package/src/lib/generators/module-di.ts +2 -0
- package/src/lib/generators/module-entities.ts +2 -0
- package/src/lib/generators/module-package-sources.ts +7 -5
- package/src/lib/generators/module-registry.ts +33 -115
- package/src/lib/generators/openapi.ts +3 -0
- package/src/lib/generators/source-exports.ts +93 -0
- package/src/lib/init-secrets.ts +37 -0
- package/src/lib/init-summary.ts +54 -0
- package/src/lib/module-github.ts +109 -0
- package/src/lib/module-identity.ts +145 -0
- package/src/lib/module-inspection.ts +133 -0
- package/src/lib/module-install.ts +170 -22
- package/src/lib/module-package.ts +243 -16
- package/src/lib/modules-config.ts +80 -32
- package/src/lib/resolver.ts +35 -39
- package/src/mercato.ts +18 -44
- package/src/types/cross-spawn.d.ts +2 -2
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test'
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..', '..')
|
|
9
|
+
const cliBin = path.join(repoRoot, 'packages', 'cli', 'dist', 'bin.js')
|
|
10
|
+
|
|
11
|
+
function writeFile(filePath: string, content: string): void {
|
|
12
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
13
|
+
fs.writeFileSync(filePath, content)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createPackage(packageRoot: string, packageName: string, moduleIds: string[]): void {
|
|
17
|
+
writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({ name: packageName, version: '0.4.7', type: 'module' }))
|
|
18
|
+
for (const moduleId of moduleIds) {
|
|
19
|
+
writeFile(path.join(packageRoot, 'src', 'modules', moduleId, 'index.ts'), `export const metadata = { id: '${moduleId}', title: '${moduleId}', ejectable: true }\n`)
|
|
20
|
+
writeFile(path.join(packageRoot, 'dist', 'modules', moduleId, 'index.js'), `export const metadata = { id: '${moduleId}', title: '${moduleId}', ejectable: true }\n`)
|
|
21
|
+
writeFile(path.join(packageRoot, 'src', 'modules', moduleId, 'di.ts'), 'export function register() {}\n')
|
|
22
|
+
writeFile(path.join(packageRoot, 'dist', 'modules', moduleId, 'di.js'), 'export function register() {}\n')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createApp(appDir: string, dependencies: Record<string, string> = {}): void {
|
|
27
|
+
writeFile(path.join(appDir, 'package.json'), JSON.stringify({ name: 'module-collision-integration', private: true, dependencies: { '@open-mercato/core': '0.4.7', ...dependencies } }))
|
|
28
|
+
writeFile(path.join(appDir, 'yarn.lock'), '')
|
|
29
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
|
|
30
|
+
createPackage(path.join(appDir, 'node_modules', '@open-mercato', 'core'), '@open-mercato/core', [])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runMercato(args: string[], appDir: string, environment: NodeJS.ProcessEnv = {}): { status: number | null; output: string } {
|
|
34
|
+
const result = spawnSync(process.execPath, [cliBin, ...args], {
|
|
35
|
+
cwd: appDir,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
env: { ...process.env, FORCE_COLOR: '0', NODE_NO_WARNINGS: '1', ...environment },
|
|
38
|
+
})
|
|
39
|
+
if (result.error) throw result.error
|
|
40
|
+
return { status: result.status, output: `${result.stdout}\n${result.stderr}` }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function installCommandStubs(appDir: string, candidateName?: string, candidateModuleId?: string): NodeJS.ProcessEnv {
|
|
44
|
+
const executableDir = path.join(appDir, 'bin')
|
|
45
|
+
const marker = path.join(appDir, 'yarn-invoked')
|
|
46
|
+
const yarnPath = path.join(executableDir, process.platform === 'win32' ? 'yarn.cmd' : 'yarn')
|
|
47
|
+
const yarnScript = `require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'invoked'); process.exit(1)\n`
|
|
48
|
+
writeFile(yarnPath, process.platform === 'win32'
|
|
49
|
+
? `@"${process.execPath}" "%~dp0yarn-stub.cjs" %*\r\n`
|
|
50
|
+
: `#!${process.execPath}\n${yarnScript}`)
|
|
51
|
+
writeFile(path.join(executableDir, 'yarn-stub.cjs'), yarnScript)
|
|
52
|
+
fs.chmodSync(yarnPath, 0o755)
|
|
53
|
+
if (candidateName && candidateModuleId) {
|
|
54
|
+
const manifest = Buffer.from(JSON.stringify({ name: candidateName })).toString('base64')
|
|
55
|
+
const ghScript = `const endpoint = process.argv[3] || ''
|
|
56
|
+
const value = endpoint === 'user' ? { login: 'candidate' }
|
|
57
|
+
: endpoint.includes('/commits/') ? { sha: 'a'.repeat(40) }
|
|
58
|
+
: endpoint.includes('/contents/') ? { encoding: 'base64', content: ${JSON.stringify(manifest)} }
|
|
59
|
+
: endpoint.includes('/git/trees/') ? { tree: [{ path: 'src/modules/${candidateModuleId}/index.ts' }, { path: 'dist/modules/${candidateModuleId}/index.js' }] }
|
|
60
|
+
: { default_branch: 'main' }
|
|
61
|
+
process.stdout.write(JSON.stringify(value))\n`
|
|
62
|
+
const ghPath = path.join(executableDir, process.platform === 'win32' ? 'gh.cmd' : 'gh')
|
|
63
|
+
writeFile(ghPath, process.platform === 'win32'
|
|
64
|
+
? `@"${process.execPath}" "%~dp0gh-stub.cjs" %*\r\n`
|
|
65
|
+
: `#!${process.execPath}\n${ghScript}`)
|
|
66
|
+
writeFile(path.join(executableDir, 'gh-stub.cjs'), ghScript)
|
|
67
|
+
fs.chmodSync(ghPath, 0o755)
|
|
68
|
+
}
|
|
69
|
+
return { PATH: executableDir + path.delimiter + process.env.PATH }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function snapshotDirectory(directory: string): Record<string, string> {
|
|
73
|
+
if (!fs.existsSync(directory)) return {}
|
|
74
|
+
const files: Record<string, string> = {}
|
|
75
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
76
|
+
const fullPath = path.join(directory, entry.name)
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
for (const [relativePath, content] of Object.entries(snapshotDirectory(fullPath))) files[path.join(entry.name, relativePath)] = content
|
|
79
|
+
} else {
|
|
80
|
+
files[entry.name] = fs.readFileSync(fullPath).toString('base64')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return files
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test.describe('TC-INT-009: CLI source collision lifecycle safeguards', () => {
|
|
87
|
+
for (const kind of ['directory', 'archive']) {
|
|
88
|
+
test(`rejects a local ${kind} with unknown provenance before replacing an active npm binding`, () => {
|
|
89
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-file-origin-'))
|
|
90
|
+
const appDir = path.join(root, 'app')
|
|
91
|
+
try {
|
|
92
|
+
const binding = '@publisher/consents'
|
|
93
|
+
createApp(appDir, { [binding]: '0.4.7' })
|
|
94
|
+
createPackage(path.join(appDir, 'node_modules', binding), binding, ['consents'])
|
|
95
|
+
const packageRoot = path.join(root, 'package')
|
|
96
|
+
createPackage(packageRoot, binding, ['consents'])
|
|
97
|
+
if (kind === 'archive') execFileSync('tar', ['-czf', path.join(root, 'candidate.tgz'), '-C', root, 'package'])
|
|
98
|
+
const modulesSource = `export const enabledModules = [{ id: 'consents', from: '${binding}', locator: 'npm:${binding}' }]\n`
|
|
99
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), modulesSource)
|
|
100
|
+
const manifestBefore = fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')
|
|
101
|
+
const installedBefore = snapshotDirectory(path.join(appDir, 'node_modules'))
|
|
102
|
+
const environment = installCommandStubs(appDir)
|
|
103
|
+
const locator = kind === 'archive' ? 'file:../candidate.tgz' : 'file:../package'
|
|
104
|
+
const result = runMercato(['module', 'add', `${binding}@${locator}`, '--eject', '--allow-third-party'], appDir, environment)
|
|
105
|
+
|
|
106
|
+
expect(result.status, result.output).not.toBe(0)
|
|
107
|
+
expect(result.output).toContain('Package binding collision')
|
|
108
|
+
expect(result.output).toContain('unknown origin')
|
|
109
|
+
expect(result.output).toContain('consents')
|
|
110
|
+
expect(fs.existsSync(path.join(appDir, 'yarn-invoked'))).toBe(false)
|
|
111
|
+
expect(fs.readFileSync(path.join(appDir, 'src', 'modules.ts'), 'utf8')).toBe(modulesSource)
|
|
112
|
+
expect(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')).toBe(manifestBefore)
|
|
113
|
+
expect(snapshotDirectory(path.join(appDir, 'node_modules'))).toEqual(installedBefore)
|
|
114
|
+
expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'consents'))).toBe(false)
|
|
115
|
+
expect(fs.existsSync(path.join(appDir, '.mercato', 'generated'))).toBe(false)
|
|
116
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }) }
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test('rejects a GitHub replacement of an active binding even when the selected module ID differs', () => {
|
|
121
|
+
const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-binding-collision-'))
|
|
122
|
+
try {
|
|
123
|
+
const binding = '@publisher/shared-package'
|
|
124
|
+
createApp(appDir, { [binding]: 'github:existing/provider#main' })
|
|
125
|
+
createPackage(path.join(appDir, 'node_modules', binding), binding, ['active_module'])
|
|
126
|
+
const modulesSource = `export const enabledModules = [{ id: 'active_module', from: '${binding}', locator: 'github:existing/provider', packageName: '${binding}' }]\n`
|
|
127
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), modulesSource)
|
|
128
|
+
const manifestBefore = fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')
|
|
129
|
+
const environment = installCommandStubs(appDir, binding, 'candidate_module')
|
|
130
|
+
const result = runMercato(['module', 'add', 'github:candidate/provider', '--allow-third-party'], appDir, environment)
|
|
131
|
+
|
|
132
|
+
expect(result.status, result.output).not.toBe(0)
|
|
133
|
+
expect(result.output).toContain('active_module')
|
|
134
|
+
expect(result.output).toContain(binding)
|
|
135
|
+
expect(result.output).toContain('github:existing/provider')
|
|
136
|
+
expect(result.output).toContain('github:candidate/provider')
|
|
137
|
+
expect(fs.existsSync(path.join(appDir, 'yarn-invoked'))).toBe(false)
|
|
138
|
+
expect(fs.readFileSync(path.join(appDir, 'src', 'modules.ts'), 'utf8')).toBe(modulesSource)
|
|
139
|
+
expect(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')).toBe(manifestBefore)
|
|
140
|
+
expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'candidate_module'))).toBe(false)
|
|
141
|
+
} finally {
|
|
142
|
+
fs.rmSync(appDir, { recursive: true, force: true })
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('rejects an npm file package runtime ID collision before invoking Yarn or copying sources', () => {
|
|
147
|
+
const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-file-collision-'))
|
|
148
|
+
try {
|
|
149
|
+
createApp(appDir)
|
|
150
|
+
const candidateDir = path.join(appDir, 'candidate')
|
|
151
|
+
createPackage(candidateDir, '@publisher/candidate', ['consents'])
|
|
152
|
+
const modulesSource = "export const enabledModules = [{ id: 'consents', from: '@app', locator: 'github:existing/consents' }]\n"
|
|
153
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), modulesSource)
|
|
154
|
+
writeFile(path.join(appDir, 'src', 'modules', 'consents', 'index.ts'), 'export const metadata = { ejectable: true }\n')
|
|
155
|
+
const sourceBefore = snapshotDirectory(path.join(appDir, 'src'))
|
|
156
|
+
const manifestBefore = fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')
|
|
157
|
+
const result = runMercato(['module', 'add', '@publisher/candidate@file:./candidate', '--allow-third-party', '--eject'], appDir, installCommandStubs(appDir))
|
|
158
|
+
|
|
159
|
+
expect(result.status, result.output).not.toBe(0)
|
|
160
|
+
expect(result.output).toContain('consents')
|
|
161
|
+
expect(result.output).toMatch(/collision|already registered|conflict/i)
|
|
162
|
+
expect(fs.existsSync(path.join(appDir, 'yarn-invoked'))).toBe(false)
|
|
163
|
+
expect(snapshotDirectory(path.join(appDir, 'src'))).toEqual(sourceBefore)
|
|
164
|
+
expect(fs.readFileSync(path.join(appDir, 'package.json'), 'utf8')).toBe(manifestBefore)
|
|
165
|
+
} finally {
|
|
166
|
+
fs.rmSync(appDir, { recursive: true, force: true })
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('every generate command rejects duplicate effective registrations before changing existing output', () => {
|
|
171
|
+
const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-generate-duplicates-'))
|
|
172
|
+
try {
|
|
173
|
+
createApp(appDir)
|
|
174
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), "export const enabledModules = [{ id: 'consents', from: '@app' }]\nif (process.env.COLLISION_DUPLICATE_ENABLED === 'true') enabledModules.push({ id: 'consents', from: '@app' })\n")
|
|
175
|
+
writeFile(path.join(appDir, 'src', 'modules', 'consents', 'index.ts'), 'export const metadata = {}\n')
|
|
176
|
+
const outputDir = path.join(appDir, '.mercato', 'generated')
|
|
177
|
+
for (const file of ['entities.ids.generated.ts', 'modules.generated.ts', 'entities.generated.ts', 'di.generated.ts', 'module-package-sources.css']) writeFile(path.join(outputDir, file), 'preserved output\n')
|
|
178
|
+
const before = snapshotDirectory(outputDir)
|
|
179
|
+
|
|
180
|
+
for (const command of ['all', 'entity-ids', 'registry', 'entities', 'di']) {
|
|
181
|
+
const result = runMercato(['generate', command, '--quiet'], appDir, { COLLISION_DUPLICATE_ENABLED: 'true' })
|
|
182
|
+
expect(result.status, `${command}: ${result.output}`).not.toBe(0)
|
|
183
|
+
expect(result.output).toContain("Duplicate enabled module ID 'consents'")
|
|
184
|
+
expect(snapshotDirectory(outputDir), command).toEqual(before)
|
|
185
|
+
}
|
|
186
|
+
const singleProvider = runMercato(['generate', 'all', '--quiet'], appDir, { COLLISION_DUPLICATE_ENABLED: 'false' })
|
|
187
|
+
expect(singleProvider.status, singleProvider.output).toBe(0)
|
|
188
|
+
} finally {
|
|
189
|
+
fs.rmSync(appDir, { recursive: true, force: true })
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('an installed alias generates binding imports, ejects with canonical provenance, and disables cleanly', () => {
|
|
194
|
+
const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-alias-lifecycle-'))
|
|
195
|
+
try {
|
|
196
|
+
const binding = 'consents_candidate'
|
|
197
|
+
const canonicalName = '@publisher/consents'
|
|
198
|
+
createApp(appDir, { [binding]: `npm:${canonicalName}@0.4.7` })
|
|
199
|
+
const packageRoot = path.join(appDir, 'node_modules', binding)
|
|
200
|
+
createPackage(packageRoot, canonicalName, ['consents', 'sibling'])
|
|
201
|
+
writeFile(path.join(packageRoot, 'src', 'modules', 'consents', 'sibling.ts'), "export { metadata } from '../sibling'\n")
|
|
202
|
+
const enabled = runMercato(['module', 'enable', binding, '--module', 'consents', '--allow-third-party'], appDir)
|
|
203
|
+
expect(enabled.status, enabled.output).toBe(0)
|
|
204
|
+
const outputDir = path.join(appDir, '.mercato', 'generated')
|
|
205
|
+
const modulesSource = fs.readFileSync(path.join(appDir, 'src', 'modules.ts'), 'utf8')
|
|
206
|
+
expect(modulesSource).toContain(`from: '${binding}'`)
|
|
207
|
+
expect(modulesSource).toContain(`packageName: '${canonicalName}'`)
|
|
208
|
+
expect(fs.readFileSync(path.join(outputDir, 'di.generated.ts'), 'utf8')).toContain(`${binding}/modules/consents/di`)
|
|
209
|
+
expect(fs.readFileSync(path.join(outputDir, 'module-package-sources.css'), 'utf8')).toContain(`node_modules/${binding}/src/`)
|
|
210
|
+
|
|
211
|
+
const ejected = runMercato(['module', 'eject', 'consents'], appDir)
|
|
212
|
+
expect(ejected.status, ejected.output).toBe(0)
|
|
213
|
+
const ejectedSource = fs.readFileSync(path.join(appDir, 'src', 'modules.ts'), 'utf8')
|
|
214
|
+
expect(ejectedSource).toContain("from: '@app'")
|
|
215
|
+
expect(ejectedSource).toContain(`packageName: '${canonicalName}'`)
|
|
216
|
+
expect(ejectedSource).toContain(`locator: 'npm:${canonicalName}'`)
|
|
217
|
+
expect(fs.readFileSync(path.join(appDir, 'src', 'modules', 'consents', 'sibling.ts'), 'utf8')).toContain(`${binding}/modules/sibling`)
|
|
218
|
+
|
|
219
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
|
|
220
|
+
const disabled = runMercato(['generate', 'all', '--quiet'], appDir)
|
|
221
|
+
expect(disabled.status, disabled.output).toBe(0)
|
|
222
|
+
expect(fs.readFileSync(path.join(outputDir, 'di.generated.ts'), 'utf8')).not.toContain('consents')
|
|
223
|
+
expect(fs.readFileSync(path.join(outputDir, 'module-package-sources.css'), 'utf8')).toBe('')
|
|
224
|
+
expect(fs.existsSync(path.join(packageRoot, 'package.json'))).toBe(true)
|
|
225
|
+
expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'consents', 'index.ts'))).toBe(true)
|
|
226
|
+
} finally {
|
|
227
|
+
fs.rmSync(appDir, { recursive: true, force: true })
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('missing alias exports fail enable and direct eject before source or config changes', () => {
|
|
232
|
+
const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-alias-exports-'))
|
|
233
|
+
try {
|
|
234
|
+
const binding = 'consents_candidate'
|
|
235
|
+
createApp(appDir, { [binding]: 'npm:@publisher/consents@0.4.7' })
|
|
236
|
+
const packageRoot = path.join(appDir, 'node_modules', binding)
|
|
237
|
+
createPackage(packageRoot, '@publisher/consents', ['consents'])
|
|
238
|
+
writeFile(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@publisher/consents', exports: { '.': './dist/modules/consents/index.js' } }))
|
|
239
|
+
const configPath = path.join(appDir, 'src', 'modules.ts')
|
|
240
|
+
const emptyConfig = fs.readFileSync(configPath, 'utf8')
|
|
241
|
+
|
|
242
|
+
const enabled = runMercato(['module', 'enable', binding, '--module', 'consents', '--eject', '--allow-third-party'], appDir)
|
|
243
|
+
expect(enabled.status, enabled.output).not.toBe(0)
|
|
244
|
+
expect(enabled.output).toContain(`${binding}/modules/consents/`)
|
|
245
|
+
expect(fs.readFileSync(configPath, 'utf8')).toBe(emptyConfig)
|
|
246
|
+
expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'consents'))).toBe(false)
|
|
247
|
+
|
|
248
|
+
const registeredConfig = `export const enabledModules = [{ id: 'consents', from: '${binding}' }]\n`
|
|
249
|
+
writeFile(configPath, registeredConfig)
|
|
250
|
+
const ejected = runMercato(['module', 'eject', 'consents'], appDir)
|
|
251
|
+
expect(ejected.status, ejected.output).not.toBe(0)
|
|
252
|
+
expect(ejected.output).toContain(`${binding}/modules/consents/`)
|
|
253
|
+
expect(fs.readFileSync(configPath, 'utf8')).toBe(registeredConfig)
|
|
254
|
+
expect(fs.existsSync(path.join(appDir, 'src', 'modules', 'consents'))).toBe(false)
|
|
255
|
+
} finally {
|
|
256
|
+
fs.rmSync(appDir, { recursive: true, force: true })
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('a single package registration with a local overlay remains valid', () => {
|
|
261
|
+
const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-single-overlay-'))
|
|
262
|
+
try {
|
|
263
|
+
const binding = '@publisher/consents'
|
|
264
|
+
createApp(appDir, { [binding]: '0.4.7' })
|
|
265
|
+
createPackage(path.join(appDir, 'node_modules', binding), binding, ['consents'])
|
|
266
|
+
writeFile(path.join(appDir, 'src', 'modules.ts'), `export const enabledModules = [{ id: 'consents', from: '${binding}' }]\n`)
|
|
267
|
+
writeFile(path.join(appDir, 'src', 'modules', 'consents', 'index.ts'), "export const metadata = { title: 'Local overlay' }\n")
|
|
268
|
+
const before = snapshotDirectory(path.join(appDir, 'src'))
|
|
269
|
+
|
|
270
|
+
const result = runMercato(['generate', 'all', '--quiet'], appDir)
|
|
271
|
+
|
|
272
|
+
expect(result.status, result.output).toBe(0)
|
|
273
|
+
expect(snapshotDirectory(path.join(appDir, 'src'))).toEqual(before)
|
|
274
|
+
expect(fs.readFileSync(path.join(appDir, '.mercato', 'generated', 'di.generated.ts'), 'utf8')).toContain(`${binding}/modules/consents/di`)
|
|
275
|
+
} finally {
|
|
276
|
+
fs.rmSync(appDir, { recursive: true, force: true })
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
})
|
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
listEjectableModules,
|
|
10
10
|
ejectModule,
|
|
11
11
|
} from '../eject'
|
|
12
|
-
import type
|
|
12
|
+
import { readEnabledModulesFromConfig, type PackageResolver } from '../resolver'
|
|
13
|
+
import { getModuleLocators } from '../module-identity'
|
|
14
|
+
import { resolveModuleRequirement } from '@open-mercato/shared/modules/module-locators'
|
|
13
15
|
|
|
14
16
|
describe('eject', () => {
|
|
15
17
|
let tmpDir: string
|
|
@@ -292,6 +294,7 @@ describe('eject', () => {
|
|
|
292
294
|
fs.mkdirSync(path.join(installedPackageRoot, 'src', 'modules', 'test_package'), { recursive: true })
|
|
293
295
|
fs.mkdirSync(path.join(installedPackageRoot, 'dist', 'modules', 'test_package'), { recursive: true })
|
|
294
296
|
|
|
297
|
+
fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ dependencies: { '@open-mercato/test-package': '^0.1.0' } }))
|
|
295
298
|
fs.writeFileSync(
|
|
296
299
|
path.join(appDir, 'src', 'modules.ts'),
|
|
297
300
|
"export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package' }]\n",
|
|
@@ -331,7 +334,7 @@ describe('eject', () => {
|
|
|
331
334
|
appBase: path.join(appDir, 'src', 'modules', 'test_package'),
|
|
332
335
|
pkgBase: path.join(baseDir, 'packages', 'test-package', 'src', 'modules', 'test_package'),
|
|
333
336
|
}),
|
|
334
|
-
getPackageRoot: () =>
|
|
337
|
+
getPackageRoot: () => installedPackageRoot,
|
|
335
338
|
} as unknown as PackageResolver
|
|
336
339
|
}
|
|
337
340
|
|
|
@@ -348,6 +351,95 @@ describe('eject', () => {
|
|
|
348
351
|
])
|
|
349
352
|
})
|
|
350
353
|
|
|
354
|
+
it('rejects duplicate module registrations before copying source', () => {
|
|
355
|
+
const resolver = createOfficialModuleResolver(tmpDir)
|
|
356
|
+
const duplicates = [
|
|
357
|
+
{ id: 'test_package', from: '@open-mercato/test-package' },
|
|
358
|
+
{ id: 'test_package', from: '@other/test-package' },
|
|
359
|
+
]
|
|
360
|
+
fs.writeFileSync(resolver.getModulesConfigPath(), `export const enabledModules = ${JSON.stringify(duplicates)}`)
|
|
361
|
+
const sourceBefore = fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')
|
|
362
|
+
expect(() => ejectModule({ ...resolver, loadEnabledModules: () => duplicates }, 'test_package')).toThrow("Duplicate enabled module ID 'test_package'")
|
|
363
|
+
expect(fs.existsSync(resolver.getModulePaths(duplicates[0]).appBase)).toBe(false)
|
|
364
|
+
expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(sourceBefore)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('rejects duplicate registrations elsewhere in the effective set before copying', () => {
|
|
368
|
+
const resolver = createOfficialModuleResolver(tmpDir)
|
|
369
|
+
const entries = [...resolver.loadEnabledModules(), { id: 'customers' }, { id: 'customers', from: '@other/customers' }]
|
|
370
|
+
const before = fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')
|
|
371
|
+
|
|
372
|
+
expect(() => ejectModule({ ...resolver, loadEnabledModules: () => entries }, 'test_package')).toThrow("Duplicate enabled module ID 'customers'")
|
|
373
|
+
expect(fs.existsSync(resolver.getModulePaths(entries[0]).appBase)).toBe(false)
|
|
374
|
+
expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(before)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('checks the config mutation before copying when effective entries cannot be edited', () => {
|
|
378
|
+
const resolver = createOfficialModuleResolver(tmpDir)
|
|
379
|
+
const before = "const entries = [{ id: 'test_package', from: '@open-mercato/test-package' }]; export const enabledModules = entries;\n"
|
|
380
|
+
fs.writeFileSync(resolver.getModulesConfigPath(), before)
|
|
381
|
+
|
|
382
|
+
expect(() => ejectModule(resolver, 'test_package')).toThrow('Could not find enabledModules array')
|
|
383
|
+
expect(fs.existsSync(resolver.getModulePaths(resolver.loadEnabledModules()[0]).appBase)).toBe(false)
|
|
384
|
+
expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(before)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('rejects contradictory observed provenance before copying source', () => {
|
|
388
|
+
const resolver = createOfficialModuleResolver(tmpDir)
|
|
389
|
+
const entry = { id: 'test_package', from: '@open-mercato/test-package', locator: 'github:unexpected/provider' }
|
|
390
|
+
const before = "export const enabledModules = [{ id: 'test_package', from: '@open-mercato/test-package', locator: 'github:unexpected/provider' }]\n"
|
|
391
|
+
fs.writeFileSync(resolver.getModulesConfigPath(), before)
|
|
392
|
+
|
|
393
|
+
expect(() => ejectModule({ ...resolver, loadEnabledModules: () => [entry] }, 'test_package')).toThrow('disagrees with observed origin')
|
|
394
|
+
expect(fs.existsSync(resolver.getModulePaths(entry).appBase)).toBe(false)
|
|
395
|
+
expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(before)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('preserves an existing local overlay and gives a recovery without deleting source', () => {
|
|
399
|
+
const resolver = createOfficialModuleResolver(tmpDir)
|
|
400
|
+
const appBase = resolver.getModulePaths(resolver.loadEnabledModules()[0]).appBase
|
|
401
|
+
fs.mkdirSync(appBase, { recursive: true })
|
|
402
|
+
fs.writeFileSync(path.join(appBase, 'local.ts'), 'export const local = true\n')
|
|
403
|
+
const before = fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')
|
|
404
|
+
|
|
405
|
+
expect(() => ejectModule(resolver, 'test_package')).toThrow('Keep the existing local source')
|
|
406
|
+
expect(fs.readFileSync(path.join(appBase, 'local.ts'), 'utf8')).toBe('export const local = true\n')
|
|
407
|
+
expect(fs.existsSync(path.join(appBase, 'index.ts'))).toBe(false)
|
|
408
|
+
expect(fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')).toBe(before)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('keeps canonical provenance and rewrites sibling imports using the installed alias', () => {
|
|
412
|
+
const resolver = createOfficialModuleResolver(tmpDir)
|
|
413
|
+
const packageRoot = resolver.getPackageRoot('@open-mercato/test-package')
|
|
414
|
+
const aliasRoot = path.join(resolver.getAppDir(), 'node_modules', 'candidate_consents')
|
|
415
|
+
fs.mkdirSync(path.dirname(aliasRoot), { recursive: true })
|
|
416
|
+
fs.symlinkSync(packageRoot, aliasRoot, 'dir')
|
|
417
|
+
fs.writeFileSync(path.join(resolver.getAppDir(), 'package.json'), JSON.stringify({ dependencies: { candidate_consents: 'npm:@open-mercato/test-package@1.0.0' } }))
|
|
418
|
+
const entry = { id: 'test_package', from: 'candidate_consents' }
|
|
419
|
+
fs.writeFileSync(resolver.getModulesConfigPath(), `export const enabledModules = [${JSON.stringify(entry)}]\n`)
|
|
420
|
+
fs.mkdirSync(path.join(packageRoot, 'src', 'modules', 'sibling'), { recursive: true })
|
|
421
|
+
fs.writeFileSync(path.join(packageRoot, 'src', 'modules', 'sibling', 'index.ts'), 'export const sibling = true\n')
|
|
422
|
+
fs.writeFileSync(path.join(packageRoot, 'src', 'modules', 'test_package', 'widget.ts'), "export { sibling } from '../sibling'\n")
|
|
423
|
+
const aliasResolver = { ...resolver, loadEnabledModules: () => [entry], getPackageRoot: () => aliasRoot }
|
|
424
|
+
|
|
425
|
+
ejectModule(aliasResolver, 'test_package')
|
|
426
|
+
|
|
427
|
+
expect(readEnabledModulesFromConfig(resolver.getModulesConfigPath())[0]).toEqual({ id: 'test_package', from: '@app', packageName: '@open-mercato/test-package', locator: 'npm:@open-mercato/test-package' })
|
|
428
|
+
const appBase = resolver.getModulePaths(entry).appBase
|
|
429
|
+
expect(fs.readFileSync(path.join(appBase, 'widget.ts'), 'utf8')).toContain("from 'candidate_consents/modules/sibling'")
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('preserves GitHub origin and canonical package attribution through legacy eject', () => {
|
|
433
|
+
const resolver = createOfficialModuleResolver(tmpDir)
|
|
434
|
+
fs.writeFileSync(path.join(resolver.getAppDir(), 'package.json'), JSON.stringify({ dependencies: { '@open-mercato/test-package': 'github:pkarw/test-package#main' } }))
|
|
435
|
+
ejectModule(resolver, 'test_package')
|
|
436
|
+
const entry = readEnabledModulesFromConfig(resolver.getModulesConfigPath())[0]
|
|
437
|
+
expect(entry).toEqual({ id: 'test_package', from: '@app', packageName: '@open-mercato/test-package', locator: 'github:pkarw/test-package' })
|
|
438
|
+
const identities = [{ id: entry.id, locators: getModuleLocators(entry, resolver) }]
|
|
439
|
+
expect(resolveModuleRequirement('npm:@open-mercato/test-package', identities)).toBeUndefined()
|
|
440
|
+
expect(resolveModuleRequirement('github:pkarw/test-package', identities)).toBe('test_package')
|
|
441
|
+
})
|
|
442
|
+
|
|
351
443
|
it('ejects module packages using module index.ts ejectable flag', () => {
|
|
352
444
|
const resolver = createOfficialModuleResolver(tmpDir)
|
|
353
445
|
const appModuleDir = path.join(tmpDir, 'apps', 'mercato', 'src', 'modules', 'test_package')
|
|
@@ -358,7 +450,7 @@ describe('eject', () => {
|
|
|
358
450
|
expect(fs.existsSync(path.join(appModuleDir, 'index.ts'))).toBe(true)
|
|
359
451
|
expect(fs.existsSync(path.join(appModuleDir, 'widget.ts'))).toBe(true)
|
|
360
452
|
expect(fs.readFileSync(modulesTsPath, 'utf8')).toContain(
|
|
361
|
-
"{ id: 'test_package', from: '@app' }",
|
|
453
|
+
"{ id: 'test_package', from: '@app', locator: 'npm:@open-mercato/test-package', packageName: '@open-mercato/test-package' }",
|
|
362
454
|
)
|
|
363
455
|
})
|
|
364
456
|
})
|
|
@@ -381,6 +473,8 @@ describe('eject', () => {
|
|
|
381
473
|
path.join(appDir, 'src', 'modules.ts'),
|
|
382
474
|
"export const enabledModules = [{ id: 'customers', from: '@open-mercato/core' }]\n",
|
|
383
475
|
)
|
|
476
|
+
fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@open-mercato/core' }))
|
|
477
|
+
fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ dependencies: { '@open-mercato/core': '^0.1.0' } }))
|
|
384
478
|
fs.writeFileSync(
|
|
385
479
|
path.join(distModuleDir, 'index.js'),
|
|
386
480
|
'var metadata = { title: "Customers", description: "Compiled", ejectable: true };\nexport { metadata };\n',
|
|
@@ -431,7 +525,7 @@ describe('eject', () => {
|
|
|
431
525
|
expect(fs.existsSync(path.join(appModuleDir, 'index.ts'))).toBe(true)
|
|
432
526
|
expect(fs.existsSync(path.join(appModuleDir, 'acl.ts'))).toBe(true)
|
|
433
527
|
expect(fs.existsSync(path.join(appModuleDir, 'index.js'))).toBe(false)
|
|
434
|
-
expect(fs.readFileSync(modulesTsPath, 'utf8')).toContain("{ id: 'customers', from: '@app' }")
|
|
528
|
+
expect(fs.readFileSync(modulesTsPath, 'utf8')).toContain("{ id: 'customers', from: '@app', locator: 'npm:@open-mercato/core', packageName: '@open-mercato/core' }")
|
|
435
529
|
})
|
|
436
530
|
|
|
437
531
|
it('falls back to compiled index.js metadata for dist-only packages', () => {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { resolveInitDerivedSecrets, resolveInitSuperadminCredentials } from '../init-secrets'
|
|
2
4
|
|
|
3
5
|
describe('resolveInitDerivedSecrets', () => {
|
|
4
6
|
it("seeds the well-known 'secret' password in non-production when no env overrides are set", () => {
|
|
@@ -77,3 +79,85 @@ describe('resolveInitDerivedSecrets', () => {
|
|
|
77
79
|
})
|
|
78
80
|
})
|
|
79
81
|
|
|
82
|
+
|
|
83
|
+
describe('production bootstrap superadmin credentials', () => {
|
|
84
|
+
const production = { NODE_ENV: 'production' }
|
|
85
|
+
|
|
86
|
+
it.each([undefined, '', ' ', 'secret', 'password', ' PASSWORD ', 'changeme', 'change-me', 'replace_me'])(
|
|
87
|
+
'rejects an absent or placeholder production password: %s', (password) => {
|
|
88
|
+
expect(() => resolveInitSuperadminCredentials([], {
|
|
89
|
+
...production, OM_INIT_SUPERADMIN_PASSWORD: password,
|
|
90
|
+
})).toThrow('requires a unique superadmin password')
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
it.each(['--skip-password-policy', '--skip-password-policy=true', '--allow-weak-password', '--allow-weak-password=true'])(
|
|
95
|
+
'enforces production policy despite %s', (flag) => {
|
|
96
|
+
expect(() => resolveInitSuperadminCredentials([flag, '--password=weak'], production))
|
|
97
|
+
.toThrow('does not meet the configured password policy')
|
|
98
|
+
expect(resolveInitSuperadminCredentials([flag, '--password=Unique-Owner-937!'], production).skipPasswordPolicy).toBe(false)
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
it('uses the configured policy and never includes the password in errors', () => {
|
|
103
|
+
const password = 'Unique-Owner-937!'
|
|
104
|
+
expect(() => resolveInitSuperadminCredentials([`--password=${password}`], {
|
|
105
|
+
...production, OM_PASSWORD_MIN_LENGTH: '40',
|
|
106
|
+
})).toThrow('does not meet the configured password policy')
|
|
107
|
+
try {
|
|
108
|
+
resolveInitSuperadminCredentials([`--password=${password}`], { ...production, OM_PASSWORD_MIN_LENGTH: '40' })
|
|
109
|
+
} catch (error) {
|
|
110
|
+
expect(String(error)).not.toContain(password)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('rejects placeholders even with a relaxed policy', () => {
|
|
115
|
+
expect(() => resolveInitSuperadminCredentials([], {
|
|
116
|
+
...production, OM_INIT_SUPERADMIN_PASSWORD: 'password', OM_PASSWORD_REQUIRE_DIGIT: 'false',
|
|
117
|
+
OM_PASSWORD_REQUIRE_UPPERCASE: 'false', OM_PASSWORD_REQUIRE_SPECIAL: 'false',
|
|
118
|
+
})).toThrow('requires a unique superadmin password')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('prefers command arguments over environment overrides', () => {
|
|
122
|
+
expect(resolveInitSuperadminCredentials(['--email=owner@example.test', '--password=Unique-Owner-937!'], {
|
|
123
|
+
NODE_ENV: ' Production ', OM_INIT_SUPERADMIN_PASSWORD: 'password', OM_INIT_SUPERADMIN_EMAIL: 'other@example.test',
|
|
124
|
+
})).toEqual({ email: 'owner@example.test', password: 'Unique-Owner-937!', skipPasswordPolicy: false })
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('uses the CLI locale for operator-facing production diagnostics', () => {
|
|
128
|
+
expect(() => resolveInitSuperadminCredentials([], { ...production, LC_ALL: 'pl_PL.UTF-8' }))
|
|
129
|
+
.toThrow('Inicjalizacja produkcyjna wymaga')
|
|
130
|
+
expect(() => resolveInitSuperadminCredentials([], { ...production, LC_ALL: 'unknown' }))
|
|
131
|
+
.toThrow('Production initialization requires')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('accepts a policy-compliant environment password', () => {
|
|
135
|
+
expect(resolveInitSuperadminCredentials([], {
|
|
136
|
+
...production, OM_INIT_SUPERADMIN_PASSWORD: 'Unique-Owner-937!',
|
|
137
|
+
}).password).toBe('Unique-Owner-937!')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it.each([undefined, 'development', 'test', 'staging'])('preserves non-production defaults for %s', (nodeEnv) => {
|
|
141
|
+
expect(resolveInitSuperadminCredentials([], { NODE_ENV: nodeEnv })).toEqual({
|
|
142
|
+
email: 'superadmin@acme.com', password: 'secret', skipPasswordPolicy: true,
|
|
143
|
+
})
|
|
144
|
+
expect(resolveInitSuperadminCredentials([], { NODE_ENV: nodeEnv, OM_INIT_SUPERADMIN_PASSWORD: 'password' }).password).toBe('password')
|
|
145
|
+
expect(resolveInitSuperadminCredentials(['--skip-password-policy=false'], { NODE_ENV: nodeEnv }).skipPasswordPolicy).toBe(false)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('full-app Compose bootstrap defaults', () => {
|
|
150
|
+
it.each(['starters/docker/compose.fullapp.yml', 'packages/create-app/template/docker-compose.fullapp.yml'])(
|
|
151
|
+
'%s cannot bootstrap predictable production credentials', (composePath) => {
|
|
152
|
+
const compose = readFileSync(path.resolve(__dirname, '../../../../..', composePath), 'utf8')
|
|
153
|
+
const fallback = compose.match(/OM_INIT_SUPERADMIN_PASSWORD: \$\{OM_INIT_SUPERADMIN_PASSWORD:-(.*?)\}/)?.[1]
|
|
154
|
+
expect(fallback).toBeDefined()
|
|
155
|
+
expect(() => resolveInitSuperadminCredentials([], {
|
|
156
|
+
NODE_ENV: 'production', OM_INIT_SUPERADMIN_PASSWORD: fallback,
|
|
157
|
+
})).toThrow('requires a unique superadmin password')
|
|
158
|
+
expect(resolveInitSuperadminCredentials([], {
|
|
159
|
+
NODE_ENV: 'development', OM_INIT_SUPERADMIN_PASSWORD: fallback,
|
|
160
|
+
}).password).toBe(fallback)
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { formatInitSummary } from '../init-summary'
|
|
2
|
+
|
|
3
|
+
const users = [
|
|
4
|
+
{ icon: '👑', label: 'Superadmin', email: 'owner@example.test', password: 'demo-superadmin' },
|
|
5
|
+
{ icon: '🧰', label: 'Admin', email: 'admin@example.test', password: 'demo-admin' },
|
|
6
|
+
{ icon: '👷', label: 'Employee', email: 'employee@example.test', password: 'demo-employee' },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
describe('formatInitSummary', () => {
|
|
10
|
+
it('renders every supplied account and its own password without side borders', () => {
|
|
11
|
+
const output = formatInitSummary({ users, autoStart: true }).join('\n')
|
|
12
|
+
for (const user of users) {
|
|
13
|
+
expect(output).toContain(`${user.icon} ${user.label}`)
|
|
14
|
+
expect(output).toContain(user.email)
|
|
15
|
+
expect(output).toContain(user.password)
|
|
16
|
+
}
|
|
17
|
+
expect(output).not.toMatch(/[╔║╚]/u)
|
|
18
|
+
expect(output).toContain('The dev server will start automatically.')
|
|
19
|
+
expect(output).not.toContain('\u001b[')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('shows the manual startup hint when initialization is invoked directly', () => {
|
|
23
|
+
const output = formatInitSummary({ users: users.slice(0, 1), autoStart: false }).join('\n')
|
|
24
|
+
expect(output).toContain('🚀 Start the dev server: yarn dev')
|
|
25
|
+
expect(output).not.toContain('automatically')
|
|
26
|
+
expect(output).not.toContain('🧰 Admin')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it.each([8, 20, 40, 80])('wraps long credentials within %i columns without dropping characters', (columns) => {
|
|
30
|
+
const password = 'long-demo-password-'.repeat(12)
|
|
31
|
+
const lines = formatInitSummary({ users: [{ ...users[0], password }], autoStart: true, columns })
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const width = [...line].reduce((total, character) => total + (/\p{Extended_Pictographic}/u.test(character) ? 2 : 1), 0)
|
|
34
|
+
expect(width).toBeLessThanOrEqual(columns)
|
|
35
|
+
}
|
|
36
|
+
expect(lines.map((line) => line.trim()).join('')).toContain(password)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('paints complete rows dark gray with light text and resets after each row', () => {
|
|
40
|
+
const lines = formatInitSummary({ users, autoStart: true, color: true, columns: 80 })
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
expect(line).toMatch(/^\u001b\[48;2;57;62;70m\u001b\[38;2;238;238;238m/u)
|
|
43
|
+
expect(line).toMatch(/\u001b\[0m$/u)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it.each([NaN, Infinity, 0, -1])('uses a safe width for invalid columns %s', (columns) => {
|
|
48
|
+
expect(formatInitSummary({ users, autoStart: false, columns })[0]).toHaveLength(80)
|
|
49
|
+
})
|
|
50
|
+
})
|