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

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 (47) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/Config.graphqls +5 -5
  3. package/__tests__/config/utils/__snapshots__/mergeEnvIntoConfig.ts.snap +48 -2
  4. package/__tests__/config/utils/configToImportMeta.ts +0 -4
  5. package/__tests__/config/utils/mergeEnvIntoConfig.ts +15 -2
  6. package/__tests__/config/utils/rewriteLegancyEnv.ts +1 -1
  7. package/__tests__/interceptors/findPlugins.ts +144 -194
  8. package/__tests__/interceptors/generateInterceptors.ts +174 -91
  9. package/__tests__/interceptors/parseStructure.ts +50 -0
  10. package/__tests__/utils/resolveDependenciesSync.ts +4 -0
  11. package/dist/config/commands/exportConfig.js +5 -0
  12. package/dist/config/commands/generateConfig.js +5 -0
  13. package/dist/config/commands/generateIntercetors.js +9 -0
  14. package/dist/config/demoConfig.js +5 -0
  15. package/dist/config/utils/mergeEnvIntoConfig.js +8 -1
  16. package/dist/generated/config.js +17 -9
  17. package/dist/index.js +1 -0
  18. package/dist/interceptors/commands/codegenInterceptors.js +23 -0
  19. package/dist/interceptors/commands/generateIntercetors.js +9 -0
  20. package/dist/interceptors/extractExports.js +21 -18
  21. package/dist/interceptors/findOriginalSource.js +17 -1
  22. package/dist/interceptors/findPlugins.js +7 -3
  23. package/dist/interceptors/generateInterceptor.js +32 -15
  24. package/dist/interceptors/generateInterceptors.js +6 -5
  25. package/dist/interceptors/parseStructure.js +9 -1
  26. package/dist/interceptors/writeInterceptors.js +7 -7
  27. package/dist/utils/resolveDependenciesSync.js +5 -4
  28. package/dist/utils/resolveDependency.js +5 -0
  29. package/dist/withGraphCommerce.js +14 -5
  30. package/package.json +1 -1
  31. package/src/config/commands/exportConfig.ts +3 -0
  32. package/src/config/commands/generateConfig.ts +3 -0
  33. package/src/config/demoConfig.ts +5 -0
  34. package/src/config/utils/mergeEnvIntoConfig.ts +9 -1
  35. package/src/generated/config.ts +57 -19
  36. package/src/index.ts +1 -0
  37. package/src/interceptors/commands/codegenInterceptors.ts +27 -0
  38. package/src/interceptors/extractExports.ts +21 -21
  39. package/src/interceptors/findOriginalSource.ts +16 -1
  40. package/src/interceptors/findPlugins.ts +7 -3
  41. package/src/interceptors/generateInterceptor.ts +39 -15
  42. package/src/interceptors/generateInterceptors.ts +9 -6
  43. package/src/interceptors/parseStructure.ts +14 -1
  44. package/src/interceptors/writeInterceptors.ts +7 -7
  45. package/src/utils/resolveDependenciesSync.ts +11 -3
  46. package/src/utils/resolveDependency.ts +7 -0
  47. package/src/withGraphCommerce.ts +15 -6
