mage-obsidian 1.0.11 → 1.1.0
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/jest.config.js +1 -1
- package/package.json +7 -5
- package/src/__tests__/fixtures/interceptors/emptyTarget.js +2 -0
- package/src/__tests__/fixtures/interceptors/pluginModule.js +8 -0
- package/src/__tests__/fixtures/interceptors/targetModule.js +12 -0
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/module.config.js +0 -6
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameB/web/module.config.js +0 -5
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameNoConfig/web/module.config.js +0 -6
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/theme.config.js +0 -12
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-b/web/theme.config.js +0 -17
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/module.config.js +0 -7
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/web/theme.config.js +0 -14
- package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-d/web/theme.config.js +0 -11
- package/src/__tests__/service/generateInterceptors.test.js +145 -0
- package/src/__tests__/service/themeResolverSync.test.js +4 -75
- package/src/config/default.js +1 -0
- package/src/service/generateInterceptors.js +266 -0
- package/src/service/moduleResolver.js +6 -18
- package/src/service/pluginManager.js +137 -0
- package/src/service/themeResolverSync.js +2 -20
- package/src/__tests__/service/moduleResolverSync.test.js +0 -188
- package/src/service/tailwindResolver.js +0 -11
package/jest.config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
testEnvironment: 'node',
|
|
3
3
|
transform: {},
|
|
4
|
-
testPathIgnorePatterns: ['/__tests__/magento_scenarios/','/node_modules/', '/__tests__/__mocks__/'],
|
|
4
|
+
testPathIgnorePatterns: ['/__tests__/magento_scenarios/','/node_modules/', '/__tests__/__mocks__/', '/__tests__/fixtures/'],
|
|
5
5
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mage-obsidian",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Mage Obsidian Ultis",
|
|
5
5
|
"main": "./src/scripts/buildThemes.js",
|
|
6
6
|
"keywords": [
|
|
@@ -25,12 +25,14 @@
|
|
|
25
25
|
"./config/": "./src/config/"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"chalk": "^5.
|
|
28
|
+
"chalk": "^5.4.1",
|
|
29
|
+
"commander": "^13.1.0",
|
|
29
30
|
"deepmerge": "^4.3.1",
|
|
31
|
+
"readline-sync": "^1.4.10"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
30
34
|
"jest": "^29.7.0",
|
|
31
|
-
"jest-environment-node": "^29.7.0"
|
|
32
|
-
"readline-sync": "^1.4.10",
|
|
33
|
-
"commander": "^13.1.0"
|
|
35
|
+
"jest-environment-node": "^29.7.0"
|
|
34
36
|
},
|
|
35
37
|
"scripts": {
|
|
36
38
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-a/web/theme.config.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
tailwind: {
|
|
3
|
-
theme: {
|
|
4
|
-
extend: {},
|
|
5
|
-
},
|
|
6
|
-
plugins: [],
|
|
7
|
-
content: [
|
|
8
|
-
'./**/*.{vue,js}',
|
|
9
|
-
'../**/*.phtml',
|
|
10
|
-
'../*/page_layout/override/base/*.xml',
|
|
11
|
-
],
|
|
12
|
-
},
|
|
13
|
-
ignoredTailwindConfigFromModules: [],
|
|
14
2
|
ignoredCssFromModules: [
|
|
15
3
|
'Vendor_ModuleNameA',
|
|
16
4
|
],
|
package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-b/web/theme.config.js
CHANGED
|
@@ -1,21 +1,4 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
tailwind: {
|
|
3
|
-
theme: {
|
|
4
|
-
extend: {
|
|
5
|
-
textColor: {
|
|
6
|
-
'primary': '#3490dc',
|
|
7
|
-
'secondary': '#ffed4a',
|
|
8
|
-
'danger': '#e3342f',
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
plugins: [],
|
|
13
|
-
content: [
|
|
14
|
-
'./**/*.{vue,js}',
|
|
15
|
-
'../**/*.phtml',
|
|
16
|
-
'../*/page_layout/override/base/*.xml',
|
|
17
|
-
],
|
|
18
|
-
},
|
|
19
2
|
ignoredCssFromModules: [
|
|
20
3
|
'Vendor_ModuleNameNoConfig'
|
|
21
4
|
]
|
package/src/__tests__/magento_scenarios/app/design/frontend/Vendor/theme-c/web/theme.config.js
CHANGED
|
@@ -1,16 +1,2 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
tailwind: {
|
|
3
|
-
theme: {
|
|
4
|
-
extend: {
|
|
5
|
-
},
|
|
6
|
-
},
|
|
7
|
-
plugins: [],
|
|
8
|
-
content: [
|
|
9
|
-
'./**/*.{vue,js}',
|
|
10
|
-
'../**/*.phtml',
|
|
11
|
-
'../*/page_layout/override/base/*.xml',
|
|
12
|
-
],
|
|
13
|
-
},
|
|
14
|
-
includeTailwindConfigFromParentThemes: false,
|
|
15
|
-
ignoredTailwindConfigFromModules: 'all'
|
|
16
2
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
|
|
2
|
+
import { jest } from '@jest/globals';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const fixturesDir = path.resolve(__dirname, '../fixtures/interceptors');
|
|
8
|
+
const targetModulePath = path.join(fixturesDir, 'targetModule.js');
|
|
9
|
+
const pluginModulePath = path.join(fixturesDir, 'pluginModule.js');
|
|
10
|
+
|
|
11
|
+
describe('generateInterceptors', () => {
|
|
12
|
+
let generateInterceptorsService;
|
|
13
|
+
let themeResolverMock;
|
|
14
|
+
let moduleResolverMock;
|
|
15
|
+
let pluginManager;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
jest.resetModules();
|
|
19
|
+
|
|
20
|
+
themeResolverMock = {
|
|
21
|
+
getThemeConfig: jest.fn(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
moduleResolverMock = {
|
|
25
|
+
getModuleConfigByThemeConfig: jest.fn(),
|
|
26
|
+
getAllJsVueFilesWithInheritanceCached: jest.fn(),
|
|
27
|
+
getAllJsVueFilesWithInheritance: jest.fn(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
jest.unstable_mockModule('../../service/themeResolverSync.js', () => ({
|
|
31
|
+
default: themeResolverMock
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
jest.unstable_mockModule('../../service/moduleResolver.js', () => ({
|
|
35
|
+
default: moduleResolverMock
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
// Import real pluginManager to reset state
|
|
39
|
+
pluginManager = (await import('../../service/pluginManager.js')).default;
|
|
40
|
+
pluginManager.plugins = {};
|
|
41
|
+
|
|
42
|
+
generateInterceptorsService = (await import('../../service/generateInterceptors.js')).default;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('should generate interceptors correctly', async () => {
|
|
46
|
+
const themeName = 'Vendor/theme-test';
|
|
47
|
+
|
|
48
|
+
themeResolverMock.getThemeConfig.mockReturnValue({
|
|
49
|
+
src: '/path/to/theme'
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
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
|
+
]
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const allFilesMap = {
|
|
68
|
+
'Vendor_TargetModule/js/target': targetModulePath,
|
|
69
|
+
'Vendor_PluginModule/js/plugin': pluginModulePath
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
moduleResolverMock.getAllJsVueFilesWithInheritanceCached.mockReturnValue(allFilesMap);
|
|
73
|
+
|
|
74
|
+
const result = await generateInterceptorsService.generateInterceptors(themeName);
|
|
75
|
+
|
|
76
|
+
const targetIdentifier = 'Vendor_TargetModule::js/target.js';
|
|
77
|
+
// Use bracket access check to avoid Jest treating '.' as nested property
|
|
78
|
+
expect(result[targetIdentifier]).toBeDefined();
|
|
79
|
+
|
|
80
|
+
const interceptorData = result[targetIdentifier];
|
|
81
|
+
expect(interceptorData).toHaveProperty('proxy');
|
|
82
|
+
expect(interceptorData).toHaveProperty('source');
|
|
83
|
+
expect(interceptorData).toHaveProperty('targetPath', targetModulePath);
|
|
84
|
+
expect(interceptorData.plugins).toHaveLength(1);
|
|
85
|
+
expect(interceptorData.plugins[0].name).toBe('TestPlugin');
|
|
86
|
+
|
|
87
|
+
// Verify Source Code Generation
|
|
88
|
+
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);`);
|
|
92
|
+
expect(source).toContain(`export const targetFunction = proxy.targetFunction;`);
|
|
93
|
+
|
|
94
|
+
// Verify Proxy Behavior
|
|
95
|
+
const proxy = interceptorData.proxy;
|
|
96
|
+
const resultFunc = await proxy.targetFunction('test');
|
|
97
|
+
expect(resultFunc).toBe('Original: Modified: test');
|
|
98
|
+
|
|
99
|
+
const resultAnother = await proxy.anotherFunction();
|
|
100
|
+
expect(resultAnother).toBe('Another - Modified');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('should handle missing target module gracefully', async () => {
|
|
104
|
+
themeResolverMock.getThemeConfig.mockReturnValue({});
|
|
105
|
+
moduleResolverMock.getModuleConfigByThemeConfig.mockResolvedValue({
|
|
106
|
+
'Vendor_TargetModule': { interceptors: [] }
|
|
107
|
+
});
|
|
108
|
+
moduleResolverMock.getAllJsVueFilesWithInheritanceCached.mockReturnValue({});
|
|
109
|
+
|
|
110
|
+
const result = await generateInterceptorsService.generateInterceptors('Vendor/theme-missing');
|
|
111
|
+
expect(result).toEqual({});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should throw error if plugin targets non-existent method', async () => {
|
|
115
|
+
const themeName = 'Vendor/theme-test';
|
|
116
|
+
themeResolverMock.getThemeConfig.mockReturnValue({});
|
|
117
|
+
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
|
+
]
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const emptyTargetModulePath = path.join(fixturesDir, 'emptyTarget.js');
|
|
132
|
+
|
|
133
|
+
const allFilesMap = {
|
|
134
|
+
'Vendor_TargetModule/js/target': emptyTargetModulePath,
|
|
135
|
+
'Vendor_PluginModule/js/plugin': pluginModulePath
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
moduleResolverMock.getAllJsVueFilesWithInheritanceCached.mockReturnValue(allFilesMap);
|
|
139
|
+
|
|
140
|
+
// We expect the promise to reject because generateInterceptors throws on invalid target method
|
|
141
|
+
await expect(generateInterceptorsService.generateInterceptors(themeName))
|
|
142
|
+
.rejects
|
|
143
|
+
.toThrow(/does not export/);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -12,90 +12,40 @@ let scenarios = [
|
|
|
12
12
|
{
|
|
13
13
|
code: 'Vendor/theme-a',
|
|
14
14
|
expected: {
|
|
15
|
-
tailwind: {
|
|
16
|
-
theme: { extend: {} },
|
|
17
|
-
plugins: [],
|
|
18
|
-
content: [
|
|
19
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-a/web/**/*.{vue,js}',
|
|
20
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-a/**/*.phtml',
|
|
21
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-a/*/page_layout/override/base/*.xml'
|
|
22
|
-
]
|
|
23
|
-
},
|
|
24
|
-
includeTailwindConfigFromParentThemes: true,
|
|
25
15
|
includeCssSourceFromParentThemes: true,
|
|
26
16
|
ignoredCssFromModules: [
|
|
27
17
|
'Vendor_ModuleNameA'
|
|
28
18
|
],
|
|
29
|
-
ignoredTailwindConfigFromModules: [],
|
|
30
19
|
exposeNpmPackages: []
|
|
31
20
|
}
|
|
32
21
|
},
|
|
33
22
|
{
|
|
34
23
|
code: 'Vendor/theme-b',
|
|
35
24
|
expected: {
|
|
36
|
-
tailwind: {
|
|
37
|
-
theme: {
|
|
38
|
-
extend: {
|
|
39
|
-
textColor: {
|
|
40
|
-
'primary': '#3490dc',
|
|
41
|
-
'secondary': '#ffed4a',
|
|
42
|
-
'danger': '#e3342f',
|
|
43
|
-
},
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
plugins: [],
|
|
47
|
-
content: [
|
|
48
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-a/web/**/*.{vue,js}',
|
|
49
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-a/**/*.phtml',
|
|
50
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-a/*/page_layout/override/base/*.xml',
|
|
51
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-b/web/**/*.{vue,js}',
|
|
52
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-b/**/*.phtml',
|
|
53
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-b/*/page_layout/override/base/*.xml'
|
|
54
|
-
]
|
|
55
|
-
},
|
|
56
|
-
includeTailwindConfigFromParentThemes: true,
|
|
57
25
|
includeCssSourceFromParentThemes: true,
|
|
58
26
|
ignoredCssFromModules: [
|
|
59
27
|
'Vendor_ModuleNameA',
|
|
60
28
|
'Vendor_ModuleNameNoConfig'
|
|
61
29
|
],
|
|
62
|
-
ignoredTailwindConfigFromModules: [],
|
|
63
30
|
exposeNpmPackages: []
|
|
64
31
|
}
|
|
65
32
|
},
|
|
66
33
|
{
|
|
67
34
|
code: 'Vendor/theme-c',
|
|
68
35
|
expected: {
|
|
69
|
-
tailwind: {
|
|
70
|
-
theme: { extend: {} },
|
|
71
|
-
plugins: [],
|
|
72
|
-
content: [
|
|
73
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-c/web/**/*.{vue,js}',
|
|
74
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-c/**/*.phtml',
|
|
75
|
-
'magento_scenarios/app/design/frontend/Vendor/theme-c/*/page_layout/override/base/*.xml'
|
|
76
|
-
]
|
|
77
|
-
},
|
|
78
|
-
includeTailwindConfigFromParentThemes: false,
|
|
79
36
|
includeCssSourceFromParentThemes: true,
|
|
80
|
-
ignoredCssFromModules: [
|
|
81
|
-
|
|
37
|
+
ignoredCssFromModules: [
|
|
38
|
+
'Vendor_ModuleNameA',
|
|
39
|
+
'Vendor_ModuleNameNoConfig'
|
|
40
|
+
],
|
|
82
41
|
exposeNpmPackages: []
|
|
83
42
|
}
|
|
84
43
|
},
|
|
85
44
|
{
|
|
86
45
|
code: 'Vendor/theme-d',
|
|
87
46
|
expected: {
|
|
88
|
-
tailwind: {
|
|
89
|
-
theme: { extend: {} },
|
|
90
|
-
plugins: [],
|
|
91
|
-
content: []
|
|
92
|
-
},
|
|
93
|
-
includeTailwindConfigFromParentThemes: true,
|
|
94
47
|
includeCssSourceFromParentThemes: false,
|
|
95
48
|
ignoredCssFromModules: [],
|
|
96
|
-
ignoredTailwindConfigFromModules: [
|
|
97
|
-
'Vendor_ModuleNameC'
|
|
98
|
-
],
|
|
99
49
|
exposeNpmPackages: []
|
|
100
50
|
}
|
|
101
51
|
}
|
|
@@ -107,7 +57,6 @@ for (const scenario of scenarios) {
|
|
|
107
57
|
for (const themeKey in scenario.themes) {
|
|
108
58
|
let theme = scenario.themes[themeKey];
|
|
109
59
|
const expected = theme.expected;
|
|
110
|
-
expected.tailwind.content = expected.tailwind.content?.map((content) => path.resolve(__dirname, '..', content));
|
|
111
60
|
}
|
|
112
61
|
}
|
|
113
62
|
|
|
@@ -145,24 +94,4 @@ describe('getThemeConfig', () => {
|
|
|
145
94
|
});
|
|
146
95
|
});
|
|
147
96
|
|
|
148
|
-
describe('getTailwindThemeConfig', () => {
|
|
149
|
-
let themeResolver;
|
|
150
|
-
|
|
151
|
-
beforeEach(() => {
|
|
152
|
-
jest.resetModules();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test.each(scenarios.flatMap(({ scenario, themes }) =>
|
|
156
|
-
themes.map(({ code, expected }) => ([ scenario, code, expected ]))
|
|
157
|
-
))('Scenario "%s", Theme "%s"', async ( scenario, code, expected ) => {
|
|
158
|
-
themeResolver = await setupThemeResolver(scenario);
|
|
159
|
-
|
|
160
|
-
const result = await themeResolver.getTailwindThemeConfig(code);
|
|
161
|
-
|
|
162
|
-
expect(result).toMatchObject(
|
|
163
|
-
expected.tailwind,
|
|
164
|
-
`Expected:\n${JSON.stringify(expected.tailwind, null, 2)}\nReceived:\n${JSON.stringify(result, null, 2)}\nScenario: "${scenario}", Theme: "${code}"`
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
97
|
|
package/src/config/default.js
CHANGED
|
@@ -22,5 +22,6 @@ export const OUTPUT_THEME_DIR = 'web/generated';
|
|
|
22
22
|
export const OUTPUT_JS_DIR = 'js/';
|
|
23
23
|
export const OUTPUT_CSS_DIR = 'css/';
|
|
24
24
|
export const PRECOMPILED_FOLDER = '.precompiled';
|
|
25
|
+
export const INTERCEPTORS_FOLDER = 'interceptors';
|
|
25
26
|
|
|
26
27
|
export const ALL_JS_VUE_FILES_WITH_INHERITANCE_FILE_NAME = 'allJsVueFilesWithInheritance.json';
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { pathToFileURL, fileURLToPath } from "url";
|
|
3
|
+
import themeResolver from './themeResolverSync.js';
|
|
4
|
+
import moduleResolver from './moduleResolver.js';
|
|
5
|
+
import pluginManager from './pluginManager.js';
|
|
6
|
+
|
|
7
|
+
const interceptorsRegisteredByTheme = new Map();
|
|
8
|
+
const generatedInterceptorsCache = new Map();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the absolute path of a file using the cached file map
|
|
12
|
+
* @param {string} identifier
|
|
13
|
+
* @param {Object} fileMap
|
|
14
|
+
* @returns {string|null}
|
|
15
|
+
*/
|
|
16
|
+
function resolvePathFromMap(identifier, fileMap) {
|
|
17
|
+
const [moduleName, relativePath] = identifier.split('::');
|
|
18
|
+
if (!moduleName || !relativePath) return null;
|
|
19
|
+
|
|
20
|
+
const parsed = path.parse(relativePath);
|
|
21
|
+
const keyPath = path.join(parsed.dir, parsed.name);
|
|
22
|
+
const key = `${moduleName}/${keyPath}`;
|
|
23
|
+
|
|
24
|
+
return fileMap[key] || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function registerInterceptors(themeName) {
|
|
28
|
+
if (interceptorsRegisteredByTheme.has(themeName)) {
|
|
29
|
+
return interceptorsRegisteredByTheme.get(themeName);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const themeConfig = await themeResolver.getThemeConfig(themeName);
|
|
33
|
+
const modulesConfig = await moduleResolver.getModuleConfigByThemeConfig(themeName, themeConfig);
|
|
34
|
+
|
|
35
|
+
// Map<Target, Map<PluginName, PluginConfig>>
|
|
36
|
+
const interceptorsMap = new Map();
|
|
37
|
+
|
|
38
|
+
for (const [moduleName, moduleConfig] of Object.entries(modulesConfig)) {
|
|
39
|
+
if (!moduleConfig.interceptors || !Array.isArray(moduleConfig.interceptors)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for (const c of moduleConfig.interceptors) {
|
|
43
|
+
const { name, target } = c;
|
|
44
|
+
if (!name || !target) continue;
|
|
45
|
+
|
|
46
|
+
if (!interceptorsMap.has(target)) {
|
|
47
|
+
interceptorsMap.set(target, new Map());
|
|
48
|
+
}
|
|
49
|
+
|
|
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
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Convert Map to structured object and sort plugins
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const [target, pluginsMap] of interceptorsMap) {
|
|
65
|
+
const plugins = Array.from(pluginsMap.values())
|
|
66
|
+
.filter(p => p.active !== false) // Filter out inactive plugins
|
|
67
|
+
.sort((a, b) => (a.sortOrder || 10) - (b.sortOrder || 10));
|
|
68
|
+
|
|
69
|
+
if (plugins.length > 0) {
|
|
70
|
+
result[target] = plugins;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interceptorsRegisteredByTheme.set(themeName, result);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function generateInterceptors(themeName) {
|
|
79
|
+
if (generatedInterceptorsCache.has(themeName)) {
|
|
80
|
+
return generatedInterceptorsCache.get(themeName);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const interceptorsConfig = await registerInterceptors(themeName);
|
|
84
|
+
const modulesConfig = await moduleResolver.getModuleConfigByThemeConfig(themeName, await themeResolver.getThemeConfig(themeName));
|
|
85
|
+
|
|
86
|
+
// Get all files map from cache
|
|
87
|
+
let allFilesMap = {};
|
|
88
|
+
try {
|
|
89
|
+
allFilesMap = moduleResolver.getAllJsVueFilesWithInheritanceCached(themeName);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
console.warn(`Cache not found for theme ${themeName}, falling back to async generation.`);
|
|
92
|
+
allFilesMap = await moduleResolver.getAllJsVueFilesWithInheritance(themeName);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const interceptors = {};
|
|
96
|
+
|
|
97
|
+
for (const [targetIdentifier, plugins] of Object.entries(interceptorsConfig)) {
|
|
98
|
+
// 1. Resolve Target Path
|
|
99
|
+
const targetPath = resolvePathFromMap(targetIdentifier, allFilesMap);
|
|
100
|
+
if (!targetPath) {
|
|
101
|
+
console.warn(`Target module not found for identifier: ${targetIdentifier}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Load Target Module to inspect exports
|
|
106
|
+
let targetModule;
|
|
107
|
+
try {
|
|
108
|
+
targetModule = await import(pathToFileURL(targetPath).href);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error(`Failed to import target module ${targetIdentifier}:`, e.message);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const targetExports = Object.keys(targetModule);
|
|
115
|
+
const methodsToIntercept = new Set();
|
|
116
|
+
|
|
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}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let pluginModule;
|
|
127
|
+
try {
|
|
128
|
+
pluginModule = await import(pathToFileURL(pluginPath).href);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error(`Failed to import plugin module ${pluginConfig.plugin}:`, e.message);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const pluginMethods = [];
|
|
135
|
+
for (const pluginExport of Object.keys(pluginModule)) {
|
|
136
|
+
let type, targetMethod;
|
|
137
|
+
|
|
138
|
+
if (pluginExport.startsWith('before')) {
|
|
139
|
+
type = 'before';
|
|
140
|
+
targetMethod = pluginExport.substring(6);
|
|
141
|
+
} else if (pluginExport.startsWith('around')) {
|
|
142
|
+
type = 'around';
|
|
143
|
+
targetMethod = pluginExport.substring(6);
|
|
144
|
+
} else if (pluginExport.startsWith('after')) {
|
|
145
|
+
type = 'after';
|
|
146
|
+
targetMethod = pluginExport.substring(5);
|
|
147
|
+
} else {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check if target method exists in target module
|
|
152
|
+
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?
|
|
158
|
+
// 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
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Register the plugin with PluginManager (Runtime)
|
|
164
|
+
const methodKey = `${targetIdentifier}::${targetMethod}`;
|
|
165
|
+
pluginManager.addPlugin(
|
|
166
|
+
methodKey,
|
|
167
|
+
pluginConfig.name,
|
|
168
|
+
type,
|
|
169
|
+
pluginModule[pluginExport],
|
|
170
|
+
pluginConfig.sortOrder
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
methodsToIntercept.add(targetMethod);
|
|
174
|
+
pluginMethods.push({
|
|
175
|
+
exportName: pluginExport,
|
|
176
|
+
type,
|
|
177
|
+
targetMethod,
|
|
178
|
+
sortOrder: pluginConfig.sortOrder
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (pluginMethods.length > 0) {
|
|
183
|
+
validPlugins.push({
|
|
184
|
+
...pluginConfig,
|
|
185
|
+
path: pluginPath,
|
|
186
|
+
methods: pluginMethods
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 4. Create Interceptor Proxy & Source Code
|
|
192
|
+
if (methodsToIntercept.size > 0) {
|
|
193
|
+
const wrapper = { ...targetModule };
|
|
194
|
+
const proxy = pluginManager.intercept(wrapper, targetIdentifier, true);
|
|
195
|
+
|
|
196
|
+
const source = generateInterceptorCode(targetIdentifier, targetPath, validPlugins, targetExports);
|
|
197
|
+
|
|
198
|
+
interceptors[targetIdentifier] = {
|
|
199
|
+
proxy,
|
|
200
|
+
targetPath,
|
|
201
|
+
targetModule,
|
|
202
|
+
plugins: validPlugins,
|
|
203
|
+
source
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
generatedInterceptorsCache.set(themeName, interceptors);
|
|
209
|
+
return interceptors;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function generateInterceptorCode(targetIdentifier, targetPath, plugins, targetExports) {
|
|
213
|
+
const imports = [];
|
|
214
|
+
const registrations = [];
|
|
215
|
+
|
|
216
|
+
// Import PluginManager (Assuming it's available via alias or relative path in the build environment)
|
|
217
|
+
// For Vite, we might need to adjust this path or use a virtual module ID.
|
|
218
|
+
// 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
|
+
// 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}';`);
|
|
223
|
+
|
|
224
|
+
// 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});`);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Create Interceptor
|
|
239
|
+
const interceptorCode = `
|
|
240
|
+
const targetWrapper = { ...originalModule };
|
|
241
|
+
const proxy = pluginManager.intercept(targetWrapper, '${targetIdentifier}', true);
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
// Exports
|
|
245
|
+
const exportsCode = targetExports.map(exp => {
|
|
246
|
+
if (exp === 'default') {
|
|
247
|
+
return `export default proxy.default;`;
|
|
248
|
+
}
|
|
249
|
+
return `export const ${exp} = proxy.${exp};`;
|
|
250
|
+
}).join('\n');
|
|
251
|
+
|
|
252
|
+
return `
|
|
253
|
+
${imports.join('\n')}
|
|
254
|
+
|
|
255
|
+
${registrations.join('\n')}
|
|
256
|
+
|
|
257
|
+
${interceptorCode}
|
|
258
|
+
|
|
259
|
+
${exportsCode}
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export default {
|
|
264
|
+
registerInterceptors,
|
|
265
|
+
generateInterceptors
|
|
266
|
+
};
|
|
@@ -110,22 +110,22 @@ function getModuleConfigPath(moduleSrc) {
|
|
|
110
110
|
return path.join(moduleSrc, 'view/frontend/web/', MODULE_CONFIG_FILE);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
function resolveFileByTheme(themeName, moduleName, filePath
|
|
113
|
+
function resolveFileByTheme(themeName, moduleName, filePath) {
|
|
114
114
|
const theme = configResolver.getMagentoConfig().themes[themeName];
|
|
115
115
|
const fullFilePath = path.join(theme.src, moduleName, 'web', filePath);
|
|
116
116
|
if (fs.existsSync(fullFilePath)) {
|
|
117
117
|
return fullFilePath;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
if (
|
|
121
|
-
return resolveFileByTheme(theme.parent, moduleName, filePath
|
|
120
|
+
if (theme.parent) {
|
|
121
|
+
return resolveFileByTheme(theme.parent, moduleName, filePath);
|
|
122
122
|
}
|
|
123
123
|
return null;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
async function resolveModuleConfig(moduleName, themeName
|
|
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_FIL);
|
|
129
129
|
|
|
130
130
|
if (!moduleConfigSourcePath && !module) {
|
|
131
131
|
return null;
|
|
@@ -158,20 +158,8 @@ async function getModuleConfigByThemeConfig(themeName, themeConfig) {
|
|
|
158
158
|
let modulesConfig = {};
|
|
159
159
|
|
|
160
160
|
for (const moduleName of modules) {
|
|
161
|
-
let moduleConfig = await resolveModuleConfig(moduleName, themeName
|
|
161
|
+
let moduleConfig = await resolveModuleConfig(moduleName, themeName);
|
|
162
162
|
if (!moduleConfig) continue;
|
|
163
|
-
if (
|
|
164
|
-
themeConfig.ignoredTailwindConfigFromModules === 'all' ||
|
|
165
|
-
themeConfig.ignoredTailwindConfigFromModules.includes(moduleName)
|
|
166
|
-
) {
|
|
167
|
-
moduleConfig.tailwind = {};
|
|
168
|
-
} else if (moduleConfig.tailwind?.content) {
|
|
169
|
-
const moduleSrc = configResolver.getMagentoConfig().modules[moduleName].src;
|
|
170
|
-
moduleConfig.tailwind.content = moduleConfig.tailwind.content.map((content) =>
|
|
171
|
-
path.join(moduleSrc, 'view/frontend/web', content)
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
163
|
modulesConfig = deepmerge(modulesConfig, moduleConfig);
|
|
176
164
|
}
|
|
177
165
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
class PluginManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.plugins = {};
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register a plugin
|
|
8
|
+
* @param {string} target - The name of the target function/method to intercept
|
|
9
|
+
* @param {string} name - Unique name for the plugin
|
|
10
|
+
* @param {string} type - 'before', 'around', 'after'
|
|
11
|
+
* @param {Function} handler - The function to execute
|
|
12
|
+
* @param {number} sortOrder - Order of execution
|
|
13
|
+
*/
|
|
14
|
+
addPlugin(target, name, type, handler, sortOrder = 10) {
|
|
15
|
+
if (!this.plugins[target]) {
|
|
16
|
+
this.plugins[target] = { before: [], around: [], after: [] };
|
|
17
|
+
}
|
|
18
|
+
if (!['before', 'around', 'after'].includes(type)) {
|
|
19
|
+
throw new Error(`Invalid plugin type: ${type}`);
|
|
20
|
+
}
|
|
21
|
+
this.plugins[target][type].push({ name, handler, sortOrder });
|
|
22
|
+
this.plugins[target][type].sort((a, b) => a.sortOrder - b.sortOrder);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute the intercepted method chain synchronously
|
|
27
|
+
* @param {string} target - The target method name
|
|
28
|
+
* @param {Function} originalMethod - The original function
|
|
29
|
+
* @param {Object} context - The 'this' context
|
|
30
|
+
* @param {Array} args - Arguments passed to the function
|
|
31
|
+
*/
|
|
32
|
+
executeSync(target, originalMethod, context, ...args) {
|
|
33
|
+
const plugins = this.plugins[target] || { before: [], around: [], after: [] };
|
|
34
|
+
|
|
35
|
+
// Execute 'before' plugins
|
|
36
|
+
for (const plugin of plugins.before) {
|
|
37
|
+
const result = plugin.handler.apply(context, args);
|
|
38
|
+
if (Array.isArray(result)) {
|
|
39
|
+
args = result;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Execute 'around' plugins
|
|
44
|
+
let methodToExecute = (...currentArgs) => {
|
|
45
|
+
return originalMethod.apply(context, currentArgs);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (plugins.around.length > 0) {
|
|
49
|
+
const aroundPlugins = [...plugins.around].reverse();
|
|
50
|
+
for (const plugin of aroundPlugins) {
|
|
51
|
+
const next = methodToExecute;
|
|
52
|
+
methodToExecute = (...currentArgs) => {
|
|
53
|
+
return plugin.handler.apply(context, [next, ...currentArgs]);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let result = methodToExecute(...args);
|
|
59
|
+
|
|
60
|
+
// Execute 'after' plugins
|
|
61
|
+
for (const plugin of plugins.after) {
|
|
62
|
+
result = plugin.handler.apply(context, [result, ...args]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Execute the intercepted method chain
|
|
70
|
+
* @param {string} target - The target method name
|
|
71
|
+
* @param {Function} originalMethod - The original function
|
|
72
|
+
* @param {Object} context - The 'this' context
|
|
73
|
+
* @param {Array} args - Arguments passed to the function
|
|
74
|
+
*/
|
|
75
|
+
async execute(target, originalMethod, context, ...args) {
|
|
76
|
+
const plugins = this.plugins[target] || { before: [], around: [], after: [] };
|
|
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);
|
|
82
|
+
if (Array.isArray(result)) {
|
|
83
|
+
args = result;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Execute 'around' plugins
|
|
88
|
+
// Around plugins receive (proceed, ...args)
|
|
89
|
+
let methodToExecute = async (...currentArgs) => {
|
|
90
|
+
return await originalMethod.apply(context, currentArgs);
|
|
91
|
+
};
|
|
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) {
|
|
97
|
+
const next = methodToExecute;
|
|
98
|
+
methodToExecute = async (...currentArgs) => {
|
|
99
|
+
return await plugin.handler.apply(context, [next, ...currentArgs]);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let result = await methodToExecute(...args);
|
|
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]);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a proxy to intercept method calls on an object
|
|
117
|
+
* @param {Object} target - The target object (e.g. module exports)
|
|
118
|
+
* @param {string} namespace - Namespace for plugins
|
|
119
|
+
* @param {boolean} useAsync - Whether to use async execution
|
|
120
|
+
*/
|
|
121
|
+
intercept(target, namespace, useAsync = true) {
|
|
122
|
+
return new Proxy(target, {
|
|
123
|
+
get: (obj, prop) => {
|
|
124
|
+
const value = obj[prop];
|
|
125
|
+
if (typeof value === 'function') {
|
|
126
|
+
return (...args) => {
|
|
127
|
+
const method = useAsync ? this.execute : this.executeSync;
|
|
128
|
+
return method.call(this, `${namespace}::${String(prop)}`, value, obj, ...args);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default new PluginManager();
|
|
@@ -50,36 +50,18 @@ export async function getThemeConfig(themeName) {
|
|
|
50
50
|
let themeConfig = await loadThemeConfig(themeDefinition, themeName);
|
|
51
51
|
if (!themeConfig) return null;
|
|
52
52
|
|
|
53
|
-
themeConfig.includeTailwindConfigFromParentThemes ??= true;
|
|
54
53
|
themeConfig.includeCssSourceFromParentThemes ??= true;
|
|
55
54
|
themeConfig.ignoredCssFromModules ??= [];
|
|
56
|
-
themeConfig.ignoredTailwindConfigFromModules ??= [];
|
|
57
55
|
themeConfig.exposeNpmPackages ??= [];
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (themeConfig.tailwind?.content) {
|
|
61
|
-
themeConfig.tailwind.content = themeConfig.tailwind.content.map((content) =>
|
|
62
|
-
path.join(themeDefinition.src, "web", content)
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const includeParent = themeConfig.includeTailwindConfigFromParentThemes;
|
|
67
|
-
if (includeParent && themeDefinition.parent) {
|
|
56
|
+
if (themeDefinition.parent) {
|
|
68
57
|
const parentConfig = await getThemeConfig(themeDefinition.parent);
|
|
69
58
|
themeConfig = deepmerge(parentConfig || {}, themeConfig);
|
|
70
59
|
}
|
|
71
|
-
|
|
72
60
|
themeConfigCache.set(themeName, themeConfig);
|
|
73
61
|
return themeConfig;
|
|
74
62
|
}
|
|
75
63
|
|
|
76
|
-
export async function getTailwindThemeConfig(themeName) {
|
|
77
|
-
const themeConfig = await getThemeConfig(themeName);
|
|
78
|
-
return themeConfig?.tailwind ?? {};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
64
|
// Export default con todo
|
|
82
65
|
export default {
|
|
83
|
-
getThemeConfig
|
|
84
|
-
getTailwindThemeConfig
|
|
66
|
+
getThemeConfig
|
|
85
67
|
};
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { jest } from '@jest/globals';
|
|
2
|
-
import createMockConfigResolver from '../__mocks__/configResolver.js';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import {fileURLToPath} from 'url';
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = path.dirname(__filename);
|
|
7
|
-
|
|
8
|
-
let scenarios = [
|
|
9
|
-
{
|
|
10
|
-
scenario: 'a',
|
|
11
|
-
themes: [
|
|
12
|
-
{
|
|
13
|
-
code: 'Vendor/theme-a',
|
|
14
|
-
expected: {
|
|
15
|
-
tailwind: {
|
|
16
|
-
content: [
|
|
17
|
-
'magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/templates/**/*.phtml',
|
|
18
|
-
'magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/from_theme',
|
|
19
|
-
'magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/templates/**/*.phtml',
|
|
20
|
-
'magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/templates/**/*.phtml'
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
code: 'Vendor/theme-b',
|
|
27
|
-
expected: {
|
|
28
|
-
tailwind: {
|
|
29
|
-
content: [
|
|
30
|
-
'magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/templates/**/*.phtml',
|
|
31
|
-
'magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/web/from_theme',
|
|
32
|
-
'magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/templates/**/*.phtml',
|
|
33
|
-
'magento_scenarios/app/code/Vendor/ModuleNameC/view/frontend/templates/**/*.phtml'
|
|
34
|
-
]
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
code: 'Vendor/theme-c',
|
|
40
|
-
expected: {
|
|
41
|
-
tailwind: {}
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
code: 'Vendor/theme-d',
|
|
46
|
-
expected: {
|
|
47
|
-
tailwind: {
|
|
48
|
-
theme: { extend: {} },
|
|
49
|
-
plugins: [],
|
|
50
|
-
content: [
|
|
51
|
-
'magento_scenarios/app/code/Vendor/ModuleNameA/view/frontend/templates/**/*.phtml',
|
|
52
|
-
'magento_scenarios/app/code/Vendor/ModuleNameB/view/frontend/templates/**/*.phtml'
|
|
53
|
-
]
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
for (const scenario of scenarios) {
|
|
62
|
-
for (const themeKey in scenario.themes) {
|
|
63
|
-
let theme = scenario.themes[themeKey];
|
|
64
|
-
const expected = theme.expected;
|
|
65
|
-
expected.tailwind.content = expected.tailwind.content?.map((content) => path.resolve(__dirname, '..', content));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
let scenariosForResolveFileByTheme = [
|
|
71
|
-
{
|
|
72
|
-
scenario: 'a',
|
|
73
|
-
themes: [
|
|
74
|
-
{
|
|
75
|
-
code: 'Vendor/theme-a',
|
|
76
|
-
moduleName: 'Vendor_ModuleNameA',
|
|
77
|
-
filePath: 'module.config.js',
|
|
78
|
-
includeTailwindConfigFromParentThemes: true,
|
|
79
|
-
expected: 'magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/module.config.js'
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
code: 'Vendor/theme-a',
|
|
83
|
-
moduleName: 'Vendor_ModuleNameA',
|
|
84
|
-
filePath: 'no_exist_file.js',
|
|
85
|
-
includeTailwindConfigFromParentThemes: true,
|
|
86
|
-
expected: null
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
code: 'Vendor/theme-b',
|
|
90
|
-
moduleName: 'Vendor_ModuleNameA',
|
|
91
|
-
filePath: 'module.config.js',
|
|
92
|
-
includeTailwindConfigFromParentThemes: true,
|
|
93
|
-
expected: 'magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/module.config.js'
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
code: 'Vendor/theme-b',
|
|
97
|
-
moduleName: 'Vendor_ModuleNameA',
|
|
98
|
-
filePath: 'module.config.js',
|
|
99
|
-
includeTailwindConfigFromParentThemes: false,
|
|
100
|
-
expected: null
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
code: 'Vendor/theme-c',
|
|
104
|
-
moduleName: 'Vendor_ModuleNameA',
|
|
105
|
-
filePath: 'module.config.js',
|
|
106
|
-
includeTailwindConfigFromParentThemes: true,
|
|
107
|
-
expected: 'magento_scenarios/app/design/frontend/Vendor/theme-a/Vendor_ModuleNameA/web/module.config.js'
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
code: 'Vendor/theme-c',
|
|
111
|
-
moduleName: 'Vendor_ModuleNameB',
|
|
112
|
-
filePath: 'module.config.js',
|
|
113
|
-
includeTailwindConfigFromParentThemes: true,
|
|
114
|
-
expected: 'magento_scenarios/app/design/frontend/Vendor/theme-c/Vendor_ModuleNameB/web/module.config.js'
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
code: 'Vendor/theme-d',
|
|
118
|
-
moduleName: 'Vendor_ModuleNameA',
|
|
119
|
-
filePath: 'module.config.js',
|
|
120
|
-
includeTailwindConfigFromParentThemes: false,
|
|
121
|
-
expected: null
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
code: 'Vendor/theme-d',
|
|
125
|
-
moduleName: 'Vendor_ModuleNameA',
|
|
126
|
-
filePath: 'module.config.js',
|
|
127
|
-
includeTailwindConfigFromParentThemes: true,
|
|
128
|
-
expected: null
|
|
129
|
-
}
|
|
130
|
-
]
|
|
131
|
-
}
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
for (const scenario of scenariosForResolveFileByTheme) {
|
|
135
|
-
for (const themeKey in scenario.themes) {
|
|
136
|
-
let theme = scenario.themes[themeKey];
|
|
137
|
-
if (!theme.expected) continue;
|
|
138
|
-
theme.expected = path.resolve(__dirname, '..', theme.expected)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function setupResolvers(scenario) {
|
|
143
|
-
jest.unstable_mockModule('#service/configResolver.js', () => ({
|
|
144
|
-
__esModule: true,
|
|
145
|
-
default: createMockConfigResolver(scenario).default,
|
|
146
|
-
}));
|
|
147
|
-
const moduleResolver = await import('#service/moduleResolver.js');
|
|
148
|
-
const themeResolver = await import('#service/themeResolverSync.js');
|
|
149
|
-
|
|
150
|
-
return { moduleResolver, themeResolver };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
describe('getModuleConfigByThemeConfig', () => {
|
|
155
|
-
beforeEach(() => {
|
|
156
|
-
jest.resetModules();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test.each(scenarios.flatMap(({ scenario, themes }) =>
|
|
160
|
-
themes.map(({ code, expected }) => ([ scenario, code, expected ]))
|
|
161
|
-
))('Scenario "%s", Theme "%s"', async ( scenario, code, expected ) => {
|
|
162
|
-
const { themeResolver, moduleResolver } = await setupResolvers(scenario);
|
|
163
|
-
const themeConfig = await themeResolver.getThemeConfig(code);
|
|
164
|
-
const result = await moduleResolver.getModuleConfigByThemeConfig(code, themeConfig);
|
|
165
|
-
expect(result).toEqual(
|
|
166
|
-
expected,
|
|
167
|
-
`Expected:\n${JSON.stringify(expected, null, 2)}\nReceived:\n${JSON.stringify(result, null, 2)}\nScenario: "${scenario}", Theme: "${code}"`
|
|
168
|
-
);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe('resolveFileByTheme', () => {
|
|
173
|
-
beforeEach(() => {
|
|
174
|
-
jest.resetModules();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test.each(scenariosForResolveFileByTheme.flatMap(({ scenario, themes }) =>
|
|
178
|
-
themes.map(({ code, expected, moduleName, filePath, includeTailwindConfigFromParentThemes }) => ([scenario, code, expected, moduleName, filePath, includeTailwindConfigFromParentThemes]))
|
|
179
|
-
))('Scenario "%s", Theme "%s"', async ( scenario, code, expected, moduleName, filePath, includeTailwindConfigFromParentThemes ) => {
|
|
180
|
-
const { moduleResolver } = await setupResolvers(scenario);
|
|
181
|
-
const result = await moduleResolver.resolveFileByTheme(code, moduleName, filePath, includeTailwindConfigFromParentThemes);
|
|
182
|
-
expect(result).toEqual(
|
|
183
|
-
expected,
|
|
184
|
-
`Expected:\n${JSON.stringify(expected, null, 2)}\nReceived:\n${JSON.stringify(result, null, 2)}\nScenario: "${scenario}", Theme: "${code}"`
|
|
185
|
-
);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import themeResolver from './themeResolverSync.js';
|
|
2
|
-
import moduleResolver from './moduleResolver.js';
|
|
3
|
-
import deepmerge from 'deepmerge';
|
|
4
|
-
|
|
5
|
-
export function getTailwindConfigByTheme(themeName) {
|
|
6
|
-
const themeConfig = themeResolver.getThemeConfig(themeName);
|
|
7
|
-
return deepmerge(
|
|
8
|
-
moduleResolver.getModuleConfigByThemeConfig(themeName, themeConfig)?.tailwind ?? {},
|
|
9
|
-
themeConfig?.tailwind ?? {}
|
|
10
|
-
);
|
|
11
|
-
}
|