@graphcommerce/next-config 8.1.0-canary.3 → 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 +132 -2
  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,230 @@
1
+ /* eslint-disable no-continue */
2
+ /* eslint-disable max-classes-per-file */
3
+ import type {
4
+ ArrayExpression,
5
+ BooleanLiteral,
6
+ Identifier,
7
+ KeyValueProperty,
8
+ Module,
9
+ Node,
10
+ NullLiteral,
11
+ NumericLiteral,
12
+ ObjectExpression,
13
+ RegExpLiteral,
14
+ StringLiteral,
15
+ TemplateLiteral,
16
+ } from '@swc/core'
17
+
18
+ export class NoSuchDeclarationError extends Error {}
19
+
20
+ function isIdentifier(node: Node): node is Identifier {
21
+ return node.type === 'Identifier'
22
+ }
23
+
24
+ function isBooleanLiteral(node: Node): node is BooleanLiteral {
25
+ return node.type === 'BooleanLiteral'
26
+ }
27
+
28
+ function isNullLiteral(node: Node): node is NullLiteral {
29
+ return node.type === 'NullLiteral'
30
+ }
31
+
32
+ function isStringLiteral(node: Node): node is StringLiteral {
33
+ return node.type === 'StringLiteral'
34
+ }
35
+
36
+ function isNumericLiteral(node: Node): node is NumericLiteral {
37
+ return node.type === 'NumericLiteral'
38
+ }
39
+
40
+ function isArrayExpression(node: Node): node is ArrayExpression {
41
+ return node.type === 'ArrayExpression'
42
+ }
43
+
44
+ function isObjectExpression(node: Node): node is ObjectExpression {
45
+ return node.type === 'ObjectExpression'
46
+ }
47
+
48
+ function isKeyValueProperty(node: Node): node is KeyValueProperty {
49
+ return node.type === 'KeyValueProperty'
50
+ }
51
+
52
+ function isRegExpLiteral(node: Node): node is RegExpLiteral {
53
+ return node.type === 'RegExpLiteral'
54
+ }
55
+
56
+ function isTemplateLiteral(node: Node): node is TemplateLiteral {
57
+ return node.type === 'TemplateLiteral'
58
+ }
59
+
60
+ export class UnsupportedValueError extends Error {
61
+ /** @example `config.runtime[0].value` */
62
+ path?: string
63
+
64
+ constructor(message: string, paths?: string[]) {
65
+ super(message)
66
+
67
+ // Generating "path" that looks like "config.runtime[0].value"
68
+ let codePath: string | undefined
69
+ if (Array.isArray(paths)) {
70
+ codePath = ''
71
+ for (const path of paths) {
72
+ if (path[0] === '[') {
73
+ // "array" + "[0]"
74
+ codePath += path
75
+ } else if (codePath === '') {
76
+ codePath = path
77
+ } else {
78
+ // "object" + ".key"
79
+ codePath += `.${path}`
80
+ }
81
+ }
82
+ }
83
+
84
+ this.path = codePath
85
+ }
86
+ }
87
+
88
+ export const RUNTIME_VALUE = Symbol('RUNTIME_VALUE')
89
+
90
+ function extractValue(node: Node, path?: string[], optional: boolean = false): any {
91
+ if (isNullLiteral(node)) {
92
+ return null
93
+ }
94
+ if (isBooleanLiteral(node)) {
95
+ // e.g. true / false
96
+ return node.value
97
+ }
98
+ if (isStringLiteral(node)) {
99
+ // e.g. "abc"
100
+ return node.value
101
+ }
102
+ if (isNumericLiteral(node)) {
103
+ // e.g. 123
104
+ return node.value
105
+ }
106
+ if (isRegExpLiteral(node)) {
107
+ // e.g. /abc/i
108
+ return new RegExp(node.pattern, node.flags)
109
+ }
110
+ if (isIdentifier(node)) {
111
+ switch (node.value) {
112
+ case 'undefined':
113
+ return undefined
114
+ default:
115
+ if (optional) return RUNTIME_VALUE
116
+ throw new UnsupportedValueError(`Unknown identifier "${node.value}"`, path)
117
+ }
118
+ } else if (isArrayExpression(node)) {
119
+ // e.g. [1, 2, 3]
120
+ const arr: any[] = []
121
+ for (let i = 0, len = node.elements.length; i < len; i++) {
122
+ const elem = node.elements[i]
123
+ if (elem) {
124
+ if (elem.spread) {
125
+ // e.g. [ ...a ]
126
+ if (optional) return RUNTIME_VALUE
127
+ throw new UnsupportedValueError(
128
+ 'Unsupported spread operator in the Array Expression',
129
+ path,
130
+ )
131
+ }
132
+
133
+ arr.push(extractValue(elem.expression, path && [...path, `[${i}]`], optional))
134
+ } else {
135
+ // e.g. [1, , 2]
136
+ // ^^
137
+ arr.push(undefined)
138
+ }
139
+ }
140
+ return arr
141
+ } else if (isObjectExpression(node)) {
142
+ // e.g. { a: 1, b: 2 }
143
+ const obj: any = {}
144
+ for (const prop of node.properties) {
145
+ if (!isKeyValueProperty(prop)) {
146
+ // e.g. { ...a }
147
+ if (optional) return RUNTIME_VALUE
148
+ throw new UnsupportedValueError(
149
+ 'Unsupported spread operator in the Object Expression',
150
+ path,
151
+ )
152
+ }
153
+
154
+ let key
155
+ if (isIdentifier(prop.key)) {
156
+ // e.g. { a: 1, b: 2 }
157
+ key = prop.key.value
158
+ } else if (isStringLiteral(prop.key)) {
159
+ // e.g. { "a": 1, "b": 2 }
160
+ key = prop.key.value
161
+ } else {
162
+ if (optional) return RUNTIME_VALUE
163
+ throw new UnsupportedValueError(
164
+ `Unsupported key type "${prop.key.type}" in the Object Expression`,
165
+ path,
166
+ )
167
+ }
168
+
169
+ obj[key] = extractValue(prop.value, path && [...path, key])
170
+ }
171
+
172
+ return obj
173
+ } else if (isTemplateLiteral(node)) {
174
+ // e.g. `abc`
175
+ if (node.expressions.length !== 0) {
176
+ // TODO: should we add support for `${'e'}d${'g'}'e'`?
177
+ if (optional) return RUNTIME_VALUE
178
+ throw new UnsupportedValueError('Unsupported template literal with expressions', path)
179
+ }
180
+
181
+ // When TemplateLiteral has 0 expressions, the length of quasis is always 1.
182
+ // Because when parsing TemplateLiteral, the parser yields the first quasi,
183
+ // then the first expression, then the next quasi, then the next expression, etc.,
184
+ // until the last quasi.
185
+ // Thus if there is no expression, the parser ends at the frst and also last quasis
186
+ //
187
+ // A "cooked" interpretation where backslashes have special meaning, while a
188
+ // "raw" interpretation where backslashes do not have special meaning
189
+ // https://exploringjs.com/impatient-js/ch_template-literals.html#template-strings-cooked-vs-raw
190
+ const [{ cooked, raw }] = node.quasis
191
+
192
+ return cooked ?? raw
193
+ } else {
194
+ if (optional) return RUNTIME_VALUE
195
+ throw new UnsupportedValueError(`Unsupported node type "${node.type}"`, path)
196
+ }
197
+ }
198
+
199
+ export function extractExports(module: Module) {
200
+ const exports: Record<string, unknown> = {}
201
+ const errors: string[] = []
202
+
203
+ for (const moduleItem of module.body) {
204
+ switch (moduleItem.type) {
205
+ case 'ExportAllDeclaration':
206
+ errors.push('You can not use export * from a plugin, exports must be explicit')
207
+ break
208
+ case 'ExportDefaultDeclaration':
209
+ errors.push('You can not use default exports from a plugin, exports must be explicit')
210
+ break
211
+ case 'ExportDeclaration':
212
+ switch (moduleItem.declaration.type) {
213
+ case 'ClassDeclaration':
214
+ case 'FunctionDeclaration':
215
+ exports[moduleItem.declaration.identifier.value] = RUNTIME_VALUE
216
+ // node.identifier.value
217
+ break
218
+ case 'VariableDeclaration':
219
+ moduleItem.declaration.declarations.forEach((decl) => {
220
+ if (isIdentifier(decl.id) && decl.init) {
221
+ exports[decl.id.value] = extractValue(decl.init, undefined, true)
222
+ }
223
+ })
224
+ break
225
+ }
226
+ }
227
+ }
228
+
229
+ return [exports, errors] as const
230
+ }
@@ -0,0 +1,105 @@
1
+ import path from 'path'
2
+ import { ResolveDependency, ResolveDependencyReturn } from '../utils/resolveDependency'
3
+ import { PluginConfig } from './generateInterceptor'
4
+ import { parseSync } from './swc'
5
+ import { ExportAllDeclaration } from '@swc/core'
6
+
7
+ function parseAndFindExport(
8
+ resolved: ResolveDependencyReturn,
9
+ findExport: string,
10
+ resolve: ResolveDependency,
11
+ ): ResolveDependencyReturn {
12
+ if (!resolved?.source) return undefined
13
+ const ast = parseSync(resolved.source)
14
+
15
+ for (const node of ast.body) {
16
+ if (node.type === 'ExportDeclaration') {
17
+ switch (node.declaration.type) {
18
+ case 'ClassDeclaration':
19
+ case 'FunctionDeclaration':
20
+ if (node.declaration.identifier.value === findExport) return resolved
21
+ break
22
+ case 'VariableDeclaration':
23
+ for (const declaration of node.declaration.declarations) {
24
+ if (declaration.type === 'VariableDeclarator') {
25
+ if (declaration.id.type === 'Identifier') {
26
+ if (declaration.id.value === findExport) return resolved
27
+ } else {
28
+ console.log(declaration)
29
+ }
30
+ }
31
+ }
32
+ break
33
+ }
34
+ }
35
+ }
36
+
37
+ const exports = ast.body
38
+ .filter((node): node is ExportAllDeclaration => node.type === 'ExportAllDeclaration')
39
+ .sort((a, b) => {
40
+ const probablyA = a.source.value.includes(findExport)
41
+ const probablyB = b.source.value.includes(findExport)
42
+ // eslint-disable-next-line no-nested-ternary
43
+ return probablyA === probablyB ? 0 : probablyA ? -1 : 1
44
+ })
45
+
46
+ for (const node of exports) {
47
+ const isRelative = node.source.value.startsWith('.')
48
+ if (isRelative) {
49
+ const d =
50
+ resolved.dependency === resolved.denormalized
51
+ ? resolved.dependency.substring(0, resolved.dependency.lastIndexOf('/'))
52
+ : resolved.dependency
53
+
54
+ const newPath = path.join(d, node.source.value)
55
+
56
+ const resolveResult = resolve(newPath, { includeSources: true })
57
+ // eslint-disable-next-line no-continue
58
+ if (!resolveResult) continue
59
+
60
+ const newResolved = parseAndFindExport(resolveResult, findExport, resolve)
61
+
62
+ if (newResolved && resolved.dependency !== newResolved.dependency) return newResolved
63
+ }
64
+ }
65
+
66
+ return undefined
67
+ }
68
+
69
+ const cachedResults = new Map<string, ResolveDependencyReturn>()
70
+
71
+ export function findOriginalSource(
72
+ plug: PluginConfig,
73
+ resolved: ResolveDependencyReturn,
74
+ resolve: ResolveDependency,
75
+ ):
76
+ | { resolved: NonNullable<ResolveDependencyReturn>; error: undefined }
77
+ | { resolved: undefined; error: Error } {
78
+ if (!resolved?.source)
79
+ return {
80
+ resolved: undefined,
81
+ error: new Error(`Could not resolve ${plug.targetModule}`),
82
+ }
83
+
84
+ // const cacheKey = `${plug.targetModule}#${plug.targetExport}`
85
+ // if (cachedResults.has(cacheKey)) {
86
+ // return {
87
+ // resolved: cachedResults.get(cacheKey) as NonNullable<ResolveDependencyReturn>,
88
+ // error: undefined,
89
+ // }
90
+ // }
91
+
92
+ const newResolved = parseAndFindExport(resolved, plug.targetExport, resolve)
93
+
94
+ if (!newResolved) {
95
+ return {
96
+ resolved: undefined,
97
+ error: new Error(
98
+ `Can not find ${plug.targetModule}#${plug.sourceExport} for plugin ${plug.sourceModule}`,
99
+ ),
100
+ }
101
+ }
102
+
103
+ // cachedResults.set(cacheKey, newResolved)
104
+ return { resolved: newResolved, error: undefined }
105
+ }
@@ -1,51 +1,12 @@
1
- import { parseFileSync } from '@swc/core'
2
1
  // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import { parseFileSync } from '@swc/core'