@@ -104,6 +104,8 @@ export type DatalayerConfig = {
104
104
  * Below is a list of all possible configurations that can be set by GraphCommerce.
105
105
  */
106
106
  export type GraphCommerceConfig = {
107
+ /** Configuration for the SidebarGallery component */
108
+ breadcrumbs?: InputMaybe<Scalars['Boolean']['input']>;
107
109
  /**
108
110
  * The canonical base URL is used for SEO purposes.
109
111
  *
@@ -157,14 +159,17 @@ export type GraphCommerceConfig = {
157
159
  * Default: 'false'
158
160
  */
159
161
  crossSellsRedirectItems?: InputMaybe<Scalars['Boolean']['input']>;
162
+ /** Enables the shipping notes field in the checkout */
163
+ customerAddressNoteEnable?: InputMaybe<Scalars['Boolean']['input']>;
160
164
  /**
161
- * Due to a limitation in the GraphQL API of Magento 2, we need to know if the
162
- * customer requires email confirmation.
163
- *
164
- * This value should match Magento 2's configuration value for
165
- * `customer/create_account/confirm` and should be removed once we can query
165
+ * Enables company fields inside the checkout:
166
+ * - Company name
167
+ * - VAT ID
166
168
  */
167
- customerRequireEmailConfirmation?: InputMaybe<Scalars['Boolean']['input']>;
169
+ customerCompanyFieldsEnable?: InputMaybe<Scalars['Boolean']['input']>;
170
+ /** Enable customer account deletion through the account section */
171
+ customerDeleteEnabled?: InputMaybe<Scalars['Boolean']['input']>;
172
+ /** Datalayer config */
168
173
  dataLayer?: InputMaybe<DatalayerConfig>;
169
174
  /** Debug configuration for GraphCommerce */
170
175
  debug?: InputMaybe<GraphCommerceDebugConfig>;
@@ -275,6 +280,12 @@ export type GraphCommerceConfig = {
275
280
  * - https://magento2.test/graphql
276
281
  */
277
282
  magentoEndpoint: Scalars['String']['input'];
283
+ /**
284
+ * Version of the Magento backend.
285
+ *
286
+ * Values: 245, 246, 247 for Magento 2.4.5, 2.4.6, 2.4.7 respectively.
287
+ */
288
+ magentoVersion: Scalars['Int']['input'];
278
289
  /** To enable next.js' preview mode, configure the secret you'd like to use. */
279
290
  previewSecret?: InputMaybe<Scalars['String']['input']>;
280
291
  /**
@@ -285,6 +296,13 @@ export type GraphCommerceConfig = {
285
296
  productFiltersLayout?: InputMaybe<ProductFiltersLayout>;
286
297
  /** Product filters with better UI for mobile and desktop. */
287
298
  productFiltersPro?: InputMaybe<Scalars['Boolean']['input']>;
299
+ /**
300
+ * Pagination variant for the product listings.
301
+ *
302
+ * COMPACT means: "< Page X of Y >"
303
+ * EXTENDED means: "< 1 2 ... 4 [5] 6 ... 10 11 >"
304
+ */
305
+ productListPaginationVariant?: InputMaybe<PaginationVariant>;
288
306
  /**
289
307
  * By default we route products to /p/[url] but you can change this to /product/[url] if you wish.
290
308
  *
@@ -345,6 +363,12 @@ export type GraphCommerceStorefrontConfig = {
345
363
  canonicalBaseUrl?: InputMaybe<Scalars['String']['input']>;
346
364
  /** Due to a limitation of the GraphQL API it is not possible to determine if a cart should be displayed including or excluding tax. */
347
365
  cartDisplayPricesInclTax?: InputMaybe<Scalars['Boolean']['input']>;
366
+ /**
367
+ * Enables company fields inside the checkout:
368
+ * - Company name
369
+ * - VAT ID
370
+ */
371
+ customerCompanyFieldsEnable?: InputMaybe<Scalars['Boolean']['input']>;
348
372
  /**
349
373
  * There can only be one entry with defaultLocale set to true.
350
374
  * - If there are more, the first one is used.
@@ -365,11 +389,7 @@ export type GraphCommerceStorefrontConfig = {
365
389
  googleTagmanagerId?: InputMaybe<Scalars['String']['input']>;
366
390
  /** Add a gcms-locales header to make sure queries return in a certain language, can be an array to define fallbacks. */
367
391
  hygraphLocales?: InputMaybe<Array<Scalars['String']['input']>>;
368
- /**
369
- * Specify a custom locale for to load translations. Must be lowercase valid locale.
370
- *
371
- * This value is also used for the Intl.
372
- */
392
+ /** Custom locale used to load the .po files. Must be a valid locale, also used for Intl functions. */
373
393
  linguiLocale?: InputMaybe<Scalars['String']['input']>;
374
394
  /**
375
395
  * Must be a [locale string](https://www.unicode.org/reports/tr35/tr35-59/tr35.html#Identifiers) for automatic redirects to work.
@@ -388,6 +408,11 @@ export type GraphCommerceStorefrontConfig = {
388
408
  * - b2b-us
389
409
  */
390
410
  magentoStoreCode: Scalars['String']['input'];
411
+ /**
412
+ * Allow the site to be indexed by search engines.
413
+ * If false, the robots.txt file will be set to disallow all.
414
+ */
415
+ robotsAllow?: InputMaybe<Scalars['Boolean']['input']>;
391
416
  };
392
417
 
393
418
  /** Options to configure which values will be replaced when a variant is selected on the product page. */
@@ -407,6 +432,10 @@ export type MagentoConfigurableVariantValues = {
407
432
  url?: InputMaybe<Scalars['Boolean']['input']>;
408
433
  };
409
434
 
435
+ export type PaginationVariant =
436
+ | 'COMPACT'
437
+ | 'EXTENDED';
438
+
410
439
  export type ProductFiltersLayout =
411
440
  | 'DEFAULT'
412
441
  | 'SIDEBAR';
@@ -443,6 +472,8 @@ export const definedNonNullAnySchema = z.any().refine((v) => isDefinedNonNullAny
443
472
 
444
473
  export const CompareVariantSchema = z.enum(['CHECKBOX', 'ICON']);
445
474
 
475
+ export const PaginationVariantSchema = z.enum(['COMPACT', 'EXTENDED']);
476
+
446
477
  export const ProductFiltersLayoutSchema = z.enum(['DEFAULT', 'SIDEBAR']);
447
478
 
448
479
  export const SidebarGalleryPaginationVariantSchema = z.enum(['DOTS', 'THUMBNAILS_BOTTOM']);
@@ -455,18 +486,21 @@ export function DatalayerConfigSchema(): z.ZodObject<Properties<DatalayerConfig>
455
486
 
456
487
  export function GraphCommerceConfigSchema(): z.ZodObject<Properties<GraphCommerceConfig>> {
457
488
  return z.object({
489
+ breadcrumbs: z.boolean().default(false).nullish(),
458
490
  canonicalBaseUrl: z.string().min(1),
459
491
  cartDisplayPricesInclTax: z.boolean().nullish(),
460
492
  compare: z.boolean().nullish(),
461
- compareVariant: CompareVariantSchema.nullish(),
462
- configurableVariantForSimple: z.boolean().nullish(),
493
+ compareVariant: CompareVariantSchema.default("ICON").nullish(),
494
+ configurableVariantForSimple: z.boolean().default(false).nullish(),
463
495
  configurableVariantValues: MagentoConfigurableVariantValuesSchema().nullish(),
464
- crossSellsHideCartItems: z.boolean().nullish(),
465
- crossSellsRedirectItems: z.boolean().nullish(),
466
- customerRequireEmailConfirmation: z.boolean().nullish(),
496
+ crossSellsHideCartItems: z.boolean().default(false).nullish(),
497
+ crossSellsRedirectItems: z.boolean().default(false).nullish(),
498
+ customerAddressNoteEnable: z.boolean().nullish(),
499
+ customerCompanyFieldsEnable: z.boolean().nullish(),
500
+ customerDeleteEnabled: z.boolean().nullish(),
467
501
  dataLayer: DatalayerConfigSchema().nullish(),
468
502
  debug: GraphCommerceDebugConfigSchema().nullish(),
469
- demoMode: z.boolean().nullish(),
503
+ demoMode: z.boolean().default(true).nullish(),
470
504
  enableGuestCheckoutLogin: z.boolean().nullish(),
471
505
  googleAnalyticsId: z.string().nullish(),
472
506
  googleRecaptchaKey: z.string().nullish(),
@@ -478,9 +512,11 @@ export function GraphCommerceConfigSchema(): z.ZodObject<Properties<GraphCommerc
478
512
  hygraphWriteAccessToken: z.string().nullish(),
479
513
  limitSsg: z.boolean().nullish(),
480
514
  magentoEndpoint: z.string().min(1),
515
+ magentoVersion: z.number(),
481
516
  previewSecret: z.string().nullish(),
482
- productFiltersLayout: ProductFiltersLayoutSchema.nullish(),
517
+ productFiltersLayout: ProductFiltersLayoutSchema.default("DEFAULT").nullish(),
483
518
  productFiltersPro: z.boolean().nullish(),
519
+ productListPaginationVariant: PaginationVariantSchema.default("COMPACT").nullish(),
484
520
  productRoute: z.string().nullish(),
485
521
  recentlyViewedProducts: RecentlyViewedProductsConfigSchema().nullish(),
486
522
  robotsAllow: z.boolean().nullish(),
@@ -504,6 +540,7 @@ export function GraphCommerceStorefrontConfigSchema(): z.ZodObject<Properties<Gr
504
540
  return z.object({
505
541
  canonicalBaseUrl: z.string().nullish(),
506
542
  cartDisplayPricesInclTax: z.boolean().nullish(),
543
+ customerCompanyFieldsEnable: z.boolean().nullish(),
507
544
  defaultLocale: z.boolean().nullish(),
508
545
  domain: z.string().nullish(),
509
546
  googleAnalyticsId: z.string().nullish(),
@@ -512,7 +549,8 @@ export function GraphCommerceStorefrontConfigSchema(): z.ZodObject<Properties<Gr
512
549
  hygraphLocales: z.array(z.string().min(1)).nullish(),
513
550
  linguiLocale: z.string().nullish(),
514
551
  locale: z.string().min(1),
515
- magentoStoreCode: z.string().min(1)
552
+ magentoStoreCode: z.string().min(1),
553
+ robotsAllow: z.boolean().nullish()
516
554
  })
517
555
  }
518
556
 
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from './withGraphCommerce'
9
9
  export * from './generated/config'
10
10
  export * from './config'
11
11
  export * from './runtimeCachingOptimizations'
12
+ export * from './interceptors/commands/codegenInterceptors'
12
13
 
13
14
  export type PluginProps<P extends Record<string, unknown> = Record<string, unknown>> = P & {
14
15
  Prev: React.FC<P>
@@ -0,0 +1,27 @@
1
+ import { loadConfig } from '../../config/loadConfig'
2
+ import { resolveDependency } from '../../utils/resolveDependency'
3
+ import { findPlugins } from '../findPlugins'
4
+ import { generateInterceptors } from '../generateInterceptors'
5
+ import { writeInterceptors } from '../writeInterceptors'
6
+ import dotenv from 'dotenv'
7
+
8
+ dotenv.config()
9
+
10
+ // eslint-disable-next-line @typescript-eslint/require-await
11
+ export async function codegenInterceptors() {
12
+ const conf = loadConfig(process.cwd())
13
+
14
+ const [plugins, errors] = findPlugins(conf)
15
+
16
+ const generatedInterceptors = await generateInterceptors(
17
+ plugins,
18
+ resolveDependency(),
19
+ conf.debug,
20
+ true,
21
+ )
22
+
23
+ // const generated = Date.now()
24
+ // console.log('Generated interceptors in', generated - found, 'ms')
25
+
26
+ await writeInterceptors(generatedInterceptors)
27
+ }
@@ -112,8 +112,8 @@ function extractValue(node: Node, path?: string[], optional: boolean = false): a
112
112
  case 'undefined':
113
113
  return undefined
114
114
  default:
115
- if (optional) return RUNTIME_VALUE
116
- throw new UnsupportedValueError(`Unknown identifier "${node.value}"`, path)
115
+ return RUNTIME_VALUE
116
+ // throw new UnsupportedValueError(`Unknown identifier "${node.value}"`, path)
117
117
  }
118
118
  } else if (isArrayExpression(node)) {
119
119
  // e.g. [1, 2, 3]
@@ -123,11 +123,11 @@ function extractValue(node: Node, path?: string[], optional: boolean = false): a
123
123
  if (elem) {
124
124
  if (elem.spread) {
125
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
- )
126
+ return RUNTIME_VALUE
127
+ // throw new UnsupportedValueError(
128
+ // 'Unsupported spread operator in the Array Expression',
129
+ // path,
130
+ // )
131
131
  }
132
132
 
133
133
  arr.push(extractValue(elem.expression, path && [...path, `[${i}]`], optional))
@@ -144,11 +144,11 @@ function extractValue(node: Node, path?: string[], optional: boolean = false): a
144
144
  for (const prop of node.properties) {
145
145
  if (!isKeyValueProperty(prop)) {
146
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
- )
147
+ return RUNTIME_VALUE
148
+ // throw new UnsupportedValueError(
149
+ // 'Unsupported spread operator in the Object Expression',
150
+ // path,
151
+ // )
152
152
  }
153
153
 
154
154
  let key
@@ -159,11 +159,11 @@ function extractValue(node: Node, path?: string[], optional: boolean = false): a
159
159
  // e.g. { "a": 1, "b": 2 }
160
160
  key = prop.key.value
161
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
- )
162
+ return RUNTIME_VALUE
163
+ // throw new UnsupportedValueError(
164
+ // `Unsupported key type "${prop.key.type}" in the Object Expression`,
165
+ // path,
166
+ // )
167
167
  }
168
168
 
169
169
  obj[key] = extractValue(prop.value, path && [...path, key])
@@ -174,8 +174,8 @@ function extractValue(node: Node, path?: string[], optional: boolean = false): a
174
174
  // e.g. `abc`
175
175
  if (node.expressions.length !== 0) {
176
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)
177
+ return RUNTIME_VALUE
178
+ // throw new UnsupportedValueError('Unsupported template literal with expressions', path)
179
179
  }
180
180
 
181
181
  // When TemplateLiteral has 0 expressions, the length of quasis is always 1.
@@ -191,8 +191,8 @@ function extractValue(node: Node, path?: string[], optional: boolean = false): a
191
191
 
192
192
  return cooked ?? raw
193
193
  } else {
194
- if (optional) return RUNTIME_VALUE
195
- throw new UnsupportedValueError(`Unsupported node type "${node.type}"`, path)
194
+ return RUNTIME_VALUE
195
+ // throw new UnsupportedValueError(`Unsupported node type "${node.type}"`, path)
196
196
  }
197
197
  }
