@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,145 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import ts from 'typescript-js'
|
|
4
|
+
import { normalizeModuleLocator, type ModuleIdentity } from '@open-mercato/shared/modules/module-locators'
|
|
5
|
+
import type { ModuleEntry, PackageResolver } from './resolver'
|
|
6
|
+
import { readEnabledModules } from './modules-config'
|
|
7
|
+
|
|
8
|
+
function readJson(filePath: string): Record<string, unknown> {
|
|
9
|
+
try {
|
|
10
|
+
const value: unknown = JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
11
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
|
12
|
+
} catch {
|
|
13
|
+
return {}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readMetadataLocator(moduleBase: string): string | undefined {
|
|
18
|
+
const indexPath = ['index.ts', 'index.js'].map((name) => path.join(moduleBase, name)).find((candidate) => fs.existsSync(candidate))
|
|
19
|
+
if (!indexPath) return undefined
|
|
20
|
+
const source = ts.createSourceFile(indexPath, fs.readFileSync(indexPath, 'utf8'), ts.ScriptTarget.Latest, true)
|
|
21
|
+
let locator: string | undefined
|
|
22
|
+
function visit(node: ts.Node): void {
|
|
23
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'metadata' && node.initializer) {
|
|
24
|
+
let initializer = node.initializer
|
|
25
|
+
while (ts.isAsExpression(initializer) || ts.isSatisfiesExpression(initializer) || ts.isParenthesizedExpression(initializer)) initializer = initializer.expression
|
|
26
|
+
if (ts.isObjectLiteralExpression(initializer)) {
|
|
27
|
+
for (const property of initializer.properties) {
|
|
28
|
+
if (ts.isPropertyAssignment(property) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) && property.name.text === 'locator' && ts.isStringLiteralLike(property.initializer)) locator = property.initializer.text
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
ts.forEachChild(node, visit)
|
|
33
|
+
}
|
|
34
|
+
visit(source)
|
|
35
|
+
return locator
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sourceLocator(value: unknown): string | undefined {
|
|
39
|
+
if (typeof value !== 'string') return undefined
|
|
40
|
+
const locator = normalizeModuleLocator(value)
|
|
41
|
+
return locator?.startsWith('npm:') || locator?.startsWith('github:') ? locator : undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function installedDependencyLocator(specifier: unknown, canonicalName: string | undefined): string | undefined {
|
|
45
|
+
if (typeof specifier !== 'string') return undefined
|
|
46
|
+
if (/^(?:github:|github\.com\/|(?:git\+)?https?:\/\/github\.com\/|git@github\.com:|(?:git\+)?ssh:\/\/|[a-z0-9][a-z0-9-]*\/[a-z0-9._-]+(?:[#@]|$))/i.test(specifier)) {
|
|
47
|
+
const locator = sourceLocator(specifier)
|
|
48
|
+
return locator?.startsWith('github:') ? locator : undefined
|
|
49
|
+
}
|
|
50
|
+
if (specifier.startsWith('npm:')) {
|
|
51
|
+
const aliasTarget = specifier.match(/^npm:(@[^/]+\/[^@]+|[^@]+)(?:@.+)?$/)?.[1]
|
|
52
|
+
return aliasTarget ? sourceLocator(`npm:${aliasTarget}`) : undefined
|
|
53
|
+
}
|
|
54
|
+
if (!canonicalName || /^(?:[a-z][a-z0-9+.-]*:|[./]|https?:)/i.test(specifier)) return undefined
|
|
55
|
+
return sourceLocator(`npm:${canonicalName}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertDeclaredIdentity(entry: ModuleEntry, observedLocator: string | undefined, canonicalName: string | undefined): void {
|
|
59
|
+
const binding = entry.from ?? '@open-mercato/core'
|
|
60
|
+
if (entry.packageName && canonicalName && entry.packageName !== canonicalName) {
|
|
61
|
+
throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${binding}': declared package '${entry.packageName}' disagrees with installed package '${canonicalName}'. Restore the intended binding or correct src/modules.ts, then regenerate.`)
|
|
62
|
+
}
|
|
63
|
+
const declaredLocator = sourceLocator(entry.locator)
|
|
64
|
+
if (declaredLocator && observedLocator && declaredLocator !== observedLocator) {
|
|
65
|
+
throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${binding}': declared origin '${declaredLocator}' disagrees with observed origin '${observedLocator}'. Disable the old provider before replacing it, then regenerate.`)
|
|
66
|
+
}
|
|
67
|
+
if (declaredLocator?.startsWith('npm:') && canonicalName && declaredLocator !== sourceLocator(`npm:${canonicalName}`)) {
|
|
68
|
+
throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${binding}': declared origin '${declaredLocator}' disagrees with installed package '${canonicalName}'. Correct src/modules.ts to select the intended provider, then regenerate.`)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readModuleLocators(
|
|
73
|
+
entry: ModuleEntry,
|
|
74
|
+
resolver: PackageResolver,
|
|
75
|
+
appManifest: Record<string, unknown>,
|
|
76
|
+
packageManifests: Map<string, Record<string, unknown>>,
|
|
77
|
+
): string[] {
|
|
78
|
+
const from = entry.from ?? '@open-mercato/core'
|
|
79
|
+
if (from === '@app') {
|
|
80
|
+
const retainedLocator = sourceLocator(entry.locator ?? readMetadataLocator(resolver.getModulePaths(entry).appBase))
|
|
81
|
+
assertDeclaredIdentity(entry, retainedLocator, entry.packageName)
|
|
82
|
+
return retainedLocator ? [retainedLocator] : []
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let manifest = packageManifests.get(from)
|
|
86
|
+
if (!manifest) {
|
|
87
|
+
manifest = readJson(path.join(resolver.getPackageRoot(from), 'package.json'))
|
|
88
|
+
packageManifests.set(from, manifest)
|
|
89
|
+
}
|
|
90
|
+
const canonicalName = typeof manifest.name === 'string' ? manifest.name : undefined
|
|
91
|
+
const observedLocators = new Set<string>()
|
|
92
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
93
|
+
const dependencies = appManifest[section]
|
|
94
|
+
if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue
|
|
95
|
+
const specifier = (dependencies as Record<string, unknown>)[from]
|
|
96
|
+
const locator = installedDependencyLocator(specifier, canonicalName)
|
|
97
|
+
if (locator?.startsWith('npm:') && canonicalName !== from && typeof specifier === 'string' && !specifier.startsWith('npm:')) {
|
|
98
|
+
throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${from}': installed package '${canonicalName}' does not match the dependency binding. Restore the intended package or declare an explicit npm alias, then regenerate.`)
|
|
99
|
+
}
|
|
100
|
+
if (locator) observedLocators.add(locator)
|
|
101
|
+
}
|
|
102
|
+
if (observedLocators.size > 1) {
|
|
103
|
+
throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${from}': dependency sections declare conflicting origins. Keep one provider for this binding in package.json, then regenerate.`)
|
|
104
|
+
}
|
|
105
|
+
const observedLocator = [...observedLocators][0]
|
|
106
|
+
if (observedLocator?.startsWith('npm:') && canonicalName && observedLocator !== sourceLocator(`npm:${canonicalName}`)) {
|
|
107
|
+
throw new Error(`[modules] Cannot resolve module '${entry.id}' from binding '${from}': dependency origin '${observedLocator}' disagrees with installed package '${canonicalName}'. Restore the intended package through the package manager, then regenerate.`)
|
|
108
|
+
}
|
|
109
|
+
assertDeclaredIdentity(entry, observedLocator, canonicalName)
|
|
110
|
+
const locator = observedLocator ?? sourceLocator(entry.locator)
|
|
111
|
+
return locator ? [locator] : []
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getModuleLocators(entry: ModuleEntry, resolver: PackageResolver): string[] {
|
|
115
|
+
return readModuleLocators(entry, resolver, readJson(path.join(resolver.getAppDir(), 'package.json')), new Map())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function assertUniqueEnabledModuleIds(enabledModules: readonly ModuleEntry[], resolver: PackageResolver): void {
|
|
119
|
+
const byId = new Map<string, ModuleEntry>()
|
|
120
|
+
for (const entry of enabledModules) {
|
|
121
|
+
const existing = byId.get(entry.id)
|
|
122
|
+
if (existing) {
|
|
123
|
+
const describe = (provider: ModuleEntry): string => {
|
|
124
|
+
const binding = provider.from ?? '@open-mercato/core'
|
|
125
|
+
const source = sourceLocator(provider.locator)
|
|
126
|
+
return binding === '@app'
|
|
127
|
+
? `@app (${resolver.getModulePaths(provider).appBase})${source ? `, origin ${source}` : ''}`
|
|
128
|
+
: `binding '${binding}'${source ? `, origin ${source}` : ''}`
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`[generate] Duplicate enabled module ID '${entry.id}': ${describe(existing)} and ${describe(entry)}. Keep one provider in src/modules.ts, then run yarn mercato generate. Installing under another package alias does not allow both module IDs to run.`)
|
|
131
|
+
}
|
|
132
|
+
byId.set(entry.id, entry)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getEnabledModuleIdentities(resolver: PackageResolver, enabledModules: readonly ModuleEntry[] = resolver.loadEnabledModules()): ModuleIdentity[] {
|
|
137
|
+
const modulesPath = typeof resolver.getModulesConfigPath === 'function'
|
|
138
|
+
? resolver.getModulesConfigPath()
|
|
139
|
+
: path.join(resolver.getAppDir(), 'src', 'modules.ts')
|
|
140
|
+
if (fs.existsSync(modulesPath)) assertUniqueEnabledModuleIds(readEnabledModules(modulesPath), resolver)
|
|
141
|
+
assertUniqueEnabledModuleIds(enabledModules, resolver)
|
|
142
|
+
const appManifest = readJson(path.join(resolver.getAppDir(), 'package.json'))
|
|
143
|
+
const packageManifests = new Map<string, Record<string, unknown>>()
|
|
144
|
+
return enabledModules.map((entry) => ({ id: entry.id, locators: readModuleLocators(entry, resolver, appManifest, packageManifests) }))
|
|
145
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { execFileSync } from 'node:child_process'
|
|
5
|
+
import spawn from 'cross-spawn'
|
|
6
|
+
import { assertModulePackageExports, isModulePackageExportError } from './module-package'
|
|
7
|
+
|
|
8
|
+
export type InspectedModuleTarget = {
|
|
9
|
+
packageName: string
|
|
10
|
+
packageSpec: string
|
|
11
|
+
moduleIds: string[]
|
|
12
|
+
locator?: string
|
|
13
|
+
version?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type ArtifactInspection = { moduleIds: string[]; version?: string }
|
|
17
|
+
|
|
18
|
+
function record(value: unknown): Record<string, unknown> {
|
|
19
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function inspectFiles(manifest: Record<string, unknown>, packageName: string, files: string[], requiredModuleIds: readonly string[], readSource: (relativePath: string) => string): ArtifactInspection {
|
|
23
|
+
if (manifest.name !== packageName) throw new Error(`Inspected package does not match "${packageName}". No package was installed.`)
|
|
24
|
+
const moduleIds = [...new Set(files.flatMap((file) => {
|
|
25
|
+
const match = /^src\/modules\/([a-z0-9]+(?:_[a-z0-9]+)*)\/index\.ts$/.exec(file)
|
|
26
|
+
return match && files.includes(`dist/modules/${match[1]}/index.js`) ? [match[1]] : []
|
|
27
|
+
}))]
|
|
28
|
+
const checkedModuleIds = requiredModuleIds.length ? requiredModuleIds : moduleIds.length === 1 ? moduleIds : []
|
|
29
|
+
for (const moduleId of checkedModuleIds) {
|
|
30
|
+
if (!moduleIds.includes(moduleId)) throw new Error(`Package "${packageName}" is missing source or compiled descriptors for "${moduleId}". No package was installed.`)
|
|
31
|
+
assertModulePackageExports(manifest, packageName, moduleId, files, readSource)
|
|
32
|
+
}
|
|
33
|
+
if (moduleIds.length === 0) throw new Error(`Package "${packageName}" is missing source or compiled module descriptors. Build the package before retrying. No package was installed.`)
|
|
34
|
+
return { moduleIds, ...(typeof manifest.version === 'string' ? { version: manifest.version } : {}) }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function inspectArchive(archive: string, packageName: string, requiredModuleIds: readonly string[]): ArtifactInspection {
|
|
38
|
+
const manifest = record(JSON.parse(execFileSync('tar', ['-xOf', archive, 'package/package.json'], { encoding: 'utf8', timeout: 15_000 })))
|
|
39
|
+
const files = execFileSync('tar', ['-tf', archive], { encoding: 'utf8', timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }).split('\n').filter((file) => file.startsWith('package/')).map((file) => file.slice('package/'.length))
|
|
40
|
+
return inspectFiles(manifest, packageName, files, requiredModuleIds, (relativePath) => execFileSync('tar', ['-xOf', archive, `package/${relativePath}`], { encoding: 'utf8', timeout: 15_000, maxBuffer: 1024 * 1024 }))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function packageFiles(packageRoot: string, relativeDir = ''): string[] {
|
|
44
|
+
return fs.readdirSync(path.join(packageRoot, relativeDir), { withFileTypes: true }).flatMap((entry) => {
|
|
45
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) return []
|
|
46
|
+
const relativePath = path.posix.join(relativeDir, entry.name)
|
|
47
|
+
return entry.isDirectory() ? packageFiles(packageRoot, relativePath) : entry.isFile() ? [relativePath] : []
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isModernYarnProject(appDir: string): boolean {
|
|
52
|
+
let directory = appDir
|
|
53
|
+
while (true) {
|
|
54
|
+
if (fs.existsSync(path.join(directory, '.yarnrc.yml'))) return true
|
|
55
|
+
const manifestPath = path.join(directory, 'package.json')
|
|
56
|
+
if (fs.existsSync(manifestPath)) {
|
|
57
|
+
const manifest = record(JSON.parse(fs.readFileSync(manifestPath, 'utf8')))
|
|
58
|
+
if (typeof manifest.packageManager === 'string' && /^yarn@(?:[2-9]|[1-9][0-9]+)\./.test(manifest.packageManager)) return true
|
|
59
|
+
}
|
|
60
|
+
const parent = path.dirname(directory)
|
|
61
|
+
if (parent === directory) return false
|
|
62
|
+
directory = parent
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function yarnNpmConfiguration(appDir: string, packageName: string, temporaryDir: string): string[] {
|
|
67
|
+
if (!isModernYarnProject(appDir)) return []
|
|
68
|
+
const settings = new Map<string, unknown>()
|
|
69
|
+
for (const setting of ['npmRegistryServer', 'npmScopes', 'npmRegistries', 'npmAuthToken', 'npmAuthIdent']) {
|
|
70
|
+
const result = spawn.sync(process.platform === 'win32' ? 'yarn.cmd' : 'yarn', ['config', 'get', setting, '--json', '--no-redacted'], {
|
|
71
|
+
cwd: appDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000,
|
|
72
|
+
})
|
|
73
|
+
if (result.error || result.status !== 0) throw new Error('Cannot inspect project package registry configuration')
|
|
74
|
+
settings.set(setting, JSON.parse(result.stdout))
|
|
75
|
+
}
|
|
76
|
+
const scope = packageName.startsWith('@') ? packageName.slice(1).split('/')[0] : undefined
|
|
77
|
+
const scopeSettings = scope ? record(record(settings.get('npmScopes'))[scope]) : {}
|
|
78
|
+
const registry = typeof scopeSettings.npmRegistryServer === 'string' ? scopeSettings.npmRegistryServer : settings.get('npmRegistryServer')
|
|
79
|
+
if (typeof registry !== 'string') throw new Error('Missing project package registry')
|
|
80
|
+
const registryUrl = new URL(registry)
|
|
81
|
+
if (!['http:', 'https:'].includes(registryUrl.protocol) || registryUrl.username || registryUrl.password) throw new Error('Invalid project package registry')
|
|
82
|
+
const registryKey = `//${registryUrl.host}${registryUrl.pathname}`.replace(/\/$/, '')
|
|
83
|
+
const registries = record(settings.get('npmRegistries'))
|
|
84
|
+
const registrySettings = record(registries[registry.replace(/\/$/, '')] ?? registries[registryKey] ?? registries[`${registryKey}/`])
|
|
85
|
+
const auth = scopeSettings.npmAuthToken || scopeSettings.npmAuthIdent ? scopeSettings
|
|
86
|
+
: registrySettings.npmAuthToken || registrySettings.npmAuthIdent ? registrySettings
|
|
87
|
+
: { npmAuthToken: settings.get('npmAuthToken'), npmAuthIdent: settings.get('npmAuthIdent') }
|
|
88
|
+
const userConfig = process.env.NPM_CONFIG_USERCONFIG ?? process.env.npm_config_userconfig ?? path.join(os.homedir(), '.npmrc')
|
|
89
|
+
const existingConfig = fs.existsSync(userConfig) ? fs.readFileSync(userConfig, 'utf8') : ''
|
|
90
|
+
const lines = [existingConfig, `registry=${JSON.stringify(registry)}`]
|
|
91
|
+
if (scope) lines.push(`@${scope}:registry=${JSON.stringify(registry)}`)
|
|
92
|
+
if (typeof auth.npmAuthToken === 'string' && auth.npmAuthToken) lines.push(`${registryKey}/:_authToken=${JSON.stringify(auth.npmAuthToken)}`)
|
|
93
|
+
else if (typeof auth.npmAuthIdent === 'string' && auth.npmAuthIdent) lines.push(`${registryKey}/:_auth=${Buffer.from(auth.npmAuthIdent).toString('base64')}`)
|
|
94
|
+
const configPath = path.join(temporaryDir, 'inspection.npmrc')
|
|
95
|
+
fs.writeFileSync(configPath, `${lines.join('\n')}\n`, { mode: 0o600 })
|
|
96
|
+
return ['--userconfig', configPath, '--registry', registry, ...(scope ? [`--@${scope}:registry=${registry}`] : [])]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function inspectNpmModuleTarget(packageSpec: string, packageName: string, appDir: string, requiredModuleIds: readonly string[] = []): InspectedModuleTarget {
|
|
100
|
+
const filePrefix = `${packageName}@file:`
|
|
101
|
+
if (packageSpec.startsWith(filePrefix)) {
|
|
102
|
+
const packageRoot = path.resolve(appDir, packageSpec.slice(filePrefix.length))
|
|
103
|
+
if (fs.statSync(packageRoot).isFile()) return { packageName, packageSpec, ...inspectArchive(packageRoot, packageName, requiredModuleIds) }
|
|
104
|
+
const manifest = record(JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')))
|
|
105
|
+
const inspected = inspectFiles(manifest, packageName, packageFiles(packageRoot), requiredModuleIds, (relativePath) => fs.readFileSync(path.join(packageRoot, relativePath), 'utf8'))
|
|
106
|
+
return { packageName, packageSpec, ...inspected }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mercato-module-inspection-'))
|
|
110
|
+
let inspectionStep = 'read the project registry configuration'
|
|
111
|
+
try {
|
|
112
|
+
const configurationArgs = yarnNpmConfiguration(appDir, packageName, temporaryDir)
|
|
113
|
+
inspectionStep = 'fetch the exact npm artifact'
|
|
114
|
+
const packedProcess = spawn.sync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['pack', packageSpec, '--ignore-scripts', '--json', '--pack-destination', temporaryDir, ...configurationArgs], {
|
|
115
|
+
cwd: appDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000, maxBuffer: 16 * 1024 * 1024,
|
|
116
|
+
})
|
|
117
|
+
if (packedProcess.error || packedProcess.status !== 0) throw new Error('Artifact inspection failed')
|
|
118
|
+
inspectionStep = 'parse the npm artifact response'
|
|
119
|
+
const packages: unknown = JSON.parse(packedProcess.stdout)
|
|
120
|
+
const packed = record(Array.isArray(packages) ? packages[0] : undefined)
|
|
121
|
+
if (packed.name !== packageName || typeof packed.version !== 'string' || !/^[0-9A-Za-z.+-]+$/.test(packed.version)
|
|
122
|
+
|| typeof packed.filename !== 'string' || path.basename(packed.filename) !== packed.filename) throw new Error('Invalid packed manifest')
|
|
123
|
+
inspectionStep = 'validate the packed module artifact'
|
|
124
|
+
const inspected = inspectArchive(path.join(temporaryDir, packed.filename), packageName, requiredModuleIds)
|
|
125
|
+
if (inspected.version !== packed.version) throw new Error('Packed artifact version disagrees with resolution')
|
|
126
|
+
return { packageName, packageSpec, ...inspected, locator: `npm:${packageName}` }
|
|
127
|
+
} catch (error) {
|
|
128
|
+
const safeDetail = isModulePackageExportError(error) && error instanceof Error ? ` ${error.message}` : ''
|
|
129
|
+
throw new Error(`Cannot inspect the exact module artifact for "${packageName}" while trying to ${inspectionStep}.${safeDetail} Check project registry credentials, package access and shipped src/dist module descriptors before retrying. No package was installed.`)
|
|
130
|
+
} finally {
|
|
131
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true })
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -8,9 +8,13 @@ import {
|
|
|
8
8
|
validateEjectBoundaries,
|
|
9
9
|
type ValidatedOfficialModulePackage,
|
|
10
10
|
} from './module-package'
|
|
11
|
-
import { assertModuleNotGatedOff,
|
|
11
|
+
import { assertModuleNotGatedOff, prepareModuleRegistration, prepareModuleRegistrationSource } from './modules-config'
|
|
12
12
|
import type { PackageResolver } from './resolver'
|
|
13
13
|
import { resolveSpawnCommand } from './spawn'
|
|
14
|
+
import { parseModuleLocator, resolveModuleRequirement } from '@open-mercato/shared/modules/module-locators'
|
|
15
|
+
import { getEnabledModuleIdentities, getModuleLocators } from './module-identity'
|
|
16
|
+
import { resolveGithubModuleTarget } from './module-github'
|
|
17
|
+
import { inspectNpmModuleTarget, type InspectedModuleTarget } from './module-inspection'
|
|
14
18
|
|
|
15
19
|
export type ModuleCommandResult = {
|
|
16
20
|
moduleId: string
|
|
@@ -91,7 +95,7 @@ function readFileIfExists(filePath: string): string | null {
|
|
|
91
95
|
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
function isDeclaredAppDependency(appDir: string, packageName: string): boolean {
|
|
98
|
+
function isDeclaredAppDependency(appDir: string, packageName: string, packageSpecifier?: string): boolean {
|
|
95
99
|
const packageJsonContent = readFileIfExists(path.join(appDir, 'package.json'))
|
|
96
100
|
if (!packageJsonContent) {
|
|
97
101
|
return false
|
|
@@ -105,10 +109,13 @@ function isDeclaredAppDependency(appDir: string, packageName: string): boolean {
|
|
|
105
109
|
throw new Error(`App package.json at ${path.join(appDir, 'package.json')} is not valid JSON. ${message}`)
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
|
|
112
|
+
const declarations = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].flatMap((field) => {
|
|
109
113
|
const dependencies = packageJson[field]
|
|
110
114
|
return typeof dependencies === 'object' && dependencies !== null && packageName in dependencies
|
|
115
|
+
? [(dependencies as Record<string, unknown>)[packageName]]
|
|
116
|
+
: []
|
|
111
117
|
})
|
|
118
|
+
return declarations.length > 0 && (packageSpecifier === undefined || declarations.every((declaration) => declaration === packageSpecifier))
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
function isGatedOffError(error: unknown): boolean {
|
|
@@ -187,7 +194,7 @@ function assertThirdPartyAllowed(packageName: string, allowThirdParty: boolean):
|
|
|
187
194
|
}
|
|
188
195
|
|
|
189
196
|
function assertSupportedPackageSpec(packageSpec: string, allowThirdParty: boolean): string {
|
|
190
|
-
const trimmed = packageSpec.trim()
|
|
197
|
+
const trimmed = packageSpec.trim().replace(/^npm:/, '')
|
|
191
198
|
|
|
192
199
|
if (!SAFE_PACKAGE_SPEC_PATTERN.test(trimmed)) {
|
|
193
200
|
throw new Error('Unsupported package spec. Provide a valid npm package name with an optional tag/version or file: locator.')
|
|
@@ -217,12 +224,71 @@ function validateBeforeRegistration(
|
|
|
217
224
|
const appModuleDir = path.join(resolver.getAppDir(), 'src', 'modules', modulePackage.metadata.moduleId)
|
|
218
225
|
|
|
219
226
|
if (eject && fs.existsSync(appModuleDir)) {
|
|
220
|
-
throw new Error(`Destination directory already exists: ${appModuleDir}.
|
|
227
|
+
throw new Error(`Destination directory already exists: ${appModuleDir}. Preserve local source and choose a different destination or resolve the conflict manually.`)
|
|
221
228
|
}
|
|
222
229
|
|
|
223
230
|
return appModuleDir
|
|
224
231
|
}
|
|
225
232
|
|
|
233
|
+
function assertRequiredModulesEnabled(
|
|
234
|
+
modulePackage: ValidatedOfficialModulePackage,
|
|
235
|
+
resolver: PackageResolver,
|
|
236
|
+
): void {
|
|
237
|
+
const enabledModules = getEnabledModuleIdentities(resolver)
|
|
238
|
+
const missingModuleIds = Array.from(new Set(modulePackage.moduleInfo.requires ?? [])).filter(
|
|
239
|
+
(requirement) => !resolveModuleRequirement(requirement, enabledModules),
|
|
240
|
+
)
|
|
241
|
+
if (missingModuleIds.length === 0) return
|
|
242
|
+
|
|
243
|
+
throw new Error(
|
|
244
|
+
[
|
|
245
|
+
`Module "${modulePackage.metadata.moduleId}" requires module(s) that are not enabled: ${missingModuleIds.join(', ')}.`,
|
|
246
|
+
'Enable the missing module(s) in src/modules.ts first, then rerun this command.',
|
|
247
|
+
...missingModuleIds.filter((requirement) => parseModuleLocator(requirement)?.kind !== 'module').map(
|
|
248
|
+
(requirement) => `Install ${requirement}: npx create-mercato-module install ${shellQuote(requirement)}`,
|
|
249
|
+
),
|
|
250
|
+
].join('\n'),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function shellQuote(value: string): string {
|
|
255
|
+
return "'" + value.replace(/'/g, "'\\''") + "'"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function assertNoModuleCollision(
|
|
259
|
+
resolver: PackageResolver,
|
|
260
|
+
moduleId: string,
|
|
261
|
+
packageName: string,
|
|
262
|
+
locator?: string,
|
|
263
|
+
): void {
|
|
264
|
+
const registrations = resolver.loadEnabledModules().filter((entry) => entry.id === moduleId)
|
|
265
|
+
if (registrations.length > 1) {
|
|
266
|
+
throw new Error(`Module name collision: "${moduleId}" is registered multiple times from ${registrations.map((entry) => entry.locator ?? entry.from ?? '@open-mercato/core').join(', ')}. Resolve duplicate registrations before retrying.`)
|
|
267
|
+
}
|
|
268
|
+
const existing = registrations[0]
|
|
269
|
+
if (existing && (existing.from ?? '@open-mercato/core') !== packageName) {
|
|
270
|
+
throw new Error(`Module name collision: "${moduleId}" is already registered from "${existing.locator ?? existing.from ?? '@open-mercato/core'}"; requested source is "${locator ?? packageName}". Two sources cannot share one module id. Remove or rename the conflicting module explicitly before retrying.`)
|
|
271
|
+
}
|
|
272
|
+
const existingGithub = existing ? getModuleLocators(existing, resolver).find((value) => value.startsWith('github:')) : undefined
|
|
273
|
+
const requestedIdentity = locator ? parseModuleLocator(locator)?.identity : undefined
|
|
274
|
+
if (existingGithub && requestedIdentity && existingGithub !== requestedIdentity) {
|
|
275
|
+
throw new Error(`Module name collision: "${moduleId}" is already registered from "${existingGithub}"; requested source is "${locator}". Remove or rename the conflicting module explicitly before retrying.`)
|
|
276
|
+
}
|
|
277
|
+
if (!existing && fs.existsSync(path.join(resolver.getAppDir(), 'src', 'modules', moduleId))) {
|
|
278
|
+
throw new Error(`Module name collision: local src/modules/${moduleId} already exists; requested source is "${locator ?? packageName}". Remove or rename the conflicting module explicitly before retrying.`)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function assertBindingPreserved(resolver: PackageResolver, target: InspectedModuleTarget): void {
|
|
283
|
+
const consumers = resolver.loadEnabledModules().filter((entry) => (entry.from ?? '@open-mercato/core') === target.packageName)
|
|
284
|
+
for (const entry of consumers) {
|
|
285
|
+
const locators = getModuleLocators(entry, resolver)
|
|
286
|
+
if (!target.locator || !locators.includes(target.locator) || !target.moduleIds.includes(entry.id)) {
|
|
287
|
+
throw new Error(`Package binding collision: "${target.packageName}" supplies enabled module "${entry.id}" from "${locators.join(', ') || 'unknown origin'}". Requested "${target.locator ?? 'unknown origin'}" must preserve its source and module layout. Disable affected entries explicitly before replacing this binding. No package was installed.`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
226
292
|
function prepareRegistrationSource(
|
|
227
293
|
modulePackage: ValidatedOfficialModulePackage,
|
|
228
294
|
resolver: PackageResolver,
|
|
@@ -256,19 +322,38 @@ async function registerResolvedOfficialModule(
|
|
|
256
322
|
modulePackage: ValidatedOfficialModulePackage,
|
|
257
323
|
packageName: string,
|
|
258
324
|
eject: boolean,
|
|
325
|
+
requestedLocator?: string,
|
|
259
326
|
): Promise<ModuleCommandResult> {
|
|
260
327
|
assertModuleNotGatedOff(resolver.getModulesConfigPath(), modulePackage.metadata.moduleId)
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
328
|
+
const locators = getModuleLocators({ id: modulePackage.metadata.moduleId, from: packageName }, resolver)
|
|
329
|
+
if (requestedLocator && locators.length > 0 && !locators.includes(requestedLocator)) {
|
|
330
|
+
throw new Error(`Installed package "${packageName}" disagrees with inspected origin "${requestedLocator}". It is installed but not enabled. Restore the binding and lockfile through your package manager before retrying.`)
|
|
331
|
+
}
|
|
332
|
+
const locator = requestedLocator ?? locators[0]
|
|
333
|
+
assertNoModuleCollision(resolver, modulePackage.metadata.moduleId, packageName, locator)
|
|
334
|
+
assertRequiredModulesEnabled(modulePackage, resolver)
|
|
335
|
+
const existing = resolver.loadEnabledModules().find((entry) => entry.id === modulePackage.metadata.moduleId)
|
|
336
|
+
const from = eject ? '@app' : packageName
|
|
337
|
+
const provenance = {
|
|
338
|
+
...(locator ? { locator } : {}),
|
|
339
|
+
...(eject || modulePackage.packageName !== packageName ? { packageName: modulePackage.packageName } : {}),
|
|
340
|
+
}
|
|
341
|
+
const modulesPath = resolver.getModulesConfigPath()
|
|
342
|
+
const modulesSource = fs.readFileSync(modulesPath, 'utf8')
|
|
343
|
+
const registration = existing && eject
|
|
344
|
+
? { changed: true, source: prepareModuleRegistrationSource(modulesSource, modulePackage.metadata.moduleId, from, provenance) }
|
|
345
|
+
: prepareModuleRegistration(
|
|
346
|
+
modulesSource,
|
|
265
347
|
{
|
|
266
348
|
id: modulePackage.metadata.moduleId,
|
|
267
349
|
from,
|
|
350
|
+
...(locator || eject || modulePackage.packageName !== packageName ? provenance : {}),
|
|
268
351
|
},
|
|
269
352
|
)
|
|
270
353
|
|
|
354
|
+
prepareRegistrationSource(modulePackage, resolver, packageName, eject)
|
|
271
355
|
if (registration.changed) {
|
|
356
|
+
fs.writeFileSync(modulesPath, registration.source)
|
|
272
357
|
await runGeneratorsWithRegistrationNotice(resolver, modulePackage.metadata.moduleId)
|
|
273
358
|
}
|
|
274
359
|
|
|
@@ -287,24 +372,81 @@ export async function addOfficialModule(
|
|
|
287
372
|
moduleId?: string,
|
|
288
373
|
allowThirdParty = false,
|
|
289
374
|
): Promise<ModuleCommandResult> {
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
375
|
+
const input = packageSpec.trim()
|
|
376
|
+
getEnabledModuleIdentities(resolver)
|
|
377
|
+
const isGithub = !input.startsWith('@') && !input.startsWith('npm:') && (
|
|
378
|
+
input.includes('/') && !input.includes('@file:') || /^(?:github:|git@|(?:git\+)?ssh:)/i.test(input)
|
|
379
|
+
)
|
|
380
|
+
let safePackageSpec: string
|
|
381
|
+
let packageName: string
|
|
382
|
+
let target: InspectedModuleTarget
|
|
383
|
+
if (isGithub) {
|
|
384
|
+
if (!allowThirdParty) {
|
|
385
|
+
throw new Error('GitHub repository installs require --allow-third-party, independently of the declared npm package scope.')
|
|
386
|
+
}
|
|
387
|
+
target = resolveGithubModuleTarget(input, resolver.getAppDir(), { moduleId, enabledModules: resolver.loadEnabledModules() })
|
|
388
|
+
assertThirdPartyAllowed(target.packageName, allowThirdParty)
|
|
389
|
+
const selectedModuleId = moduleId ?? (target.moduleIds.length === 1 ? target.moduleIds[0] : undefined)
|
|
390
|
+
if (!selectedModuleId || !target.moduleIds.includes(selectedModuleId)) {
|
|
391
|
+
throw new Error(`Select a module with --module <moduleId>. Available in ${target.locator ?? target.packageName}: ${target.moduleIds.join(', ')}. No package was installed.`)
|
|
392
|
+
}
|
|
393
|
+
assertNoModuleCollision(resolver, selectedModuleId, target.packageName, target.locator)
|
|
394
|
+
safePackageSpec = target.packageSpec
|
|
395
|
+
packageName = target.packageName
|
|
396
|
+
} else {
|
|
397
|
+
safePackageSpec = assertSupportedPackageSpec(input, allowThirdParty)
|
|
398
|
+
const parsedName = parsePackageNameFromSpec(safePackageSpec)
|
|
399
|
+
assertPackageName(parsedName)
|
|
400
|
+
packageName = parsedName
|
|
401
|
+
if (moduleId) assertNoModuleCollision(resolver, moduleId, packageName, safePackageSpec.startsWith(`${packageName}@file:`) ? undefined : `npm:${packageName}`)
|
|
402
|
+
const requiredModuleIds = resolver.loadEnabledModules().filter((entry) => (entry.from ?? '@open-mercato/core') === packageName).map((entry) => entry.id)
|
|
403
|
+
target = inspectNpmModuleTarget(safePackageSpec, packageName, resolver.getAppDir(), [...new Set([...(moduleId ? [moduleId] : []), ...requiredModuleIds])])
|
|
404
|
+
}
|
|
405
|
+
const selectedModuleId = moduleId ?? (target.moduleIds.length === 1 ? target.moduleIds[0] : undefined)
|
|
406
|
+
if (!selectedModuleId || !target.moduleIds.includes(selectedModuleId)) {
|
|
407
|
+
throw new Error(`Select a module with --module <moduleId>. Available in ${target.locator ?? target.packageName}: ${target.moduleIds.join(', ')}. No package was installed.`)
|
|
408
|
+
}
|
|
409
|
+
assertNoModuleCollision(resolver, selectedModuleId, packageName, target.locator)
|
|
410
|
+
const existing = resolver.loadEnabledModules().find((entry) => entry.id === selectedModuleId)
|
|
411
|
+
if (!eject && existing && !target.locator
|
|
412
|
+
&& target.packageSpec.startsWith(`${packageName}@file:`)
|
|
413
|
+
&& getModuleLocators(existing, resolver).length === 0
|
|
414
|
+
&& isDeclaredAppDependency(resolver.getAppDir(), packageName, target.packageSpec.slice(packageName.length + 1))) {
|
|
415
|
+
return enableOfficialModule(resolver, packageName, selectedModuleId, false, allowThirdParty)
|
|
416
|
+
}
|
|
417
|
+
assertBindingPreserved(resolver, target)
|
|
418
|
+
const modulesSource = fs.readFileSync(resolver.getModulesConfigPath(), 'utf8')
|
|
419
|
+
const from = eject ? '@app' : packageName
|
|
420
|
+
if (existing && eject) prepareModuleRegistrationSource(modulesSource, selectedModuleId, from, { locator: target.locator, packageName })
|
|
421
|
+
else prepareModuleRegistration(modulesSource, { id: selectedModuleId, from, locator: target.locator })
|
|
422
|
+
if (eject && fs.existsSync(path.join(resolver.getAppDir(), 'src', 'modules', selectedModuleId))) {
|
|
423
|
+
throw new Error(`Destination directory already exists for "${selectedModuleId}". Preserve local source and resolve the conflict manually. No package was installed.`)
|
|
424
|
+
}
|
|
425
|
+
safePackageSpec = target.packageSpec
|
|
426
|
+
assertModuleNotGatedOff(resolver.getModulesConfigPath(), selectedModuleId)
|
|
294
427
|
const wasDeclaredBefore = isDeclaredAppDependency(resolver.getAppDir(), packageName)
|
|
295
428
|
const modulesConfigBefore = readFileIfExists(resolver.getModulesConfigPath())
|
|
296
|
-
|
|
297
429
|
await installPackageSpec(resolver, safePackageSpec)
|
|
298
430
|
|
|
299
431
|
try {
|
|
300
|
-
const modulePackage = resolveInstalledOfficialModulePackage(resolver, packageName,
|
|
301
|
-
|
|
432
|
+
const modulePackage = resolveInstalledOfficialModulePackage(resolver, packageName, selectedModuleId)
|
|
433
|
+
if (modulePackage.packageName !== packageName) throw new Error('Installed manifest differs from inspected package name.')
|
|
434
|
+
if (target.version && modulePackage.packageJson.version !== target.version) {
|
|
435
|
+
throw new Error(`Installed version "${modulePackage.packageJson.version ?? 'unknown'}" differs from inspected version "${target.version}". No registration or source edits were made.`)
|
|
436
|
+
}
|
|
437
|
+
for (const entry of resolver.loadEnabledModules().filter((entry) => (entry.from ?? '@open-mercato/core') === packageName)) {
|
|
438
|
+
resolveInstalledOfficialModulePackage(resolver, packageName, entry.id)
|
|
439
|
+
}
|
|
440
|
+
return await registerResolvedOfficialModule(resolver, modulePackage, packageName, eject, target.locator)
|
|
302
441
|
} catch (error) {
|
|
303
442
|
const moduleWasRegistered = readFileIfExists(resolver.getModulesConfigPath()) !== modulesConfigBefore
|
|
304
|
-
if (wasDeclaredBefore || moduleWasRegistered || isGatedOffError(error))
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
443
|
+
if (wasDeclaredBefore || moduleWasRegistered || isGatedOffError(error)) throw error
|
|
444
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
445
|
+
throw withLeftoverPackageHint(
|
|
446
|
+
new Error(`Package "${packageName}" is installed; enablement did not complete. Review src/modules.ts and restore the package binding/lockfile through the package manager if needed. ${message}`),
|
|
447
|
+
resolver,
|
|
448
|
+
packageName,
|
|
449
|
+
)
|
|
308
450
|
}
|
|
309
451
|
}
|
|
310
452
|
|
|
@@ -315,8 +457,14 @@ export async function enableOfficialModule(
|
|
|
315
457
|
eject = false,
|
|
316
458
|
allowThirdParty = false,
|
|
317
459
|
): Promise<ModuleCommandResult> {
|
|
318
|
-
|
|
319
|
-
|
|
460
|
+
if (!SAFE_PACKAGE_SPEC_PATTERN.test(packageName) || parsePackageNameFromSpec(packageName) !== packageName) {
|
|
461
|
+
throw new Error('Unsupported installed package binding. Use the installed npm package name or alias, without a locator, path or version selector.')
|
|
462
|
+
}
|
|
463
|
+
getEnabledModuleIdentities(resolver)
|
|
320
464
|
const modulePackage = resolveInstalledOfficialModulePackage(resolver, packageName, moduleId)
|
|
465
|
+
assertThirdPartyAllowed(modulePackage.packageName, allowThirdParty)
|
|
466
|
+
if (!allowThirdParty && getModuleLocators({ id: modulePackage.metadata.moduleId, from: packageName }, resolver).some((locator) => locator.startsWith('github:'))) {
|
|
467
|
+
throw new Error('GitHub repository installs require --allow-third-party, independently of the declared npm package scope.')
|
|
468
|
+
}
|
|
321
469
|
return registerResolvedOfficialModule(resolver, modulePackage, packageName, eject)
|
|
322
470
|
}
|