@open-mercato/cli 0.4.9-develop-94fb251ed3 → 0.4.9-develop-8d8db18714

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.
Files changed (58) hide show
  1. package/dist/agentic/shared/AGENTS.md.template +2 -0
  2. package/dist/agentic/shared/ai/skills/eject-and-customize/SKILL.md +3 -1
  3. package/dist/bin.js +1 -0
  4. package/dist/bin.js.map +2 -2
  5. package/dist/lib/__fixtures__/official-module-package/src/index.js +1 -0
  6. package/dist/lib/__fixtures__/official-module-package/src/index.js.map +7 -0
  7. package/dist/lib/__fixtures__/official-module-package/src/modules/test_package/index.js +10 -0
  8. package/dist/lib/__fixtures__/official-module-package/src/modules/test_package/index.js.map +7 -0
  9. package/dist/lib/eject.js +30 -38
  10. package/dist/lib/eject.js.map +2 -2
  11. package/dist/lib/generators/index.js +2 -0
  12. package/dist/lib/generators/index.js.map +2 -2
  13. package/dist/lib/generators/module-package-sources.js +45 -0
  14. package/dist/lib/generators/module-package-sources.js.map +7 -0
  15. package/dist/lib/module-install-args.js +40 -0
  16. package/dist/lib/module-install-args.js.map +7 -0
  17. package/dist/lib/module-install.js +157 -0
  18. package/dist/lib/module-install.js.map +7 -0
  19. package/dist/lib/module-package.js +245 -0
  20. package/dist/lib/module-package.js.map +7 -0
  21. package/dist/lib/modules-config.js +255 -0
  22. package/dist/lib/modules-config.js.map +7 -0
  23. package/dist/lib/resolver.js +19 -5
  24. package/dist/lib/resolver.js.map +2 -2
  25. package/dist/lib/testing/integration-discovery.js +20 -9
  26. package/dist/lib/testing/integration-discovery.js.map +2 -2
  27. package/dist/lib/testing/integration.js +86 -47
  28. package/dist/lib/testing/integration.js.map +2 -2
  29. package/dist/mercato.js +120 -43
  30. package/dist/mercato.js.map +3 -3
  31. package/package.json +5 -4
  32. package/src/__tests__/mercato.test.ts +6 -1
  33. package/src/bin.ts +1 -0
  34. package/src/lib/__fixtures__/official-module-package/dist/modules/test_package/index.js +2 -0
  35. package/src/lib/__fixtures__/official-module-package/package.json +33 -0
  36. package/src/lib/__fixtures__/official-module-package/src/index.ts +1 -0
  37. package/src/lib/__fixtures__/official-module-package/src/modules/test_package/index.ts +6 -0
  38. package/src/lib/__fixtures__/official-module-package/src/modules/test_package/widgets/injection/test/widget.tsx +3 -0
  39. package/src/lib/__tests__/eject.test.ts +107 -1
  40. package/src/lib/__tests__/module-install-args.test.ts +35 -0
  41. package/src/lib/__tests__/module-install.test.ts +217 -0
  42. package/src/lib/__tests__/module-package.test.ts +215 -0
  43. package/src/lib/__tests__/modules-config.test.ts +104 -0
  44. package/src/lib/__tests__/resolve-environment.test.ts +141 -0
  45. package/src/lib/eject.ts +45 -55
  46. package/src/lib/generators/__tests__/generators.test.ts +11 -0
  47. package/src/lib/generators/__tests__/module-package-sources.test.ts +121 -0
  48. package/src/lib/generators/index.ts +1 -0
  49. package/src/lib/generators/module-package-sources.ts +59 -0
  50. package/src/lib/module-install-args.ts +50 -0
  51. package/src/lib/module-install.ts +234 -0
  52. package/src/lib/module-package.ts +355 -0
  53. package/src/lib/modules-config.ts +393 -0
  54. package/src/lib/resolver.ts +46 -4
  55. package/src/lib/testing/__tests__/integration-discovery.test.ts +30 -0
  56. package/src/lib/testing/integration-discovery.ts +23 -8
  57. package/src/lib/testing/integration.ts +97 -57
  58. package/src/mercato.ts +128 -49
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { ensureModuleRegistration, setModuleRegistrationSource } from '../modules-config'
5
+
6
+ describe('modules-config', () => {
7
+ let tmpDir: string
8
+
9
+ beforeEach(() => {
10
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'modules-config-test-'))
11
+ })
12
+
13
+ afterEach(() => {
14
+ fs.rmSync(tmpDir, { recursive: true, force: true })
15
+ })
16
+
17
+ it('adds a package-backed module without touching conditional pushes', () => {
18
+ const filePath = path.join(tmpDir, 'modules.ts')
19
+ fs.writeFileSync(
20
+ filePath,
21
+ [
22
+ "export const enabledModules = [",
23
+ " { id: 'auth', from: '@open-mercato/core' },",
24
+ "]",
25
+ '',
26
+ "if (process.env.OM_ENABLE_ENTERPRISE_MODULES === 'true') {",
27
+ " enabledModules.push({ id: 'record_locks', from: '@open-mercato/enterprise' })",
28
+ '}',
29
+ ].join('\n'),
30
+ )
31
+
32
+ const result = ensureModuleRegistration(filePath, {
33
+ id: 'test_package',
34
+ from: '@open-mercato/test-package',
35
+ })
36
+
37
+ expect(result).toEqual({
38
+ changed: true,
39
+ registeredAs: '@open-mercato/test-package',
40
+ })
41
+
42
+ const updated = fs.readFileSync(filePath, 'utf8')
43
+ expect(updated).toContain("{ id: 'test_package', from: '@open-mercato/test-package' }")
44
+ expect(updated).toContain("enabledModules.push({ id: 'record_locks', from: '@open-mercato/enterprise' })")
45
+ })
46
+
47
+ it('returns no-op when the same registration already exists', () => {
48
+ const filePath = path.join(tmpDir, 'modules.ts')
49
+ fs.writeFileSync(
50
+ filePath,
51
+ [
52
+ "export const enabledModules = [",
53
+ " { id: 'test_package', from: '@open-mercato/test-package' },",
54
+ "]",
55
+ ].join('\n'),
56
+ )
57
+
58
+ const result = ensureModuleRegistration(filePath, {
59
+ id: 'test_package',
60
+ from: '@open-mercato/test-package',
61
+ })
62
+
63
+ expect(result).toEqual({
64
+ changed: false,
65
+ registeredAs: '@open-mercato/test-package',
66
+ })
67
+ })
68
+
69
+ it('fails when the module is already registered from another source', () => {
70
+ const filePath = path.join(tmpDir, 'modules.ts')
71
+ fs.writeFileSync(
72
+ filePath,
73
+ [
74
+ "export const enabledModules = [",
75
+ " { id: 'test_package', from: '@app' },",
76
+ "]",
77
+ ].join('\n'),
78
+ )
79
+
80
+ expect(() =>
81
+ ensureModuleRegistration(filePath, {
82
+ id: 'test_package',
83
+ from: '@open-mercato/test-package',
84
+ }),
85
+ ).toThrow('already registered from "@app"')
86
+ })
87
+
88
+ it('updates an existing module source to @app', () => {
89
+ const filePath = path.join(tmpDir, 'modules.ts')
90
+ fs.writeFileSync(
91
+ filePath,
92
+ [
93
+ "export const enabledModules = [",
94
+ " { id: 'test_package', from: '@open-mercato/test-package' },",
95
+ "]",
96
+ ].join('\n'),
97
+ )
98
+
99
+ const result = setModuleRegistrationSource(filePath, 'test_package', '@app')
100
+
101
+ expect(result).toEqual({ changed: true })
102
+ expect(fs.readFileSync(filePath, 'utf8')).toContain("{ id: 'test_package', from: '@app' }")
103
+ })
104
+ })
@@ -0,0 +1,141 @@
1
+ import os from 'node:os'
2
+ import path from 'node:path'
3
+ import { realpathSync } from 'node:fs'
4
+ import { mkdtemp, mkdir, rm, symlink } from 'node:fs/promises'
5
+ import { resolveEnvironment } from '../resolver'
6
+
7
+ const normalizePath = (p: string) => p.replace(/\\/g, '/')
8
+
9
+ async function makeDir(root: string, ...segments: string[]): Promise<string> {
10
+ const dir = path.join(root, ...segments)
11
+ await mkdir(dir, { recursive: true })
12
+ return dir
13
+ }
14
+
15
+ describe('resolveEnvironment', () => {
16
+ let tempRoot = ''
17
+
18
+ beforeEach(async () => {
19
+ // realpathSync resolves macOS /var -> /private/var symlink so comparisons are stable
20
+ tempRoot = realpathSync(await mkdtemp(path.join(os.tmpdir(), 'om-resolve-env-')))
21
+ })
22
+
23
+ afterEach(async () => {
24
+ if (tempRoot) {
25
+ await rm(tempRoot, { recursive: true, force: true })
26
+ }
27
+ })
28
+
29
+ describe('standalone — local node_modules', () => {
30
+ it('returns standalone mode with rootDir = cwd when @open-mercato/core is a real directory at cwd', async () => {
31
+ await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
32
+
33
+ const env = resolveEnvironment(tempRoot)
34
+
35
+ expect(env.mode).toBe('standalone')
36
+ expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
37
+ expect(normalizePath(env.appDir)).toBe(normalizePath(tempRoot))
38
+ })
39
+
40
+ it('resolves packageRoot under node_modules in standalone mode', async () => {
41
+ await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
42
+
43
+ const env = resolveEnvironment(tempRoot)
44
+
45
+ expect(normalizePath(env.packageRoot('@open-mercato/core'))).toBe(
46
+ normalizePath(path.join(tempRoot, 'node_modules', '@open-mercato', 'core')),
47
+ )
48
+ })
49
+
50
+ // In local mode (cwd === nodeModulesRoot), appDir === cwd.
51
+ // apps/ subdirectory detection only fires in the hoisted case — see hoisted tests below.
52
+ it('sets appDir to cwd even when apps/mercato subdirectory exists', async () => {
53
+ await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
54
+ await makeDir(tempRoot, 'apps', 'mercato')
55
+
56
+ const env = resolveEnvironment(tempRoot)
57
+
58
+ expect(env.mode).toBe('standalone')
59
+ expect(normalizePath(env.appDir)).toBe(normalizePath(tempRoot))
60
+ })
61
+ })
62
+
63
+ describe('standalone — hoisted node_modules', () => {
64
+ it('returns standalone mode with rootDir set to parent when @open-mercato/core is hoisted', async () => {
65
+ // Structure: tempRoot/node_modules/@open-mercato/core (real dir)
66
+ // tempRoot/myapp/ (the cwd)
67
+ await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
68
+ const childDir = await makeDir(tempRoot, 'myapp')
69
+
70
+ const env = resolveEnvironment(childDir)
71
+
72
+ expect(env.mode).toBe('standalone')
73
+ expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
74
+ // appDir falls back to childDir since no apps/ found at rootDir
75
+ expect(normalizePath(env.appDir)).toBe(normalizePath(childDir))
76
+ })
77
+
78
+ it('detects apps/mercato as appDir when hoisted and apps/mercato exists at rootDir', async () => {
79
+ // Structure: tempRoot/node_modules/@open-mercato/core (real dir)
80
+ // tempRoot/apps/mercato/ (app directory)
81
+ // tempRoot/myapp/ (the cwd — simulates running CLI from a sub-directory)
82
+ await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
83
+ await makeDir(tempRoot, 'apps', 'mercato')
84
+ const childDir = await makeDir(tempRoot, 'myapp')
85
+
86
+ const env = resolveEnvironment(childDir)
87
+
88
+ expect(env.mode).toBe('standalone')
89
+ expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
90
+ expect(normalizePath(env.appDir)).toBe(normalizePath(path.join(tempRoot, 'apps', 'mercato')))
91
+ })
92
+
93
+ it('resolves packageRoot under hoisted node_modules', async () => {
94
+ await makeDir(tempRoot, 'node_modules', '@open-mercato', 'core')
95
+ const childDir = await makeDir(tempRoot, 'myapp')
96
+
97
+ const env = resolveEnvironment(childDir)
98
+
99
+ expect(normalizePath(env.packageRoot('@open-mercato/core'))).toBe(
100
+ normalizePath(path.join(tempRoot, 'node_modules', '@open-mercato', 'core')),
101
+ )
102
+ })
103
+ })
104
+
105
+ describe('monorepo — symlinked node_modules', () => {
106
+ it('returns monorepo mode when @open-mercato/core is a symlink', async () => {
107
+ // Structure: tempRoot/packages/core (real source)
108
+ // tempRoot/node_modules/@open-mercato/core -> ../../packages/core (symlink)
109
+ const packagesCore = await makeDir(tempRoot, 'packages', 'core')
110
+ const nmOpen = await makeDir(tempRoot, 'node_modules', '@open-mercato')
111
+ await symlink(packagesCore, path.join(nmOpen, 'core'))
112
+
113
+ const env = resolveEnvironment(tempRoot)
114
+
115
+ expect(env.mode).toBe('monorepo')
116
+ expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
117
+ })
118
+
119
+ it('resolves packageRoot under packages/ in monorepo mode', async () => {
120
+ const packagesCore = await makeDir(tempRoot, 'packages', 'core')
121
+ const nmOpen = await makeDir(tempRoot, 'node_modules', '@open-mercato')
122
+ await symlink(packagesCore, path.join(nmOpen, 'core'))
123
+
124
+ const env = resolveEnvironment(tempRoot)
125
+
126
+ expect(normalizePath(env.packageRoot('@open-mercato/core'))).toBe(
127
+ normalizePath(path.join(tempRoot, 'packages', 'core')),
128
+ )
129
+ })
130
+ })
131
+
132
+ describe('no @open-mercato packages present', () => {
133
+ it('falls back to standalone mode with rootDir = cwd', async () => {
134
+ const env = resolveEnvironment(tempRoot)
135
+
136
+ expect(env.mode).toBe('standalone')
137
+ expect(normalizePath(env.rootDir)).toBe(normalizePath(tempRoot))
138
+ expect(normalizePath(env.appDir)).toBe(normalizePath(tempRoot))
139
+ })
140
+ })
141
+ })
package/src/lib/eject.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path'
2
2
  import fs from 'node:fs'
