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 +3 -2
- package/src/__tests__/fixtures/interceptors/pluginForNonFunction.js +7 -0
- package/src/__tests__/fixtures/interceptors/pluginModule.js +5 -0
- package/src/__tests__/fixtures/interceptors/targetNonFunction.js +7 -0
- package/src/__tests__/service/generateInterceptors.test.js +81 -30
- package/src/__tests__/service/interceptorManager_magento_behavior.test.js +137 -0
- package/src/__tests__/vite-plugins/interceptorsPlugin.test.js +121 -0
- package/src/scripts/buildThemes.js +2 -1
- package/src/service/configResolver.js +25 -3
- package/src/service/generateInterceptors.js +99 -97
- package/src/service/{pluginManager.js → interceptorManager.js} +40 -40
- package/src/service/interceptorsPlugin.js +70 -0
- package/src/service/moduleResolver.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mage-obsidian",
|
|
3
|
-
"version": "1.1.
|
|
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",
|
|
@@ -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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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.
|
|
85
|
-
expect(interceptorData.
|
|
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
|
|
91
|
-
expect(source).toContain(`
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 '
|
|
4
|
-
import moduleResolver from '
|
|
5
|
-
import
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
47
|
-
interceptorsMap.set(target, new Map());
|
|
48
|
-
}
|
|
50
|
+
const targetPlugins = interceptorsMap.get(target);
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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 [
|
|
95
|
+
for (const [target, plugins] of Object.entries(interceptorsConfig)) {
|
|
98
96
|
// 1. Resolve Target Path
|
|
99
|
-
const targetPath = resolvePathFromMap(
|
|
97
|
+
const targetPath = resolvePathFromMap(target, allFilesMap);
|
|
100
98
|
if (!targetPath) {
|
|
101
|
-
console.warn(`Target module not found for identifier: ${
|
|
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 ${
|
|
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
|
|
118
|
-
const
|
|
119
|
-
for (const
|
|
120
|
-
const
|
|
121
|
-
if (!
|
|
122
|
-
console.warn(`
|
|
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
|
|
123
|
+
let interceptorModule;
|
|
127
124
|
try {
|
|
128
|
-
|
|
125
|
+
interceptorModule = await import(pathToFileURL(interceptorPath).href);
|
|
129
126
|
} catch (e) {
|
|
130
|
-
console.error(`Failed to import
|
|
127
|
+
console.error(`Failed to import interceptor module ${interceptorConfig.interceptor}:`, e.message);
|
|
131
128
|
continue;
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
const
|
|
135
|
-
for (const
|
|
131
|
+
const interceptorMethods = [];
|
|
132
|
+
for (const interceptorExport of Object.keys(interceptorModule)) {
|
|
136
133
|
let type, targetMethod;
|
|
137
|
-
|
|
138
|
-
if (
|
|
134
|
+
|
|
135
|
+
if (interceptorExport.startsWith('before')) {
|
|
139
136
|
type = 'before';
|
|
140
|
-
targetMethod =
|
|
141
|
-
} else if (
|
|
137
|
+
targetMethod = interceptorExport.substring(6);
|
|
138
|
+
} else if (interceptorExport.startsWith('around')) {
|
|
142
139
|
type = 'around';
|
|
143
|
-
targetMethod =
|
|
144
|
-
} else if (
|
|
140
|
+
targetMethod = interceptorExport.substring(6);
|
|
141
|
+
} else if (interceptorExport.startsWith('after')) {
|
|
145
142
|
type = 'after';
|
|
146
|
-
targetMethod =
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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(`
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
+
interceptorConfig.name,
|
|
168
169
|
type,
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
interceptorModule[interceptorExport],
|
|
171
|
+
interceptorConfig.sortOrder
|
|
171
172
|
);
|
|
172
|
-
|
|
173
|
+
|
|
173
174
|
methodsToIntercept.add(targetMethod);
|
|
174
|
-
|
|
175
|
-
exportName:
|
|
175
|
+
interceptorMethods.push({
|
|
176
|
+
exportName: interceptorExport,
|
|
176
177
|
type,
|
|
177
178
|
targetMethod,
|
|
178
|
-
sortOrder:
|
|
179
|
+
sortOrder: interceptorConfig.sortOrder
|
|
179
180
|
});
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
...
|
|
185
|
-
path:
|
|
186
|
-
methods:
|
|
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 =
|
|
195
|
-
|
|
196
|
-
const source = generateInterceptorCode(targetIdentifier, targetPath, validPlugins, targetExports);
|
|
195
|
+
const proxy = interceptorManager.intercept(wrapper, target, true);
|
|
197
196
|
|
|
198
|
-
|
|
197
|
+
const source = generateInterceptorCode(target, targetPath, validInterceptors, targetExports);
|
|
198
|
+
|
|
199
|
+
interceptors[target] = {
|
|
199
200
|
proxy,
|
|
200
201
|
targetPath,
|
|
201
202
|
targetModule,
|
|
202
|
-
|
|
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(
|
|
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
|
|
222
|
-
imports.push(`import
|
|
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
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
imports.push(`import * as ${
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const methodKey = `${
|
|
234
|
-
registrations.push(`
|
|
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 =
|
|
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
|
|
1
|
+
class InterceptorManager {
|
|
2
2
|
constructor() {
|
|
3
|
-
this.
|
|
3
|
+
this.interceptors = {};
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Register
|
|
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
|
|
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
|
-
|
|
15
|
-
if (!this.
|
|
16
|
-
this.
|
|
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
|
|
19
|
+
throw new Error(`Invalid interceptor type: ${type}`);
|
|
20
20
|
}
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
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
|
|
33
|
+
const interceptors = this.interceptors[target] || { before: [], around: [], after: [] };
|
|
34
34
|
|
|
35
|
-
// Execute 'before'
|
|
36
|
-
for (const
|
|
37
|
-
const result =
|
|
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'
|
|
43
|
+
// Execute 'around' interceptors
|
|
44
44
|
let methodToExecute = (...currentArgs) => {
|
|
45
45
|
return originalMethod.apply(context, currentArgs);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
if (
|
|
49
|
-
const
|
|
50
|
-
for (const
|
|
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
|
|
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'
|
|
61
|
-
for (const
|
|
62
|
-
result =
|
|
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
|
|
76
|
+
const interceptors = this.interceptors[target] || { before: [], around: [], after: [] };
|
|
77
77
|
|
|
78
|
-
// Execute 'before'
|
|
79
|
-
// Before
|
|
80
|
-
for (const
|
|
81
|
-
const result = await
|
|
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'
|
|
88
|
-
// Around
|
|
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
|
|
94
|
-
if (
|
|
95
|
-
const
|
|
96
|
-
for (const
|
|
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
|
|
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'
|
|
107
|
-
// After
|
|
108
|
-
for (const
|
|
109
|
-
result = await
|
|
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
|
|
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
|
|
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,
|
|
128
|
+
let moduleConfigSourcePath = resolveFileByTheme(themeName, moduleName, MODULE_CONFIG_FILE);
|
|
129
129
|
|
|
130
130
|
if (!moduleConfigSourcePath && !module) {
|
|
131
131
|
return null;
|