@graphcommerce/next-config 6.1.1-canary.4 → 6.2.0-canary.6

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Change Log
2
2
 
3
+ ## 6.2.0-canary.6
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1915](https://github.com/graphcommerce-org/graphcommerce/pull/1915) [`f4a8c3881`](https://github.com/graphcommerce-org/graphcommerce/commit/f4a8c388183e17c52e7f66536c5448749f494d7f) - Added the ability to create functional plugins for usage in non-component areas and hooks ([@paales](https://github.com/paales))
8
+
9
+ ## 6.1.1-canary.5
10
+
3
11
  ## 6.1.1-canary.4
4
12
 
5
13
  ## 6.1.1-canary.3
@@ -48,6 +48,10 @@ it('replaces config in string', () => {
48
48
  },
49
49
  {
50
50
  "cartDisplayPricesInclTax": true,
51
+ "hygraphLocales": [
52
+ "nl",
53
+ "en_us",
54
+ ],
51
55
  "locale": "nl",
52
56
  "magentoStoreCode": "nl_NL",
53
57
  },
@@ -9,10 +9,12 @@ it('finds plugins', () => {
9
9
  googleAnalyticsId: '123',
10
10
  } as GraphCommerceConfig
11
11
 
12
- const plugins = findPlugins(fakeconfig, projectRoot)
12
+ const [plugins, errors] = findPlugins(fakeconfig, projectRoot)
13
13
  const disabled = plugins.filter((p) => !p.enabled)
14
14
  const enabled = plugins.filter((p) => p.enabled)
15
15
 
16
+ expect(errors).toMatchInlineSnapshot(`[]`)
17
+
16
18
  expect(enabled).toMatchInlineSnapshot(`
17
19
  [
18
20
  {
@@ -96,6 +98,7 @@ it('finds plugins', () => {
96
98
  "component": "ProductPageMeta",
97
99
  "enabled": true,
98
100
  "exported": "@graphcommerce/magento-product",
101
+ "ifConfig": "googleAnalyticsId",
99
102
  "plugin": "@graphcommerce/googleanalytics/plugins/GaViewItem",
100
103
  },
101
104
  {
@@ -127,10 +130,10 @@ it('finds plugins', () => {
127
130
  "plugin": "@graphcommerce/googlerecaptcha/plugins/GrecaptchaGraphQLProvider",
128
131
  },
129
132
  {
130
- "component": "GraphQLProvider",
131
133
  "enabled": true,
132
- "exported": "@graphcommerce/graphql",
133
- "plugin": "@graphcommerce/graphcms-ui/plugins/HygraphGraphqlProvider",
134
+ "exported": "@graphcommerce/graphql/config",
135
+ "func": "graphqlConfig",
136
+ "plugin": "@graphcommerce/graphcms-ui/plugins/hygraphGraphqlConfig",
134
137
  },
135
138
  {
136
139
  "component": "PaymentMethodContextProvider",
@@ -157,16 +160,10 @@ it('finds plugins', () => {
157
160
  "plugin": "@graphcommerce/magento-customer/plugins/MagentoCustomerGraphqlProvider",
158
161
  },
159
162
  {
160
- "component": "GraphQLProvider",
161
163
  "enabled": true,
162
- "exported": "@graphcommerce/graphql",
163
- "plugin": "@graphcommerce/magento-store/plugins/MagentoStoreGraphqlProvider",
164
- },
165
- {
166
- "component": "GraphQLProvider",
167
- "enabled": true,
168
- "exported": "@graphcommerce/graphql",
169
- "plugin": "@graphcommerce/magento-graphql/plugins/MagentoGraphqlGraphqlProvider",
164
+ "exported": "@graphcommerce/graphql/config",
165
+ "func": "graphqlConfig",
166
+ "plugin": "@graphcommerce/magento-store/plugins/magentoStoreGraphqlConfig",
170
167
  },
171
168
  ]
172
169
  `)
@@ -267,3 +267,146 @@ it('it handles root plugins deeper nested', () => {
267
267
  interceptors['packages/next-ui/Overlay/components/OverlaySsr'].components.OverlaySsr[0].plugin,
268
268
  ).toMatchInlineSnapshot(`"../../../../examples/magento-graphcms/plugins/EnableCrosssellsPlugin"`)
269
269
  })
270
+
271
+ it('generates method interceptors alognside component interceptors', () => {
272
+ const plugins = [
273
+ {
274
+ enabled: true,
275
+ exported: '@graphcommerce/graphql',
276
+ component: 'GraphQLProvider',
277
+ plugin: '@graphcommerce/magento-graphql/plugins/MagentoGraphqlGraphqlProvider',
278
+ },
279
+ {
280
+ enabled: true,
281
+ exported: '@graphcommerce/graphql',
282
+ func: 'inMemoryCache',
283
+ plugin: '@graphcommerce/magento-graphql/plugins/magentoInitMemoryCache',
284
+ },
285
+ {
286
+ enabled: true,
287
+ exported: '@graphcommerce/graphql',
288
+ func: 'inMemoryCache',
289
+ plugin: '@graphcommerce/magento-hygraph/plugins/hygraphInitMemoryCache',
290
+ },
291
+ ]
292
+
293
+ const resolve = resolveDependency(projectRoot)
294
+ const interceptors = generateInterceptors(plugins, resolve)
295
+
296
+ expect(interceptors['packages/graphql/index']?.template).toMatchInlineSnapshot(`
297
+ "/* This file is automatically generated for @graphcommerce/graphql */
298
+
299
+ export * from '.'
300
+ import { Plugin as MagentoGraphqlGraphqlProvider } from '@graphcommerce/magento-graphql/plugins/MagentoGraphqlGraphqlProvider'
301
+ import { plugin as magentoInitMemoryCache } from '@graphcommerce/magento-graphql/plugins/magentoInitMemoryCache'
302
+ import { plugin as hygraphInitMemoryCache } from '@graphcommerce/magento-hygraph/plugins/hygraphInitMemoryCache'
303
+ import { ComponentProps } from 'react'
304
+ import {
305
+ GraphQLProvider as GraphQLProviderBase,
306
+ inMemoryCache as inMemoryCacheBase,
307
+ } from '.'
308
+
309
+ /**
310
+ * Interceptor for \`<GraphQLProvider/>\` with these plugins:
311
+ *
312
+ * - \`@graphcommerce/magento-graphql/plugins/MagentoGraphqlGraphqlProvider\`
313
+ */
314
+ type GraphQLProviderProps = ComponentProps<typeof GraphQLProviderBase>
315
+
316
+ function MagentoGraphqlGraphqlProviderInterceptor(props: GraphQLProviderProps) {
317
+ return <MagentoGraphqlGraphqlProvider {...props} Prev={GraphQLProviderBase} />
318
+ }
319
+ export const GraphQLProvider = MagentoGraphqlGraphqlProviderInterceptor
320
+
321
+ /**
322
+ * Interceptor for \`inMemoryCache()\` with these plugins:
323
+ *
324
+ * - \`@graphcommerce/magento-hygraph/plugins/hygraphInitMemoryCache\`
325
+ * - \`@graphcommerce/magento-graphql/plugins/magentoInitMemoryCache\`
326
+ */
327
+ const hygraphInitMemoryCacheInterceptor: typeof inMemoryCacheBase = (...args) => {
328
+ return hygraphInitMemoryCache(inMemoryCacheBase, ...args)
329
+ }
330
+ const magentoInitMemoryCacheInterceptor: typeof inMemoryCacheBase = (...args) => {
331
+ return magentoInitMemoryCache(hygraphInitMemoryCacheInterceptor, ...args)
332
+ }
333
+ export const inMemoryCache = magentoInitMemoryCacheInterceptor
334
+ "
335
+ `)
336
+ })
337
+
338
+ it('adds debug logging to interceptors for components', () => {
339
+ const plugins = [
340
+ {
341
+ enabled: true,
342
+ exported: '@graphcommerce/graphql',
343
+ component: 'GraphQLProvider',
344
+ plugin: '@graphcommerce/magento-graphql/plugins/MagentoGraphqlGraphqlProvider',
345
+ },
346
+ {
347
+ enabled: true,
348
+ exported: '@graphcommerce/graphql',
349
+ func: 'inMemoryCache',
350
+ plugin: '@graphcommerce/magento-graphql/plugins/magentoInitMemoryCache',
351
+ },
352
+ {
353
+ enabled: true,
354
+ exported: '@graphcommerce/graphql',
355
+ func: 'inMemoryCache',
356
+ plugin: '@graphcommerce/magento-hygraph/plugins/hygraphInitMemoryCache',
357
+ },
358
+ ]
359
+
360
+ const resolve = resolveDependency(projectRoot)
361
+ const interceptors = generateInterceptors(plugins, resolve, { pluginStatus: true })
362
+
363
+ expect(interceptors['packages/graphql/index']?.template).toMatchInlineSnapshot(`
364
+ "/* This file is automatically generated for @graphcommerce/graphql */
365
+
366
+ export * from '.'
367
+ import { Plugin as MagentoGraphqlGraphqlProvider } from '@graphcommerce/magento-graphql/plugins/MagentoGraphqlGraphqlProvider'
368
+ import { plugin as magentoInitMemoryCache } from '@graphcommerce/magento-graphql/plugins/magentoInitMemoryCache'
369
+ import { plugin as hygraphInitMemoryCache } from '@graphcommerce/magento-hygraph/plugins/hygraphInitMemoryCache'
370
+ import { ComponentProps } from 'react'
371
+ import {
372
+ GraphQLProvider as GraphQLProviderBase,
373
+ inMemoryCache as inMemoryCacheBase,
374
+ } from '.'
375
+
376
+ const logged: Set<string> = new Set();
377
+ const logInterceptor = (log: string, ...additional: unknown[]) => {
378
+ if (logged.has(log)) return
379
+ logged.add(log)
380
+ console.log(log, ...additional)
381
+ }
382
+
383
+ /**
384
+ * Interceptor for \`<GraphQLProvider/>\` with these plugins:
385
+ *
386
+ * - \`@graphcommerce/magento-graphql/plugins/MagentoGraphqlGraphqlProvider\`
387
+ */
388
+ type GraphQLProviderProps = ComponentProps<typeof GraphQLProviderBase>
389
+
390
+ function MagentoGraphqlGraphqlProviderInterceptor(props: GraphQLProviderProps) {
391
+ logInterceptor(\`🔌 Rendering GraphQLProvider with plugin(s): <MagentoGraphqlGraphqlProvider/> wrapping <GraphQLProvider/>\`)
392
+ return <MagentoGraphqlGraphqlProvider {...props} Prev={GraphQLProviderBase} />
393
+ }
394
+ export const GraphQLProvider = MagentoGraphqlGraphqlProviderInterceptor
395
+
396
+ /**
397
+ * Interceptor for \`inMemoryCache()\` with these plugins:
398
+ *
399
+ * - \`@graphcommerce/magento-hygraph/plugins/hygraphInitMemoryCache\`
400
+ * - \`@graphcommerce/magento-graphql/plugins/magentoInitMemoryCache\`
401
+ */
402
+ const hygraphInitMemoryCacheInterceptor: typeof inMemoryCacheBase = (...args) => {
403
+ logInterceptor(\`🔌 Calling inMemoryCache with plugin(s): magentoInitMemoryCache() wrapping hygraphInitMemoryCache() wrapping inMemoryCache()\`)
404
+ return hygraphInitMemoryCache(inMemoryCacheBase, ...args)
405
+ }
406
+ const magentoInitMemoryCacheInterceptor: typeof inMemoryCacheBase = (...args) => {
407
+ return magentoInitMemoryCache(hygraphInitMemoryCacheInterceptor, ...args)
408
+ }
409
+ export const inMemoryCache = magentoInitMemoryCacheInterceptor
410
+ "
411
+ `)
412
+ })
@@ -7,7 +7,12 @@ exports.demoConfig = {
7
7
  magentoEndpoint: 'https://backend.reachdigital.dev/graphql',
8
8
  storefront: [
9
9
  { locale: 'en', magentoStoreCode: 'en_US', defaultLocale: true },
10
- { locale: 'nl', magentoStoreCode: 'nl_NL', cartDisplayPricesInclTax: true },
10
+ {
11
+ locale: 'nl',
12
+ magentoStoreCode: 'nl_NL',
13
+ hygraphLocales: ['nl', 'en_us'],
14
+ cartDisplayPricesInclTax: true,
15
+ },
11
16
  { locale: 'fr-be', magentoStoreCode: 'fr_BE', cartDisplayPricesInclTax: true },
12
17
  { locale: 'nl-be', magentoStoreCode: 'nl_BE', cartDisplayPricesInclTax: true },
13
18
  { locale: 'en-gb', magentoStoreCode: 'en_GB', cartDisplayPricesInclTax: true },
@@ -13,7 +13,8 @@ class InterceptorPlugin {
13
13
  constructor(config) {
14
14
  this.config = config;
15
15
  this.resolveDependency = (0, resolveDependency_1.resolveDependency)();
16
- this.interceptors = (0, generateInterceptors_1.generateInterceptors)((0, findPlugins_1.findPlugins)(this.config), this.resolveDependency);
16
+ const [plugins, errors] = (0, findPlugins_1.findPlugins)(this.config);
17
+ this.interceptors = (0, generateInterceptors_1.generateInterceptors)(plugins, this.resolveDependency, this.config.debug);
17
18
  this.interceptorByDepependency = Object.fromEntries(Object.values(this.interceptors).map((i) => [i.dependency, i]));
18
19
  (0, writeInterceptors_1.writeInterceptors)(this.interceptors);
19
20
  }
@@ -21,12 +22,12 @@ class InterceptorPlugin {
21
22
  const logger = compiler.getInfrastructureLogger('InterceptorPlugin');
22
23
  // After the compilation has succeeded we watch all possible plugin locations.
23
24
  compiler.hooks.afterCompile.tap('InterceptorPlugin', (compilation) => {
24
- const plugins = (0, findPlugins_1.findPlugins)(this.config);
25
+ const [plugins, errors] = (0, findPlugins_1.findPlugins)(this.config);
25
26
  plugins.forEach((p) => {
26
27
  const absoluteFilePath = `${path_1.default.join(process.cwd(), this.resolveDependency(p.plugin).fromRoot)}.tsx`;
27
28
  compilation.fileDependencies.add(absoluteFilePath);
28
29
  });
29
- this.interceptors = (0, generateInterceptors_1.generateInterceptors)(plugins, this.resolveDependency);
30
+ this.interceptors = (0, generateInterceptors_1.generateInterceptors)(plugins, this.resolveDependency, this.config.debug);
30
31
  this.interceptorByDepependency = Object.fromEntries(Object.values(this.interceptors).map((i) => [i.dependency, i]));
31
32
  (0, writeInterceptors_1.writeInterceptors)(this.interceptors);
32
33
  });
@@ -4,35 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.findPlugins = void 0;
7
- const console_1 = require("console");
8
- const stream_1 = require("stream");
9
7
  const core_1 = require("@swc/core");
10
8
  // eslint-disable-next-line import/no-extraneous-dependencies
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ // eslint-disable-next-line import/no-extraneous-dependencies
11
11
  const glob_1 = __importDefault(require("glob"));
12
12
  const get_1 = __importDefault(require("lodash/get"));
13
- const diff_1 = __importDefault(require("../config/utils/diff"));
14
13
  const resolveDependenciesSync_1 = require("../utils/resolveDependenciesSync");
15
- function table(input) {
16
- // @see https://stackoverflow.com/a/67859384
17
- const ts = new stream_1.Transform({
18
- transform(chunk, enc, cb) {
19
- cb(null, chunk);
20
- },
21
- });
22
- const logger = new console_1.Console({ stdout: ts });
23
- logger.table(input);
24
- const t = (ts.read() || '').toString();
25
- let result = '';
26
- for (const row of t.split(/[\r\n]+/)) {
27
- let r = row.replace(/[^┬]*┬/, '┌');
28
- r = r.replace(/^├─*┼/, '├');
29
- r = r.replace(/│[^│]*/, '');
30
- r = r.replace(/^└─*┴/, '└');
31
- r = r.replace(/'/g, ' ');
32
- result += `${r}\n`;
33
- }
34
- console.log(result);
35
- }
14
+ const generateInterceptors_1 = require("./generateInterceptors");
36
15
  function parseStructure(file) {
37
16
  const ast = (0, core_1.parseFileSync)(file, { syntax: 'typescript', tsx: true });
38
17
  const imports = {};
@@ -55,45 +34,66 @@ function parseStructure(file) {
55
34
  });
56
35
  return exports;
57
36
  }
58
- let prevlog;
37
+ const pluginLogs = {};
59
38
  function findPlugins(config, cwd = process.cwd()) {
60
39
  const dependencies = (0, resolveDependenciesSync_1.resolveDependenciesSync)(cwd);
61
40
  const debug = Boolean(config.debug?.pluginStatus);
62
- if (debug)
63
- console.time('findPlugins');
41
+ const errors = [];
64
42
  const plugins = [];
65
43
  dependencies.forEach((dependency, path) => {
66
- const files = glob_1.default.sync(`${dependency}/plugins/**/*.tsx`);
44
+ const files = glob_1.default.sync(`${dependency}/plugins/**/*.{ts,tsx}`);
67
45
  files.forEach((file) => {
68
46
  try {
69
47
  const result = parseStructure(file);
70
48
  if (!result)
71
49
  return;
72
- plugins.push({
73
- plugin: file.replace(dependency, path).replace('.tsx', ''),
50
+ const pluginConfig = {
51
+ plugin: file.replace(dependency, path).replace('.tsx', '').replace('.ts', ''),
74
52
  ...result,
75
53
  enabled: !result.ifConfig || Boolean((0, get_1.default)(config, result.ifConfig)),
76
- });
54
+ };
55
+ if (!(0, generateInterceptors_1.isPluginConfig)(pluginConfig)) {
56
+ if (!(0, generateInterceptors_1.isPluginBaseConfig)(pluginConfig))
57
+ errors.push(`Plugin ${file} is not a valid plugin, make it has "export const exported = '@graphcommerce/my-package"`);
58
+ else if (file.endsWith('.ts')) {
59
+ errors.push(`Plugin ${file} is not a valid plugin, please define the method to create a plugin for "export const method = 'someMethod'"`);
60
+ }
61
+ else if (file.endsWith('.tsx')) {
62
+ errors.push(`Plugin ${file} is not a valid plugin, please define the compoennt to create a plugin for "export const component = 'SomeComponent'"`);
63
+ }
64
+ }
65
+ else {
66
+ plugins.push(pluginConfig);
67
+ }
77
68
  }
78
69
  catch (e) {
79
70
  console.error(`Error parsing ${file}`, e);
80
71
  }
81
72
  });
82
73
  });
83
- if (debug) {
84
- const formatted = plugins.map(({ plugin, component, ifConfig, enabled, exported }) => ({
85
- '💉': enabled ? '✅' : '',
86
- Reason: `${ifConfig ? `${ifConfig}` : ''}`,
87
- Plugin: plugin,
88
- Target: `${exported}#${component}`,
89
- }));
90
- const res = (0, diff_1.default)(prevlog, formatted);
91
- if (res) {
92
- table(formatted);
93
- }
94
- prevlog = formatted;
95
- console.timeEnd('findPlugins');
74
+ if (process.env.NODE_ENV === 'development' && debug) {
75
+ const byExported = plugins.reduce((acc, plugin) => {
76
+ const componentStr = (0, generateInterceptors_1.isReactPluginConfig)(plugin) ? plugin.component : '';
77
+ const funcStr = (0, generateInterceptors_1.isMethodPluginConfig)(plugin) ? plugin.func : '';
78
+ const key = `🔌 ${chalk_1.default.greenBright(`Plugins loaded for ${plugin.exported}#${componentStr}${funcStr}`)}`;
79
+ if (!acc[key])
80
+ acc[key] = [];
81
+ acc[key].push(plugin);
82
+ return acc;
83
+ }, {});
84
+ const toLog = [];
85
+ Object.entries(byExported).forEach(([key, p]) => {
86
+ const logStr = p
87
+ .filter((c) => debug || c.enabled)
88
+ .map((c) => `${c.enabled ? `🟢` : `⚪️`} ${c.plugin} ${c.ifConfig ? `(${c.ifConfig}: ${c.enabled ? 'true' : 'false'})` : ''}`)
89
+ .join('\n');
90
+ if (logStr && pluginLogs[key] !== logStr) {
91
+ toLog.push(`${key}\n${logStr}`);
92
+ pluginLogs[key] = logStr;
93
+ }
94
+ });
95
+ console.log(toLog.join('\n\n'));
96
96
  }
97
- return plugins;
97
+ return [plugins, errors];
98
98
  }
99
99
  exports.findPlugins = findPlugins;
@@ -3,8 +3,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.generateInterceptors = exports.generateInterceptor = void 0;
6
+ exports.generateInterceptors = exports.generateInterceptor = exports.isPluginConfig = exports.isMethodPluginConfig = exports.isReactPluginConfig = exports.isPluginBaseConfig = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
+ function isPluginBaseConfig(plugin) {
9
+ return (typeof plugin.exported === 'string' &&
10
+ typeof plugin.plugin === 'string' &&
11
+ typeof plugin.enabled === 'boolean');
12
+ }
13
+ exports.isPluginBaseConfig = isPluginBaseConfig;
14
+ function isReactPluginConfig(plugin) {
15
+ if (!isPluginBaseConfig(plugin))
16
+ return false;
17
+ return plugin.component !== undefined;
18
+ }
19
+ exports.isReactPluginConfig = isReactPluginConfig;
20
+ function isMethodPluginConfig(plugin) {
21
+ if (!isPluginBaseConfig(plugin))
22
+ return false;
23
+ return plugin.func !== undefined;
24
+ }
25
+ exports.isMethodPluginConfig = isMethodPluginConfig;
26
+ function isPluginConfig(plugin) {
27
+ return isReactPluginConfig(plugin) || isMethodPluginConfig(plugin);
28
+ }
29
+ exports.isPluginConfig = isPluginConfig;
8
30
  function moveRelativeDown(plugins) {
9
31
  return [...plugins].sort((a, b) => {
10
32
  if (a.plugin.startsWith('.') && !b.plugin.startsWith('.'))
@@ -14,15 +36,19 @@ function moveRelativeDown(plugins) {
14
36
  return 0;
15
37
  });
16
38
  }
17
- function generateInterceptor(interceptor) {
18
- const { fromModule, dependency, components } = interceptor;
19
- const flattended = Object.entries(components)
39
+ function generateInterceptor(interceptor, config) {
40
+ const { fromModule, dependency, components, funcs } = interceptor;
41
+ const pluginConfigs = [...Object.entries(components), ...Object.entries(funcs)]
20
42
  .map(([, plugins]) => plugins)
21
43
  .flat();
22
44
  const duplicateImports = new Set();
23
- const pluginImports = moveRelativeDown([...flattended].sort((a, b) => a.plugin.localeCompare(b.plugin)))
24
- .map((p) => p.plugin)
25
- .map((p) => `import { Plugin as ${p.split('/')[p.split('/').length - 1]} } from '${p}'`)
45
+ const pluginImports = moveRelativeDown([...pluginConfigs].sort((a, b) => a.plugin.localeCompare(b.plugin)))
46
+ .map((plugin) => {
47
+ const { plugin: p } = plugin;
48
+ if (isReactPluginConfig(plugin))
49
+ return `import { Plugin as ${p.split('/')[p.split('/').length - 1]} } from '${p}'`;
50
+ return `import { plugin as ${p.split('/')[p.split('/').length - 1]} } from '${p}'`;
51
+ })
26
52
  .filter((str) => {
27
53
  if (duplicateImports.has(str))
28
54
  return false;
@@ -30,46 +56,87 @@ function generateInterceptor(interceptor) {
30
56
  return true;
31
57
  })
32
58
  .join('\n');
33
- const imports = Object.entries(components).map(([component]) => `${component} as ${component}Base`);
59
+ const imports = [
60
+ ...Object.entries(components).map(([component]) => `${component} as ${component}Base`),
61
+ ...Object.entries(funcs).map(([func]) => `${func} as ${func}Base`),
62
+ ];
34
63
  const importInjectables = imports.length > 1
35
64
  ? `import {
36
65
  ${imports.join(',\n ')},
37
66
  } from '${fromModule}'`
38
67
  : `import { ${imports[0]} } from '${fromModule}'`;
39
- const pluginExports = Object.entries(components)
40
- .map(([component, plugins]) => {
68
+ const entries = [
69
+ ...Object.entries(components),
70
+ ...Object.entries(funcs),
71
+ ];
72
+ const pluginExports = entries
73
+ .map(([base, plugins]) => {
41
74
  const duplicateInterceptors = new Set();
42
- let carry = `${component}Base`;
75
+ const name = (p) => p.plugin.split('/')[p.plugin.split('/').length - 1];
76
+ const filterNoDuplicate = (p) => {
77
+ if (duplicateInterceptors.has(name(p)))
78
+ return false;
79
+ duplicateInterceptors.add(name(p));
80
+ return true;
81
+ };
82
+ let carry = `${base}Base`;
43
83
  const pluginStr = plugins
44
84
  .reverse()
45
- .map((p) => p.plugin.split('/')[p.plugin.split('/').length - 1])
46
- .filter((importStr) => {
47
- if (duplicateInterceptors.has(importStr)) {
48
- return false;
85
+ .filter(filterNoDuplicate)
86
+ .map((p) => {
87
+ let result;
88
+ if (isReactPluginConfig(p)) {
89
+ const wrapChain = plugins
90
+ .reverse()
91
+ .map((pl) => `<${name(pl)}/>`)
92
+ .join(' wrapping ');
93
+ const debugLog = carry === `${base}Base` && config.pluginStatus
94
+ ? `\n logInterceptor(\`🔌 Rendering ${base} with plugin(s): ${wrapChain} wrapping <${base}/>\`)`
95
+ : '';
96
+ result = `function ${name(p)}Interceptor(props: ${base}Props) {${debugLog}
97
+ return <${name(p)} {...props} Prev={${carry}} />
98
+ }`;
49
99
  }
50
- duplicateInterceptors.add(importStr);
51
- return true;
52
- })
53
- .map((name) => {
54
- const result = `function ${name}Interceptor(props: ${component}Props) {
55
- return <${name} {...props} Prev={${carry}} />
100
+ else {
101
+ const wrapChain = plugins
102
+ .reverse()
103
+ .map((pl) => `${name(pl)}()`)
104
+ .join(' wrapping ');
105
+ const debugLog = carry === `${base}Base` && config.pluginStatus
106
+ ? `\n logInterceptor(\`🔌 Calling ${base} with plugin(s): ${wrapChain} wrapping ${base}()\`)`
107
+ : '';
108
+ result = `const ${name(p)}Interceptor: typeof ${base}Base = (...args) => {${debugLog}
109
+ return ${name(p)}(${carry}, ...args)
56
110
  }`;
57
- carry = `${name}Interceptor`;
111
+ }
112
+ carry = `${name(p)}Interceptor`;
58
113
  return result;
59
114
  })
60
115
  .join('\n');
116
+ const isComponent = plugins.every((p) => isReactPluginConfig(p));
117
+ if (isComponent && plugins.some((p) => isMethodPluginConfig(p))) {
118
+ throw new Error(`Cannot mix React and Method plugins for ${base} in ${dependency}.`);
119
+ }
61
120
  return `
62
121
  /**
63
- * Interceptor for \`<${component}/>\` with these plugins:
122
+ * Interceptor for \`${isComponent ? `<${base}/>` : `${base}()`}\` with these plugins:
64
123
  *
65
124
  ${plugins.map((p) => ` * - \`${p.plugin}\``).join('\n')}
66
125
  */
67
- type ${component}Props = ComponentProps<typeof ${component}Base>
68
-
69
- ${pluginStr}
70
- export const ${component} = ${carry}`;
126
+ ${isComponent ? `type ${base}Props = ComponentProps<typeof ${base}Base>\n\n` : ``}${pluginStr}
127
+ export const ${base} = ${carry}`;
71
128
  })
72
129
  .join('\n');
130
+ const logOnce = config.pluginStatus
131
+ ? `
132
+ const logged: Set<string> = new Set();
133
+ const logInterceptor = (log: string, ...additional: unknown[]) => {
134
+ if (logged.has(log)) return
135
+ logged.add(log)
136
+ console.log(log, ...additional)
137
+ }
138
+ `
139
+ : '';
73
140
  const componentExports = `export * from '${fromModule}'`;
74
141
  const template = `/* This file is automatically generated for ${dependency} */
75
142
 
@@ -77,16 +144,16 @@ ${componentExports}
77
144
  ${pluginImports}
78
145
  import { ComponentProps } from 'react'
79
146
  ${importInjectables}
80
- ${pluginExports}
147
+ ${logOnce}${pluginExports}
81
148
  `;
82
149
  return { ...interceptor, template };
83
150
  }
84
151
  exports.generateInterceptor = generateInterceptor;
85
- function generateInterceptors(plugins, resolve) {
152
+ function generateInterceptors(plugins, resolve, config) {
86
153
  // todo: Do not use reduce as we're passing the accumulator to the next iteration
87
154
  const byExportedComponent = moveRelativeDown(plugins).reduce((acc, plug) => {
88
- const { exported, component, enabled, plugin } = plug;
89
- if (!exported || !component || !enabled)
155
+ const { exported, plugin } = plug;
156
+ if (!isPluginConfig(plug) || !plug.enabled)
90
157
  return acc;
91
158
  const resolved = resolve(exported);
92
159
  let pluginPathFromResolved = plugin;
@@ -99,18 +166,31 @@ function generateInterceptors(plugins, resolve) {
99
166
  ...resolved,
100
167
  target: `${resolved.fromRoot}.interceptor`,
101
168
  components: {},
169
+ funcs: {},
102
170
  };
103
- if (!acc[resolved.fromRoot].components[component])
104
- acc[resolved.fromRoot].components[component] = [];
105
- acc[resolved.fromRoot].components[component].push({
106
- ...plug,
107
- plugin: pluginPathFromResolved,
108
- });
171
+ if (isReactPluginConfig(plug)) {
172
+ const { component } = plug;
173
+ if (!acc[resolved.fromRoot].components[component])
174
+ acc[resolved.fromRoot].components[component] = [];
175
+ acc[resolved.fromRoot].components[component].push({
176
+ ...plug,
177
+ plugin: pluginPathFromResolved,
178
+ });
179
+ }
180
+ if (isMethodPluginConfig(plug)) {
181
+ const { func } = plug;
182
+ if (!acc[resolved.fromRoot].funcs[func])
183
+ acc[resolved.fromRoot].funcs[func] = [];
184
+ acc[resolved.fromRoot].funcs[func].push({
185
+ ...plug,
186
+ plugin: pluginPathFromResolved,
187
+ });
188
+ }
109
189
  return acc;
110
190
  }, {});
111
191
  return Object.fromEntries(Object.entries(byExportedComponent).map(([target, interceptor]) => [
112
192
  target,
113
- generateInterceptor(interceptor),
193
+ generateInterceptor(interceptor, config ?? {}),
114
194
  ]));
115
195
  }
116
196
  exports.generateInterceptors = generateInterceptors;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/next-config",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "6.1.1-canary.4",
5
+ "version": "6.2.0-canary.6",
6
6
  "type": "commonjs",
7
7
  "main": "dist/index.js",
8
8
  "types": "src/index.ts",
@@ -6,7 +6,12 @@ export const demoConfig: Partial<GraphCommerceConfig> & Record<string, unknown>
6
6
  magentoEndpoint: 'https://backend.reachdigital.dev/graphql',
7
7
  storefront: [
8
8
  { locale: 'en', magentoStoreCode: 'en_US', defaultLocale: true },
9
- { locale: 'nl', magentoStoreCode: 'nl_NL', cartDisplayPricesInclTax: true },
9
+ {
10
+ locale: 'nl',
11
+ magentoStoreCode: 'nl_NL',
12
+ hygraphLocales: ['nl', 'en_us'],
13
+ cartDisplayPricesInclTax: true,
14
+ },
10
15
  { locale: 'fr-be', magentoStoreCode: 'fr_BE', cartDisplayPricesInclTax: true },
11
16
  { locale: 'nl-be', magentoStoreCode: 'nl_BE', cartDisplayPricesInclTax: true },
12
17
  { locale: 'en-gb', magentoStoreCode: 'en_GB', cartDisplayPricesInclTax: true },
package/src/index.ts CHANGED
@@ -9,3 +9,12 @@ export * from './config'
9
9
  export type PluginProps<P extends Record<string, unknown> = Record<string, unknown>> = P & {
10
10
  Prev: React.FC<P>
11
11
  }
12
+
13
+ export type ReactPlugin<T extends React.FC<any>> = (
14
+ props: Parameters<T>[0] & { Prev: React.FC<Parameters<T>[0]> },
15
+ ) => ReturnType<T>
16
+
17
+ export type MethodPlugin<T extends (...args: any[]) => any> = (
18
+ prev: T,
19
+ ...args: Parameters<T>
20
+ ) => ReturnType<T>
@@ -16,7 +16,8 @@ export class InterceptorPlugin {
16
16
  constructor(private config: GraphCommerceConfig) {
17
17
  this.resolveDependency = resolveDependency()
18
18
 
19
- this.interceptors = generateInterceptors(findPlugins(this.config), this.resolveDependency)
19
+ const [plugins, errors] = findPlugins(this.config)
20
+ this.interceptors = generateInterceptors(plugins, this.resolveDependency, this.config.debug)
20
21
  this.interceptorByDepependency = Object.fromEntries(
21
22
  Object.values(this.interceptors).map((i) => [i.dependency, i]),
22
23
  )
@@ -29,7 +30,7 @@ export class InterceptorPlugin {
29
30
 
30
31
  // After the compilation has succeeded we watch all possible plugin locations.
31
32
  compiler.hooks.afterCompile.tap('InterceptorPlugin', (compilation) => {
32
- const plugins = findPlugins(this.config)
33
+ const [plugins, errors] = findPlugins(this.config)
33
34
 
34
35
  plugins.forEach((p) => {
35
36
  const absoluteFilePath = `${path.join(
@@ -39,7 +40,7 @@ export class InterceptorPlugin {
39
40
  compilation.fileDependencies.add(absoluteFilePath)
40
41
  })
41
42
 
42
- this.interceptors = generateInterceptors(plugins, this.resolveDependency)
43
+ this.interceptors = generateInterceptors(plugins, this.resolveDependency, this.config.debug)
43
44
  this.interceptorByDepependency = Object.fromEntries(
44
45
  Object.values(this.interceptors).map((i) => [i.dependency, i]),
45
46
  )
@@ -1,36 +1,19 @@
1
- import { Console } from 'console'
2
- import { Transform } from 'stream'
3
1
  import { parseFileSync } from '@swc/core'
4
2
  // eslint-disable-next-line import/no-extraneous-dependencies
3
+ import chalk from 'chalk'
4
+ // eslint-disable-next-line import/no-extraneous-dependencies
5
5
  import glob from 'glob'
6
6
  import get from 'lodash/get'
7
7
  import type { Path } from 'react-hook-form'
8
- import diff from '../config/utils/diff'
9
8
  import { GraphCommerceConfig } from '../generated/config'
10
9
  import { resolveDependenciesSync } from '../utils/resolveDependenciesSync'
11
- import type { PluginConfig } from './generateInterceptors'
12
-
13
- function table(input: any) {
14
- // @see https://stackoverflow.com/a/67859384
15
- const ts = new Transform({
16
- transform(chunk, enc, cb) {
17
- cb(null, chunk)
18
- },
19
- })
20
- const logger = new Console({ stdout: ts })
21
- logger.table(input)
22
- const t = (ts.read() || '').toString()
23
- let result = ''
24
- for (const row of t.split(/[\r\n]+/)) {
25
- let r = row.replace(/[^┬]*┬/, '┌')
26
- r = r.replace(/^├─*┼/, '├')
27
- r = r.replace(/│[^│]*/, '')
28
- r = r.replace(/^└─*┴/, '└')
29
- r = r.replace(/'/g, ' ')
30
- result += `${r}\n`
31
- }
32
- console.log(result)
33
- }
10
+ import {
11
+ isMethodPluginConfig,
12
+ isPluginBaseConfig,
13
+ isPluginConfig,
14
+ isReactPluginConfig,
15
+ PluginConfig,
16
+ } from './generateInterceptors'
34
17
 
35
18
  type ParseResult = {
36
19
  component?: string
@@ -64,51 +47,83 @@ function parseStructure(file: string): ParseResult {
64
47
  return exports as ParseResult
65
48
  }
66
49
 
67
- let prevlog: any
50
+ const pluginLogs: Record<string, string> = {}
68
51
 
69
52
  export function findPlugins(config: GraphCommerceConfig, cwd: string = process.cwd()) {
70
53
  const dependencies = resolveDependenciesSync(cwd)
71
54
 
72
55
  const debug = Boolean(config.debug?.pluginStatus)
73
56
 
74
- if (debug) console.time('findPlugins')
75
-
57
+ const errors: string[] = []
76
58
  const plugins: PluginConfig[] = []
77
59
  dependencies.forEach((dependency, path) => {
78
- const files = glob.sync(`${dependency}/plugins/**/*.tsx`)
60
+ const files = glob.sync(`${dependency}/plugins/**/*.{ts,tsx}`)
79
61
  files.forEach((file) => {
80
62
  try {
81
63
  const result = parseStructure(file)
82
64
  if (!result) return
83
65
 
84
- plugins.push({
85
- plugin: file.replace(dependency, path).replace('.tsx', ''),
66
+ const pluginConfig = {
67
+ plugin: file.replace(dependency, path).replace('.tsx', '').replace('.ts', ''),
86
68
  ...result,
87
69
  enabled: !result.ifConfig || Boolean(get(config, result.ifConfig)),
88
- })
70
+ }
71
+
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
+ }
89
89
  } catch (e) {
90
90
  console.error(`Error parsing ${file}`, e)
91
91
  }
92
92
  })
93
93
  })
94
94
 
95
- if (debug) {
96
- const formatted = plugins.map(({ plugin, component, ifConfig, enabled, exported }) => ({
97
- '💉': enabled ? '✅' : '',
98
- Reason: `${ifConfig ? `${ifConfig}` : ''}`,
99
- Plugin: plugin,
100
- Target: `${exported}#${component}`,
101
- }))
102
-
103
- const res = diff(prevlog, formatted)
104
-
105
- if (res) {
106
- table(formatted)
107
- }
108
- prevlog = formatted
95
+ 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'>[]>)
106
+
107
+ const toLog: string[] = []
108
+ Object.entries(byExported).forEach(([key, p]) => {
109
+ const logStr = p
110
+ .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
+ )
117
+ .join('\n')
118
+
119
+ if (logStr && pluginLogs[key] !== logStr) {
120
+ toLog.push(`${key}\n${logStr}`)
121
+ pluginLogs[key] = logStr
122
+ }
123
+ })
109
124
 
110
- console.timeEnd('findPlugins')
125
+ console.log(toLog.join('\n\n'))
111
126
  }
112
127
 
113
- return plugins
128
+ return [plugins, errors] as const
114
129
  }
@@ -1,16 +1,46 @@
1
1
  import path from 'node:path'
2
+ import { GraphCommerceConfig, GraphCommerceDebugConfig } from '../generated/config'
2
3
  import { ResolveDependency, ResolveDependencyReturn } from '../utils/resolveDependency'
3
4
 
4
- export type PluginConfig = {
5
- component?: string
6
- exported?: string
5
+ type PluginBaseConfig = {
6
+ exported: string
7
7
  plugin: string
8
- ifConfig?: string
9
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)
10
39
  }
11
40
 
12
41
  type Interceptor = ResolveDependencyReturn & {
13
- components: Record<string, PluginConfig[]>
42
+ components: Record<string, ReactPluginConfig[]>
43
+ funcs: Record<string, MethodPluginConfig[]>
14
44
  target: string
15
45
  template?: string
16
46
  }
@@ -25,19 +55,27 @@ function moveRelativeDown(plugins: PluginConfig[]) {
25
55
  })
26
56
  }
27
57
 
28
- export function generateInterceptor(interceptor: Interceptor): MaterializedPlugin {
29
- const { fromModule, dependency, components } = interceptor
58
+ export function generateInterceptor(
59
+ interceptor: Interceptor,
60
+ config: GraphCommerceDebugConfig,
61
+ ): MaterializedPlugin {
62
+ const { fromModule, dependency, components, funcs } = interceptor
30
63
 
31
- const flattended = Object.entries(components)
64
+ const pluginConfigs = [...Object.entries(components), ...Object.entries(funcs)]
32
65
  .map(([, plugins]) => plugins)
33
66
  .flat()
67
+
34
68
  const duplicateImports = new Set()
35
69
 
36
70
  const pluginImports = moveRelativeDown(
37
- [...flattended].sort((a, b) => a.plugin.localeCompare(b.plugin)),
71
+ [...pluginConfigs].sort((a, b) => a.plugin.localeCompare(b.plugin)),
38
72
  )
39
- .map((p) => p.plugin)
40
- .map((p) => `import { Plugin as ${p.split('/')[p.split('/').length - 1]} } from '${p}'`)
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
+ })
41
79
  .filter((str) => {
42
80
  if (duplicateImports.has(str)) return false
43
81
  duplicateImports.add(str)
@@ -45,9 +83,11 @@ export function generateInterceptor(interceptor: Interceptor): MaterializedPlugi
45
83
  })
46
84
  .join('\n')
47
85
 
48
- const imports = Object.entries(components).map(
49
- ([component]) => `${component} as ${component}Base`,
50
- )
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
+
51
91
  const importInjectables =
52
92
  imports.length > 1
53
93
  ? `import {
@@ -55,43 +95,89 @@ export function generateInterceptor(interceptor: Interceptor): MaterializedPlugi
55
95
  } from '${fromModule}'`
56
96
  : `import { ${imports[0]} } from '${fromModule}'`
57
97
 
58
- const pluginExports = Object.entries(components)
59
- .map(([component, plugins]) => {
98
+ const entries: [string, PluginConfig[]][] = [
99
+ ...Object.entries(components),
100
+ ...Object.entries(funcs),
101
+ ]
102
+ const pluginExports = entries
103
+ .map(([base, plugins]) => {
60
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`
61
114
 
62
- let carry = `${component}Base`
63
115
  const pluginStr = plugins
64
116
  .reverse()
65
- .map((p) => p.plugin.split('/')[p.plugin.split('/').length - 1])
66
- .filter((importStr) => {
67
- if (duplicateInterceptors.has(importStr)) {
68
- return false
69
- }
70
- duplicateInterceptors.add(importStr)
71
- return true
72
- })
73
- .map((name) => {
74
- const result = `function ${name}Interceptor(props: ${component}Props) {
75
- return <${name} {...props} Prev={${carry}} />
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}} />
76
133
  }`
77
- carry = `${name}Interceptor`
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`
78
150
  return result
79
151
  })
80
152
  .join('\n')
81
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
+
82
159
  return `
83
160
  /**
84
- * Interceptor for \`<${component}/>\` with these plugins:
161
+ * Interceptor for \`${isComponent ? `<${base}/>` : `${base}()`}\` with these plugins:
85
162
  *
86
163
  ${plugins.map((p) => ` * - \`${p.plugin}\``).join('\n')}
87
164
  */
88
- type ${component}Props = ComponentProps<typeof ${component}Base>
89
-
90
- ${pluginStr}
91
- export const ${component} = ${carry}`
165
+ ${isComponent ? `type ${base}Props = ComponentProps<typeof ${base}Base>\n\n` : ``}${pluginStr}
166
+ export const ${base} = ${carry}`
92
167
  })
93
168
  .join('\n')
94
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
+
95
181
  const componentExports = `export * from '${fromModule}'`
96
182
 
97
183
  const template = `/* This file is automatically generated for ${dependency} */
@@ -100,7 +186,7 @@ ${componentExports}
100
186
  ${pluginImports}
101
187
  import { ComponentProps } from 'react'
102
188
  ${importInjectables}
103
- ${pluginExports}
189
+ ${logOnce}${pluginExports}
104
190
  `
105
191
 
106
192
  return { ...interceptor, template }
@@ -111,11 +197,12 @@ export type GenerateInterceptorsReturn = Record<string, MaterializedPlugin>
111
197
  export function generateInterceptors(
112
198
  plugins: PluginConfig[],
113
199
  resolve: ResolveDependency,
200
+ config?: GraphCommerceDebugConfig | null | undefined,
114
201
  ): GenerateInterceptorsReturn {
115
202
  // todo: Do not use reduce as we're passing the accumulator to the next iteration
116
203
  const byExportedComponent = moveRelativeDown(plugins).reduce((acc, plug) => {
117
- const { exported, component, enabled, plugin } = plug
118
- if (!exported || !component || !enabled) return acc
204
+ const { exported, plugin } = plug
205
+ if (!isPluginConfig(plug) || !plug.enabled) return acc
119
206
 
120
207
  const resolved = resolve(exported)
121
208
 
@@ -133,15 +220,28 @@ export function generateInterceptors(
133
220
  ...resolved,
134
221
  target: `${resolved.fromRoot}.interceptor`,
135
222
  components: {},
223
+ funcs: {},
136
224
  } as Interceptor
137
225
 
138
- if (!acc[resolved.fromRoot].components[component])
139
- acc[resolved.fromRoot].components[component] = []
226
+ if (isReactPluginConfig(plug)) {
227
+ const { component } = plug
228
+ if (!acc[resolved.fromRoot].components[component])
229
+ acc[resolved.fromRoot].components[component] = []
140
230
 
141
- acc[resolved.fromRoot].components[component].push({
142
- ...plug,
143
- plugin: pluginPathFromResolved,
144
- })
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] = []
239
+
240
+ acc[resolved.fromRoot].funcs[func].push({
241
+ ...plug,
242
+ plugin: pluginPathFromResolved,
243
+ })
244
+ }
145
245
 
146
246
  return acc
147
247
  }, {} as Record<string, Interceptor>)
@@ -149,7 +249,7 @@ export function generateInterceptors(
149
249
  return Object.fromEntries(
150
250
  Object.entries(byExportedComponent).map(([target, interceptor]) => [
151
251
  target,
152
- generateInterceptor(interceptor),
252
+ generateInterceptor(interceptor, config ?? {}),
153
253
  ]),
154
254
  )
155
255
  }
@@ -1,10 +1,9 @@
1
1
  import CircularDependencyPlugin from 'circular-dependency-plugin'
2
2
  import { DuplicatesPlugin } from 'inspectpack/plugin'
3
3
  import type { NextConfig } from 'next'
4
- import { Redirect, Rewrite } from 'next/dist/lib/load-custom-routes'
5
4
  import { DomainLocale } from 'next/dist/server/config'
6
5
  import { RemotePattern } from 'next/dist/shared/lib/image-config'
7
- import { DefinePlugin, Configuration, WebpackPluginInstance } from 'webpack'
6
+ import { DefinePlugin, Configuration } from 'webpack'
8
7
  import { loadConfig } from './config/loadConfig'
9
8
  import { configToImportMeta } from './config/utils/configToImportMeta'
10
9
  import { GraphCommerceConfig } from './generated/config'
@@ -134,7 +133,7 @@ export function withGraphCommerce(nextConfig: NextConfig, cwd: string): NextConf
134
133
  config.plugins.push(
135
134
  new CircularDependencyPlugin({
136
135
  exclude: /readable-stream|duplexer2|node_modules\/next/,
137
- }) as WebpackPluginInstance,
136
+ }),
138
137
  )
139
138
  }
140
139
  if (graphcommerceConfig.debug?.webpackDuplicatesPlugin) {