3
3
  import chalk from 'chalk'
4
4
  // eslint-disable-next-line import/no-extraneous-dependencies
5
- import glob from 'glob'
6
- import get from 'lodash/get'
7
- import type { Path } from 'react-hook-form'
5
+ import { sync as globSync } from 'glob'
8
6
  import { GraphCommerceConfig } from '../generated/config'
9
7
  import { resolveDependenciesSync } from '../utils/resolveDependenciesSync'
10
- import {
11
- isMethodPluginConfig,
12
- isPluginBaseConfig,
13
- isPluginConfig,
14
- isReactPluginConfig,
15
- PluginConfig,
16
- } from './generateInterceptors'
17
-
18
- type ParseResult = {
19
- component?: string
20
- exported?: string
21
- ifConfig?: Path<GraphCommerceConfig>
22
- }
23
-
24
- function parseStructure(file: string): ParseResult {
25
- const ast = parseFileSync(file, { syntax: 'typescript', tsx: true })
26
-
27
- const imports: Record<string, string> = {}
28
- const exports: Record<string, string> = {}
29
-
30
- ast.body.forEach((node) => {
31
- if (node.type === 'ImportDeclaration') {
32
- node.specifiers.forEach((s) => {
33
- if (s.type === 'ImportSpecifier') {
34
- imports[s.local.value] = node.source.value
35
- }
36
- })
37
- }
38
- if (node.type === 'ExportDeclaration' && node.declaration.type === 'VariableDeclaration') {
39
- node.declaration.declarations.forEach((declaration) => {
40
- if (declaration.init?.type === 'StringLiteral' && declaration.id.type === 'Identifier') {
41
- exports[declaration.id.value] = declaration.init.value
42
- }
43
- })
44
- }
45
- })
46
-
47
- return exports as ParseResult
48
- }
8
+ import { PluginConfig } from './generateInterceptor'
9
+ import { parseStructure } from './parseStructure'
49
10
 
