@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.
- package/CHANGELOG.md +133 -1
- package/Config.graphqls +4 -2
- package/__tests__/config/utils/__snapshots__/mergeEnvIntoConfig.ts.snap +19 -2
- package/__tests__/config/utils/replaceConfigInString.ts +4 -0
- package/__tests__/interceptors/findPlugins.ts +473 -113
- package/__tests__/interceptors/generateInterceptors.ts +610 -322
- package/__tests__/interceptors/parseStructure.ts +158 -0
- package/__tests__/interceptors/writeInterceptors.ts +23 -14
- package/__tests__/utils/resolveDependenciesSync.ts +28 -25
- package/dist/config/commands/generateConfig.js +5 -2
- package/dist/config/demoConfig.js +19 -4
- package/dist/generated/config.js +8 -1
- package/dist/interceptors/InterceptorPlugin.js +70 -25
- package/dist/interceptors/RenameVisitor.js +19 -0
- package/dist/interceptors/Visitor.js +1418 -0
- package/dist/interceptors/extractExports.js +201 -0
- package/dist/interceptors/findOriginalSource.js +87 -0
- package/dist/interceptors/findPlugins.js +21 -53
- package/dist/interceptors/generateInterceptor.js +200 -0
- package/dist/interceptors/generateInterceptors.js +38 -179
- package/dist/interceptors/parseStructure.js +71 -0
- package/dist/interceptors/swc.js +16 -0
- package/dist/interceptors/writeInterceptors.js +19 -10
- package/dist/utils/resolveDependency.js +27 -5
- package/dist/withGraphCommerce.js +2 -1
- package/package.json +4 -1
- package/src/config/commands/generateConfig.ts +5 -2
- package/src/config/demoConfig.ts +19 -4
- package/src/config/index.ts +4 -2
- package/src/generated/config.ts +25 -3
- package/src/index.ts +16 -6
- package/src/interceptors/InterceptorPlugin.ts +90 -32
- package/src/interceptors/RenameVisitor.ts +17 -0
- package/src/interceptors/Visitor.ts +1847 -0
- package/src/interceptors/extractExports.ts +230 -0
- package/src/interceptors/findOriginalSource.ts +105 -0
- package/src/interceptors/findPlugins.ts +36 -87
- package/src/interceptors/generateInterceptor.ts +271 -0
- package/src/interceptors/generateInterceptors.ts +67 -237
- package/src/interceptors/parseStructure.ts +82 -0
- package/src/interceptors/swc.ts +13 -0
- package/src/interceptors/writeInterceptors.ts +26 -10
- package/src/utils/resolveDependency.ts +51 -12
- 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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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[
|
|
232
|
-
|
|
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[
|
|
241
|
-
...plug,
|
|
242
|
-
plugin: pluginPathFromResolved,
|
|
243
|
-
})
|
|
244
|
-
}
|
|
59
|
+
acc[fromRoot].targetExports[plug.targetExport].push({ ...plug, sourceModule: pluginPath })
|
|
245
60
|
|
|
246
|
-
|
|
247
|
-
|
|
61
|
+
return acc
|
|
62
|
+
},
|
|
63
|
+
{},
|
|
64
|
+
)
|
|
248
65
|
|
|
249
66
|
return Object.fromEntries(
|
|
250
|
-
|
|
251
|
-
target,
|
|
252
|
-
|
|
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
|
+
}
|