@graphcommerce/next-config 4.30.0 → 4.31.0-canary.1

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/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { NextConfig } from 'next/dist/server/config-shared';
2
+ import React from 'react';
2
3
  export * from './utils/isMonorepo';
3
4
  export * from './utils/resolveDependenciesSync';
4
5
  export * from './withGraphCommerce';
5
6
  export declare function withYarn1Workspaces(packages?: string[]): (config: NextConfig) => NextConfig;
6
7
  export declare function withYarn1Scopes(packages?: string[]): (config: NextConfig) => NextConfig;
8
+ export declare type PluginProps<P extends Record<string, unknown> = Record<string, unknown>> = P & {
9
+ Component: React.FC<P>;
10
+ };
@@ -0,0 +1,9 @@
1
+ import { Compiler } from 'webpack';
2
+ export declare class InterceptorPlugin {
3
+ private interceptors;
4
+ private resolveDependency;
5
+ constructor();
6
+ private watched;
7
+ watchList(): string[];
8
+ apply(compiler: Compiler): void;
9
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.InterceptorPlugin = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const resolveDependency_1 = require("../utils/resolveDependency");
9
+ const findPlugins_1 = require("./findPlugins");
10
+ const generateInterceptors_1 = require("./generateInterceptors");
11
+ const writeInterceptors_1 = require("./writeInterceptors");
12
+ class InterceptorPlugin {
13
+ constructor() {
14
+ this.watched = [];
15
+ this.resolveDependency = (0, resolveDependency_1.resolveDependency)();
16
+ this.watched = this.watchList();
17
+ this.interceptors = (0, generateInterceptors_1.generateInterceptors)((0, findPlugins_1.findPlugins)(), this.resolveDependency);
18
+ (0, writeInterceptors_1.writeInterceptors)(this.interceptors);
19
+ }
20
+ watchList() {
21
+ return [
22
+ ...new Set((0, findPlugins_1.findPlugins)()
23
+ .map((p) => this.resolveDependency(p.plugin))
24
+ .map((p) => p.fromRoot)),
25
+ ];
26
+ }
27
+ apply(compiler) {
28
+ const logger = compiler.getInfrastructureLogger('InterceptorPlugin');
29
+ // After the compilation has succeeded we watch all possible plugin locations.
30
+ compiler.hooks.afterCompile.tap('InterceptorPlugin', (compilation) => {
31
+ const watchList = this.watchList();
32
+ const added = watchList.filter((d) => !this.watched.includes(d));
33
+ const removed = this.watched.filter((d) => !watchList.includes(d));
34
+ this.watched = watchList;
35
+ if (added.length > 0) {
36
+ added.forEach((context) => compilation.contextDependencies.add(context));
37
+ }
38
+ if (removed.length > 0) {
39
+ removed.forEach((context) => compilation.contextDependencies.delete(context));
40
+ }
41
+ if (added.length > 0 || removed.length > 0) {
42
+ this.interceptors = (0, generateInterceptors_1.generateInterceptors)((0, findPlugins_1.findPlugins)(), this.resolveDependency);
43
+ (0, writeInterceptors_1.writeInterceptors)(this.interceptors);
44
+ }
45
+ });
46
+ compiler.hooks.normalModuleFactory.tap('InterceptorPlugin', (nmf) => {
47
+ nmf.hooks.beforeResolve.tap('InterceptorPlugin', (resource) => {
48
+ const issuer = resource.contextInfo.issuer ?? '';
49
+ const requestPath = path_1.default.relative(process.cwd(), path_1.default.resolve(resource.context, resource.request));
50
+ if (issuer.endsWith('interceptor.tsx') && this.interceptors[requestPath]) {
51
+ logger.log(`Interceptor ${issuer} is requesting the original ${requestPath}`);
52
+ return;
53
+ }
54
+ if (this.interceptors[requestPath]) {
55
+ logger.log(`Intercepting... ${this.interceptors[requestPath].fromRoot}}`);
56
+ resource.request = `${resource.request}.interceptor.tsx`;
57
+ }
58
+ });
59
+ });
60
+ }
61
+ }
62
+ exports.InterceptorPlugin = InterceptorPlugin;
@@ -0,0 +1,2 @@
1
+ import type { PluginConfig } from './generateInterceptors';
2
+ export declare function findPlugins(cwd?: string): PluginConfig[];
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.findPlugins = void 0;
7
+ const core_1 = require("@swc/core");
8
+ const glob_1 = __importDefault(require("glob"));
9
+ const resolveDependenciesSync_1 = require("../utils/resolveDependenciesSync");
10
+ function parseStructure(file) {
11
+ const ast = (0, core_1.parseFileSync)(file, { syntax: 'typescript', tsx: true });
12
+ // const ast = swc.parseFileSync(file, { syntax: 'typescript' })
13
+ const imports = {};
14
+ const exports = {};
15
+ ast.body.forEach((node) => {
16
+ switch (node.type) {
17
+ case 'ImportDeclaration':
18
+ node.specifiers.forEach((s) => {
19
+ if (s.type === 'ImportSpecifier') {
20
+ imports[s.local.value] = node.source.value;
21
+ }
22
+ });
23
+ break;
24
+ case 'ExportDeclaration':
25
+ switch (node.declaration.type) {
26
+ case 'VariableDeclaration':
27
+ node.declaration.declarations.forEach((declaration) => {
28
+ if (declaration.id.type !== 'Identifier')
29
+ return;
30
+ switch (declaration.init?.type) {
31
+ case 'StringLiteral':
32
+ exports[declaration.id.value] = declaration.init.value;
33
+ break;
34
+ }
35
+ });
36
+ break;
37
+ case 'FunctionDeclaration':
38
+ if (node.declaration.type === 'FunctionDeclaration') {
39
+ // console.log('func', node.declaration)
40
+ }
41
+ break;
42
+ default:
43
+ console.log('unknown', node.declaration);
44
+ }
45
+ break;
46
+ // default:
47
+ // console.log('hallo', node)
48
+ }
49
+ });
50
+ return exports.component && exports.exported
51
+ ? exports
52
+ : undefined;
53
+ }
54
+ function findPlugins(cwd = process.cwd()) {
55
+ const dependencies = (0, resolveDependenciesSync_1.resolveDependenciesSync)(cwd);
56
+ const plugins = [];
57
+ dependencies.forEach((dependency, path) => {
58
+ const files = glob_1.default.sync(`${dependency}/plugins/**/*.tsx`);
59
+ files.forEach((file) => {
60
+ try {
61
+ const result = parseStructure(file);
62
+ if (!result)
63
+ return;
64
+ plugins.push({ ...result, plugin: file.replace(dependency, path).replace('.tsx', '') });
65
+ }
66
+ catch (e) {
67
+ console.error(`Error parsing ${file}`, e);
68
+ }
69
+ });
70
+ });
71
+ return plugins;
72
+ }
73
+ exports.findPlugins = findPlugins;
@@ -0,0 +1,18 @@
1
+ import { ResolveDependency, ResolveDependencyReturn } from '../utils/resolveDependency';
2
+ export declare type PluginConfig = {
3
+ component: string;
4
+ exported: string;
5
+ plugin: string;
6
+ };
7
+ declare type Plugin = ResolveDependencyReturn & {
8
+ components: Record<string, PluginConfig[]>;
9
+ target: string;
10
+ template?: string;
11
+ };
12
+ export declare type MaterializedPlugin = Plugin & {
13
+ template: string;
14
+ };
15
+ export declare function generateInterceptor(plugin: Plugin): MaterializedPlugin;
16
+ export declare type GenerateInterceptorsReturn = Record<string, MaterializedPlugin>;
17
+ export declare function generateInterceptors(plugins: PluginConfig[], resolve: ResolveDependency): GenerateInterceptorsReturn;
18
+ export {};
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateInterceptors = exports.generateInterceptor = void 0;
4
+ function generateInterceptor(plugin) {
5
+ const { fromModule, components } = plugin;
6
+ const pluginImports = Object.entries(components)
7
+ .map(([_, plugins]) => {
8
+ const duplicateImports = new Set();
9
+ return plugins
10
+ .sort((a, b) => a.plugin.localeCompare(b.plugin))
11
+ .map((p) => `import { Plugin as ${p.plugin.split('/')[p.plugin.split('/').length - 1]} } from '${p.plugin}'`)
12
+ .filter((importStr) => {
13
+ if (duplicateImports.has(importStr)) {
14
+ return false;
15
+ }
16
+ duplicateImports.add(importStr);
17
+ return true;
18
+ })
19
+ .join('\n');
20
+ })
21
+ .join('\n');
22
+ const importInjectables = `import { ${Object.entries(components)
23
+ .map(([component]) => `${component} as ${component}Base`)
24
+ .join(', ')} } from '${fromModule}'`;
25
+ const pluginExports = Object.entries(components)
26
+ .map(([component, plugins]) => {
27
+ const duplicateImports = new Set();
28
+ let carry = `${component}Base`;
29
+ const pluginStr = plugins
30
+ .reverse()
31
+ .map((p) => p.plugin.split('/')[p.plugin.split('/').length - 1])
32
+ .filter((importStr) => {
33
+ if (duplicateImports.has(importStr)) {
34
+ return false;
35
+ }
36
+ duplicateImports.add(importStr);
37
+ return true;
38
+ })
39
+ .map((name) => {
40
+ const result = `const ${name}Interceptor = (props: ${component}BaseProps) => (
41
+ <${name} {...props} Component={${carry}} />
42
+ )`;
43
+ carry = `${name}Interceptor`;
44
+ return result;
45
+ })
46
+ .join('\n');
47
+ return `type ${component}BaseProps = React.ComponentProps<typeof ${component}Base>
48
+
49
+ ${pluginStr}
50
+ export const ${component} = ${carry}`;
51
+ })
52
+ .join('\n');
53
+ const componentExports = `export * from '${fromModule}'`;
54
+ const template = `/* This file is automatically generated. */
55
+
56
+ /* eslint-disable import/no-extraneous-dependencies */
57
+ /* eslint-disable import/export */
58
+
59
+ ${pluginImports}
60
+ ${importInjectables}
61
+
62
+ ${componentExports}
63
+
64
+ ${pluginExports}
65
+ `;
66
+ return { ...plugin, template };
67
+ }
68
+ exports.generateInterceptor = generateInterceptor;
69
+ function generateInterceptors(plugins, resolve) {
70
+ // todo: Do not use reduce as we're passing the accumulator to the next iteration
71
+ const byExportedComponent = plugins.reduce((acc, plug) => {
72
+ const { exported } = plug;
73
+ const resolved = resolve(exported);
74
+ if (!acc[resolved.fromRoot])
75
+ acc[resolved.fromRoot] = {
76
+ ...resolved,
77
+ target: `${resolved.fromRoot}.interceptor`,
78
+ components: {},
79
+ };
80
+ if (!acc[resolved.fromRoot].components[plug.component])
81
+ acc[resolved.fromRoot].components[plug.component] = [];
82
+ acc[resolved.fromRoot].components[plug.component].push(plug);
83
+ return acc;
84
+ }, {});
85
+ return Object.fromEntries(Object.entries(byExportedComponent).map(([target, plg]) => [target, generateInterceptor(plg)]));
86
+ }
87
+ exports.generateInterceptors = generateInterceptors;
@@ -0,0 +1 @@
1
+ export declare function rmInterceptors(cwd?: string): string[];
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.rmInterceptors = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const glob_1 = __importDefault(require("glob"));
9
+ const resolveDependenciesSync_1 = require("../utils/resolveDependenciesSync");
10
+ function rmInterceptors(cwd = process.cwd()) {
11
+ const dependencies = (0, resolveDependenciesSync_1.resolveDependenciesSync)(cwd);
12
+ const removed = [];
13
+ dependencies.forEach((dependency) => {
14
+ const files = glob_1.default.sync(`${dependency}/**/*.interceptor.tsx`, { cwd });
15
+ files.forEach((file) => {
16
+ node_fs_1.default.unlinkSync(file);
17
+ removed.push(file);
18
+ });
19
+ });
20
+ return removed;
21
+ }
22
+ exports.rmInterceptors = rmInterceptors;
@@ -0,0 +1,2 @@
1
+ import { GenerateInterceptorsReturn } from './generateInterceptors';
2
+ export declare function writeInterceptors(interceptors: GenerateInterceptorsReturn, cwd?: string): void;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.writeInterceptors = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ function writeInterceptors(interceptors, cwd = process.cwd()) {
10
+ Object.entries(interceptors).forEach(([target, plugin]) => {
11
+ // eslint-disable-next-line no-console
12
+ const fileToWrite = `${path_1.default.join(cwd, plugin.fromRoot)}.interceptor.tsx`;
13
+ if (!node_fs_1.default.existsSync(fileToWrite) ||
14
+ node_fs_1.default.readFileSync(fileToWrite, 'utf8').toString() !== plugin.template) {
15
+ node_fs_1.default.writeFileSync(fileToWrite, plugin.template);
16
+ }
17
+ });
18
+ }
19
+ exports.writeInterceptors = writeInterceptors;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.resolveDependenciesSync = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
+ const resolveCache = new Map();
9
10
  function resolveRecursivePackageJson(packageJsonFilename, packageNames) {
10
11
  try {
11
12
  const packageJsonFile = node_fs_1.default.readFileSync(packageJsonFilename, 'utf-8').toString();
@@ -52,6 +53,11 @@ function resolveRecursivePackageJson(packageJsonFilename, packageNames) {
52
53
  * and stop there, not checking children.
53
54
  */
54
55
  function resolveDependenciesSync(root = process.cwd()) {
55
- return resolveRecursivePackageJson(node_path_1.default.join(root, 'package.json'), new Map());
56
+ const cached = resolveCache.get(root);
57
+ if (cached)
58
+ return cached;
59
+ const result = resolveRecursivePackageJson(node_path_1.default.join(root, 'package.json'), new Map());
60
+ resolveCache.set(root, result);
61
+ return result;
56
62
  }
57
63
  exports.resolveDependenciesSync = resolveDependenciesSync;
@@ -0,0 +1,8 @@
1
+ export declare type ResolveDependencyReturn = {
2
+ dependency: string;
3
+ root: string;
4
+ fromRoot: string;
5
+ fromModule: string;
6
+ };
7
+ export declare type ResolveDependency = (req: string) => ResolveDependencyReturn;
8
+ export declare const resolveDependency: (cwd?: string) => (dependency: string) => ResolveDependencyReturn;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveDependency = void 0;
4
+ const resolveDependenciesSync_1 = require("./resolveDependenciesSync");
5
+ const resolveDependency = (cwd = process.cwd()) => {
6
+ const dependencies = (0, resolveDependenciesSync_1.resolveDependenciesSync)(cwd);
7
+ return (dependency) => {
8
+ let dependencyPaths = { root: '.', dependency, fromRoot: dependency, fromModule: dependency };
9
+ dependencies.forEach((root, depCandidate) => {
10
+ if (dependency.startsWith(depCandidate)) {
11
+ const relative = dependency.replace(depCandidate, '');
12
+ const fromRoot = dependency.replace(depCandidate, root);
13
+ dependencyPaths = {
14
+ root,
15
+ dependency,
16
+ fromRoot,
17
+ fromModule: !relative ? '.' : `./${relative.split('/')[relative.split('/').length - 1]}`,
18
+ };
19
+ }
20
+ });
21
+ return dependencyPaths;
22
+ };
23
+ };
24
+ exports.resolveDependency = resolveDependency;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.withGraphCommerce = void 0;
7
7
  const next_transpile_modules_1 = __importDefault(require("next-transpile-modules"));
8
8
  const webpack_1 = require("webpack");
9
+ const InterceptorPlugin_1 = require("./interceptors/InterceptorPlugin");
9
10
  const resolveDependenciesSync_1 = require("./utils/resolveDependenciesSync");
10
11
  function extendConfig(nextConfig, modules) {
11
12
  return {
@@ -42,13 +43,14 @@ function extendConfig(nextConfig, modules) {
42
43
  '@mui/styled-engine': '@mui/styled-engine/modern',
43
44
  '@mui/system': '@mui/system/modern',
44
45
  };
46
+ config.plugins = [...(config.plugins ?? []), new InterceptorPlugin_1.InterceptorPlugin()];
45
47
  return typeof nextConfig.webpack === 'function' ? nextConfig.webpack(config, options) : config;
46
48
  },
47
49
  };
48
50
  }
49
51
  function withGraphCommerce(conf = {}) {
50
52
  const { packages = [] } = conf;
51
- const dependencies = [...(0, resolveDependenciesSync_1.resolveDependenciesSync)().keys()];
53
+ const dependencies = [...(0, resolveDependenciesSync_1.resolveDependenciesSync)().keys()].slice(1);
52
54
  const modules = [...dependencies, ...packages];
53
55
  return (config) => extendConfig((0, next_transpile_modules_1.default)(modules)(config), modules);
54
56
  }
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": "4.30.0",
5
+ "version": "4.31.0-canary.1",
6
6
  "type": "commonjs",
7
7
  "main": "dist/index.js",
8
8
  "typings": "dist/index.d.ts",
@@ -13,8 +13,9 @@
13
13
  "prepack": "yarn build"
14
14
  },
15
15
  "dependencies": {
16
- "@graphcommerce/cli": "4.30.0",
16
+ "@graphcommerce/cli": "4.31.0-canary.1",
17
17
  "@lingui/loader": "^3.14.0",
18
+ "@swc/core": "^1.3.10",
18
19
  "js-yaml-loader": "^1.2.2",
19
20
  "next-transpile-modules": "^9.0.0",
20
21
  "webpack": "^5.73.0"
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { NextConfig } from 'next/dist/server/config-shared'
2
+ import React from 'react'
2
3
  import { withGraphCommerce } from './withGraphCommerce'
3
4
 
4
5
  export * from './utils/isMonorepo'
@@ -12,3 +13,7 @@ export function withYarn1Workspaces(packages: string[] = []): (config: NextConfi
12
13
  export function withYarn1Scopes(packages?: string[]): (config: NextConfig) => NextConfig {
13
14
  return withGraphCommerce({ packages })
14
15
  }
16
+
17
+ export type PluginProps<P extends Record<string, unknown> = Record<string, unknown>> = P & {
18
+ Component: React.FC<P>
19
+ }
@@ -0,0 +1,76 @@
1
+ import path from 'path'
2
+ import { Compiler } from 'webpack'
3
+ import { ResolveDependency, resolveDependency } from '../utils/resolveDependency'
4
+ import { findPlugins } from './findPlugins'
5
+ import { generateInterceptors, GenerateInterceptorsReturn } from './generateInterceptors'
6
+ import { writeInterceptors } from './writeInterceptors'
7
+
8
+ export class InterceptorPlugin {
9
+ private interceptors: GenerateInterceptorsReturn
10
+
11
+ private resolveDependency: ResolveDependency
12
+
13
+ constructor() {
14
+ this.resolveDependency = resolveDependency()
15
+
16
+ this.watched = this.watchList()
17
+ this.interceptors = generateInterceptors(findPlugins(), this.resolveDependency)
18
+ writeInterceptors(this.interceptors)
19
+ }
20
+
21
+ private watched: string[] = []
22
+
23
+ watchList() {
24
+ return [
25
+ ...new Set(
26
+ findPlugins()
27
+ .map((p) => this.resolveDependency(p.plugin))
28
+ .map((p) => p.fromRoot),
29
+ ),
30
+ ]
31
+ }
32
+
33
+ apply(compiler: Compiler): void {
34
+ const logger = compiler.getInfrastructureLogger('InterceptorPlugin')
35
+
36
+ // After the compilation has succeeded we watch all possible plugin locations.
37
+ compiler.hooks.afterCompile.tap('InterceptorPlugin', (compilation) => {
38
+ const watchList = this.watchList()
39
+ const added = watchList.filter((d) => !this.watched.includes(d))
40
+ const removed = this.watched.filter((d) => !watchList.includes(d))
41
+ this.watched = watchList
42
+
43
+ if (added.length > 0) {
44
+ added.forEach((context) => compilation.contextDependencies.add(context))
45
+ }
46
+ if (removed.length > 0) {
47
+ removed.forEach((context) => compilation.contextDependencies.delete(context))
48
+ }
49
+ if (added.length > 0 || removed.length > 0) {
50
+ this.interceptors = generateInterceptors(findPlugins(), this.resolveDependency)
51
+ writeInterceptors(this.interceptors)
52
+ }
53
+ })
54
+
55
+ compiler.hooks.normalModuleFactory.tap('InterceptorPlugin', (nmf) => {
56
+ nmf.hooks.beforeResolve.tap('InterceptorPlugin', (resource) => {
57
+ const issuer = resource.contextInfo.issuer ?? ''
58
+
59
+ const requestPath = path.relative(
60
+ process.cwd(),
61
+ path.resolve(resource.context, resource.request),
62
+ )
63
+
64
+ if (issuer.endsWith('interceptor.tsx') && this.interceptors[requestPath]) {
65
+ logger.log(`Interceptor ${issuer} is requesting the original ${requestPath}`)
66
+ return
67
+ }
68
+
69
+ if (this.interceptors[requestPath]) {
70
+ logger.log(`Intercepting... ${this.interceptors[requestPath].fromRoot}}`)
71
+ resource.request = `${resource.request}.interceptor.tsx`
72
+ }
73
+ })
74
+ })
75
+ }
76
+ }
@@ -0,0 +1,72 @@
1
+ import { parseFileSync } from '@swc/core'
2
+ import glob from 'glob'
3
+ import { resolveDependenciesSync } from '../utils/resolveDependenciesSync'
4
+ import type { PluginConfig } from './generateInterceptors'
5
+
6
+ function parseStructure(file: string): { component: string; exported: string } | undefined {
7
+ const ast = parseFileSync(file, { syntax: 'typescript', tsx: true })
8
+ // const ast = swc.parseFileSync(file, { syntax: 'typescript' })
9
+
10
+ const imports: Record<string, string> = {}
11
+ const exports: Record<string, string> = {}
12
+
13
+ ast.body.forEach((node) => {
14
+ switch (node.type) {
15
+ case 'ImportDeclaration':
16
+ node.specifiers.forEach((s) => {
17
+ if (s.type === 'ImportSpecifier') {
18
+ imports[s.local.value] = node.source.value
19
+ }
20
+ })
21
+ break
22
+ case 'ExportDeclaration':
23
+ switch (node.declaration.type) {
24
+ case 'VariableDeclaration':
25
+ node.declaration.declarations.forEach((declaration) => {
26
+ if (declaration.id.type !== 'Identifier') return
27
+
28
+ switch (declaration.init?.type) {
29
+ case 'StringLiteral':
30
+ exports[declaration.id.value] = declaration.init.value
31
+ break
32
+ }
33
+ })
34
+ break
35
+ case 'FunctionDeclaration':
36
+ if (node.declaration.type === 'FunctionDeclaration') {
37
+ // console.log('func', node.declaration)
38
+ }
39
+ break
40
+ default:
41
+ console.log('unknown', node.declaration)
42
+ }
43
+ break
44
+ // default:
45
+ // console.log('hallo', node)
46
+ }
47
+ })
48
+
49
+ return exports.component && exports.exported
50
+ ? (exports as { component: string; exported: string })
51
+ : undefined
52
+ }
53
+
54
+ export function findPlugins(cwd: string = process.cwd()) {
55
+ const dependencies = resolveDependenciesSync(cwd)
56
+
57
+ const plugins: PluginConfig[] = []
58
+ dependencies.forEach((dependency, path) => {
59
+ const files = glob.sync(`${dependency}/plugins/**/*.tsx`)
60
+ files.forEach((file) => {
61
+ try {
62
+ const result = parseStructure(file)
63
+ if (!result) return
64
+
65
+ plugins.push({ ...result, plugin: file.replace(dependency, path).replace('.tsx', '') })
66
+ } catch (e) {
67
+ console.error(`Error parsing ${file}`, e)
68
+ }
69
+ })
70
+ })
71
+ return plugins
72
+ }
@@ -0,0 +1,123 @@
1
+ import { ResolveDependency, ResolveDependencyReturn } from '../utils/resolveDependency'
2
+
3
+ export type PluginConfig = {
4
+ component: string
5
+ exported: string
6
+ plugin: string
7
+ }
8
+
9
+ type Plugin = ResolveDependencyReturn & {
10
+ components: Record<string, PluginConfig[]>
11
+ target: string
12
+ template?: string
13
+ }
14
+
15
+ export type MaterializedPlugin = Plugin & { template: string }
16
+
17
+ export function generateInterceptor(plugin: Plugin): MaterializedPlugin {
18
+ const { fromModule, components } = plugin
19
+
20
+ const pluginImports = Object.entries(components)
21
+ .map(([_, plugins]) => {
22
+ const duplicateImports = new Set()
23
+ return plugins
24
+ .sort((a, b) => a.plugin.localeCompare(b.plugin))
25
+ .map(
26
+ (p) =>
27
+ `import { Plugin as ${p.plugin.split('/')[p.plugin.split('/').length - 1]} } from '${
28
+ p.plugin
29
+ }'`,
30
+ )
31
+ .filter((importStr) => {
32
+ if (duplicateImports.has(importStr)) {
33
+ return false
34
+ }
35
+ duplicateImports.add(importStr)
36
+ return true
37
+ })
38
+ .join('\n')
39
+ })
40
+ .join('\n')
41
+
42
+ const importInjectables = `import { ${Object.entries(components)
43
+ .map(([component]) => `${component} as ${component}Base`)
44
+ .join(', ')} } from '${fromModule}'`
45
+
46
+ const pluginExports = Object.entries(components)
47
+ .map(([component, plugins]) => {
48
+ const duplicateImports = new Set()
49
+
50
+ let carry = `${component}Base`
51
+ const pluginStr = plugins
52
+ .reverse()
53
+ .map((p) => p.plugin.split('/')[p.plugin.split('/').length - 1])
54
+ .filter((importStr) => {
55
+ if (duplicateImports.has(importStr)) {
56
+ return false
57
+ }
58
+ duplicateImports.add(importStr)
59
+ return true
60
+ })
61
+ .map((name) => {
62
+ const result = `const ${name}Interceptor = (props: ${component}BaseProps) => (
63
+ <${name} {...props} Component={${carry}} />
64
+ )`
65
+ carry = `${name}Interceptor`
66
+ return result
67
+ })
68
+ .join('\n')
69
+
70
+ return `type ${component}BaseProps = React.ComponentProps<typeof ${component}Base>
71
+
72
+ ${pluginStr}
73
+ export const ${component} = ${carry}`
74
+ })
75
+ .join('\n')
76
+
77
+ const componentExports = `export * from '${fromModule}'`
78
+
79
+ const template = `/* This file is automatically generated. */
80
+
81
+ /* eslint-disable import/no-extraneous-dependencies */
82
+ /* eslint-disable import/export */
83
+
84
+ ${pluginImports}
85
+ ${importInjectables}
86
+
87
+ ${componentExports}
88
+
89
+ ${pluginExports}
90
+ `
91
+
92
+ return { ...plugin, template }
93
+ }
94
+
95
+ export type GenerateInterceptorsReturn = Record<string, MaterializedPlugin>
96
+
97
+ export function generateInterceptors(
98
+ plugins: PluginConfig[],
99
+ resolve: ResolveDependency,
100
+ ): GenerateInterceptorsReturn {
101
+ // todo: Do not use reduce as we're passing the accumulator to the next iteration
102
+ const byExportedComponent = plugins.reduce((acc, plug) => {
103
+ const { exported } = plug
104
+ const resolved = resolve(exported)
105
+
106
+ if (!acc[resolved.fromRoot])
107
+ acc[resolved.fromRoot] = {
108
+ ...resolved,
109
+ target: `${resolved.fromRoot}.interceptor`,
110
+ components: {},
111
+ } as Plugin
112
+
113
+ if (!acc[resolved.fromRoot].components[plug.component])
114
+ acc[resolved.fromRoot].components[plug.component] = []
115
+
116
+ acc[resolved.fromRoot].components[plug.component].push(plug)
117
+ return acc
118
+ }, {} as Record<string, Plugin>)
119
+
120
+ return Object.fromEntries(
121
+ Object.entries(byExportedComponent).map(([target, plg]) => [target, generateInterceptor(plg)]),
122
+ )
123
+ }
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs'
2
+ import glob from 'glob'
3
+ import { resolveDependenciesSync } from '../utils/resolveDependenciesSync'
4
+
5
+ export function rmInterceptors(cwd: string = process.cwd()): string[] {
6
+ const dependencies = resolveDependenciesSync(cwd)
7
+
8
+ const removed: string[] = []
9
+ dependencies.forEach((dependency) => {
10
+ const files = glob.sync(`${dependency}/**/*.interceptor.tsx`, { cwd })
11
+ files.forEach((file) => {
12
+ fs.unlinkSync(file)
13
+ removed.push(file)
14
+ })
15
+ })
16
+ return removed
17
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs'
2
+ import path from 'path'
3
+ import { GenerateInterceptorsReturn } from './generateInterceptors'
4
+
5
+ export function writeInterceptors(
6
+ interceptors: GenerateInterceptorsReturn,
7
+ cwd: string = process.cwd(),
8
+ ) {
9
+ Object.entries(interceptors).forEach(([target, plugin]) => {
10
+ // eslint-disable-next-line no-console
11
+ const fileToWrite = `${path.join(cwd, plugin.fromRoot)}.interceptor.tsx`
12
+
13
+ if (
14
+ !fs.existsSync(fileToWrite) ||
15
+ fs.readFileSync(fileToWrite, 'utf8').toString() !== plugin.template
16
+ ) {
17
+ fs.writeFileSync(fileToWrite, plugin.template)
18
+ }
19
+ })
20
+ }
@@ -2,6 +2,8 @@ import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import type { PackageJson } from 'type-fest'
4
4
 
5
+ const resolveCache: Map<string, Map<string, string>> = new Map<string, Map<string, string>>()
6
+
5
7
  function resolveRecursivePackageJson(
6
8
  packageJsonFilename: string,
7
9
  packageNames: Map<string, string>,
@@ -55,5 +57,9 @@ function resolveRecursivePackageJson(
55
57
  * and stop there, not checking children.
56
58
  */
57
59
  export function resolveDependenciesSync(root = process.cwd()) {
58
- return resolveRecursivePackageJson(path.join(root, 'package.json'), new Map())
60
+ const cached = resolveCache.get(root)
61
+ if (cached) return cached
62
+ const result = resolveRecursivePackageJson(path.join(root, 'package.json'), new Map())
63
+ resolveCache.set(root, result)
64
+ return result
59
65
  }
@@ -0,0 +1,32 @@
1
+ import { resolveDependenciesSync } from './resolveDependenciesSync'
2
+
3
+ export type ResolveDependencyReturn = {
4
+ dependency: string
5
+ root: string
6
+ fromRoot: string
7
+ fromModule: string
8
+ }
9
+
10
+ export type ResolveDependency = (req: string) => ResolveDependencyReturn
11
+
12
+ export const resolveDependency = (cwd: string = process.cwd()) => {
13
+ const dependencies = resolveDependenciesSync(cwd)
14
+ return (dependency: string): ResolveDependencyReturn => {
15
+ let dependencyPaths = { root: '.', dependency, fromRoot: dependency, fromModule: dependency }
16
+
17
+ dependencies.forEach((root, depCandidate) => {
18
+ if (dependency.startsWith(depCandidate)) {
19
+ const relative = dependency.replace(depCandidate, '')
20
+ const fromRoot = dependency.replace(depCandidate, root)
21
+
22
+ dependencyPaths = {
23
+ root,
24
+ dependency,
25
+ fromRoot,
26
+ fromModule: !relative ? '.' : `./${relative.split('/')[relative.split('/').length - 1]}`,
27
+ }
28
+ }
29
+ })
30
+ return dependencyPaths
31
+ }
32
+ }
@@ -1,6 +1,7 @@
1
1
  import type { NextConfig } from 'next'
2
2
  import withTranspileModules from 'next-transpile-modules'
3
3
  import { DefinePlugin, Configuration } from 'webpack'
4
+ import { InterceptorPlugin } from './interceptors/InterceptorPlugin'
4
5
  import { resolveDependenciesSync } from './utils/resolveDependenciesSync'
5
6
 
6
7
  function extendConfig(nextConfig: NextConfig, modules: string[]): NextConfig {
@@ -44,6 +45,8 @@ function extendConfig(nextConfig: NextConfig, modules: string[]): NextConfig {
44
45
  '@mui/system': '@mui/system/modern',
45
46
  }
46
47
 
48
+ config.plugins = [...(config.plugins ?? []), new InterceptorPlugin()]
49
+
47
50
  return typeof nextConfig.webpack === 'function' ? nextConfig.webpack(config, options) : config
48
51
  },
49
52
  }
@@ -58,7 +61,7 @@ export function withGraphCommerce(
58
61
  conf: GraphCommerceConfig = {},
59
62
  ): (config: NextConfig) => NextConfig {
60
63
  const { packages = [] } = conf
61
- const dependencies = [...resolveDependenciesSync().keys()]
64
+ const dependencies = [...resolveDependenciesSync().keys()].slice(1)
62
65
 
63
66
  const modules = [...dependencies, ...packages]
64
67
  return (config) => extendConfig(withTranspileModules(modules)(config), modules)