3
+ import { setModuleRegistrationSource } from './modules-config'
3
4
  import type { PackageResolver, ModuleEntry } from './resolver'
5
+ import { resolveInstalledOfficialModulePackage } from './module-package'
4
6
 
5
7
  type ModuleMetadata = {
6
8
  ejectable?: boolean
@@ -11,11 +13,15 @@ type ModuleMetadata = {
11
13
  const SKIP_DIRS = new Set(['__tests__', '__mocks__', 'node_modules'])
12
14
  const SOURCE_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']
13
15
 
16
+ function shouldSkipEntryName(name: string): boolean {
17
+ return SKIP_DIRS.has(name) || name === '.DS_Store' || name.startsWith('._')
18
+ }
19
+
14
20
  function collectSourceFiles(dir: string): string[] {
15
21
  const files: string[] = []
16
22
  const entries = fs.readdirSync(dir, { withFileTypes: true })
17
23
  for (const entry of entries) {
18
- if (SKIP_DIRS.has(entry.name)) continue
24
+ if (shouldSkipEntryName(entry.name)) continue
19
25
  const fullPath = path.join(dir, entry.name)
20
26
  if (entry.isDirectory()) {
21
27
  files.push(...collectSourceFiles(fullPath))
@@ -159,7 +165,7 @@ export function copyDirRecursive(src: string, dest: string): void {
159
165
 
160
166
  const entries = fs.readdirSync(src, { withFileTypes: true })
161
167
  for (const entry of entries) {
162
- if (SKIP_DIRS.has(entry.name)) continue
168
+ if (shouldSkipEntryName(entry.name)) continue
163
169
 
164
170
  const srcPath = path.join(src, entry.name)
165
171
  const destPath = path.join(dest, entry.name)
@@ -173,58 +179,47 @@ export function copyDirRecursive(src: string, dest: string): void {
173
179
  }
174
180
 
175
181
  export function updateModulesTs(modulesPath: string, moduleId: string): void {
176
- if (!fs.existsSync(modulesPath)) {
177
- throw new Error(`modules.ts not found at ${modulesPath}`)
178
- }
179
-
180
- const source = fs.readFileSync(modulesPath, 'utf8')
181
- const objectPattern = /\{[^{}]*\}/g
182
-
183
- let match: RegExpExecArray | null
184
- let updatedSource: string | null = null
185
-
186
- while ((match = objectPattern.exec(source)) !== null) {
187
- const objectLiteral = match[0]
188
- const idMatch = objectLiteral.match(/\bid\s*:\s*(['"])([^'"]+)\1/)
189
- if (!idMatch || idMatch[2] !== moduleId) {
190
- continue
191
- }
192
-
193
- const updatedObject = upsertModuleSource(objectLiteral)
194
- updatedSource =
195
- source.slice(0, match.index) +
196
- updatedObject +
197
- source.slice(match.index + objectLiteral.length)
198
- break
199
- }
182
+ setModuleRegistrationSource(modulesPath, moduleId, '@app')
183
+ }
200
184
 
201
- if (!updatedSource) {
202
- throw new Error(
203
- `Could not find module entry for "${moduleId}" in ${modulesPath}. ` +
204
- `Expected a pattern like: { id: '${moduleId}', from: '...' } or { id: '${moduleId}' }`,
205
- )
206
- }
185
+ export type EjectableModule = {
186
+ id: string
187
+ title?: string
188
+ description?: string
189
+ from: string
190
+ }
207
191
 
208
- fs.writeFileSync(modulesPath, updatedSource)
192
+ type ResolvedModuleSource = {
193
+ pkgBase: string
194
+ metadata: ModuleMetadata
209
195
  }
210
196
 
211
- function upsertModuleSource(objectLiteral: string): string {
212
- if (/from\s*:\s*'[^']*'/.test(objectLiteral)) {
213
- return objectLiteral.replace(/from\s*:\s*'[^']*'/, "from: '@app'")
214
- }
197
+ function resolveModuleSource(
198
+ resolver: PackageResolver,
199
+ entry: ModuleEntry,
200
+ ): ResolvedModuleSource {
201
+ const { pkgBase } = resolver.getModulePaths(entry)
202
+ const fallbackMetadata = parseModuleMetadata(path.join(pkgBase, 'index.ts'))
203
+ const from = entry.from || '@open-mercato/core'
215
204
 
216
- if (/from\s*:\s*"[^"]*"/.test(objectLiteral)) {
217
- return objectLiteral.replace(/from\s*:\s*"[^"]*"/, 'from: "@app"')
205
+ if (from === '@app' || from === '@open-mercato/core') {
206
+ return { pkgBase, metadata: fallbackMetadata }
218
207
  }
219
208
 
220
- return objectLiteral.replace(/\}\s*$/, ", from: '@app' }")
221
- }
209
+ try {
210
+ const modulePackage = resolveInstalledOfficialModulePackage(resolver, from, entry.id)
222
211
 
223
- export type EjectableModule = {
224
- id: string
225
- title?: string
226
- description?: string
227
- from: string
212
+ return {
213
+ pkgBase: modulePackage.sourceModuleDir,
214
+ metadata: {
215
+ ejectable: modulePackage.metadata.ejectable,
216
+ title: modulePackage.moduleInfo.title ?? modulePackage.metadata.moduleId,
217
+ description: modulePackage.moduleInfo.description,
218
+ },
219
+ }
220
+ } catch {
221
+ return { pkgBase, metadata: fallbackMetadata }
222
+ }
228
223
  }
229
224
 
230
225
  export function listEjectableModules(resolver: PackageResolver): EjectableModule[] {
@@ -234,9 +229,7 @@ export function listEjectableModules(resolver: PackageResolver): EjectableModule
234
229
  for (const entry of modules) {
235
230
  if (entry.from === '@app') continue
236
231
 
237
- const { pkgBase } = resolver.getModulePaths(entry)
238
- const indexPath = path.join(pkgBase, 'index.ts')
239
- const metadata = parseModuleMetadata(indexPath)
232
+ const { metadata } = resolveModuleSource(resolver, entry)
240
233
 
241
234
  if (metadata.ejectable) {
242
235
  ejectable.push({
@@ -268,7 +261,8 @@ export function ejectModule(resolver: PackageResolver, moduleId: string): void {
268
261
  )
269
262
  }
270
263
 
271
- const { pkgBase, appBase } = resolver.getModulePaths(entry)
264
+ const { appBase } = resolver.getModulePaths(entry)
265
+ const { pkgBase, metadata } = resolveModuleSource(resolver, entry)
272
266
 
273
267
  if (!fs.existsSync(pkgBase)) {
274
268
  throw new Error(
@@ -277,13 +271,9 @@ export function ejectModule(resolver: PackageResolver, moduleId: string): void {
277
271
  )
278
272
  }
279
273
 
280
- const indexPath = path.join(pkgBase, 'index.ts')
281
- const metadata = parseModuleMetadata(indexPath)
282
-
283
274
  if (!metadata.ejectable) {
284
275
  throw new Error(
285
- `Module "${moduleId}" is not marked as ejectable. ` +
286
- `Only modules with \`ejectable: true\` in their metadata can be ejected.`,
276
+ `Module "${moduleId}" is not marked as ejectable. Only modules with \`ejectable: true\` in their metadata can be ejected.`,
287
277
  )
288
278
  }
289
279
 
@@ -26,6 +26,11 @@ describe('generators', () => {
26
26
  expect(typeof module.generateModuleDi).toBe('function')
27
27
  })
28
28
 
29
+ it('should export generateModulePackageSources', async () => {
30
+ const module = await import('../module-package-sources')
31
+ expect(typeof module.generateModulePackageSources).toBe('function')
32
+ })
33
+
29
34
  // Note: api-client uses openapi-typescript which is ESM-only
30
35
  // and doesn't work with Jest's CommonJS environment
31
36
  it.skip('should export generateApiClient', async () => {
@@ -172,6 +177,12 @@ describe('generator file output patterns', () => {
172
177
  expect(expectedPath).toContain('injection-widgets.generated.ts')
173
178
  })
174
179
 
180
+ it('should output module package CSS sources', () => {
181
+ const outputDir = '/project/generated'
182
+ const expectedPath = `${outputDir}/module-package-sources.css`
183
+ expect(expectedPath).toContain('module-package-sources.css')
184
+ })
185
+
175
186
  it('should output search config', () => {
176
187
  const outputDir = '/project/generated'
177
188
  const expectedPath = `${outputDir}/search.generated.ts`
@@ -0,0 +1,121 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { generateModulePackageSources } from '../module-package-sources'
5
+ import type { PackageResolver } from '../../resolver'
6
+
7
+ const fixturePackageRoot = path.resolve(
8
+ __dirname,
9
+ '..',
10
+ '..',
11
+ '__fixtures__',
12
+ 'official-module-package',
13
+ )
14
+
15
+ function copyDir(src: string, dest: string): void {
16
+ fs.mkdirSync(dest, { recursive: true })
17
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
18
+ const srcPath = path.join(src, entry.name)
19
+ const destPath = path.join(dest, entry.name)
20
+ if (entry.isDirectory()) {
21
+ copyDir(srcPath, destPath)
22
+ } else {
23
+ fs.copyFileSync(srcPath, destPath)
24
+ }
25
+ }
26
+ }
27
+
28
+ function createResolver(tmpDir: string, packageRoot: string, from: string): PackageResolver {
29
+ const appDir = path.join(tmpDir, 'app')
30
+ const outputDir = path.join(appDir, '.mercato', 'generated')
31
+ fs.mkdirSync(outputDir, { recursive: true })
32
+
33
+ return {
34
+ isMonorepo: () => false,
35
+ getRootDir: () => appDir,
36
+ getAppDir: () => appDir,
37
+ getOutputDir: () => outputDir,
38
+ getModulesConfigPath: () => path.join(appDir, 'src', 'modules.ts'),
39
+ discoverPackages: () => [],
40
+ loadEnabledModules: () => [{ id: 'test_package', from }],
41
+ getModulePaths: () => ({
42
+ appBase: path.join(appDir, 'src', 'modules', 'test_package'),
43
+ pkgBase: path.join(packageRoot, 'src', 'modules', 'test_package'),
44
+ }),
45
+ getModuleImportBase: () => ({
46
+ appBase: '@/modules/test_package',
47
+ pkgBase: `${from}/modules/test_package`,
48
+ }),
49
+ getPackageOutputDir: () => outputDir,
50
+ getPackageRoot: () => packageRoot,
51
+ }
52
+ }
53
+
54
+ describe('generateModulePackageSources', () => {
55
+ let tmpDir: string
56
+
57
+ beforeEach(() => {
58
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'module-package-sources-test-'))
59
+ })
60
+
61
+ afterEach(() => {
62
+ fs.rmSync(tmpDir, { recursive: true, force: true })
63
+ })
64
+
65
+ it('writes @source entries for official package-backed modules', async () => {
66
+ const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
67
+ copyDir(fixturePackageRoot, packageRoot)
68
+ const resolver = createResolver(tmpDir, packageRoot, '@open-mercato/test-package')
69
+
70
+ const result = await generateModulePackageSources({ resolver, quiet: true })
71
+ expect(result.errors).toEqual([])
72
+
73
+ const output = fs.readFileSync(path.join(resolver.getOutputDir(), 'module-package-sources.css'), 'utf8')
74
+ expect(output).toContain('@source')
75
+ expect(output).toContain('node_modules/@open-mercato/test-package/src/**/*.{ts,tsx}')
76
+ })
77
+
78
+ it('skips app-backed modules', async () => {
79
+ const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
80
+ copyDir(fixturePackageRoot, packageRoot)
81
+ const resolver = createResolver(tmpDir, packageRoot, '@app')
82
+
83
+ await generateModulePackageSources({ resolver, quiet: true })
84
+
85
+ const output = fs.readFileSync(path.join(resolver.getOutputDir(), 'module-package-sources.css'), 'utf8')
86
+ expect(output).toBe('')
87
+ })
88
+
89
+ it('resolves hoisted package-backed modules for monorepo apps', async () => {
90
+ const appDir = path.join(tmpDir, 'apps', 'mercato')
91
+ const outputDir = path.join(appDir, '.mercato', 'generated')
92
+ const installedPackageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
93
+ copyDir(fixturePackageRoot, installedPackageRoot)
94
+ fs.mkdirSync(outputDir, { recursive: true })
95
+
96
+ const resolver = {
97
+ isMonorepo: () => true,
98
+ getRootDir: () => tmpDir,
99
+ getAppDir: () => appDir,
100
+ getOutputDir: () => outputDir,
101
+ getModulesConfigPath: () => path.join(appDir, 'src', 'modules.ts'),
102
+ discoverPackages: () => [],
103
+ loadEnabledModules: () => [{ id: 'test_package', from: '@open-mercato/test-package' }],
104
+ getModulePaths: () => ({
105
+ appBase: path.join(appDir, 'src', 'modules', 'test_package'),
106
+ pkgBase: path.join(installedPackageRoot, 'src', 'modules', 'test_package'),
107
+ }),
108
+ getModuleImportBase: () => ({
109
+ appBase: '@/modules/test_package',
110
+ pkgBase: '@open-mercato/test-package/modules/test_package',
111
+ }),
112
+ getPackageOutputDir: () => outputDir,
113
+ getPackageRoot: () => installedPackageRoot,
114
+ } as PackageResolver
115
+
116
+ await generateModulePackageSources({ resolver, quiet: true })
117
+
118
+ const output = fs.readFileSync(path.join(outputDir, 'module-package-sources.css'), 'utf8')
119
+ expect(output).toContain('node_modules/@open-mercato/test-package/src/**/*.{ts,tsx}')
120
+ })
121
+ })
@@ -2,4 +2,5 @@ export { generateEntityIds, type EntityIdsOptions } from './entity-ids'
2
2
  export { generateModuleRegistry, generateModuleRegistryCli, type ModuleRegistryOptions } from './module-registry'
3
3
  export { generateModuleEntities, type ModuleEntitiesOptions } from './module-entities'
4
4
  export { generateModuleDi, type ModuleDiOptions } from './module-di'
5
+ export { generateModulePackageSources, type ModulePackageSourcesOptions } from './module-package-sources'
5
6
  export { generateOpenApi, type GenerateOpenApiOptions } from './openapi'
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { readOfficialModulePackageFromRoot, resolveInstalledPackageRoot } from '../module-package'
4
+ import type { PackageResolver } from '../resolver'
5
+ import { calculateStructureChecksum, createGeneratorResult, type GeneratorResult, writeGeneratedFile } from '../utils'
6
+
7
+ export interface ModulePackageSourcesOptions {
8
+ resolver: PackageResolver
9
+ quiet?: boolean
10
+ }
11
+
12
+ function normalizeCssSourcePath(value: string): string {
13
+ const normalized = value.split(path.sep).join('/')
14
+ return normalized.startsWith('.') ? normalized : `./${normalized}`
15
+ }
16
+
17
+ export async function generateModulePackageSources(
18
+ options: ModulePackageSourcesOptions,
19
+ ): Promise<GeneratorResult> {
20
+ const { resolver, quiet } = options
21
+ const result = createGeneratorResult()
22
+ const outputDir = resolver.getOutputDir()
23
+ const outFile = path.join(outputDir, 'module-package-sources.css')
24
+ const checksumFile = path.join(outputDir, 'module-package-sources.checksum')
25
+ const sourcePaths = new Set<string>()
26
+ const checksumTargets: string[] = []
27
+
28
+ for (const entry of resolver.loadEnabledModules()) {
29
+ if (!entry.from || entry.from === '@app' || entry.from === '@open-mercato/core') continue
30
+
31
+ const packageRoot = resolveInstalledPackageRoot(resolver, entry.from)
32
+ checksumTargets.push(packageRoot)
33
+
34
+ let modulePackage
35
+ try {
36
+ modulePackage = readOfficialModulePackageFromRoot(packageRoot, entry.from, entry.id)
37
+ } catch {
38
+ continue
39
+ }
40
+
41
+ const packageSourceRoot = path.join(modulePackage.packageRoot, 'src')
42
+ const relativeSourcePath = normalizeCssSourcePath(path.relative(path.dirname(outFile), packageSourceRoot))
43
+ sourcePaths.add(`@source "${relativeSourcePath}/**/*.{ts,tsx}";`)
44
+ }
45
+
46
+ const content = `${Array.from(sourcePaths).sort((left, right) => left.localeCompare(right)).join('\n')}${sourcePaths.size > 0 ? '\n' : ''}`
47
+ const structureChecksum = calculateStructureChecksum(checksumTargets)
48
+
49
+ writeGeneratedFile({
50
+ outFile,
51
+ checksumFile,
52
+ content,
53
+ structureChecksum,
54
+ result,
55
+ quiet,
56
+ })
57
+
58
+ return result
59
+ }
@@ -0,0 +1,50 @@
1
+ export type ParsedModuleInstallArgs = {
2
+ packageSpec: string | null
3
+ eject: boolean
4
+ moduleId: string | null
5
+ }
6
+
7
+ export function parseModuleInstallArgs(args: string[]): ParsedModuleInstallArgs {
8
+ let packageSpec: string | null = null
9
+ let eject = false
10
+ let moduleId: string | null = null
11
+
12
+ for (let index = 0; index < args.length; index += 1) {
13
+ const arg = args[index]
14
+ if (!arg) continue
15
+
16
+ if (arg === '--eject') {
17
+ eject = true
18
+ continue
19
+ }
20
+
21
+ if (arg.startsWith('--eject=')) {
22
+ throw new Error('--eject does not accept a value')
23
+ }
24
+
25
+ if (arg === '--module') {
26
+ const next = args[index + 1]
27
+ if (next && !next.startsWith('-')) {
28
+ moduleId = next
29
+ index += 1
30
+ continue
31
+ }
32
+ throw new Error(`--module requires a moduleId value`)
33
+ }
34
+
35
+ if (arg.startsWith('--module=')) {
36
+ moduleId = arg.slice('--module='.length) || null
37
+ continue
38
+ }
39
+
40
+ if (arg.startsWith('-')) {
41
+ throw new Error(`Unsupported option: ${arg}`)
42
+ }
43
+
44
+ if (!arg.startsWith('-') && !packageSpec) {
45
+ packageSpec = arg
46
+ }
47
+ }
48
+
49
+ return { packageSpec, eject, moduleId }
50
+ }