@graphcommerce/next-config 8.1.0-canary.2 → 8.1.0-canary.5

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 (44) hide show
  1. package/CHANGELOG.md +133 -1
  2. package/Config.graphqls +4 -2
  3. package/__tests__/config/utils/__snapshots__/mergeEnvIntoConfig.ts.snap +19 -2
  4. package/__tests__/config/utils/replaceConfigInString.ts +4 -0
  5. package/__tests__/interceptors/findPlugins.ts +473 -113
  6. package/__tests__/interceptors/generateInterceptors.ts +610 -322
  7. package/__tests__/interceptors/parseStructure.ts +158 -0
  8. package/__tests__/interceptors/writeInterceptors.ts +23 -14
  9. package/__tests__/utils/resolveDependenciesSync.ts +28 -25
  10. package/dist/config/commands/generateConfig.js +5 -2
  11. package/dist/config/demoConfig.js +19 -4
  12. package/dist/generated/config.js +8 -1
  13. package/dist/interceptors/InterceptorPlugin.js +70 -25
  14. package/dist/interceptors/RenameVisitor.js +19 -0
  15. package/dist/interceptors/Visitor.js +1418 -0
  16. package/dist/interceptors/extractExports.js +201 -0
  17. package/dist/interceptors/findOriginalSource.js +87 -0
  18. package/dist/interceptors/findPlugins.js +21 -53
  19. package/dist/interceptors/generateInterceptor.js +200 -0
  20. package/dist/interceptors/generateInterceptors.js +38 -179
  21. package/dist/interceptors/parseStructure.js +71 -0
  22. package/dist/interceptors/swc.js +16 -0
  23. package/dist/interceptors/writeInterceptors.js +19 -10
  24. package/dist/utils/resolveDependency.js +27 -5
  25. package/dist/withGraphCommerce.js +2 -1
  26. package/package.json +4 -1
  27. package/src/config/commands/generateConfig.ts +5 -2
  28. package/src/config/demoConfig.ts +19 -4
  29. package/src/config/index.ts +4 -2
  30. package/src/generated/config.ts +25 -3
  31. package/src/index.ts +16 -6
  32. package/src/interceptors/InterceptorPlugin.ts +90 -32
  33. package/src/interceptors/RenameVisitor.ts +17 -0
  34. package/src/interceptors/Visitor.ts +1847 -0
  35. package/src/interceptors/extractExports.ts +230 -0
  36. package/src/interceptors/findOriginalSource.ts +105 -0
  37. package/src/interceptors/findPlugins.ts +36 -87
  38. package/src/interceptors/generateInterceptor.ts +271 -0
  39. package/src/interceptors/generateInterceptors.ts +67 -237
  40. package/src/interceptors/parseStructure.ts +82 -0
  41. package/src/interceptors/swc.ts +13 -0
  42. package/src/interceptors/writeInterceptors.ts +26 -10
  43. package/src/utils/resolveDependency.ts +51 -12
  44. package/src/withGraphCommerce.ts +2 -1