198
198
 
@@ -32,6 +32,21 @@ function parseAndFindExport(
32
32
  break
33
33
  }
34
34
  }
35
+
36
+ if (node.type === 'ExportNamedDeclaration') {
37
+ for (const specifier of node.specifiers) {
38
+ if (specifier.type === 'ExportSpecifier') {
39
+ if (specifier.exported?.value === findExport) return resolved
40
+ } else if (specifier.type === 'ExportDefaultSpecifier') {
41
+ // todo
42
+ } else if (specifier.type === 'ExportNamespaceSpecifier') {
43
+ // todo
44
+ }
45
+ }
46
+ }
47
+
48
+ // todo: if (node.type === 'ExportDefaultDeclaration') {}
49
+ // todo: if (node.type === 'ExportDefaultExpression') {}
35
50
  }
36
51
 
37
52
  const exports = ast.body
@@ -95,7 +110,7 @@ export function findOriginalSource(
95
110
  return {
96
111
  resolved: undefined,
97
112
  error: new Error(
98
- `Can not find ${plug.targetModule}#${plug.sourceExport} for plugin ${plug.sourceModule}`,
113
+ `Plugin target not found ${plug.targetModule}#${plug.sourceExport} for plugin ${plug.sourceModule}#${plug.sourceExport}`,
99
114
  ),
100
115
  }
101
116
  }
