@open-mercato/cli 0.10.1-develop.7983.1.fd91207efe → 0.10.1-develop.7994.1.c9f31972aa
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/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/manifest.json +1 -1
- package/dist/agentic/shared/ai/harness/cases.json +23 -9
- package/dist/agentic/shared/ai/harness/source-link-inventory.json +90 -4
- 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-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 +14 -23
- package/dist/mercato.js.map +2 -2
- package/package.json +7 -7
- package/src/__tests__/mercato.test.ts +14 -4
- 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-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-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 +14 -24
- 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', () => {
|
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { resolveGithubModuleTarget } from '../module-github'
|
|
3
|
+
|
|
4
|
+
jest.mock('node:child_process', () => ({ execFileSync: jest.fn() }))
|
|
5
|
+
const execute = jest.mocked(execFileSync)
|
|
6
|
+
const sha = 'a'.repeat(40)
|
|
7
|
+
|
|
8
|
+
function mockGithub(manifest: unknown = { name: '@pkarw/consents' }, files = ['src/modules/marketing_consents/index.ts']): void {
|
|
9
|
+
execute.mockImplementation((_command, args) => {
|
|
10
|
+
const endpoint = args?.[1]
|
|
11
|
+
const value = endpoint === 'user' ? { login: 'pkarw' }
|
|
12
|
+
: typeof endpoint === 'string' && endpoint.includes('/commits/') ? { sha }
|
|
13
|
+
: typeof endpoint === 'string' && endpoint.includes('/contents/') ? { encoding: 'base64', content: Buffer.from(JSON.stringify(manifest)).toString('base64') }
|
|
14
|
+
: typeof endpoint === 'string' && endpoint.includes('/git/trees/') ? { tree: files.map((filePath) => ({ path: filePath })) }
|
|
15
|
+
: { default_branch: 'main' }
|
|
16
|
+
return JSON.stringify(value)
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
beforeEach(() => execute.mockReset())
|
|
21
|
+
|
|
22
|
+
test.each([
|
|
23
|
+
'pkarw/consents', 'github:pkarw/consents', 'https://github.com/pkarw/consents.git/',
|
|
24
|
+
'git@github.com:pkarw/consents.git', 'ssh://git@github.com/pkarw/consents.git',
|
|
25
|
+
'git+https://github.com/pkarw/consents', 'github:consents',
|
|
26
|
+
'git+ssh://git@github.com/pkarw/consents.git', 'http://github.com/pkarw/consents',
|
|
27
|
+
'https://github.com/https://github.com/pkarw/consents', 'github.com/pkarw/consents',
|
|
28
|
+
])('pins GitHub locator %s to one revision', (locator) => {
|
|
29
|
+
mockGithub()
|
|
30
|
+
expect(resolveGithubModuleTarget(locator, '/app')).toEqual({
|
|
31
|
+
packageName: '@pkarw/consents', packageSpec: `@pkarw/consents@github:pkarw/consents#${sha}`,
|
|
32
|
+
locator: 'github:pkarw/consents', moduleIds: ['marketing_consents'],
|
|
33
|
+
})
|
|
34
|
+
expect(execute).toHaveBeenCalledWith('gh', ['api', `repos/pkarw/consents/contents/package.json?ref=${sha}`], expect.objectContaining({ cwd: '/app' }))
|
|
35
|
+
expect(execute).toHaveBeenCalledWith('gh', expect.any(Array), expect.objectContaining({ maxBuffer: 16 * 1024 * 1024 }))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('resolves selected refs once and inspects the same immutable revision', () => {
|
|
39
|
+
mockGithub()
|
|
40
|
+
resolveGithubModuleTarget('github:pkarw/consents#release/v1', '/app')
|
|
41
|
+
expect(execute).toHaveBeenCalledWith('gh', ['api', 'repos/pkarw/consents/commits/release%2Fv1'], expect.anything())
|
|
42
|
+
expect(execute).toHaveBeenCalledTimes(3)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test.each(['https://example.com/owner/repo', 'pkarw/consents/tree/main', 'github:pkarw/consents#--bad'])('rejects invalid locator %s', (locator) => {
|
|
46
|
+
mockGithub()
|
|
47
|
+
expect(() => resolveGithubModuleTarget(locator, '/app')).toThrow()
|
|
48
|
+
expect(execute).not.toHaveBeenCalled()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('accepts a GitHub .git@ref locator', () => {
|
|
52
|
+
mockGithub()
|
|
53
|
+
resolveGithubModuleTarget('git+ssh://git@github.com/pkarw/consents.git@release/v1', '/app')
|
|
54
|
+
expect(execute).toHaveBeenCalledWith('gh', ['api', 'repos/pkarw/consents/commits/release%2Fv1'], expect.anything())
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('rejects malformed manifest package identities', () => {
|
|
58
|
+
mockGithub({ name: '--bad' })
|
|
59
|
+
expect(() => resolveGithubModuleTarget('pkarw/consents', '/app')).toThrow('valid npm package name')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('reports authentication and repository errors without leaking command stderr', () => {
|
|
63
|
+
execute.mockImplementation(() => { throw new Error('private token stderr') })
|
|
64
|
+
expect(() => resolveGithubModuleTarget('pkarw/consents', '/app')).toThrow('gh auth login')
|
|
65
|
+
expect(() => resolveGithubModuleTarget('pkarw/consents', '/app')).not.toThrow('private token')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('rejects an exact GitHub revision denying a generated subpath before installation', () => {
|
|
69
|
+
mockGithub({ name: '@pkarw/consents', exports: { './modules/marketing_consents/index': './dist/modules/marketing_consents/index.js' } }, [
|
|
70
|
+
'src/modules/marketing_consents/index.ts', 'src/modules/marketing_consents/di.ts',
|
|
71
|
+
])
|
|
72
|
+
expect(() => resolveGithubModuleTarget('github:pkarw/consents#main', '/app')).toThrow('@pkarw/consents/modules/marketing_consents/di')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('allows source-only GitHub repositories to produce generated runtime artifacts during installation', () => {
|
|
76
|
+
mockGithub({ name: '@pkarw/consents', exports: { './modules/marketing_consents/*': './dist/modules/marketing_consents/*.js' } }, [
|
|
77
|
+
'src/modules/marketing_consents/index.ts', 'src/modules/marketing_consents/di.ts',
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
expect(() => resolveGithubModuleTarget('github:pkarw/consents#main', '/app')).not.toThrow()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('checks every active sibling against GitHub exports even when selecting another module', () => {
|
|
84
|
+
mockGithub({ name: '@pkarw/consents', exports: { './modules/alpha/index': null, './modules/beta/index': './dist/modules/beta/index.js' } }, [
|
|
85
|
+
'src/modules/alpha/index.ts', 'src/modules/beta/index.ts',
|
|
86
|
+
])
|
|
87
|
+
expect(() => resolveGithubModuleTarget('github:pkarw/consents#main', '/app', {
|
|
88
|
+
moduleId: 'beta', enabledModules: [{ id: 'alpha', from: '@pkarw/consents' }],
|
|
89
|
+
})).toThrow('@pkarw/consents/modules/alpha/index')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
test('reads exact-commit page source without executing private lowercase helpers', () => {
|
|
94
|
+
const moduleRoot = 'src/modules/marketing_consents/'
|
|
95
|
+
mockGithub({ name: '@pkarw/consents', exports: { './modules/marketing_consents/index': './dist/modules/marketing_consents/index.js' } }, [moduleRoot + 'index.ts', moduleRoot + 'backend/form-utils.tsx', moduleRoot + 'backend/orders/page.tsx'])
|
|
96
|
+
const metadataImplementation = execute.getMockImplementation()!
|
|
97
|
+
execute.mockImplementation((command, args, options) => {
|
|
98
|
+
const endpoint = String(args?.[1])
|
|
99
|
+
if (endpoint.includes('/backend/')) {
|
|
100
|
+
expect(endpoint).toContain(`?ref=${sha}`)
|
|
101
|
+
const content = endpoint.includes('form-utils') ? "throw new Error('must not execute'); export const renderLabel = () => null" : 'export default function Page() {}'
|
|
102
|
+
return JSON.stringify({ encoding: 'base64', content: Buffer.from(content).toString('base64') })
|
|
103
|
+
}
|
|
104
|
+
return metadataImplementation(command, args, options)
|
|
105
|
+
})
|
|
106
|
+
expect(() => resolveGithubModuleTarget('github:pkarw/consents', '/app')).toThrow('@pkarw/consents/modules/marketing_consents/backend/orders/page')
|
|
107
|
+
expect(execute.mock.calls.filter(([, args]) => String(args?.[1]).includes('form-utils'))).toHaveLength(1)
|
|
108
|
+
})
|