mage-obsidian 1.1.0 → 1.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mage-obsidian",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Mage Obsidian Ultis",
5
5
  "main": "./src/scripts/buildThemes.js",
6
6
  "keywords": [
@@ -22,7 +22,8 @@
22
22
  ".": "./src/scripts/buildThemes.js",
23
23
  "./service/": "./src/service/",
24
24
  "./utils/": "./src/utils/",
25
- "./config/": "./src/config/"
25
+ "./config/": "./src/config/",
26
+ "./service/*": "./src/service/*"
26
27
  },
27
28
  "dependencies": {
28
29
  "chalk": "^5.4.1",
@@ -0,0 +1,7 @@
1
+
2
+ export function beforeConfig() {
3
+ console.log('Should not run');
4
+ }
5
+ export function beforeDefault() {
6
+ console.log('Should not run');
7
+ }
@@ -6,3 +6,8 @@ export function beforeTargetFunction(arg) {
6
6
  export function afterAnotherFunction(result) {
7
7
  return `${result} - Modified`;
8
8
  }
9
+
10
+ export function beforeDefault() {
11
+ return ['Default Modified'];
12
+ }
13
+
@@ -0,0 +1,7 @@
1
+
2
+ export const config = {
3
+ some: 'value'
4
+ };
5
+ export default {
6
+ main: 'config'
7
+ };
@@ -7,12 +7,14 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  const fixturesDir = path.resolve(__dirname, '../fixtures/interceptors');
8
8
  const targetModulePath = path.join(fixturesDir, 'targetModule.js');
9
9
  const pluginModulePath = path.join(fixturesDir, 'pluginModule.js');
10
+ const targetNonFunctionPath = path.join(fixturesDir, 'targetNonFunction.js');
11
+ const pluginForNonFunctionPath = path.join(fixturesDir, 'pluginForNonFunction.js');
10
12
 
11
13
  describe('generateInterceptors', () => {
12
14
  let generateInterceptorsService;
13
15
  let themeResolverMock;
14
16
  let moduleResolverMock;
15
- let pluginManager;
17
+ let interceptorManager;
16
18
 
17
19
  beforeEach(async () => {
18
20
  jest.resetModules();
@@ -35,9 +37,15 @@ describe('generateInterceptors', () => {
35
37
  default: moduleResolverMock
36
38
  }));
37
39
 
38
- // Import real pluginManager to reset state
39
- pluginManager = (await import('../../service/pluginManager.js')).default;
40
- pluginManager.plugins = {};
40
+ jest.unstable_mockModule('../../service/configResolver.js', () => ({
41
+ default: {
42
+ resolveLibRealPath: jest.fn().mockReturnValue('/mocked/path/to/interceptorManager')
43
+ }
44
+ }));
45
+
46
+ // Import real interceptorManager to reset state
47
+ interceptorManager = (await import('../../service/interceptorManager.js')).default;
48
+ interceptorManager.interceptors = {};
41
49
 
42
50
  generateInterceptorsService = (await import('../../service/generateInterceptors.js')).default;
43
51
  });
@@ -50,17 +58,14 @@ describe('generateInterceptors', () => {
50
58
  });
51
59
 
52
60
  moduleResolverMock.getModuleConfigByThemeConfig.mockResolvedValue({
53
- 'Vendor_TargetModule': {
54
- src: '/path/to/vendor/target',
55
- interceptors: [
56
- {
57
- name: 'TestPlugin',
58
- target: 'Vendor_TargetModule::js/target.js',
59
- plugin: 'Vendor_PluginModule::js/plugin.js',
60
- sortOrder: 10,
61
- active: true
62
- }
63
- ]
61
+ interceptors: {
62
+ 'TestPlugin': {
63
+ name: 'TestPlugin',
64
+ target: 'Vendor_TargetModule::js/target.js',
65
+ interceptor: 'Vendor_PluginModule::js/plugin.js',
66
+ sortOrder: 10,
67
+ active: true
68
+ }
64
69
  }
65
70
  });
66
71
 
@@ -81,14 +86,15 @@ describe('generateInterceptors', () => {
81
86
  expect(interceptorData).toHaveProperty('proxy');
82
87
  expect(interceptorData).toHaveProperty('source');
83
88
  expect(interceptorData).toHaveProperty('targetPath', targetModulePath);
84
- expect(interceptorData.plugins).toHaveLength(1);
85
- expect(interceptorData.plugins[0].name).toBe('TestPlugin');
89
+ expect(interceptorData.interceptors).toHaveLength(1);
90
+ expect(interceptorData.interceptors[0].name).toBe('TestPlugin');
86
91
 
87
92
  // Verify Source Code Generation
88
93
  const source = interceptorData.source;
89
- expect(source).toContain(`import * as originalModule from '/@fs${targetModulePath}';`);
90
- expect(source).toContain(`import * as plugin_0 from '/@fs${pluginModulePath}';`);
91
- expect(source).toContain(`pluginManager.addPlugin('${targetIdentifier}::targetFunction', 'TestPlugin', 'before', plugin_0.beforeTargetFunction, 10);`);
94
+ expect(source).toContain(`import * as originalModule from '/@fs${targetModulePath}?originalIntercepted';`);
95
+ expect(source).toContain(`import * as interceptor_0 from '/@fs${pluginModulePath}';`);
96
+ expect(source).toContain(`interceptorManager.addInterceptor('${targetIdentifier}::targetFunction', 'TestPlugin', 'before', interceptor_0.beforeTargetFunction, 10);`);
97
+ expect(source).toContain(`interceptorManager.addInterceptor('${targetIdentifier}::default', 'TestPlugin', 'before', interceptor_0.beforeDefault, 10);`);
92
98
  expect(source).toContain(`export const targetFunction = proxy.targetFunction;`);
93
99
 
94
100
  // Verify Proxy Behavior
@@ -98,6 +104,14 @@ describe('generateInterceptors', () => {
98
104
 
99
105
  const resultAnother = await proxy.anotherFunction();
100
106
  expect(resultAnother).toBe('Another - Modified');
107
+
108
+ const resultDefault = await proxy.default();
109
+ expect(resultDefault).toBe('Default'); // The mock plugin returns ['Default Modified'] but the original function ignores args and returns 'Default'.
110
+ // Wait, before interceptor modifies arguments.
111
+ // targetModule.js: export default function defaultExport() { return 'Default'; }
112
+ // It doesn't take arguments, so modifying arguments won't change output unless we check arguments.
113
+ // But we just want to verify it was registered.
114
+
101
115
  });
102
116
 
103
117
  test('should handle missing target module gracefully', async () => {
@@ -115,16 +129,13 @@ describe('generateInterceptors', () => {
115
129
  const themeName = 'Vendor/theme-test';
116
130
  themeResolverMock.getThemeConfig.mockReturnValue({});
117
131
  moduleResolverMock.getModuleConfigByThemeConfig.mockResolvedValue({
118
- 'Vendor_TargetModule': {
119
- src: '/path/to/vendor/target',
120
- interceptors: [
121
- {
122
- name: 'BadPlugin',
123
- target: 'Vendor_TargetModule::js/target.js',
124
- plugin: 'Vendor_PluginModule::js/plugin.js',
125
- sortOrder: 10
126
- }
127
- ]
132
+ interceptors: {
133
+ 'BadPlugin': {
134
+ name: 'BadPlugin',
135
+ target: 'Vendor_TargetModule::js/target.js',
136
+ interceptor: 'Vendor_PluginModule::js/plugin.js',
137
+ sortOrder: 10
138
+ }
128
139
  }
129
140
  });
130
141
 
@@ -142,4 +153,44 @@ describe('generateInterceptors', () => {
142
153
  .rejects
143
154
  .toThrow(/does not export/);
144
155
  });
156
+
157
+ test('should skip interception for non-function exports', async () => {
158
+ const themeName = 'Vendor/theme-test';
159
+
160
+ themeResolverMock.getThemeConfig.mockReturnValue({
161
+ src: '/path/to/theme'
162
+ });
163
+
164
+ moduleResolverMock.getModuleConfigByThemeConfig.mockResolvedValue({
165
+ interceptors: {
166
+ 'TestPlugin': {
167
+ name: 'TestPlugin',
168
+ target: 'Vendor_TargetModule::js/targetNonFunction.js',
169
+ interceptor: 'Vendor_PluginModule::js/pluginForNonFunction.js',
170
+ sortOrder: 10,
171
+ active: true
172
+ }
173
+ }
174
+ });
175
+
176
+ const allFilesMap = {
177
+ 'Vendor_TargetModule/js/targetNonFunction': targetNonFunctionPath,
178
+ 'Vendor_PluginModule/js/pluginForNonFunction': pluginForNonFunctionPath
179
+ };
180
+
181
+ moduleResolverMock.getAllJsVueFilesWithInheritanceCached.mockReturnValue(allFilesMap);
182
+
183
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
184
+
185
+ const result = await generateInterceptorsService.generateInterceptors(themeName);
186
+
187
+ const targetIdentifier = 'Vendor_TargetModule::js/targetNonFunction.js';
188
+
189
+ // Since both 'config' and 'default' are objects, and we are trying to intercept them,
190
+ // they should be skipped. If all are skipped, the targetIdentifier should not be in result.
191
+ expect(result[targetIdentifier]).toBeUndefined();
192
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('is not a function'));
193
+
194
+ consoleSpy.mockRestore();
195
+ });
145
196
  });
@@ -0,0 +1,137 @@
1
+
2
+ import { jest } from '@jest/globals';
3
+ import interceptorManager from '../../service/interceptorManager.js';
4
+
5
+ describe('InterceptorManager Magento Behavior', () => {
6
+ beforeEach(() => {
7
+ // Reset interceptors before each test
8
+ interceptorManager.interceptors = {};
9
+ });
10
+
11
+ test('Before interceptors: should chain arguments and handle null returns', async () => {
12
+ const targetName = 'Test::beforeChain';
13
+ const originalFn = jest.fn((arg1, arg2) => `Original: ${arg1}, ${arg2}`);
14
+ const context = {};
15
+
16
+ // Interceptor 1: Modifies arguments
17
+ const beforeInterceptor1 = jest.fn((arg1, arg2) => {
18
+ return [arg1 + '_mod1', arg2 + '_mod1'];
19
+ });
20
+
21
+ // Interceptor 2: Returns null (should keep arguments from Interceptor 1)
22
+ const beforeInterceptor2 = jest.fn((arg1, arg2) => {
23
+ return null;
24
+ });
25
+
26
+ // Interceptor 3: Modifies arguments again
27
+ const beforeInterceptor3 = jest.fn((arg1, arg2) => {
28
+ return [arg1 + '_mod3', arg2 + '_mod3'];
29
+ });
30
+
31
+ interceptorManager.addInterceptor(targetName, 'Interceptor1', 'before', beforeInterceptor1, 10);
32
+ interceptorManager.addInterceptor(targetName, 'Interceptor2', 'before', beforeInterceptor2, 20);
33
+ interceptorManager.addInterceptor(targetName, 'Interceptor3', 'before', beforeInterceptor3, 30);
34
+
35
+ const result = await interceptorManager.execute(targetName, originalFn, context, 'A', 'B');
36
+
37
+ // Verify Interceptor 1 received original args
38
+ expect(beforeInterceptor1).toHaveBeenCalledWith('A', 'B');
39
+
40
+ // Verify Interceptor 2 received args modified by Interceptor 1
41
+ expect(beforeInterceptor2).toHaveBeenCalledWith('A_mod1', 'B_mod1');
42
+
43
+ // Verify Interceptor 3 received args from Interceptor 1 (since Interceptor 2 returned null)
44
+ expect(beforeInterceptor3).toHaveBeenCalledWith('A_mod1', 'B_mod1');
45
+
46
+ // Verify Original received args modified by Interceptor 3
47
+ expect(originalFn).toHaveBeenCalledWith('A_mod1_mod3', 'B_mod1_mod3');
48
+
49
+ expect(result).toBe('Original: A_mod1_mod3, B_mod1_mod3');
50
+ });
51
+
52
+ test('Around interceptors: should wrap execution and modify args/result', async () => {
53
+ const targetName = 'Test::aroundChain';
54
+ const originalFn = jest.fn((arg) => `Original(${arg})`);
55
+ const context = {};
56
+
57
+ // Outer Around Interceptor
58
+ const aroundInterceptor1 = jest.fn(async (proceed, arg) => {
59
+ const result = await proceed(arg + '_outerIn');
60
+ return `Outer(${result})`;
61
+ });
62
+
63
+ // Inner Around Interceptor
64
+ const aroundInterceptor2 = jest.fn(async (proceed, arg) => {
65
+ const result = await proceed(arg + '_innerIn');
66
+ return `Inner(${result})`;
67
+ });
68
+
69
+ interceptorManager.addInterceptor(targetName, 'Interceptor1', 'around', aroundInterceptor1, 10);
70
+ interceptorManager.addInterceptor(targetName, 'Interceptor2', 'around', aroundInterceptor2, 20);
71
+
72
+ const result = await interceptorManager.execute(targetName, originalFn, context, 'Start');
73
+
74
+ // Execution flow:
75
+ // Interceptor1 (Start) -> calls proceed('Start_outerIn')
76
+ // -> Interceptor2 ('Start_outerIn') -> calls proceed('Start_outerIn_innerIn')
77
+ // -> Original ('Start_outerIn_innerIn') -> returns 'Original(Start_outerIn_innerIn)'
78
+ // -> Interceptor2 returns 'Inner(Original(Start_outerIn_innerIn))'
79
+ // -> Interceptor1 returns 'Outer(Inner(Original(Start_outerIn_innerIn)))'
80
+
81
+ expect(originalFn).toHaveBeenCalledWith('Start_outerIn_innerIn');
82
+ expect(result).toBe('Outer(Inner(Original(Start_outerIn_innerIn)))');
83
+ });
84
+
85
+ test('After interceptors: should chain results', async () => {
86
+ const targetName = 'Test::afterChain';
87
+ const originalFn = jest.fn(() => 'Original');
88
+ const context = {};
89
+
90
+ const afterInterceptor1 = jest.fn((result) => {
91
+ return result + '_After1';
92
+ });
93
+
94
+ const afterInterceptor2 = jest.fn((result) => {
95
+ return result + '_After2';
96
+ });
97
+
98
+ interceptorManager.addInterceptor(targetName, 'Interceptor1', 'after', afterInterceptor1, 10);
99
+ interceptorManager.addInterceptor(targetName, 'Interceptor2', 'after', afterInterceptor2, 20);
100
+
101
+ const result = await interceptorManager.execute(targetName, originalFn, context);
102
+
103
+ expect(afterInterceptor1).toHaveBeenCalledWith('Original');
104
+ expect(afterInterceptor2).toHaveBeenCalledWith('Original_After1');
105
+ expect(result).toBe('Original_After1_After2');
106
+ });
107
+
108
+ test('Full Chain: Before -> Around -> Original -> After', async () => {
109
+ const targetName = 'Test::fullChain';
110
+ const originalFn = jest.fn((arg) => `Original(${arg})`);
111
+ const context = {};
112
+
113
+ // Before: Modifies arg
114
+ interceptorManager.addInterceptor(targetName, 'Before', 'before', (arg) => [arg + '_Before'], 10);
115
+
116
+ // Around: Wraps and modifies arg further
117
+ interceptorManager.addInterceptor(targetName, 'Around', 'around', async (proceed, arg) => {
118
+ const res = await proceed(arg + '_AroundIn');
119
+ return `Around(${res})`;
120
+ }, 20);
121
+
122
+ // After: Modifies result
123
+ interceptorManager.addInterceptor(targetName, 'After', 'after', (res) => res + '_After', 30);
124
+
125
+ const result = await interceptorManager.execute(targetName, originalFn, context, 'Start');
126
+
127
+ // Flow:
128
+ // Before: Start -> Start_Before
129
+ // Around: Start_Before -> calls proceed(Start_Before_AroundIn)
130
+ // Original: Start_Before_AroundIn -> returns Original(Start_Before_AroundIn)
131
+ // Around: returns Around(Original(Start_Before_AroundIn))
132
+ // After: receives Around(...) -> returns Around(...)_After
133
+
134
+ expect(originalFn).toHaveBeenCalledWith('Start_Before_AroundIn');
135
+ expect(result).toBe('Around(Original(Start_Before_AroundIn))_After');
136
+ });
137
+ });
@@ -0,0 +1,121 @@
1
+
2
+ import { jest } from '@jest/globals';
3
+ // import interceptorsPlugin from '../../service/interceptorsPlugin.js'; // Removed static import
4
+
5
+ describe('interceptorsPlugin', () => {
6
+ let plugin;
7
+ let mockGenerateInterceptors;
8
+ let mockResolve;
9
+ const themeName = 'Vendor/theme-test';
10
+
11
+ beforeEach(async () => {
12
+ jest.resetModules(); // Reset modules to ensure fresh imports and mocks
13
+ jest.clearAllMocks();
14
+
15
+ // Mock configResolver to prevent process.exit
16
+ jest.unstable_mockModule('../../service/configResolver.js', () => ({
17
+ default: {
18
+ resolveLibRealPath: jest.fn()
19
+ }
20
+ }));
21
+
22
+ // Mock generateInterceptorsService
23
+ mockGenerateInterceptors = jest.fn();
24
+
25
+ // Mock the service module
26
+ jest.unstable_mockModule('../../service/generateInterceptors.js', () => ({
27
+ default: {
28
+ generateInterceptors: mockGenerateInterceptors
29
+ }
30
+ }));
31
+
32
+ // Re-import the plugin to use the mock
33
+ const pluginModule = await import('../../service/interceptorsPlugin.js');
34
+ const createPlugin = pluginModule.default;
35
+
36
+ plugin = createPlugin({ themeName });
37
+
38
+ // Mock Vite context
39
+ mockResolve = jest.fn();
40
+ plugin.resolve = mockResolve; // Bind mock to the plugin instance context if needed, but resolveId is called on context
41
+ });
42
+
43
+ test('should generate interceptors on buildStart', async () => {
44
+ mockGenerateInterceptors.mockResolvedValue({});
45
+ await plugin.buildStart.call({});
46
+ expect(mockGenerateInterceptors).toHaveBeenCalledWith(themeName);
47
+ });
48
+
49
+ test('should resolve to virtual ID if interceptor exists', async () => {
50
+ const targetPath = '/abs/path/to/target.js';
51
+ const interceptors = {
52
+ 'Target::Method': {
53
+ targetPath: targetPath,
54
+ source: 'export const intercepted = true;'
55
+ }
56
+ };
57
+ mockGenerateInterceptors.mockResolvedValue(interceptors);
58
+
59
+ // Initialize plugin
60
+ await plugin.buildStart.call({});
61
+
62
+ // Mock resolve to return the absolute path
63
+ const context = {
64
+ resolve: jest.fn().mockResolvedValue({ id: targetPath })
65
+ };
66
+
67
+ const result = await plugin.resolveId.call(context, './target.js', '/importer.js');
68
+
69
+ expect(context.resolve).toHaveBeenCalledWith('./target.js', '/importer.js', { skipSelf: true });
70
+ expect(result).toBe(`\0interceptor:${targetPath}`);
71
+ });
72
+
73
+ test('should NOT resolve to virtual ID if importer IS the virtual ID (loop prevention)', async () => {
74
+ const targetPath = '/abs/path/to/target.js';
75
+ const interceptors = {
76
+ 'Target::Method': {
77
+ targetPath: targetPath,
78
+ source: 'export const intercepted = true;'
79
+ }
80
+ };
81
+ mockGenerateInterceptors.mockResolvedValue(interceptors);
82
+
83
+ await plugin.buildStart.call({});
84
+
85
+ const context = {
86
+ resolve: jest.fn().mockResolvedValue({ id: targetPath })
87
+ };
88
+
89
+ const virtualId = `\0interceptor:${targetPath}`;
90
+ const result = await plugin.resolveId.call(context, './target.js', virtualId);
91
+
92
+ expect(result).toBeNull();
93
+ });
94
+
95
+ test('should load generated source for virtual ID', async () => {
96
+ const targetPath = '/abs/path/to/target.js';
97
+ const sourceCode = 'export const intercepted = true;';
98
+ const interceptors = {
99
+ 'Target::Method': {
100
+ targetPath: targetPath,
101
+ source: sourceCode
102
+ }
103
+ };
104
+ mockGenerateInterceptors.mockResolvedValue(interceptors);
105
+
106
+ await plugin.buildStart.call({});
107
+
108
+ const virtualId = `\0interceptor:${targetPath}`;
109
+ const result = plugin.load.call({}, virtualId);
110
+
111
+ expect(result).toBe(sourceCode);
112
+ });
113
+
114
+ test('should return null for unknown virtual ID', async () => {
115
+ mockGenerateInterceptors.mockResolvedValue({});
116
+ await plugin.buildStart.call({});
117
+
118
+ const result = plugin.load.call({}, '\0interceptor:/unknown.js');
119
+ expect(result).toBeNull();
120
+ });
121
+ });
@@ -43,6 +43,7 @@ function tryCreateEnvFile() {
43
43
  VITE_SERVER_SECURE: 'true',
44
44
  VITE_HMR_PATH: '/__vite_ping',
45
45
  MAGENTO_HOST: 'magento.test',
46
+ VITE_SERVER_ALLOWED_HOSTS: 'magento.test,localhost'
46
47
  };
47
48
 
48
49
  console.log(chalk.blue('Creating `.env` file with default or user-provided values...'));
@@ -66,7 +67,7 @@ function tryCreateEnvFile() {
66
67
  }
67
68
 
68
69
  function validateEnv() {
69
- const requiredEnvVars = ['VITE_SERVER_HOST', 'VITE_SERVER_PORT', 'VITE_SERVER_SECURE', 'VITE_HMR_PATH', 'MAGENTO_HOST'];
70
+ const requiredEnvVars = ['VITE_SERVER_HOST', 'VITE_SERVER_PORT', 'VITE_SERVER_SECURE', 'VITE_HMR_PATH', 'MAGENTO_HOST', 'VITE_SERVER_ALLOWED_HOSTS'];
70
71
  let missingEnvVars = [];
71
72
  for (const envVar of requiredEnvVars) {
72
73
  if (!process.env[envVar]) {
@@ -14,7 +14,6 @@ try {
14
14
  }
15
15
 
16
16
  export const getMagentoConfig = () => MAGENTO_CONFIG;
17
-
18
17
  export const getModulesConfigArray = () => Object.entries(MAGENTO_CONFIG.modules);
19
18
  export const getThemesConfigArray = () => Object.entries(MAGENTO_CONFIG.themes);
20
19
  export const getAllMagentoModulesEnabled = () => MAGENTO_CONFIG.allModules;
@@ -29,6 +28,26 @@ export const getModuleDefinition = (moduleName) =>
29
28
  export const getThemeDefinition = (themeName) =>
30
29
  MAGENTO_CONFIG.themes[themeName];
31
30
 
31
+ export const MODE = process.env.NODE_ENV;
32
+
33
+ export function resolveLibPath(lib) {
34
+ return path.join(MAGENTO_CONFIG.LIB_PATH, lib);
35
+ }
36
+
37
+ export function resolveNodePath(packageName) {
38
+ try {
39
+ const resolvedPath = import.meta.resolve(packageName);
40
+ return resolvedPath.replace('file://', '');
41
+ } catch (error) {
42
+ console.error(`The package ${packageName} can't be resolved.`);
43
+ throw error;
44
+ }
45
+ }
46
+
47
+ export function resolveLibRealPath(lib) {
48
+ return MODE === 'production' ? lib : resolveNodePath(lib);
49
+ }
50
+
32
51
  export default {
33
52
  getMagentoConfig,
34
53
  getModulesConfigArray,
@@ -37,5 +56,8 @@ export default {
37
56
  isDev,
38
57
  getOutputDirFromTheme,
39
58
  getModuleDefinition,
40
- getThemeDefinition
41
- };
59
+ getThemeDefinition,
60
+ resolveLibPath,
61
+ resolveNodePath,
62
+ resolveLibRealPath
63
+ };
@@ -1,26 +1,28 @@
1
1
  import path from "path";
2
2
  import { pathToFileURL, fileURLToPath } from "url";
3
- import themeResolver from './themeResolverSync.js';
4
- import moduleResolver from './moduleResolver.js';
5
- import pluginManager from './pluginManager.js';
3
+ import themeResolver from 'mage-obsidian/service/themeResolverSync.js';
4
+ import moduleResolver from 'mage-obsidian/service/moduleResolver.js';
5
+ import interceptorManager from 'mage-obsidian/service/interceptorManager.js';
6
+ import configResolver from "mage-obsidian/service/configResolver.js";
6
7
 
7
8
  const interceptorsRegisteredByTheme = new Map();
8
9
  const generatedInterceptorsCache = new Map();
10
+ export const KEY_INTERCEPTED = 'originalIntercepted';
9
11
 
10
12
  /**
11
13
  * Resolves the absolute path of a file using the cached file map
12
- * @param {string} identifier
13
- * @param {Object} fileMap
14
+ * @param {string} identifier
15
+ * @param {Object} fileMap
14
16
  * @returns {string|null}
15
17
  */
16
18
  function resolvePathFromMap(identifier, fileMap) {
17
19
  const [moduleName, relativePath] = identifier.split('::');
18
20
  if (!moduleName || !relativePath) return null;
19
-
21
+
20
22
  const parsed = path.parse(relativePath);
21
23
  const keyPath = path.join(parsed.dir, parsed.name);
22
24
  const key = `${moduleName}/${keyPath}`;
23
-
25
+
24
26
  return fileMap[key] || null;
25
27
  }
26
28
 
@@ -31,31 +33,28 @@ async function registerInterceptors(themeName) {
31
33
 
32
34
  const themeConfig = await themeResolver.getThemeConfig(themeName);
33
35
  const modulesConfig = await moduleResolver.getModuleConfigByThemeConfig(themeName, themeConfig);
34
-
36
+ if (!modulesConfig || modulesConfig.interceptors === undefined) {
37
+ interceptorsRegisteredByTheme.set(themeName, {});
38
+ return {};
39
+ }
35
40
  // Map<Target, Map<PluginName, PluginConfig>>
36
41
  const interceptorsMap = new Map();
42
+ for (const [interceptorName, interceptorDefinition] of Object.entries( modulesConfig.interceptors)) {
43
+ const { target } = interceptorDefinition;
44
+ if (!interceptorName || !target) continue;
37
45
 
38
- for (const [moduleName, moduleConfig] of Object.entries(modulesConfig)) {
39
- if (!moduleConfig.interceptors || !Array.isArray(moduleConfig.interceptors)) {
40
- continue;
46
+ if (!interceptorsMap.has(target)) {
47
+ interceptorsMap.set(target, new Map());
41
48
  }
42
- for (const c of moduleConfig.interceptors) {
43
- const { name, target } = c;
44
- if (!name || !target) continue;
45
49
 
46
- if (!interceptorsMap.has(target)) {
47
- interceptorsMap.set(target, new Map());
48
- }
50
+ const targetPlugins = interceptorsMap.get(target);
49
51
 
50
- const targetPlugins = interceptorsMap.get(target);
51
-
52
- if (targetPlugins.has(name)) {
53
- // Merge existing plugin config with new config (allows overriding sortOrder, active, etc.)
54
- const existing = targetPlugins.get(name);
55
- targetPlugins.set(name, { ...existing, ...c });
56
- } else {
57
- targetPlugins.set(name, { ...c, module: moduleName });
58
- }
52
+ if (targetPlugins.has(interceptorName)) {
53
+ // Merge existing plugin config with new config (allows overriding sortOrder, active, etc.)
54
+ const existing = targetPlugins.get(interceptorName);
55
+ targetPlugins.set(interceptorName, { ...existing, ...interceptorDefinition });
56
+ } else {
57
+ targetPlugins.set(interceptorName, { ...interceptorDefinition });
59
58
  }
60
59
  }
61
60
 
@@ -65,7 +64,7 @@ async function registerInterceptors(themeName) {
65
64
  const plugins = Array.from(pluginsMap.values())
66
65
  .filter(p => p.active !== false) // Filter out inactive plugins
67
66
  .sort((a, b) => (a.sortOrder || 10) - (b.sortOrder || 10));
68
-
67
+
69
68
  if (plugins.length > 0) {
70
69
  result[target] = plugins;
71
70
  }
@@ -81,8 +80,7 @@ async function generateInterceptors(themeName) {
81
80
  }
82
81
 
83
82
  const interceptorsConfig = await registerInterceptors(themeName);
84
- const modulesConfig = await moduleResolver.getModuleConfigByThemeConfig(themeName, await themeResolver.getThemeConfig(themeName));
85
-
83
+
86
84
  // Get all files map from cache
87
85
  let allFilesMap = {};
88
86
  try {
@@ -94,96 +92,99 @@ async function generateInterceptors(themeName) {
94
92
 
95
93
  const interceptors = {};
96
94
 
97
- for (const [targetIdentifier, plugins] of Object.entries(interceptorsConfig)) {
95
+ for (const [target, plugins] of Object.entries(interceptorsConfig)) {
98
96
  // 1. Resolve Target Path
99
- const targetPath = resolvePathFromMap(targetIdentifier, allFilesMap);
97
+ const targetPath = resolvePathFromMap(target, allFilesMap);
100
98
  if (!targetPath) {
101
- console.warn(`Target module not found for identifier: ${targetIdentifier}`);
99
+ console.warn(`Target module not found for identifier: ${target}`);
102
100
  continue;
103
101
  }
104
102
 
105
- // 2. Load Target Module to inspect exports
106
103
  let targetModule;
107
104
  try {
108
105
  targetModule = await import(pathToFileURL(targetPath).href);
109
106
  } catch (e) {
110
- console.error(`Failed to import target module ${targetIdentifier}:`, e.message);
107
+ console.error(`Failed to import target module ${target}:`, e.message);
111
108
  continue;
112
109
  }
113
110
 
114
111
  const targetExports = Object.keys(targetModule);
115
112
  const methodsToIntercept = new Set();
116
113
 
117
- // 3. Validate and Register Plugins
118
- const validPlugins = [];
119
- for (const pluginConfig of plugins) {
120
- const pluginPath = resolvePathFromMap(pluginConfig.plugin, allFilesMap);
121
- if (!pluginPath) {
122
- console.warn(`Plugin module not found: ${pluginConfig.plugin}`);
114
+ // 3. Validate and Register Interceptors
115
+ const validInterceptors = [];
116
+ for (const interceptorConfig of plugins) {
117
+ const interceptorPath = resolvePathFromMap(interceptorConfig.interceptor, allFilesMap);
118
+ if (!interceptorPath) {
119
+ console.warn(`Interceptor module not found: ${interceptorConfig.interceptor}`);
123
120
  continue;
124
121
  }
125
122
 
126
- let pluginModule;
123
+ let interceptorModule;
127
124
  try {
128
- pluginModule = await import(pathToFileURL(pluginPath).href);
125
+ interceptorModule = await import(pathToFileURL(interceptorPath).href);
129
126
  } catch (e) {
130
- console.error(`Failed to import plugin module ${pluginConfig.plugin}:`, e.message);
127
+ console.error(`Failed to import interceptor module ${interceptorConfig.interceptor}:`, e.message);
131
128
  continue;
132
129
  }
133
130
 
134
- const pluginMethods = [];
135
- for (const pluginExport of Object.keys(pluginModule)) {
131
+ const interceptorMethods = [];
132
+ for (const interceptorExport of Object.keys(interceptorModule)) {
136
133
  let type, targetMethod;
137
-
138
- if (pluginExport.startsWith('before')) {
134
+
135
+ if (interceptorExport.startsWith('before')) {
139
136
  type = 'before';
140
- targetMethod = pluginExport.substring(6);
141
- } else if (pluginExport.startsWith('around')) {
137
+ targetMethod = interceptorExport.substring(6);
138
+ } else if (interceptorExport.startsWith('around')) {
142
139
  type = 'around';
143
- targetMethod = pluginExport.substring(6);
144
- } else if (pluginExport.startsWith('after')) {
140
+ targetMethod = interceptorExport.substring(6);
141
+ } else if (interceptorExport.startsWith('after')) {
145
142
  type = 'after';
146
- targetMethod = pluginExport.substring(5);
143
+ targetMethod = interceptorExport.substring(5);
147
144
  } else {
148
145
  continue;
149
146
  }
150
147
 
151
148
  // Check if target method exists in target module
152
149
  if (!targetExports.includes(targetMethod) && targetMethod !== 'default') {
153
- const lowerFirst = targetMethod.charAt(0).toLowerCase() + targetMethod.slice(1);
154
- if (targetExports.includes(lowerFirst)) {
155
- targetMethod = lowerFirst;
156
- } else {
157
- // Skip invalid methods but don't crash the whole process?
150
+ const lowerFirst = targetMethod.charAt(0).toLowerCase() + targetMethod.slice(1);
151
+ if (targetExports.includes(lowerFirst)) {
152
+ targetMethod = lowerFirst;
153
+ } else {
154
+ // Skip invalid methods but don't crash the whole process?
158
155
  // User requested error if not found.
159
- throw new Error(`Plugin ${pluginConfig.name} (${pluginConfig.plugin}) exports '${pluginExport}' but target ${targetIdentifier} does not export '${targetMethod}'`);
160
- }
156
+ throw new Error(`Interceptor ${interceptorConfig.name} (${interceptorConfig.interceptor}) exports '${interceptorExport}' but target ${target} does not export '${targetMethod}'`);
157
+ }
161
158
  }
162
159
 
163
- // Register the plugin with PluginManager (Runtime)
164
- const methodKey = `${targetIdentifier}::${targetMethod}`;
165
- pluginManager.addPlugin(
160
+ if (typeof targetModule[targetMethod] !== 'function') {
161
+ console.warn(`Interceptor ${interceptorConfig.name} (${interceptorConfig.interceptor}) exports '${interceptorExport}' but target ${target} export '${targetMethod}' is not a function.`);
162
+ continue;
163
+ }
164
+
165
+ const methodKey = `${target}::${targetMethod}`;
166
+ interceptorManager.addInterceptor(
166
167
  methodKey,
167
- pluginConfig.name,
168
+ interceptorConfig.name,
168
169
  type,
169
- pluginModule[pluginExport],
170
- pluginConfig.sortOrder
170
+ interceptorModule[interceptorExport],
171
+ interceptorConfig.sortOrder
171
172
  );
172
-
173
+
173
174
  methodsToIntercept.add(targetMethod);
174
- pluginMethods.push({
175
- exportName: pluginExport,
175
+ interceptorMethods.push({
176
+ exportName: interceptorExport,
176
177
  type,
177
178
  targetMethod,
178
- sortOrder: pluginConfig.sortOrder
179
+ sortOrder: interceptorConfig.sortOrder
179
180
  });
180
181
  }
181
182
 
182
- if (pluginMethods.length > 0) {
183
- validPlugins.push({
184
- ...pluginConfig,
185
- path: pluginPath,
186
- methods: pluginMethods
183
+ if (interceptorMethods.length > 0) {
184
+ validInterceptors.push({
185
+ ...interceptorConfig,
186
+ path: interceptorPath,
187
+ methods: interceptorMethods
187
188
  });
188
189
  }
189
190
  }
@@ -191,15 +192,15 @@ async function generateInterceptors(themeName) {
191
192
  // 4. Create Interceptor Proxy & Source Code
192
193
  if (methodsToIntercept.size > 0) {
193
194
  const wrapper = { ...targetModule };
194
- const proxy = pluginManager.intercept(wrapper, targetIdentifier, true);
195
-
196
- const source = generateInterceptorCode(targetIdentifier, targetPath, validPlugins, targetExports);
195
+ const proxy = interceptorManager.intercept(wrapper, target, true);
197
196
 
198
- interceptors[targetIdentifier] = {
197
+ const source = generateInterceptorCode(target, targetPath, validInterceptors, targetExports);
198
+
199
+ interceptors[target] = {
199
200
  proxy,
200
201
  targetPath,
201
202
  targetModule,
202
- plugins: validPlugins,
203
+ interceptors: validInterceptors,
203
204
  source
204
205
  };
205
206
  }
@@ -209,36 +210,36 @@ async function generateInterceptors(themeName) {
209
210
  return interceptors;
210
211
  }
211
212
 
212
- function generateInterceptorCode(targetIdentifier, targetPath, plugins, targetExports) {
213
+ function generateInterceptorCode(target, targetPath, interceptors, targetExports) {
213
214
  const imports = [];
214
215
  const registrations = [];
215
-
216
+
216
217
  // Import PluginManager (Assuming it's available via alias or relative path in the build environment)
217
218
  // For Vite, we might need to adjust this path or use a virtual module ID.
218
219
  // Using a relative path from this service file might not work in the generated code context.
219
- // We'll assume '@mage-obsidian/plugin-manager' or similar alias is set up,
220
+ // We'll assume '@mage-obsidian/plugin-manager' or similar alias is set up,
220
221
  // or use the absolute path which Vite handles.
221
- const pluginManagerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'pluginManager.js');
222
- imports.push(`import pluginManager from '/@fs${pluginManagerPath}';`);
222
+ const resolvedInterceptorManagerPath = configResolver.resolveLibRealPath('mage-obsidian/service/interceptorManager');
223
+ imports.push(`import interceptorManager from "${resolvedInterceptorManagerPath}";`);
223
224
 
224
225
  // Import Original Module
225
- imports.push(`import * as originalModule from '/@fs${targetPath}';`);
226
-
227
- // Import Plugins
228
- plugins.forEach((plugin, index) => {
229
- const pluginVar = `plugin_${index}`;
230
- imports.push(`import * as ${pluginVar} from '/@fs${plugin.path}';`);
231
-
232
- plugin.methods.forEach(method => {
233
- const methodKey = `${targetIdentifier}::${method.targetMethod}`;
234
- registrations.push(`pluginManager.addPlugin('${methodKey}', '${plugin.name}', '${method.type}', ${pluginVar}.${method.exportName}, ${method.sortOrder});`);
226
+ imports.push(`import * as originalModule from '/@fs${targetPath}?${KEY_INTERCEPTED}';`);
227
+
228
+ // Import Interceptors
229
+ interceptors.forEach((interceptor, index) => {
230
+ const interceptorVar = `interceptor_${index}`;
231
+ imports.push(`import * as ${interceptorVar} from '/@fs${interceptor.path}';`);
232
+
233
+ interceptor.methods.forEach(method => {
234
+ const methodKey = `${target}::${method.targetMethod}`;
235
+ registrations.push(`interceptorManager.addInterceptor('${methodKey}', '${interceptor.name}', '${method.type}', ${interceptorVar}.${method.exportName}, ${method.sortOrder});`);
235
236
  });
236
237
  });
237
238
 
238
239
  // Create Interceptor
239
240
  const interceptorCode = `
240
241
  const targetWrapper = { ...originalModule };
241
- const proxy = pluginManager.intercept(targetWrapper, '${targetIdentifier}', true);
242
+ const proxy = interceptorManager.intercept(targetWrapper, '${target}', true);
242
243
  `;
243
244
 
244
245
  // Exports
@@ -262,5 +263,6 @@ ${exportsCode}
262
263
 
263
264
  export default {
264
265
  registerInterceptors,
265
- generateInterceptors
266
- };
266
+ generateInterceptors,
267
+ KEY_INTERCEPTED
268
+ };
@@ -1,25 +1,25 @@
1
- class PluginManager {
1
+ class InterceptorManager {
2
2
  constructor() {
3
- this.plugins = {};
3
+ this.interceptors = {};
4
4
  }
5
5
 
6
6
  /**
7
- * Register a plugin
7
+ * Register an interceptor
8
8
  * @param {string} target - The name of the target function/method to intercept
9
- * @param {string} name - Unique name for the plugin
9
+ * @param {string} name - Unique name for the interceptor
10
10
  * @param {string} type - 'before', 'around', 'after'
11
11
  * @param {Function} handler - The function to execute
12
12
  * @param {number} sortOrder - Order of execution
13
13
  */
14
- addPlugin(target, name, type, handler, sortOrder = 10) {
15
- if (!this.plugins[target]) {
16
- this.plugins[target] = { before: [], around: [], after: [] };
14
+ addInterceptor(target, name, type, handler, sortOrder = 10) {
15
+ if (!this.interceptors[target]) {
16
+ this.interceptors[target] = { before: [], around: [], after: [] };
17
17
  }
18
18
  if (!['before', 'around', 'after'].includes(type)) {
19
- throw new Error(`Invalid plugin type: ${type}`);
19
+ throw new Error(`Invalid interceptor type: ${type}`);
20
20
  }
21
- this.plugins[target][type].push({ name, handler, sortOrder });
22
- this.plugins[target][type].sort((a, b) => a.sortOrder - b.sortOrder);
21
+ this.interceptors[target][type].push({ name, handler, sortOrder });
22
+ this.interceptors[target][type].sort((a, b) => a.sortOrder - b.sortOrder);
23
23
  }
24
24
 
25
25
  /**
@@ -30,36 +30,36 @@ class PluginManager {
30
30
  * @param {Array} args - Arguments passed to the function
31
31
  */
32
32
  executeSync(target, originalMethod, context, ...args) {
33
- const plugins = this.plugins[target] || { before: [], around: [], after: [] };
33
+ const interceptors = this.interceptors[target] || { before: [], around: [], after: [] };
34
34
 
35
- // Execute 'before' plugins
36
- for (const plugin of plugins.before) {
37
- const result = plugin.handler.apply(context, args);
35
+ // Execute 'before' interceptors
36
+ for (const interceptor of interceptors.before) {
37
+ const result = interceptor.handler.apply(context, args);
38
38
  if (Array.isArray(result)) {
39
39
  args = result;
40
40
  }
41
41
  }
42
42
 
43
- // Execute 'around' plugins
43
+ // Execute 'around' interceptors
44
44
  let methodToExecute = (...currentArgs) => {
45
45
  return originalMethod.apply(context, currentArgs);
46
46
  };
47
47
 
48
- if (plugins.around.length > 0) {
49
- const aroundPlugins = [...plugins.around].reverse();
50
- for (const plugin of aroundPlugins) {
48
+ if (interceptors.around.length > 0) {
49
+ const aroundInterceptors = [...interceptors.around].reverse();
50
+ for (const interceptor of aroundInterceptors) {
51
51
  const next = methodToExecute;
52
52
  methodToExecute = (...currentArgs) => {
53
- return plugin.handler.apply(context, [next, ...currentArgs]);
53
+ return interceptor.handler.apply(context, [next, ...currentArgs]);
54
54
  };
55
55
  }
56
56
  }
57
57
 
58
58
  let result = methodToExecute(...args);
59
59
 
60
- // Execute 'after' plugins
61
- for (const plugin of plugins.after) {
62
- result = plugin.handler.apply(context, [result, ...args]);
60
+ // Execute 'after' interceptors
61
+ for (const interceptor of interceptors.after) {
62
+ result = interceptor.handler.apply(context, [result, ...args]);
63
63
  }
64
64
 
65
65
  return result;
@@ -73,40 +73,40 @@ class PluginManager {
73
73
  * @param {Array} args - Arguments passed to the function
74
74
  */
75
75
  async execute(target, originalMethod, context, ...args) {
76
- const plugins = this.plugins[target] || { before: [], around: [], after: [] };
76
+ const interceptors = this.interceptors[target] || { before: [], around: [], after: [] };
77
77
 
78
- // Execute 'before' plugins
79
- // Before plugins can modify args by returning an array
80
- for (const plugin of plugins.before) {
81
- const result = await plugin.handler.apply(context, args);
78
+ // Execute 'before' interceptors
79
+ // Before interceptors can modify args by returning an array
80
+ for (const interceptor of interceptors.before) {
81
+ const result = await interceptor.handler.apply(context, args);
82
82
  if (Array.isArray(result)) {
83
83
  args = result;
84
84
  }
85
85
  }
86
86
 
87
- // Execute 'around' plugins
88
- // Around plugins receive (proceed, ...args)
87
+ // Execute 'around' interceptors
88
+ // Around interceptors receive (proceed, ...args)
89
89
  let methodToExecute = async (...currentArgs) => {
90
90
  return await originalMethod.apply(context, currentArgs);
91
91
  };
92
92
 
93
- // Wrap around plugins: first registered is outer-most
94
- if (plugins.around.length > 0) {
95
- const aroundPlugins = [...plugins.around].reverse();
96
- for (const plugin of aroundPlugins) {
93
+ // Wrap around interceptors: first registered is outer-most
94
+ if (interceptors.around.length > 0) {
95
+ const aroundInterceptors = [...interceptors.around].reverse();
96
+ for (const interceptor of aroundInterceptors) {
97
97
  const next = methodToExecute;
98
98
  methodToExecute = async (...currentArgs) => {
99
- return await plugin.handler.apply(context, [next, ...currentArgs]);
99
+ return await interceptor.handler.apply(context, [next, ...currentArgs]);
100
100
  };
101
101
  }
102
102
  }
103
103
 
104
104
  let result = await methodToExecute(...args);
105
105
 
106
- // Execute 'after' plugins
107
- // After plugins receive (result, ...args) and must return result
108
- for (const plugin of plugins.after) {
109
- result = await plugin.handler.apply(context, [result, ...args]);
106
+ // Execute 'after' interceptors
107
+ // After interceptors receive (result, ...args) and must return result
108
+ for (const interceptor of interceptors.after) {
109
+ result = await interceptor.handler.apply(context, [result, ...args]);
110
110
  }
111
111
 
112
112
  return result;
@@ -115,7 +115,7 @@ class PluginManager {
115
115
  /**
116
116
  * Create a proxy to intercept method calls on an object
117
117
  * @param {Object} target - The target object (e.g. module exports)
118
- * @param {string} namespace - Namespace for plugins
118
+ * @param {string} namespace - Namespace for interceptors
119
119
  * @param {boolean} useAsync - Whether to use async execution
120
120
  */
121
121
  intercept(target, namespace, useAsync = true) {
@@ -134,4 +134,4 @@ class PluginManager {
134
134
  }
135
135
  }
136
136
 
137
- export default new PluginManager();
137
+ export default new InterceptorManager();
@@ -0,0 +1,70 @@
1
+ import generateInterceptorsService from './generateInterceptors.js';
2
+
3
+ export default function interceptorsPlugin(options = {}) {
4
+ const { themeName } = options;
5
+ let interceptorsMap = new Map(); // path -> interceptorData
6
+
7
+ return {
8
+ name: 'mage-obsidian:interceptors',
9
+ enforce: 'pre',
10
+
11
+ async buildStart() {
12
+ if (!themeName) {
13
+ console.warn('[mage-obsidian:interceptors] themeName option is missing. Interceptors will not be generated.');
14
+ return;
15
+ }
16
+
17
+ try {
18
+ const interceptors = await generateInterceptorsService.generateInterceptors(themeName);
19
+
20
+ // Create a map for fast lookup by file path
21
+ for (const key in interceptors) {
22
+ const data = interceptors[key];
23
+ if (data.targetPath) {
24
+ interceptorsMap.set(data.targetPath, data);
25
+ }
26
+ }
27
+ } catch (error) {
28
+ console.error('[mage-obsidian:interceptors] Failed to generate interceptors:', error);
29
+ }
30
+ },
31
+
32
+ async resolveId(source, importer) {
33
+ // Skip if we haven't loaded interceptors or if it's a virtual module
34
+ if (interceptorsMap.size === 0 || source.startsWith('\0')) return null;
35
+
36
+ // Try to resolve the import to a full path
37
+ const resolution = await this.resolve(source, importer, { skipSelf: true });
38
+
39
+ if (!resolution || !resolution.id) return null;
40
+
41
+ // Clean up the ID (remove query params)
42
+ const resolvedId = resolution.id.split('?')[0];
43
+
44
+ if (interceptorsMap.has(resolvedId)) {
45
+ const virtualId = `\0interceptor:${resolvedId}`;
46
+
47
+ // Check if we are inside the interceptor trying to import the original
48
+ if (importer === virtualId) {
49
+ return null; // Allow original import
50
+ }
51
+
52
+ return virtualId;
53
+ }
54
+
55
+ return null;
56
+ },
57
+
58
+ load(id) {
59
+ if (id.startsWith('\0interceptor:')) {
60
+ const originalPath = id.slice('\0interceptor:'.length);
61
+ const data = interceptorsMap.get(originalPath);
62
+
63
+ if (data) {
64
+ return data.source;
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ };
70
+ }
@@ -125,7 +125,7 @@ function resolveFileByTheme(themeName, moduleName, filePath) {
125
125
 
126
126
  async function resolveModuleConfig(moduleName, themeName) {
127
127
  const module = configResolver.getMagentoConfig().modules[moduleName];
128
- let moduleConfigSourcePath = resolveFileByTheme(themeName, moduleName, MODULE_CONFIG_FIL);
128
+ let moduleConfigSourcePath = resolveFileByTheme(themeName, moduleName, MODULE_CONFIG_FILE);
129
129
 
130
130
  if (!moduleConfigSourcePath && !module) {
131
131
  return null;