@@ -17,10 +17,14 @@ export function findPlugins(config: GraphCommerceConfig, cwd: string = process.c
17
17
 
18
18
  const errors: string[] = []
19
19
  const plugins: PluginConfig[] = []
20
- dependencies.forEach((dependency, path) => {
21
- const files = globSync(`${dependency}/plugins/**/*.{ts,tsx}`, { dotRelative: true })
20
+ dependencies.forEach((filePath, packageName) => {
21
+ const files = globSync(`${filePath}/plugins/**/*.{ts,tsx}`)
22
22
  files.forEach((file) => {
23
- const sourceModule = file.replace(dependency, path).replace('.tsx', '').replace('.ts', '')
23
+ let sourceModule = file.replace('.tsx', '').replace('.ts', '')
24
+ if (file.startsWith(filePath))
25
+ sourceModule = `${packageName}/${sourceModule.slice(filePath.length + 1)}`
26
+
27
+ if (packageName === '.' && !sourceModule.startsWith('.')) sourceModule = `./${sourceModule}`
24
28
 
25
29
  try {
26
30
  const ast = parseFileSync(file, { syntax: 'typescript', tsx: true })
@@ -66,11 +66,11 @@ export type Interceptor = ResolveDependencyReturn & {
66
66
 
67
67
  export type MaterializedPlugin = Interceptor & { template: string }
68
68
 
69
- export const SOURCE_START = '/** ❗️ Original (modified) source starts here **/'
70
- export const SOURCE_END = '/** ❗️ Original (modified) source ends here **/'
69
+ export const SOURCE_START = '/** Original source starts here (do not modify!): **/'
70
+ export const SOURCE_END = '/** Original source ends here (do not modify!) **/'
71
71
 
72
72
  const originalSuffix = 'Original'
73
- const sourceSuffix = 'Source'
73
+ const sourceSuffix = 'Plugin'
74
74
  const interceptorSuffix = 'Interceptor'
75
75
  const disabledSuffix = 'Disabled'
76
76
  const name = (plugin: PluginConfig) =>
@@ -81,9 +81,9 @@ const name = (plugin: PluginConfig) =>
81
81
  const fileName = (plugin: PluginConfig) => `${plugin.sourceModule}#${plugin.sourceExport}`
82
82
 
83
83
  const originalName = (n: string) => `${n}${originalSuffix}`
84
- const sourceName = (n: string) => `${n}${sourceSuffix}`
84
+ const sourceName = (n: string) => `${n}`
85
85
  const interceptorName = (n: string) => `${n}${interceptorSuffix}`
86
- const interceptorPropsName = (n: string) => `${interceptorName(n)}Props`
86
+ const interceptorPropsName = (n: string) => `${n}Props`
87
87
 
88
88
  export function moveRelativeDown(plugins: PluginConfig[]) {
89
89
  return [...plugins].sort((a, b) => {
@@ -153,7 +153,12 @@ export async function generateInterceptor(
153
153
  const duplicateInterceptors = new Set()
154
154
 
155
155
  let carry = originalName(base)
156
- const carryProps: string[] = []
156
+ let carryProps: string[] = []
157
+ const pluginSee: string[] = []
158
+
159
+ pluginSee.push(
160
+ `@see {@link file://${interceptor.sourcePathRelative}} for original source file`,
161
+ )
157
162
 
158
163
  const pluginStr = plugins
159
164
  .reverse()
@@ -175,17 +180,20 @@ export async function generateInterceptor(
175
180
  s.replace(originalSuffix, disabledSuffix),
176
181
  ).visitModule(ast)
177
182
 
178
- carryProps.push(interceptorPropsName(name(p)))
183
+ carryProps.push(`React.ComponentProps<typeof ${sourceName(name(p))}>`)
179
184
 
180
- result = `type ${interceptorPropsName(name(p))} = React.ComponentProps<typeof ${sourceName(name(p))}>`
185
+ pluginSee.push(
186
+ `@see {${sourceName(name(p))}} for replacement of the original source (original source not used)`,
187
+ )
181
188
  }
182
189
 
183
190
  if (isReactPluginConfig(p)) {
184
- carryProps.push(interceptorPropsName(name(p)))
191
+ const withBraces = config.pluginStatus || process.env.NODE_ENV === 'development'
185
192
 
186
193
  result = `
187
- type ${interceptorPropsName(name(p))} = DistributedOmit<React.ComponentProps<typeof ${sourceName(name(p))}>, 'Prev'>
188
- const ${interceptorName(name(p))} = (props: ${carryProps.join(' & ')}) => {
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 ? `{` : '('}
189
197
  ${config.pluginStatus ? `logOnce(\`🔌 Rendering ${base} with plugin(s): ${wrapChain} wrapping <${base}/>\`)` : ''}
190
198
 
191
199
  ${
@@ -194,8 +202,11 @@ export async function generateInterceptor(
194
202
  logOnce('${fileName(p)} does not spread props to prev: <Prev {...props}/>. This will cause issues if multiple plugins are applied to this component.')`
195
203
  : ''
196
204
  }
197
- return <${sourceName(name(p))} {...props} Prev={${carry} as React.FC} />
198
- }`
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`)
199
210
  }
200
211
 
201
212
  if (isMethodPluginConfig(p)) {
@@ -203,6 +214,7 @@ export async function generateInterceptor(
203
214
  ${config.pluginStatus ? `logOnce(\`🔌 Calling ${base} with plugin(s): ${wrapChain} wrapping ${base}()\`)` : ''}
204
215
  return ${sourceName(name(p))}(${carry}, ...args)
205
216
  }`
217
+ pluginSee.push(`@see {${sourceName(name(p))}} for source of applied plugin`)
206
218
  }
207
219
 
208
220
  carry = p.type === 'replace' ? sourceName(name(p)) : interceptorName(name(p))
@@ -211,13 +223,23 @@ export async function generateInterceptor(
211
223
  .filter((v) => !!v)
212
224
  .join('\n')
213
225
 
214
- const isComponent = plugins.every((p) => isReplacePluginConfig(p) || isReactPluginConfig(p))
226
+ const isComponent = plugins.every((p) => isReactPluginConfig(p))
215
227
  if (isComponent && plugins.some((p) => isMethodPluginConfig(p))) {
216
228
  throw new Error(`Cannot mix React and Method plugins for ${base} in ${dependency}.`)
217
229
  }
218
230
 
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
+
219
240
  if (process.env.NODE_ENV === 'development' && isComponent) {
220
241
  return `${pluginStr}
242
+ ${seeString}
221
243
  export const ${base}: typeof ${carry} = (props) => {
222
244
  return <${carry} {...props} data-plugin />
223
245
  }`
@@ -225,6 +247,7 @@ export async function generateInterceptor(
225
247
 
226
248
  return `
227
249
  ${pluginStr}
250
+ ${seeString}
228
251
  export const ${base} = ${carry}
229
252
  `
230
253
  })
@@ -247,12 +270,13 @@ export async function generateInterceptor(
247
270
  /* This file is automatically generated for ${dependency} */
248
271
  ${
249
272
  Object.values(targetExports).some((t) => t.some((p) => p.type === 'component'))
250
- ? `import type { DistributedOmit } from 'type-fest'`
273
+ ? `import type { DistributedOmit as OmitPrev } from 'type-fest'`
251
274
  : ''
252
275
  }
253
276
 
254
277
  ${pluginImports}
255
278
 
279
+ /** @see {@link file://${interceptor.sourcePathRelative}} for source of original */
256
280
  ${SOURCE_START}
257
281
  ${printSync(ast).code}
258
282
  ${SOURCE_END}
@@ -19,6 +19,7 @@ export async function generateInterceptors(
19
19
  plugins: PluginConfig[],
20
20
  resolve: ResolveDependency,
21
21
  config?: GraphCommerceDebugConfig | null | undefined,
22
+ force?: boolean,
22
23
  ): Promise<GenerateInterceptorsReturn> {
23
24
  const byTargetModuleAndExport = moveRelativeDown(plugins).reduce<Record<string, Interceptor>>(
24
25
  (acc, plug) => {
@@ -68,12 +69,14 @@ export async function generateInterceptors(
68
69
  Object.entries(byTargetModuleAndExport).map(async ([target, interceptor]) => {
69
70
  const file = `${interceptor.fromRoot}.interceptor.tsx`
70
71
 
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
72
+ const originalSource =
73
+ !force &&
74
+ (await fs
75
+ .access(file, fs.constants.F_OK)
76
+ .then(() => true)
77
+ .catch(() => false))
78
+ ? (await fs.readFile(file)).toString()
79
+ : undefined
77
80
 
78
81
  return [
79
82
  target,
@@ -34,10 +34,11 @@ export function parseStructure(ast: Module, gcConfig: GraphCommerceConfig, sourc
34
34
  } = exports
35
35
 
36
36
  const exportVals = Object.keys(rest)
37
+
37
38
  if (component && !moduleConfig) exportVals.push('Plugin')
38
39
  if (func && !moduleConfig) exportVals.push('plugin')
39
40
 
40
- return exportVals
41
+ const pluginConfigs = exportVals
41
42
  .map((exportVal) => {
42
43
  let config = isObject(moduleConfig) ? moduleConfig : {}
43
44
 
@@ -49,6 +50,7 @@ export function parseStructure(ast: Module, gcConfig: GraphCommerceConfig, sourc
49
50
  config = { ...moduleConfig, export: exportVal }
50
51
  } else {
51
52
  console.error(`Plugin configuration invalid! See ${sourceModule}`)
53
+ return null
52
54
  }
53
55
 
54
56
  const parsed = pluginConfigParsed.safeParse(config)
@@ -79,4 +81,15 @@ export function parseStructure(ast: Module, gcConfig: GraphCommerceConfig, sourc
79
81
  return val
80
82
  })
81
83
  .filter(nonNullable)
84
+
85
+ const newPluginConfigs = pluginConfigs.reduce<PluginConfig[]>((acc, pluginConfig) => {
86
+ if (
87
+ !acc.find((accPluginConfig) => accPluginConfig.sourceExport === pluginConfig.sourceExport)
88
+ ) {
89
+ acc.push(pluginConfig)
90
+ }
91
+ return acc
92
+ }, [])
93
+
94
+ return newPluginConfigs
82
95
  }
@@ -17,24 +17,24 @@ export async function writeInterceptors(
17
17
  cwd: string = process.cwd(),
18
18
  ) {
19
19
  const dependencies = resolveDependenciesSync(cwd)
20
- const existing: string[] = []
20
+ const existing = new Set<string>()
21
21
  dependencies.forEach((dependency) => {
22
22
  const files = globSync(
23
23
  [`${dependency}/**/*.interceptor.tsx`, `${dependency}/**/*.interceptor.ts`],
24
24
  { cwd },
25
25
  )
26
- existing.push(...files)
26
+ files.forEach((file) => existing.add(file))
27
27
  })
28
28
 
29
29
  const written = Object.entries(interceptors).map(async ([, plugin]) => {
30
30
  const extension = plugin.sourcePath.endsWith('.tsx') ? '.tsx' : '.ts'
31
31
  const relativeFile = `${plugin.fromRoot}.interceptor${extension}`
32
32
 
33
- if (existing.includes(relativeFile)) {
34
- delete existing[existing.indexOf(relativeFile)]
33
+ if (existing.has(relativeFile)) {
34
+ existing.delete(relativeFile)
35
35
  }
36
- if (existing.includes(`./${relativeFile}`)) {
37
- delete existing[existing.indexOf(`./${relativeFile}`)]
36
+ if (existing.has(`./${relativeFile}`)) {
37
+ existing.delete(`./${relativeFile}`)
38
38
  }
39
39
 
40
40
  const fileToWrite = path.join(cwd, relativeFile)
@@ -47,7 +47,7 @@ export async function writeInterceptors(
47
47
  })
48
48
 
49
49
  // Cleanup unused interceptors
50
- const cleaned = existing.map(
50
+ const cleaned = [...existing].map(
51
51
  async (file) => (await checkFileExists(file)) && (await fs.unlink(file)),
52
52
  )
53
53
 
@@ -12,6 +12,7 @@ function resolveRecursivePackageJson(
12
12
  dependencyPath: string,
13
13
  dependencyStructure: DependencyStructure,
14
14
  root: string,
15
+ additionalDependencies: string[] = [],
15
16
  ) {
16
17
  const isRoot = dependencyPath === root
17
18
  const fileName = require.resolve(path.join(dependencyPath, 'package.json'))
@@ -28,8 +29,9 @@ function resolveRecursivePackageJson(
28
29
  const dependencies = [
29
30
  ...new Set(
30
31
  [
31
- ...Object.keys(packageJson.dependencies ?? {}),
32
- ...Object.keys(packageJson.devDependencies ?? {}),
32
+ ...Object.keys(packageJson.dependencies ?? []),
33
+ ...Object.keys(packageJson.devDependencies ?? []),
34
+ ...additionalDependencies,
33
35
  // ...Object.keys(packageJson.peerDependencies ?? {}),
34
36
  ].filter((name) => name.includes('graphcommerce')),
35
37
  ),
@@ -77,7 +79,13 @@ export function sortDependencies(dependencyStructure: DependencyStructure): Pack
77
79
  export function resolveDependenciesSync(root = process.cwd()) {
78
80
  const cached = resolveCache.get(root)
79
81
  if (cached) return cached
80
- const dependencyStructure = resolveRecursivePackageJson(root, {}, root)
82
+
83
+ const dependencyStructure = resolveRecursivePackageJson(
84
+ root,
85
+ {},
86
+ root,
87
+ process.env.PRIVATE_ADDITIONAL_DEPENDENCIES?.split(',') ?? [],
88
+ )
81
89
 
82
90
  const sorted = sortDependencies(dependencyStructure)
83
91
  resolveCache.set(root, sorted)
@@ -11,6 +11,7 @@ export type ResolveDependencyReturn =
11
11
  fromModule: string
12
12
  source: string
13
13
  sourcePath: string
14
+ sourcePathRelative: string
14
15
  }
15
16
 
16
17
  export type ResolveDependency = (
@@ -31,6 +32,7 @@ export const resolveDependency = (cwd: string = process.cwd()) => {
31
32
  root: '.',
32
33
  source: '',
33
34
  sourcePath: '',
35
+ sourcePathRelative: '',
34
36
  dependency,
35
37
  fromRoot: dependency,
36
38
  fromModule: dependency,
@@ -73,6 +75,10 @@ export const resolveDependency = (cwd: string = process.cwd()) => {
73
75
  ? '.'
74
76
  : `./${relative.split('/')[relative.split('/').length - 1]}`
75
77
 
78
+ const sourcePathRelative = !sourcePath
79
+ ? '.'
80
+ : `./${sourcePath.split('/')[sourcePath.split('/').length - 1]}`
81
+
76
82
  if (dependency.startsWith('./')) fromModule = `.${relative}`
77
83
 
78
84
  dependencyPaths = {
@@ -83,6 +89,7 @@ export const resolveDependency = (cwd: string = process.cwd()) => {
83
89
  fromModule,
84
90
  source,
85
91
  sourcePath,
92
+ sourcePathRelative,
86
93
  }
87
94
  }
88
95
  })