@graphcommerce/next-config 9.1.0-canary.55 → 10.0.0-canary.56

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 (35) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/Config.graphqls +3 -3
  3. package/__tests__/config/utils/__snapshots__/mergeEnvIntoConfig.ts.snap +1 -1
  4. package/__tests__/interceptors/generateInterceptors.ts +133 -150
  5. package/dist/config/loadConfig.js +7 -0
  6. package/dist/generated/config.js +9 -9
  7. package/dist/index.js +804 -2436
  8. package/dist/loadConfig-nJiCKeL1.js +311 -0
  9. package/dist/utils/findParentPath.js +36 -0
  10. package/package.json +41 -20
  11. package/src/commands/cleanupInterceptors.ts +26 -0
  12. package/src/commands/codegen.ts +13 -15
  13. package/src/commands/codegenInterceptors.ts +31 -0
  14. package/src/{config/commands → commands}/exportConfig.ts +3 -3
  15. package/src/{config/commands → commands}/generateConfig.ts +12 -9
  16. package/src/commands/generateConfigValues.ts +265 -0
  17. package/src/commands/index.ts +7 -0
  18. package/src/config/index.ts +0 -9
  19. package/src/config/loadConfig.ts +0 -1
  20. package/src/config/utils/mergeEnvIntoConfig.ts +27 -4
  21. package/src/generated/config.ts +13 -14
  22. package/src/index.ts +7 -39
  23. package/src/interceptors/generateInterceptor.ts +192 -157
  24. package/src/interceptors/generateInterceptors.ts +9 -2
  25. package/src/interceptors/updatePackageExports.ts +147 -0
  26. package/src/interceptors/writeInterceptors.ts +90 -35
  27. package/src/types.ts +26 -0
  28. package/src/utils/index.ts +7 -0
  29. package/src/utils/resolveDependenciesSync.ts +5 -7
  30. package/src/withGraphCommerce.ts +30 -49
  31. package/tsconfig.json +3 -1
  32. package/__tests__/config/utils/configToImportMeta.ts +0 -121
  33. package/src/interceptors/InterceptorPlugin.ts +0 -141
  34. package/src/interceptors/commands/codegenInterceptors.ts +0 -27
  35. /package/src/utils/{isMonorepo.ts → findParentPath.ts} +0 -0
@@ -2,8 +2,6 @@ import prettierConf from '@graphcommerce/prettier-config-pwa'
2
2
  import prettier from 'prettier'
3
3
  import type { GraphCommerceDebugConfig } from '../generated/config'
4
4
  import type { ResolveDependencyReturn } from '../utils/resolveDependency'
5
- import { RenameVisitor } from './RenameVisitor'
6
- import { parseSync, printSync } from './swc'
7
5
 