@@ -0,0 +1,271 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import prettierConf from '@graphcommerce/prettier-config-pwa'
3
+ // eslint-disable-next-line import/no-extraneous-dependencies
4
+ import prettier from 'prettier'
5
+ import { GraphCommerceDebugConfig } from '../generated/config'
6
+ import { ResolveDependencyReturn } from '../utils/resolveDependency'
7
+ import { RenameVisitor } from './RenameVisitor'
8
+ import { parseSync, printSync } from './swc'
9
+
10
+ type PluginBaseConfig = {
11
+ type: 'component' | 'function' | 'replace'
12
+ targetModule: string
13
+ sourceExport: string
14
+ sourceModule: string
15
+ targetExport: string
16
+ enabled: boolean
17
+ ifConfig?: string | [string, string]
18
+ }
19
+
20
+ export function isPluginBaseConfig(plugin: Partial<PluginBaseConfig>): plugin is PluginBaseConfig {
21
+ return (
22
+ typeof plugin.type === 'string' &&
23
+ typeof plugin.sourceModule === 'string' &&
24
+ typeof plugin.enabled === 'boolean' &&
25
+ typeof plugin.targetExport === 'string'
26
+ )
27
+ }
28
+
29
+ type ReactPluginConfig = PluginBaseConfig & { type: 'component' }
30
+ type MethodPluginConfig = PluginBaseConfig & { type: 'function' }
31
+ type ReplacePluginConfig = PluginBaseConfig & { type: 'replace' }
32
+
33
+ export function isReactPluginConfig(
34
+ plugin: Partial<PluginBaseConfig>,
35
+ ): plugin is ReactPluginConfig {
36
+ if (!isPluginBaseConfig(plugin)) return false
37
+ return plugin.type === 'component'
38
+ }
39
+
40
+ export function isMethodPluginConfig(
41
+ plugin: Partial<PluginBaseConfig>,
42
+ ): plugin is MethodPluginConfig {
43
+ if (!isPluginBaseConfig(plugin)) return false
44
+ return plugin.type === 'function'
45
+ }
46
+
47
+ export function isReplacePluginConfig(
48
+ plugin: Partial<PluginBaseConfig>,
49
+ ): plugin is ReactPluginConfig {
50
+ if (!isPluginBaseConfig(plugin)) return false
51
+ return plugin.type === 'replace'
52
+ }
53
+
54
+ export type PluginConfig = ReactPluginConfig | MethodPluginConfig | ReplacePluginConfig
55
+
56
+ export function isPluginConfig(plugin: Partial<PluginConfig>): plugin is PluginConfig {
57
+ return isPluginBaseConfig(plugin)
58
+ }
59
+
60
+ export type Interceptor = ResolveDependencyReturn & {
61
+ targetExports: Record<string, PluginConfig[]>
62
+ target: string
63
+ source: string
64
+ template?: string
65
+ }
66
+
67
+ export type MaterializedPlugin = Interceptor & { template: string }
68
+
69
+ export const SOURCE_START = '/** ❗️ Original (modified) source starts here **/'
70
+ export const SOURCE_END = '/** ❗️ Original (modified) source ends here **/'
71
+
72
+ const originalSuffix = 'Original'
73
+ const sourceSuffix = 'Source'
74
+ const interceptorSuffix = 'Interceptor'
75
+ const disabledSuffix = 'Disabled'
76
+ const name = (plugin: PluginConfig) =>
77
+ `${plugin.sourceExport}${plugin.sourceModule
78
+ .split('/')
79
+ [plugin.sourceModule.split('/').length - 1].replace(/[^a-zA-Z0-9]/g, '')}`
80
+
81
+ const fileName = (plugin: PluginConfig) => `${plugin.sourceModule}#${plugin.sourceExport}`
82
+
83
+ const originalName = (n: string) => `${n}${originalSuffix}`
84
+ const sourceName = (n: string) => `${n}${sourceSuffix}`
85
+ const interceptorName = (n: string) => `${n}${interceptorSuffix}`
86
+ const interceptorPropsName = (n: string) => `${interceptorName(n)}Props`
87
+
88
+ export function moveRelativeDown(plugins: PluginConfig[]) {
89
+ return [...plugins].sort((a, b) => {
90
+ if (a.sourceModule.startsWith('.') && !b.sourceModule.startsWith('.')) return 1
91
+ if (!a.sourceModule.startsWith('.') && b.sourceModule.startsWith('.')) return -1
92
+ return 0
93
+ })
94
+ }
95
+
96
+ const generateIdentifyer = (s: string) =>
97
+ Math.abs(
98
+ s.split('').reduce((a, b) => {
99
+ // eslint-disable-next-line no-param-reassign, no-bitwise
100
+ a = (a << 5) - a + b.charCodeAt(0)
101
+ // eslint-disable-next-line no-bitwise
102
+ return a & a
103
+ }, 0),
104
+ ).toString()
105
+
106
+ /**
107
+ * The is on the first line, with the format: \/* hash:${identifer} *\/
108
+ */
109
+ function extractIdentifier(source: string | undefined) {
110
+ if (!source) return null
111
+ const match = source.match(/\/\* hash:(\d+) \*\//)
112
+ if (!match) return null
113
+ return match[1]
114
+ }
115
+
116
+ export async function generateInterceptor(
117
+ interceptor: Interceptor,
118
+ config: GraphCommerceDebugConfig,
119
+ oldInterceptorSource?: string,
120
+ ): Promise<MaterializedPlugin> {
121
+ const identifer = generateIdentifyer(JSON.stringify(interceptor) + JSON.stringify(config))
122
+
123
+ const { dependency, targetExports, source } = interceptor
124
+
125
+ if (oldInterceptorSource && identifer === extractIdentifier(oldInterceptorSource))
126
+ return { ...interceptor, template: oldInterceptorSource }
127
+
128
+ const pluginConfigs = [...Object.entries(targetExports)].map(([, plugins]) => plugins).flat()
129
+
130
+ // console.log('pluginConfigs', pluginConfigs)
131
+ const duplicateImports = new Set()
132
+
133
+ const pluginImports = moveRelativeDown(
134
+ [...pluginConfigs].sort((a, b) => a.sourceModule.localeCompare(b.sourceModule)),
135
+ )
136
+ .map(
137
+ (plugin) =>
138
+ `import { ${plugin.sourceExport} as ${sourceName(name(plugin))} } from '${plugin.sourceModule}'`,
139
+ )
140
+ .filter((str) => {
141
+ if (duplicateImports.has(str)) return false
142
+ duplicateImports.add(str)
143
+ return true
144
+ })
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
+ const carryProps: string[] = []
157
+
158
+ const pluginStr = plugins
159
+ .reverse()
160
+ .filter((p: PluginConfig) => {
161
+ if (duplicateInterceptors.has(name(p))) return false
162
+ duplicateInterceptors.add(name(p))
163
+ return true
164
+ })
165
+ .map((p) => {
166
+ let result
167
+
168
+ const wrapChain = plugins
169
+ .reverse()
170
+ .map((pl) => name(pl))
171
+ .join(' wrapping ')
172
+
173
+ if (isReplacePluginConfig(p)) {
174
+ new RenameVisitor([originalName(p.targetExport)], (s) =>
175
+ s.replace(originalSuffix, disabledSuffix),
176
+ ).visitModule(ast)
177
+
178
+ carryProps.push(interceptorPropsName(name(p)))
179
+
180
+ result = `type ${interceptorPropsName(name(p))} = React.ComponentProps<typeof ${sourceName(name(p))}>`
181
+ }
182
+
183
+ if (isReactPluginConfig(p)) {
184
+ carryProps.push(interceptorPropsName(name(p)))
185
+
186
+ result = `
187
+ type ${interceptorPropsName(name(p))} = DistributedOmit<React.ComponentProps<typeof ${sourceName(name(p))}>, 'Prev'>
188
+ const ${interceptorName(name(p))} = (props: ${carryProps.join(' & ')}) => {
189
+ ${config.pluginStatus ? `logOnce(\`🔌 Rendering ${base} with plugin(s): ${wrapChain} wrapping <${base}/>\`)` : ''}
190
+
191
+ ${
192
+ process.env.NODE_ENV === 'development'
193
+ ? `if(!props['data-plugin'])
194
+ logOnce('${fileName(p)} does not spread props to prev: <Prev {...props}/>. This will cause issues if multiple plugins are applied to this component.')`
195
+ : ''
196
+ }
197
+ return <${sourceName(name(p))} {...props} Prev={${carry} as React.FC} />
198
+ }`
199
+ }
200
+
201
+ if (isMethodPluginConfig(p)) {
202
+ result = `const ${interceptorName(name(p))}: typeof ${carry} = (...args) => {
203
+ ${config.pluginStatus ? `logOnce(\`🔌 Calling ${base} with plugin(s): ${wrapChain} wrapping ${base}()\`)` : ''}
204
+ return ${sourceName(name(p))}(${carry}, ...args)
205
+ }`
206
+ }
207
+
208
+ carry = p.type === 'replace' ? sourceName(name(p)) : interceptorName(name(p))
209
+ return result
210
+ })
211
+ .filter((v) => !!v)
212
+ .join('\n')
213
+
214
+ const isComponent = plugins.every((p) => isReplacePluginConfig(p) || isReactPluginConfig(p))
215
+ if (isComponent && plugins.some((p) => isMethodPluginConfig(p))) {
216
+ throw new Error(`Cannot mix React and Method plugins for ${base} in ${dependency}.`)
217
+ }
218
+
219
+ if (process.env.NODE_ENV === 'development' && isComponent) {
220
+ return `${pluginStr}
221
+ export const ${base}: typeof ${carry} = (props) => {
222
+ return <${carry} {...props} data-plugin />
223
+ }`
224
+ }
225
+
226
+ return `
227
+ ${pluginStr}
228
+ export const ${base} = ${carry}
229
+ `
230
+ })
231
+ .join('\n')
232
+
233
+ const logOnce =
234
+ config.pluginStatus || process.env.NODE_ENV === 'development'
235
+ ? `
236
+ const logged: Set<string> = new Set();
237
+ const logOnce = (log: string, ...additional: unknown[]) => {
238
+ if (logged.has(log)) return
239
+ logged.add(log)
240
+ console.warn(log, ...additional)
241
+ }
242
+ `
243
+ : ''
244
+
245
+ const template = `/* hash:${identifer} */
246
+ /* eslint-disable */
247
+ /* This file is automatically generated for ${dependency} */
248
+ ${
249
+ Object.values(targetExports).some((t) => t.some((p) => p.type === 'component'))
250
+ ? `import type { DistributedOmit } from 'type-fest'`
251
+ : ''
252
+ }
253
+
254
+ ${pluginImports}
255
+
256
+ ${SOURCE_START}
257
+ ${printSync(ast).code}
258
+ ${SOURCE_END}
259
+ ${logOnce}${pluginExports}
260
+ `
261
+
262
+ let templateFormatted
263
+ try {
264
+ templateFormatted = await prettier.format(template, { ...prettierConf, parser: 'typescript' })
265
+ } catch (e) {
266
+ console.log('Error formatting interceptor: ', e, 'using raw template.')
267
+ templateFormatted = template
268
+ }
269
+
270
+ return { ...interceptor, template: templateFormatted }
271
+ }
@@ -1,255 +1,85 @@
1
1
  import path from 'node:path'
2
+ import fs from 'node:fs/promises'
3
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
4
  import { GraphCommerceDebugConfig } from '../generated/config'
3
- import { ResolveDependency, ResolveDependencyReturn } from '../utils/resolveDependency'
4
-
5
- type PluginBaseConfig = {
6
- exported: string
7
- plugin: string
8
- enabled: boolean
9
- ifConfig?: string
10
- }
11
- export function isPluginBaseConfig(plugin: Partial<PluginBaseConfig>): plugin is PluginBaseConfig {
12
- return (
13
- typeof plugin.exported === 'string' &&
14
- typeof plugin.plugin === 'string' &&
15
- typeof plugin.enabled === 'boolean'
16
- )
17
- }
18
-
19
- type ReactPluginConfig = PluginBaseConfig & { component: string }
20
- type MethodPluginConfig = PluginBaseConfig & { func: string }
21
-
22
- export function isReactPluginConfig(
23
- plugin: Partial<PluginBaseConfig>,
24
- ): plugin is ReactPluginConfig {
25
- if (!isPluginBaseConfig(plugin)) return false
26
- return (plugin as ReactPluginConfig).component !== undefined
27
- }
28
-
29
- export function isMethodPluginConfig(
30
- plugin: Partial<PluginBaseConfig>,
31
- ): plugin is MethodPluginConfig {
32
- if (!isPluginBaseConfig(plugin)) return false
33
- return (plugin as MethodPluginConfig).func !== undefined
34
- }
35
-
36
- export type PluginConfig = ReactPluginConfig | MethodPluginConfig
37
- export function isPluginConfig(plugin: Partial<PluginConfig>): plugin is PluginConfig {
38
- return isReactPluginConfig(plugin) || isMethodPluginConfig(plugin)
39
- }
40
-
41
- type Interceptor = ResolveDependencyReturn & {
42
- components: Record<string, ReactPluginConfig[]>
43
- funcs: Record<string, MethodPluginConfig[]>
44
- target: string
45
- template?: string
46
- }
47
-
48
- export type MaterializedPlugin = Interceptor & { template: string }
49
-
50
- function moveRelativeDown(plugins: PluginConfig[]) {
51
- return [...plugins].sort((a, b) => {
52
- if (a.plugin.startsWith('.') && !b.plugin.startsWith('.')) return 1
53
- if (!a.plugin.startsWith('.') && b.plugin.startsWith('.')) return -1
54
- return 0
55
- })
56
- }
57
-
58
- export function generateInterceptor(
59
- interceptor: Interceptor,
60
- config: GraphCommerceDebugConfig,
61
- ): MaterializedPlugin {
62
- const { fromModule, dependency, components, funcs } = interceptor
63
-
64
- const pluginConfigs = [...Object.entries(components), ...Object.entries(funcs)]
65
- .map(([, plugins]) => plugins)
66
- .flat()
67
-
68
- const duplicateImports = new Set()
69
-
70
- const pluginImports = moveRelativeDown(
71
- [...pluginConfigs].sort((a, b) => a.plugin.localeCompare(b.plugin)),
72
- )
73
- .map((plugin) => {
74
- const { plugin: p } = plugin
75
- if (isReactPluginConfig(plugin))
76
- return `import { Plugin as ${p.split('/')[p.split('/').length - 1]} } from '${p}'`
77
- return `import { plugin as ${p.split('/')[p.split('/').length - 1]} } from '${p}'`
78
- })
79
- .filter((str) => {
80
- if (duplicateImports.has(str)) return false
81
- duplicateImports.add(str)
82
- return true
83
- })
84
- .join('\n')
85
-
86
- const imports = [
87
- ...Object.entries(components).map(([component]) => `${component} as ${component}Base`),
88
- ...Object.entries(funcs).map(([func]) => `${func} as ${func}Base`),
89
- ]
90
-
91
- const importInjectables =
92
- imports.length > 1
93
- ? `import {
94
- ${imports.join(',\n ')},
95
- } from '${fromModule}'`
96
- : `import { ${imports[0]} } from '${fromModule}'`
97
-
98
- const entries: [string, PluginConfig[]][] = [
99
- ...Object.entries(components),
100
- ...Object.entries(funcs),
101
- ]
102
- const pluginExports = entries
103
- .map(([base, plugins]) => {
104
- const duplicateInterceptors = new Set()
105
- const name = (p: PluginConfig) => p.plugin.split('/')[p.plugin.split('/').length - 1]
106
-
107
- const filterNoDuplicate = (p: PluginConfig) => {
108
- if (duplicateInterceptors.has(name(p))) return false
109
- duplicateInterceptors.add(name(p))
110
- return true
111
- }
112
-
113
- let carry = `${base}Base`
114
-
115
- const pluginStr = plugins
116
- .reverse()
117
- .filter(filterNoDuplicate)
118
- .map((p) => {
119
- let result
120
-
121
- if (isReactPluginConfig(p)) {
122
- const wrapChain = plugins
123
- .reverse()
124
- .map((pl) => `<${name(pl)}/>`)
125
- .join(' wrapping ')
126
- const debugLog =
127
- carry === `${base}Base` && config.pluginStatus
128
- ? `\n logInterceptor(\`🔌 Rendering ${base} with plugin(s): ${wrapChain} wrapping <${base}/>\`)`
129
- : ''
130
-
131
- result = `function ${name(p)}Interceptor(props: ${base}Props) {${debugLog}
132
- return <${name(p)} {...props} Prev={${carry}} />
133
- }`
134
- } else {
135
- const wrapChain = plugins
136
- .reverse()
137
- .map((pl) => `${name(pl)}()`)
138
- .join(' wrapping ')
139
-
140
- const debugLog =
141
- carry === `${base}Base` && config.pluginStatus
142
- ? `\n logInterceptor(\`🔌 Calling ${base} with plugin(s): ${wrapChain} wrapping ${base}()\`)`
143
- : ''
144
-
145
- result = `const ${name(p)}Interceptor: typeof ${base}Base = (...args) => {${debugLog}
146
- return ${name(p)}(${carry}, ...args)
147
- }`
148
- }
149
- carry = `${name(p)}Interceptor`
150
- return result
151
- })
152
- .join('\n')
153
-
154
- const isComponent = plugins.every((p) => isReactPluginConfig(p))
155
- if (isComponent && plugins.some((p) => isMethodPluginConfig(p))) {
156
- throw new Error(`Cannot mix React and Method plugins for ${base} in ${dependency}.`)
157
- }
158
-
159
- return `
160
- /**
161
- * Interceptor for \`${isComponent ? `<${base}/>` : `${base}()`}\` with these plugins:
162
- *
163
- ${plugins.map((p) => ` * - \`${p.plugin}\``).join('\n')}
164
- */
165
- ${isComponent ? `type ${base}Props = ComponentProps<typeof ${base}Base>\n\n` : ``}${pluginStr}
166
- export const ${base} = ${carry}`
167
- })
168
- .join('\n')
169
-
170
- const logOnce = config.pluginStatus
171
- ? `
172
- const logged: Set<string> = new Set();
173
- const logInterceptor = (log: string, ...additional: unknown[]) => {
174
- if (logged.has(log)) return
175
- logged.add(log)
176
- console.log(log, ...additional)
177
- }
178
- `
179
- : ''
180
-
181
- const componentExports = `export * from '${fromModule}'`
182
-
183
- const template = `/* This file is automatically generated for ${dependency} */
184
-
185
- ${componentExports}
186
- ${pluginImports}
187
- import { ComponentProps } from 'react'
188
- ${importInjectables}
189
- ${logOnce}${pluginExports}
190
- `
191
-
192
- return { ...interceptor, template }
193
- }
5
+ import { ResolveDependency } from '../utils/resolveDependency'
6
+ import { findOriginalSource } from './findOriginalSource'
7
+ import {
8
+ Interceptor,
9
+ MaterializedPlugin,
10
+ PluginConfig,
11
+ generateInterceptor,
12
+ isPluginConfig,
13
+ moveRelativeDown,
14
+ } from './generateInterceptor'
194
15
 
195
16
  export type GenerateInterceptorsReturn = Record<string, MaterializedPlugin>
196
17
 
197
- export function generateInterceptors(
18
+ export async function generateInterceptors(
198
19
  plugins: PluginConfig[],
199
20
  resolve: ResolveDependency,
200
21
  config?: GraphCommerceDebugConfig | null | undefined,
201
- ): GenerateInterceptorsReturn {
202
- // todo: Do not use reduce as we're passing the accumulator to the next iteration
203
- const byExportedComponent = moveRelativeDown(plugins).reduce((acc, plug) => {
204
- const { exported, plugin } = plug
205
- if (!isPluginConfig(plug) || !plug.enabled) return acc
206
-
207
- const resolved = resolve(exported)
22
+ ): Promise<GenerateInterceptorsReturn> {
23
+ const byTargetModuleAndExport = moveRelativeDown(plugins).reduce<Record<string, Interceptor>>(
24
+ (acc, plug) => {
25
+ let { sourceModule: pluginPath } = plug
26
+ if (!isPluginConfig(plug) || !plug.enabled) return acc
27
+
28
+ const result = resolve(plug.targetModule, { includeSources: true })
29
+ const { error, resolved } = findOriginalSource(plug, result, resolve)
30
+
31
+ if (error) {
32
+ console.log(error.message)
33
+ return acc
34
+ }
208
35
 
209
- let pluginPathFromResolved = plugin
210
- if (plugin.startsWith('.')) {
211
- const resolvedPlugin = resolve(plugin)
212
- pluginPathFromResolved = path.relative(
213
- resolved.fromRoot.split('/').slice(0, -1).join('/'),
214
- resolvedPlugin.fromRoot,
215
- )
216
- }
36
+ const { fromRoot } = resolved
217
37
 
218
- if (!acc[resolved.fromRoot])
219
- acc[resolved.fromRoot] = {
220
- ...resolved,
221
- target: `${resolved.fromRoot}.interceptor`,
222
- components: {},
223
- funcs: {},
224
- } as Interceptor
38
+ if (pluginPath.startsWith('.')) {
39
+ const resolvedPlugin = resolve(pluginPath)
40
+ if (resolvedPlugin) {
41
+ pluginPath = path.relative(
42
+ resolved.fromRoot.split('/').slice(0, -1).join('/'),
43
+ resolvedPlugin.fromRoot,
44
+ )
45
+ }
46
+ }
225
47
 
226
- if (isReactPluginConfig(plug)) {
227
- const { component } = plug
228
- if (!acc[resolved.fromRoot].components[component])
229
- acc[resolved.fromRoot].components[component] = []
48
+ if (!acc[resolved.fromRoot]) {
49
+ acc[resolved.fromRoot] = {
50
+ ...resolved,
51
+ target: `${resolved.fromRoot}.interceptor`,
52
+ targetExports: {},
53
+ } as Interceptor
54
+ }
230
55
 
231
- acc[resolved.fromRoot].components[component].push({
232
- ...plug,
233
- plugin: pluginPathFromResolved,
234
- })
235
- }
236
- if (isMethodPluginConfig(plug)) {
237
- const { func } = plug
238
- if (!acc[resolved.fromRoot].funcs[func]) acc[resolved.fromRoot].funcs[func] = []
56
+ if (!acc[fromRoot].targetExports[plug.targetExport])
57
+ acc[fromRoot].targetExports[plug.targetExport] = []
239
58
 
240
- acc[resolved.fromRoot].funcs[func].push({
241
- ...plug,
242
- plugin: pluginPathFromResolved,
243
- })
244
- }
59
+ acc[fromRoot].targetExports[plug.targetExport].push({ ...plug, sourceModule: pluginPath })
245
60
 
246
- return acc
247
- }, {} as Record<string, Interceptor>)
61
+ return acc
62
+ },
63
+ {},
64
+ )
248
65
 
249
66
  return Object.fromEntries(
250
- Object.entries(byExportedComponent).map(([target, interceptor]) => [
251
- target,
252
- generateInterceptor(interceptor, config ?? {}),
253
- ]),
67
+ await Promise.all(
68
+ Object.entries(byTargetModuleAndExport).map(async ([target, interceptor]) => {
69
+ const file = `${interceptor.fromRoot}.interceptor.tsx`
70
+
71
+ const originalSource = (await fs
72
+ .access(file, fs.constants.F_OK)
73
+ .then(() => true)
74
+ .catch(() => false))
75
+ ? (await fs.readFile(file)).toString()
76
+ : undefined
77
+
78
+ return [
79
+ target,
80
+ await generateInterceptor(interceptor, config ?? {}, originalSource),
81
+ ] as const
82
+ }),
83
+ ),
254
84
  )
255
85
  }
@@ -0,0 +1,82 @@
1
+ import { Module } from '@swc/core'
2
+ import get from 'lodash/get'
3
+ import { z } from 'zod'
4
+ import { GraphCommerceConfig } from '../generated/config'
5
+ import { extractExports } from './extractExports'
6
+ import { PluginConfig } from './generateInterceptor'
7
+
8
+ const pluginConfigParsed = z.object({
9
+ type: z.enum(['component', 'function', 'replace']),
10
+ module: z.string(),
11
+ export: z.string(),
12
+ ifConfig: z.union([z.string(), z.tuple([z.string(), z.string()])]).optional(),
13
+ })
14
+
15
+ function nonNullable<T>(value: T): value is NonNullable<T> {
16
+ return value !== null && value !== undefined
17
+ }
18
+ const isObject = (input: unknown): input is Record<string, unknown> =>
19
+ typeof input === 'object' && input !== null && !Array.isArray(input)
20
+
21
+ export function parseStructure(ast: Module, gcConfig: GraphCommerceConfig, sourceModule: string) {
22
+ const [exports, errors] = extractExports(ast)
23
+ if (errors.length) console.error(`Plugin error for`, errors.join('\n'))
24
+
25
+ const {
26
+ config: moduleConfig,
27
+ component,
28
+ func,
29
+ exported,
30
+ ifConfig,
31
+ plugin,
32
+ Plugin,
33
+ ...rest
34
+ } = exports
35
+
36
+ const exportVals = Object.keys(rest)
37
+ if (component && !moduleConfig) exportVals.push('Plugin')
38
+ if (func && !moduleConfig) exportVals.push('plugin')
39
+
40
+ return exportVals
41
+ .map((exportVal) => {
42
+ let config = isObject(moduleConfig) ? moduleConfig : {}
43
+
44
+ if (!moduleConfig && component) {
45
+ config = { type: 'component', module: exported, ifConfig, export: 'Plugin' }
46
+ } else if (!moduleConfig && func) {
47
+ config = { type: 'function', module: exported, ifConfig, export: 'plugin' }
48
+ } else if (isObject(moduleConfig)) {
49
+ config = { ...moduleConfig, export: exportVal }
50
+ } else {
51
+ console.error(`Plugin configuration invalid! See ${sourceModule}`)
52
+ }
53
+
54
+ const parsed = pluginConfigParsed.safeParse(config)
55
+
56
+ if (!parsed.success) {
57
+ if (errors.length)
58
+ console.error(parsed.error.errors.map((e) => `${e.path} ${e.message}`).join('\n'))
59
+ return undefined
60
+ }
61
+
62
+ let enabled = true
63
+ if (parsed.data.ifConfig) {
64
+ enabled = Array.isArray(parsed.data.ifConfig)
65
+ ? get(gcConfig, parsed.data.ifConfig[0]) === parsed.data.ifConfig[1]
66
+ : Boolean(get(gcConfig, parsed.data.ifConfig))
67
+ }
68
+
69
+ const val: PluginConfig = {
70
+ targetExport:
71
+ (exports.component as string) || (exports.func as string) || parsed.data.export,
72
+ sourceModule,
73
+ sourceExport: parsed.data.export,
74
+ targetModule: parsed.data.module,
75
+ type: parsed.data.type,
76
+ enabled,
77
+ }
78
+ if (parsed.data.ifConfig) val.ifConfig = parsed.data.ifConfig
79
+ return val
80
+ })
81
+ .filter(nonNullable)
82
+ }
@@ -0,0 +1,13 @@
1
+ import { Output, Program, parseSync as parseSyncCore, printSync as printSyncCode } from '@swc/core'
2
+
3
+ export function parseSync(src: string) {
4
+ return parseSyncCore(src, {
5
+ syntax: 'typescript',
6
+ tsx: true,
7
+ comments: true,
8
+ })
9
+ }
10
+
11
+ export function printSync(m: Program): Output {
12
+ return printSyncCode(m)
13
+ }