50
11
  const pluginLogs: Record<string, string> = {}
51
12
 
@@ -57,35 +18,15 @@ export function findPlugins(config: GraphCommerceConfig, cwd: string = process.c
57
18
  const errors: string[] = []
58
19
  const plugins: PluginConfig[] = []
59
20
  dependencies.forEach((dependency, path) => {
60
- const files = glob.sync(`${dependency}/plugins/**/*.{ts,tsx}`)
21
+ const files = globSync(`${dependency}/plugins/**/*.{ts,tsx}`, { dotRelative: true })
61
22
  files.forEach((file) => {
62
- try {
63
- const result = parseStructure(file)
64
- if (!result) return
65
-
66
- const pluginConfig = {
67
- plugin: file.replace(dependency, path).replace('.tsx', '').replace('.ts', ''),
68
- ...result,
69
- enabled: !result.ifConfig || Boolean(get(config, result.ifConfig)),
70
- }
23
+ const sourceModule = file.replace(dependency, path).replace('.tsx', '').replace('.ts', '')
71
24
 
72
- if (!isPluginConfig(pluginConfig)) {
73
- if (!isPluginBaseConfig(pluginConfig))
74
- errors.push(
75
- `Plugin ${file} is not a valid plugin, make it has "export const exported = '@graphcommerce/my-package"`,
76
- )
77
- else if (file.endsWith('.ts')) {
78
- errors.push(
79
- `Plugin ${file} is not a valid plugin, please define the method to create a plugin for "export const method = 'someMethod'"`,
80
- )
81
- } else if (file.endsWith('.tsx')) {
82
- errors.push(
83
- `Plugin ${file} is not a valid plugin, please define the compoennt to create a plugin for "export const component = 'SomeComponent'"`,
84
- )
85
- }
86
- } else {
87
- plugins.push(pluginConfig)
88
- }
25
+ try {
26
+ const ast = parseFileSync(file, { syntax: 'typescript', tsx: true })
27
+ parseStructure(ast, config, sourceModule).forEach((result) => {
28
+ plugins.push(result)
29
+ })
89
30
  } catch (e) {
90
31
  console.error(`Error parsing ${file}`, e)
91
32
  }
@@ -93,27 +34,35 @@ export function findPlugins(config: GraphCommerceConfig, cwd: string = process.c
93
34
  })
94
35
 
95
36
  if (process.env.NODE_ENV === 'development' && debug) {
96
- const byExported = plugins.reduce((acc, plugin) => {
97
- const componentStr = isReactPluginConfig(plugin) ? plugin.component : ''
98
- const funcStr = isMethodPluginConfig(plugin) ? plugin.func : ''
99
- const key = `🔌 ${chalk.greenBright(
100
- `Plugins loaded for ${plugin.exported}#${componentStr}${funcStr}`,
101
- )}`
102
- if (!acc[key]) acc[key] = []
103
- acc[key].push(plugin)
104
- return acc
105
- }, {} as Record<string, Pick<PluginConfig, 'plugin' | 'ifConfig' | 'enabled'>[]>)
37
+ const byExported = plugins.reduce(
38
+ (acc, plugin) => {
39
+ const key = `🔌 ${chalk.greenBright(
40
+ `Plugins loaded for ${plugin.targetModule}#${plugin.targetExport}`,
41
+ )}`
42
+ if (!acc[key]) acc[key] = []
43
+ acc[key].push(plugin)
44
+ return acc
45
+ },
46
+ {} as Record<
47
+ string,
48
+ Pick<PluginConfig, 'sourceModule' | 'sourceExport' | 'ifConfig' | 'enabled'>[]
49
+ >,
50
+ )
106
51
 
107
52
  const toLog: string[] = []
108
53
  Object.entries(byExported).forEach(([key, p]) => {
109
54
  const logStr = p
110
55
  .filter((c) => debug || c.enabled)
111
- .map(
112
- (c) =>
113
- `${c.enabled ? `🟢` : `⚪️`} ${c.plugin} ${
114
- c.ifConfig ? `(${c.ifConfig}: ${c.enabled ? 'true' : 'false'})` : ''
115
- }`,
116
- )
56
+ .map((c) => {
57
+ // eslint-disable-next-line no-nested-ternary
58
+ const ifConfigStr = c.ifConfig
59
+ ? Array.isArray(c.ifConfig)
60
+ ? `${c.ifConfig[0]}=${c.ifConfig[1]}`
61
+ : `${c.ifConfig}`
62
+ : ''
63
+
64
+ return `${c.enabled ? `🟢` : `⚪️`} ${c.sourceModule} ${ifConfigStr}`
65
+ })
117
66
  .join('\n')
118
67
 
119
68
  if (logStr && pluginLogs[key] !== logStr) {
@@ -123,7 +72,7 @@ export function findPlugins(config: GraphCommerceConfig, cwd: string = process.c
123
72
  })
124
73
 
125
74
  // eslint-disable-next-line no-console
126
- console.log(toLog.join('\n\n'))
75
+ if (toLog.length) console.log(toLog.join('\n\n'))
127
76
  }
128
77
 
129
78
  return [plugins, errors] as const