8
6
  type PluginBaseConfig = {
9
7
  type: 'component' | 'function' | 'replace'
@@ -49,7 +47,7 @@ export function isMethodPluginConfig(
49
47
  /** @public */
50
48
  export function isReplacePluginConfig(
51
49
  plugin: Partial<PluginBaseConfig>,
52
- ): plugin is ReactPluginConfig {
50
+ ): plugin is ReplacePluginConfig {
53
51
  if (!isPluginBaseConfig(plugin)) return false
54
52
  return plugin.type === 'replace'
55
53
  }
@@ -63,14 +61,19 @@ export function isPluginConfig(plugin: Partial<PluginConfig>): plugin is PluginC
63
61
  export type Interceptor = ResolveDependencyReturn & {
64
62
  targetExports: Record<string, PluginConfig[]>
65
63
  target: string
66
- source: string
67
- template?: string
68
64
  }
69
65
 
70
- export type MaterializedPlugin = Interceptor & { template: string }
66
+ export type MaterializedPlugin = Interceptor & {
67
+ template: string
68
+ }
71
69
 
72
- export const SOURCE_START = '/** SOURCE_START */'
73
- export const SOURCE_END = '/** SOURCE_END */'
70
+ export function moveRelativeDown(plugins: PluginConfig[]) {
71
+ return [...plugins].sort((a, b) => {
72
+ if (a.sourceModule.startsWith('.') && !b.sourceModule.startsWith('.')) return 1
73
+ if (!a.sourceModule.startsWith('.') && b.sourceModule.startsWith('.')) return -1
74
+ return 0
75
+ })
76
+ }
74
77
 
75
78
  const originalSuffix = 'Original'
76
79
  const interceptorSuffix = 'Interceptor'
@@ -87,25 +90,29 @@ const sourceName = (n: string) => `${n}`
87
90
  const interceptorName = (n: string) => `${n}${interceptorSuffix}`
88
91
  const interceptorPropsName = (n: string) => `${n}Props`
89
92
 
90
- export function moveRelativeDown(plugins: PluginConfig[]) {
91
- return [...plugins].sort((a, b) => {
92
- if (a.sourceModule.startsWith('.') && !b.sourceModule.startsWith('.')) return 1
93
- if (!a.sourceModule.startsWith('.') && b.sourceModule.startsWith('.')) return -1
94
- return 0
95
- })
96
- }
97
-
98
93
  const generateIdentifyer = (s: string) =>
99
94
  Math.abs(
100
95
  s.split('').reduce((a, b) => {
101
- // eslint-disable-next-line no-param-reassign, no-bitwise
102
- a = (a << 5) - a + b.charCodeAt(0)
103
- // eslint-disable-next-line no-bitwise
104
- return a & a
96
+ const value = ((a << 5) - a + b.charCodeAt(0)) & 0xffffffff
97
+ return value < 0 ? value * -2 : value
105
98
  }, 0),
106
99
  ).toString()
107
100
 
108
- /** The is on the first line, with the format: /* hash:${identifer} */
101
+ // Create a stable string representation of an object by sorting keys recursively
102
+ const stableStringify = (obj: any): string => {
103
+ if (obj === null || obj === undefined) return String(obj)
104
+ if (typeof obj !== 'object') return String(obj)
105
+ if (Array.isArray(obj)) return `[${obj.map(stableStringify).join(',')}]`
106
+
107
+ const keys = Object.keys(obj).sort()
108
+ const pairs = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`)
109
+ return `{${pairs.join(',')}}`
110
+ }
111
+
112
+ /**
113
+ * Extract the identifier from the first line of the source file. The identifier is in the format:
114
+ * slash-star hash:${identifer} star-slash
115
+ */
109
116
  function extractIdentifier(source: string | undefined) {
110
117
  if (!source) return null
111
118
  const match = source.match(/\/\* hash:(\d+) \*\//)
@@ -118,18 +125,24 @@ export async function generateInterceptor(
118
125
  config: GraphCommerceDebugConfig,
119
126
  oldInterceptorSource?: string,
120
127
  ): Promise<MaterializedPlugin> {
121
- const identifer = generateIdentifyer(JSON.stringify(interceptor) + JSON.stringify(config))
128
+ // Create a stable hash based only on the content that affects the output
129
+ const hashInput = {
130
+ dependency: interceptor.dependency,
131
+ targetExports: interceptor.targetExports,
132
+ // Only include config properties that affect the output
133
+ debugConfig: config.pluginStatus ? { pluginStatus: config.pluginStatus } : {},
134
+ }
135
+ const identifer = generateIdentifyer(stableStringify(hashInput))
122
136
 
123
- const { dependency, targetExports, source } = interceptor
137
+ const { dependency, targetExports } = interceptor
124
138
 
125
139
  if (oldInterceptorSource && identifer === extractIdentifier(oldInterceptorSource))
126
140
  return { ...interceptor, template: oldInterceptorSource }
127
141
 
128
142
  const pluginConfigs = [...Object.entries(targetExports)].map(([, plugins]) => plugins).flat()
129
143
 
130
- // console.log('pluginConfigs', pluginConfigs)
144
+ // Generate plugin imports
131
145
  const duplicateImports = new Set()
132
-
133
146
  const pluginImports = moveRelativeDown(
134
147
  [...pluginConfigs].sort((a, b) => a.sourceModule.localeCompare(b.sourceModule)),
135
148
  )
@@ -142,128 +155,153 @@ export async function generateInterceptor(
142
155
  duplicateImports.add(str)
143
156
  return true
144
157
  })
145
- .join('\n')
146
-
147
- const ast = parseSync(source)
148
-
149
- new RenameVisitor(Object.keys(targetExports), (s) => originalName(s)).visitModule(ast)
150
-
151
- const pluginExports = Object.entries(targetExports)
152
- .map(([base, plugins]) => {
153
- const duplicateInterceptors = new Set()
154
-
155
- let carry = originalName(base)
156
- let carryProps: string[] = []
157
- const pluginSee: string[] = []
158
-
159
- pluginSee.push(
160
- `@see {@link file://${interceptor.sourcePathRelative}} for original source file`,
161
- )
162
-
163
- const pluginStr = plugins
164
- .reverse()
165
- .filter((p: PluginConfig) => {
166
- if (duplicateInterceptors.has(name(p))) return false
167
- duplicateInterceptors.add(name(p))
168
- return true
169
- })
170
- .map((p) => {
171
- let result
172
-
173
- const wrapChain = plugins
174
- .reverse()
175
- .map((pl) => name(pl))
176
- .join(' wrapping ')
177
-
178
- if (isReplacePluginConfig(p)) {
179
- new RenameVisitor([originalName(p.targetExport)], (s) =>
180
- s.replace(originalSuffix, disabledSuffix),
181
- ).visitModule(ast)
182
-
183
- carryProps.push(`React.ComponentProps<typeof ${sourceName(name(p))}>`)
184
-
185
- pluginSee.push(
186
- `@see {${sourceName(name(p))}} for replacement of the original source (original source not used)`,
187
- )
188
- }
189
-
190
- if (isReactPluginConfig(p)) {
191
- const withBraces = config.pluginStatus || process.env.NODE_ENV === 'development'
192
-
193
- result = `
194
- type ${interceptorPropsName(name(p))} = ${carryProps.join(' & ')} & OmitPrev<React.ComponentProps<typeof ${sourceName(name(p))}>, 'Prev'>
195
-
196
- const ${interceptorName(name(p))} = (props: ${interceptorPropsName(name(p))}) => ${withBraces ? '{' : '('}
197
- ${config.pluginStatus ? `logOnce(\`🔌 Rendering ${base} with plugin(s): ${wrapChain} wrapping <${base}/>\`)` : ''}
198
-
199
- ${
200
- process.env.NODE_ENV === 'development'
201
- ? `if(!props['data-plugin'])
202
- logOnce('${fileName(p)} does not spread props to prev: <Prev {...props}/>. This will cause issues if multiple plugins are applied to this component.')`
203
- : ''
204
- }
205
- ${withBraces ? 'return' : ''} <${sourceName(name(p))} {...props} Prev={${carry}} />
206
- ${withBraces ? '}' : ')'}`
207
-
208
- carryProps = [interceptorPropsName(name(p))]
209
- pluginSee.push(`@see {${sourceName(name(p))}} for source of applied plugin`)
210
- }
211
-
212
- if (isMethodPluginConfig(p)) {
213
- result = `const ${interceptorName(name(p))}: typeof ${carry} = (...args) => {
214
- ${config.pluginStatus ? `logOnce(\`🔌 Calling ${base} with plugin(s): ${wrapChain} wrapping ${base}()\`)` : ''}
215
- return ${sourceName(name(p))}(${carry}, ...args)
216
- }`
217
- pluginSee.push(`@see {${sourceName(name(p))}} for source of applied plugin`)
218
- }
219
-
220
- carry = p.type === 'replace' ? sourceName(name(p)) : interceptorName(name(p))
221
- return result
222
- })
223
- .filter((v) => !!v)
224
- .join('\n')
225
-
226
- const isComponent = plugins.every((p) => isReactPluginConfig(p))
227
- if (isComponent && plugins.some((p) => isMethodPluginConfig(p))) {
228
- throw new Error(`Cannot mix React and Method plugins for ${base} in ${dependency}.`)
229
- }
158
+ .join('\n ')
230
159
 
231
- const seeString = `
232
- /**
233
- * Here you see the 'interceptor' that is applying all the configured plugins.
234
- *
235
- * This file is NOT meant to be modified directly and is auto-generated if the plugins or the original source changes.
236
- *
237
- ${pluginSee.map((s) => `* ${s}`).join('\n')}
238
- */`
239
-
240
- if (process.env.NODE_ENV === 'development' && isComponent) {
241
- return `${pluginStr}
242
- ${seeString}
243
- export const ${base}: typeof ${carry} = (props) => {
244
- return <${carry} {...props} data-plugin />
245
- }`
246
- }
247
-
248
- return `
249
- ${pluginStr}
250
- ${seeString}
251
- export const ${base} = ${carry}
252
- `
160
+ // Generate imports for original components (only when no replace plugin exists)
161
+ const originalImports = [...Object.entries(targetExports)]
162
+ .filter(([targetExport, plugins]) => {
163
+ // Only import original if there's no replace plugin for this export
164
+ return !plugins.some((p) => p.type === 'replace')
253
165
  })
254
- .join('\n')
255
-
256
- const logOnce =
257
- config.pluginStatus || process.env.NODE_ENV === 'development'
258
- ? `
259
- const logged: Set<string> = new Set();
260
- const logOnce = (log: string, ...additional: unknown[]) => {
261
- if (logged.has(log)) return
262
- logged.add(log)
263
- console.warn(log, ...additional)
166
+ .map(([targetExport]) => {
167
+ const extension = interceptor.sourcePath.endsWith('.tsx') ? '.tsx' : '.ts'
168
+ const importPath = `./${interceptor.target.split('/').pop()}.original`
169
+ return `import { ${targetExport} as ${targetExport}${originalSuffix} } from '${importPath}'`
170
+ })
171
+ .join('\n ')
172
+
173
+ let logOnce = ''
174
+ // Note: logInterceptors config option removed for now
175
+ // if (config.logInterceptors)
176
+ // logOnce = `
177
+ // if (process.env.NODE_ENV === 'development') {
178
+ // console.log('🚦 Intercepted ${dependency}')
179
+ // }
180
+ // `
181
+
182
+ // Generate the plugin chain for each target export
183
+ const pluginExports = [...Object.entries(targetExports)]
184
+ .map(([targetExport, plugins]) => {
185
+ if (plugins.some((p) => p.type === 'component')) {
186
+ // Component plugins
187
+ const componentPlugins = plugins.filter((p) => p.type === 'component')
188
+
189
+ // Build interceptor chain - each plugin wraps the previous one
190
+ // Check if there's a replace plugin to use as the base instead of original
191
+ const replacePlugin = plugins.find((p) => p.type === 'replace')
192
+ let carry = replacePlugin
193
+ ? sourceName(name(replacePlugin))
194
+ : `${targetExport}${originalSuffix}`
195
+ const pluginSee: string[] = []
196
+
197
+ if (replacePlugin) {
198
+ pluginSee.push(
199
+ `@see {${sourceName(name(replacePlugin))}} for source of replaced component`,
200
+ )
201
+ } else {
202
+ pluginSee.push(`@see {@link file://./${targetExport}.tsx} for original source file`)
203
+ }
204
+
205
+ const pluginInterceptors = componentPlugins
206
+ .reverse() // Start from the last plugin and work backwards
207
+ .map((plugin) => {
208
+ const pluginName = sourceName(name(plugin))
209
+ const interceptorName = `${pluginName}${interceptorSuffix}`
210
+ const propsName = `${pluginName}Props`
211
+
212
+ pluginSee.push(`@see {${pluginName}} for source of applied plugin`)
213
+
214
+ const result = `type ${propsName} = OmitPrev<
215
+ React.ComponentProps<typeof ${pluginName}>,
216
+ 'Prev'
217
+ >
218
+
219
+ const ${interceptorName} = (
220
+ props: ${propsName},
221
+ ) => (
222
+ <${pluginName}
223
+ {...props}
224
+ Prev={${carry}}
225
+ />
226
+ )`
227
+ carry = interceptorName
228
+ return result
229
+ })
230
+ .join('\n\n')
231
+
232
+ const seeString = `/**
233
+ * Here you see the 'interceptor' that is applying all the configured plugins.
234
+ *
235
+ * This file is NOT meant to be modified directly and is auto-generated if the plugins or the
236
+ * original source changes.
237
+ *
238
+ ${pluginSee.map((s) => ` * ${s}`).join('\n')}
239
+ */`
240
+
241
+ return `${pluginInterceptors}
242
+
243
+ ${seeString}
244
+ export const ${targetExport} = ${carry}`
245
+ } else if (plugins.some((p) => p.type === 'function')) {
246
+ // Function plugins
247
+ const functionPlugins = plugins.filter((p) => p.type === 'function')
248
+
249
+ // Build interceptor chain - each plugin wraps the previous one
250
+ // Check if there's a replace plugin to use as the base instead of original
251
+ const replacePlugin = plugins.find((p) => p.type === 'replace')
252
+ let carry = replacePlugin
253
+ ? sourceName(name(replacePlugin))
254
+ : `${targetExport}${originalSuffix}`
255
+ const pluginSee: string[] = []
256
+
257
+ if (replacePlugin) {
258
+ pluginSee.push(
259
+ `@see {${sourceName(name(replacePlugin))}} for source of replaced function`,
260
+ )
261
+ } else {
262
+ pluginSee.push(`@see {@link file://./${targetExport}.ts} for original source file`)
263
+ }
264
+
265
+ const pluginInterceptors = functionPlugins
266
+ .reverse() // Start from the last plugin and work backwards
267
+ .map((plugin) => {
268
+ const pluginName = sourceName(name(plugin))
269
+ const interceptorName = `${pluginName}${interceptorSuffix}`
270
+
271
+ pluginSee.push(`@see {${pluginName}} for source of applied plugin`)
272
+
273
+ const result = `const ${interceptorName}: typeof ${carry} = (...args) => {
274
+ return ${pluginName}(${carry}, ...args)
275
+ }`
276
+ carry = interceptorName
277
+ return result
278
+ })
279
+ .join('\n')
280
+
281
+ const seeString = `/**
282
+ * Here you see the 'interceptor' that is applying all the configured plugins.
283
+ *
284
+ * This file is NOT meant to be modified directly and is auto-generated if the plugins or the
285
+ * original source changes.
286
+ *
287
+ ${pluginSee.map((s) => ` * ${s}`).join('\n')}
288
+ */`
289
+
290
+ return `${pluginInterceptors}
291
+
292
+ ${seeString}
293
+ export const ${targetExport} = ${carry}`
294
+ } else if (plugins.some((p) => p.type === 'replace')) {
295
+ // Replace plugins (just export the replacement)
296
+ const replacePlugin = plugins.find((p) => p.type === 'replace')
297
+ if (replacePlugin) {
298
+ return `export { ${replacePlugin.sourceExport} as ${targetExport} } from '${replacePlugin.sourceModule}'`
264
299
  }
265
- `
266
- : ''
300
+ }
301
+ return ''
302
+ })
303
+ .filter(Boolean)
304
+ .join('\n\n ')
267
305
 
268
306
  const template = `/* hash:${identifer} */
269
307
  /* eslint-disable */
@@ -276,21 +314,18 @@ export async function generateInterceptor(
276
314
 
277
315
  ${pluginImports}
278
316
 
279
- /** @see {@link file://${interceptor.sourcePathRelative}} for source of original */
280
- ${SOURCE_START}
281
- ${printSync(ast).code}
282
- ${SOURCE_END}
317
+ ${originalImports}
318
+
319
+ // Re-export everything from the original file except the intercepted exports
320
+ export * from './${interceptor.target.split('/').pop()}.original'
321
+
283
322
  ${logOnce}${pluginExports}
284
323
  `
285
324
 
286
- let templateFormatted
287
- try {
288
- templateFormatted = await prettier.format(template, { ...prettierConf, parser: 'typescript' })
289
- } catch (e) {
290
- // eslint-disable-next-line no-console
291
- console.log('Error formatting interceptor: ', e, 'using raw template.')
292
- templateFormatted = template
293
- }
325
+ const formatted = await prettier.format(template, {
326
+ ...prettierConf,
327
+ parser: 'typescript',
328
+ })
294
329
 
295
- return { ...interceptor, template: templateFormatted }
330
+ return { ...interceptor, template: formatted }
296
331
  }
@@ -43,7 +43,7 @@ export async function generateInterceptors(
43
43
  if (!acc[resolved.fromRoot]) {
44
44
  acc[resolved.fromRoot] = {
45
45
  ...resolved,
46
- target: `${resolved.fromRoot}.interceptor`,
46
+ target: resolved.fromRoot,
47
47
  targetExports: {},
48
48
  } as Interceptor
49
49
  }
@@ -61,7 +61,14 @@ export async function generateInterceptors(
61
61
  return Object.fromEntries(
62
62
  await Promise.all(
63
63
  Object.entries(byTargetModuleAndExport).map(async ([target, interceptor]) => {
64
- const file = `${interceptor.fromRoot}.interceptor.tsx`
64
+ // In the new system, we don't look for existing .interceptor files
65
+ // Instead, we check if we need to regenerate based on the main file
66
+ const mainFile = `${interceptor.fromRoot}.tsx`
67
+ const tsFile = `${interceptor.fromRoot}.ts`
68
+
69
+ // Try .tsx first, then .ts
70
+ const extension = interceptor.sourcePath.endsWith('.tsx') ? '.tsx' : '.ts'
71
+ const file = `${interceptor.fromRoot}${extension}`
65
72
 
66
73
  const originalSource =
67
74
  !force &&
@@ -0,0 +1,147 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { sync as globSync } from 'glob'
4
+ import { packageRoots } from '../utils'
5
+ import { resolveDependenciesSync } from '../utils/resolveDependenciesSync'
6
+ import type { PluginConfig } from './generateInterceptor'
7
+
8
+ export interface PackageJson {
9
+ name: string
10
+ exports?: Record<string, string | { types?: string; default?: string }>
11
+ [key: string]: any
12
+ }
13
+
14
+ /** Updates package.json exports to include all plugin files automatically */
15
+ export async function updatePackageExports(
16
+ plugins: PluginConfig[],
17
+ cwd: string = process.cwd(),
18
+ ): Promise<void> {
19
+ // Use packageRoots to discover ALL packages in the monorepo, not just dependencies
20
+ const deps = resolveDependenciesSync()
21
+ const packages = [...deps.values()].filter((p) => p !== '.')
22
+ const roots = packageRoots(packages)
23
+
24
+ console.log(`🔍 Scanning ${roots.length} package roots for plugins...`)
25
+
26
+ // Group plugins by package - discover ALL plugin files in ALL packages in the monorepo
27
+ const pluginsByPackage = new Map<string, Set<string>>()
28
+
29
+ // Scan all individual packages within the package roots
30
+ for (const root of roots) {
31
+ // Find all package directories within this root
32
+ const packageDirs = globSync(`${root}/*/package.json`).map((pkgPath) => path.dirname(pkgPath))
33
+
34
+ for (const packagePath of packageDirs) {
35
+ const pluginFiles = globSync(`${packagePath}/plugins/**/*.{ts,tsx}`)
36
+
37
+ if (pluginFiles.length > 0) {
38
+ const exportPaths = new Set<string>()
39
+
40
+ pluginFiles.forEach((file) => {
41
+ // Convert file path to export path
42
+ const relativePath = path.relative(packagePath, file)
43
+ const exportPath = `./${relativePath.replace(/\.(ts|tsx)$/, '')}`
44
+ exportPaths.add(exportPath)
45
+ })
46
+
47
+ if (exportPaths.size > 0) {
48
+ const packageJsonPath = path.join(packagePath, 'package.json')
49
+ try {
50
+ // Read package.json to get the package name for logging
51
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8')
52
+ const packageJson = JSON.parse(packageJsonContent)
53
+ const packageName = packageJson.name || path.basename(packagePath)
54
+
55
+ pluginsByPackage.set(packagePath, exportPaths)
56
+ // console.log(`🔍 Found ${exportPaths.size} plugin files in ${packageName}`)
57
+ } catch (error) {
58
+ console.warn(`⚠️ Could not read package.json for ${packagePath}:`, error)
59
+ }
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ console.log(`📦 Total packages with plugins: ${pluginsByPackage.size}`)
66
+
67
+ // Update package.json for each package that has plugins
68
+ const updatePromises = Array.from(pluginsByPackage.entries()).map(
69
+ async ([packagePath, exportPaths]) => {
70
+ const packageJsonPath = path.join(packagePath, 'package.json')
71
+
72
+ try {
73
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8')
74
+ const packageJson: PackageJson = JSON.parse(packageJsonContent)
75
+
76
+ // Initialize exports if it doesn't exist
77
+ if (!packageJson.exports) {
78
+ packageJson.exports = { '.': './index.ts' }
79
+ }
80
+
81
+ // Ensure main export exists
82
+ if (typeof packageJson.exports === 'object' && !packageJson.exports['.']) {
83
+ packageJson.exports['.'] = './index.ts'
84
+ }
85
+
86
+ let hasChanges = false
87
+
88
+ // Add plugin exports
89
+ exportPaths.forEach((exportPath) => {
90
+ const exportKey = exportPath.startsWith('./') ? exportPath : `./${exportPath}`
91
+ const filePath = `${exportPath}.tsx`
92
+ const tsFilePath = `${exportPath}.ts`
93
+
94
+ // Check if .tsx or .ts file exists
95
+ const targetFile = globSync(path.join(packagePath, `${exportPath.slice(2)}.{ts,tsx}`))[0]
96
+ if (targetFile) {
97
+ const extension = targetFile.endsWith('.tsx') ? '.tsx' : '.ts'
98
+ const targetPath = `${exportPath}${extension}`
99
+
100
+ if (packageJson.exports && !packageJson.exports[exportKey]) {
101
+ packageJson.exports[exportKey] = targetPath
102
+ hasChanges = true
103
+ }
104
+ }
105
+ })
106
+
107
+ if (hasChanges) {
108
+ // Sort exports for consistency (. first, then alphabetically)
109
+ const sortedExports: Record<string, string | { types?: string; default?: string }> = {}
110
+
111
+ if (packageJson.exports['.']) {
112
+ sortedExports['.'] = packageJson.exports['.']
113
+ }
114
+
115
+ Object.keys(packageJson.exports)
116
+ .filter((key) => key !== '.')
117
+ .sort()
118
+ .forEach((key) => {
119
+ sortedExports[key] = packageJson.exports![key]
120
+ })
121
+
122
+ packageJson.exports = sortedExports
123
+
124
+ const updatedContent = JSON.stringify(packageJson, null, 2) + '\n'
125
+ await fs.writeFile(packageJsonPath, updatedContent)
126
+
127
+ console.log(`✅ Updated exports in ${packageJson.name}`)
128
+
129
+ // Log the new exports
130
+ const newExports = Object.keys(packageJson.exports).filter((key) => key !== '.')
131
+ if (newExports.length > 0) {
132
+ console.log(` Added exports: ${newExports.join(', ')}`)
133
+ }
134
+ } else {
135
+ // // Log packages that were scanned but had no changes
136
+ // console.log(
137
+ // `ℹ️ No changes needed for ${packageJson.name} (${exportPaths.size} plugins already exported)`,
138
+ // )
139
+ }
140
+ } catch (error) {
141
+ console.error(`❌ Failed to update package.json for ${packagePath}:`, error)
142
+ }
143
+ },
144
+ )
145
+
146
+ await Promise.all(updatePromises)
147
+ }