@shopgate/webpack 7.32.0-beta.2 → 7.32.0-beta.20
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/lib/__fixtures__/app/extensions/ext-broken/frontend/widgets/EmptyWidget/config.json +1 -0
- package/lib/__fixtures__/app/extensions/ext-broken/frontend/widgets/InvalidWidget/config.json +5 -0
- package/lib/__fixtures__/app/extensions/ext-broken/frontend/widgets/UnparsableWidget/config.json +1 -0
- package/lib/__fixtures__/app/extensions/ext-fixture/frontend/src/widgets/DistWidget/config.json +5 -0
- package/lib/__fixtures__/app/extensions/ext-fixture/frontend/widgets/AlphaWidget/config.json +6 -0
- package/lib/__fixtures__/app/extensions/ext-fixture/frontend/widgets/LegacyWidget/config.json +5 -0
- package/lib/__fixtures__/app/themes/theme-empty/config/components.json +7 -0
- package/lib/__fixtures__/app/themes/theme-invalid/config/components.json +7 -0
- package/lib/__fixtures__/app/themes/theme-test/config/components.json +29 -0
- package/lib/__fixtures__/app/themes/theme-test/widgets/widgets.json +3 -0
- package/lib/__fixtures__/app/themes/theme-unparsable/config/components.json +7 -0
- package/lib/getComponentsSettings.js +188 -31
- package/lib/getComponentsSettings.spec.js +204 -0
- package/package.json +1 -1
- package/templates/default.ejs +15 -0
- package/webpack.config.js +7 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
null
|
package/lib/__fixtures__/app/extensions/ext-broken/frontend/widgets/UnparsableWidget/config.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "layout": { "applyMediaMargins": true
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"widgets": {
|
|
3
|
+
"@fixture/ext/LegacyWidget": {
|
|
4
|
+
"path": "ext-fixture/frontend/widgets/LegacyWidget"
|
|
5
|
+
}
|
|
6
|
+
},
|
|
7
|
+
"widgetsV2": {
|
|
8
|
+
"@fixture/ext/AlphaWidget": {
|
|
9
|
+
"path": "ext-fixture/frontend/widgets/AlphaWidget"
|
|
10
|
+
},
|
|
11
|
+
"@fixture/ext/BetaWidget": {
|
|
12
|
+
"path": "ext-fixture/frontend/widgets/BetaWidget"
|
|
13
|
+
},
|
|
14
|
+
"@fixture/ext/DistWidget": {
|
|
15
|
+
"path": "ext-fixture/frontend/dist/widgets/DistWidget"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"portals": {
|
|
19
|
+
"@fixture/ext/SomePortal": {
|
|
20
|
+
"path": "ext-fixture/frontend/portals/Some",
|
|
21
|
+
"target": "app.routes"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"reducers": {
|
|
25
|
+
"@shopgate/product-reviews/reducer": {
|
|
26
|
+
"path": "ext-product-reviews/frontend/reducer"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const Ajv = require('ajv');
|
|
2
4
|
const importFresh = require('import-fresh');
|
|
5
|
+
const { isDev } = require('./variables');
|
|
6
|
+
|
|
7
|
+
const ENGAGE_PACKAGE = '@shopgate/engage';
|
|
8
|
+
const WIDGET_CONFIG_FILE = 'config.json';
|
|
9
|
+
const WIDGET_CONFIG_SCHEMA = '@shopgate/engage/page/widget.config.schema.json';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Raised when a widget ships a config file that cannot be used. It travels through the catch-all
|
|
13
|
+
* of the exported function, so that a broken config fails the build instead of silently removing
|
|
14
|
+
* every component mapping of the app.
|
|
15
|
+
*/
|
|
16
|
+
class WidgetConfigError extends Error {}
|
|
3
17
|
|
|
4
18
|
/**
|
|
5
19
|
* @typedef {Object} ComponentSettingsEntry
|
|
@@ -54,6 +68,136 @@ const filterIgnoredExtensions = (config = {}) => Object.keys(config).reduce((acc
|
|
|
54
68
|
[sectionName]: filterIgnoredEntries(config[sectionName]),
|
|
55
69
|
}), {});
|
|
56
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Loads a JSON config file using `import-fresh`, if it exists.
|
|
73
|
+
* @param {string} configPath - The absolute path to the config file.
|
|
74
|
+
* @returns {Object} The imported configuration object, or an empty object if the file doesn't
|
|
75
|
+
* exist.
|
|
76
|
+
*/
|
|
77
|
+
const loadConfig = configPath => (
|
|
78
|
+
fs.existsSync(configPath) || configPath.startsWith(ENGAGE_PACKAGE) ?
|
|
79
|
+
importFresh(configPath) :
|
|
80
|
+
{}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Reads the component mappings of the theme, without the config files that widgets provide.
|
|
85
|
+
* @param {string} themePath The path of the theme.
|
|
86
|
+
* @returns {ComponentSettings}
|
|
87
|
+
*/
|
|
88
|
+
const readComponentsConfig = (themePath) => {
|
|
89
|
+
/** @type {ComponentSettings} */
|
|
90
|
+
const defaultConfig = filterIgnoredExtensions(
|
|
91
|
+
importFresh(path.join(themePath, 'config', 'components.json'))
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...defaultConfig,
|
|
96
|
+
widgets: {
|
|
97
|
+
...defaultConfig.widgets,
|
|
98
|
+
...loadConfig(path.join(themePath, 'widgets', 'widgets.json')),
|
|
99
|
+
},
|
|
100
|
+
widgetsV2: {
|
|
101
|
+
// To enable backwards compatibility for custom legacy widgets that are provided by
|
|
102
|
+
// extensions we include them in the list of V2 widgets.
|
|
103
|
+
...defaultConfig.widgets,
|
|
104
|
+
...defaultConfig.widgetsV2,
|
|
105
|
+
...loadConfig(`${ENGAGE_PACKAGE}/page/widgets/widgets.json`),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const validateWidgetConfig = new Ajv({ allErrors: true })
|
|
111
|
+
.compile(importFresh(WIDGET_CONFIG_SCHEMA));
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Builds the list of locations the config file of a widget can live at. Widgets provided by the
|
|
115
|
+
* PWA are referenced by their package path, widgets provided by extensions relative to the
|
|
116
|
+
* extensions folder of the app.
|
|
117
|
+
* @param {string} componentPath Path of the widget from the components config.
|
|
118
|
+
* @param {string} themePath The path of the theme.
|
|
119
|
+
* @returns {string[]}
|
|
120
|
+
*/
|
|
121
|
+
const getWidgetConfigPaths = (componentPath, themePath) => {
|
|
122
|
+
if (componentPath.startsWith(ENGAGE_PACKAGE)) {
|
|
123
|
+
const packageRoot = path.dirname(require.resolve(`${ENGAGE_PACKAGE}/package.json`));
|
|
124
|
+
const subPath = componentPath.slice(ENGAGE_PACKAGE.length + 1);
|
|
125
|
+
|
|
126
|
+
return [path.join(packageRoot, subPath, WIDGET_CONFIG_FILE)];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const relativePaths = [componentPath];
|
|
130
|
+
|
|
131
|
+
if (isDev) {
|
|
132
|
+
// Extensions that are attached as a build are read from their sources while developing. The
|
|
133
|
+
// Set keeps a single entry for the extensions that are attached as sources anyway.
|
|
134
|
+
relativePaths.unshift(componentPath.replace('/dist/', '/src/'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return Array.from(new Set(relativePaths)).map(relativePath => path.resolve(
|
|
138
|
+
themePath,
|
|
139
|
+
'..',
|
|
140
|
+
'..',
|
|
141
|
+
'extensions',
|
|
142
|
+
relativePath,
|
|
143
|
+
WIDGET_CONFIG_FILE
|
|
144
|
+
));
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Loads the config file of a single widget. Widgets are not required to provide one.
|
|
149
|
+
* @param {string} componentPath Path of the widget from the components config.
|
|
150
|
+
* @param {string} themePath The path of the theme.
|
|
151
|
+
* @returns {Object} Config of the widget, or an empty object.
|
|
152
|
+
*/
|
|
153
|
+
const loadWidgetConfig = (componentPath, themePath) => {
|
|
154
|
+
if (!componentPath) {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Whether a widget brings a config is decided by the file system. Every file that is there has
|
|
159
|
+
// to be readable and valid, no matter what it contains.
|
|
160
|
+
const configPath = getWidgetConfigPaths(componentPath, themePath)
|
|
161
|
+
.find(candidate => fs.existsSync(candidate));
|
|
162
|
+
|
|
163
|
+
if (!configPath) {
|
|
164
|
+
return {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let config;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
config = importFresh(configPath);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
throw new WidgetConfigError(`Widget config "${configPath}" could not be read: ${e.message}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!validateWidgetConfig(config)) {
|
|
176
|
+
throw new WidgetConfigError([
|
|
177
|
+
`Widget config of "${componentPath}" is invalid:`,
|
|
178
|
+
...validateWidgetConfig.errors.map(error => ` ${error.instancePath || '/'} ${error.message}`),
|
|
179
|
+
].join('\n'));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Only known sections are taken over, so that editor helpers like "$schema" don't end up in the
|
|
183
|
+
// config that is injected into the app.
|
|
184
|
+
return config.layout ? { config: { layout: config.layout } } : {};
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Adds the config every widget provides via its own config file to the widget entries.
|
|
189
|
+
* @param {ComponentSettingsMap} widgets The widget entries from the components config.
|
|
190
|
+
* @param {string} themePath The path of the theme.
|
|
191
|
+
* @returns {ComponentSettingsMap}
|
|
192
|
+
*/
|
|
193
|
+
const withWidgetConfigs = (widgets, themePath) => Object.keys(widgets).reduce((acc, id) => ({
|
|
194
|
+
...acc,
|
|
195
|
+
[id]: {
|
|
196
|
+
...widgets[id],
|
|
197
|
+
...loadWidgetConfig((widgets[id] || {}).path, themePath),
|
|
198
|
+
},
|
|
199
|
+
}), {});
|
|
200
|
+
|
|
57
201
|
/**
|
|
58
202
|
* Returns contents of the `config/components.json` file from the theme.
|
|
59
203
|
*
|
|
@@ -70,41 +214,54 @@ const filterIgnoredExtensions = (config = {}) => Object.keys(config).reduce((acc
|
|
|
70
214
|
*/
|
|
71
215
|
module.exports = function getComponentsSettings(themePath) {
|
|
72
216
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const themeWidgetsV1Config = `${themeWidgetsPath}/widgets.json`;
|
|
76
|
-
const themeWidgetsV2Config = '@shopgate/engage/page/widgets/widgets.json';
|
|
77
|
-
|
|
78
|
-
/** @type {ComponentSettings} */
|
|
79
|
-
const defaultConfig = filterIgnoredExtensions(importFresh(`${themePath}/config/components.json`));
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Loads a JSON config file using `import-fresh`, if it exists.
|
|
83
|
-
*
|
|
84
|
-
* @param {string} path - The absolute path to the config file.
|
|
85
|
-
* @returns {Object} The imported configuration object, or an empty object if the file doesn't
|
|
86
|
-
* exist.
|
|
87
|
-
*/
|
|
88
|
-
const loadConfig = path => (fs.existsSync(path) || path.startsWith('@shopgate/engage') ?
|
|
89
|
-
importFresh(path) :
|
|
90
|
-
{}
|
|
91
|
-
);
|
|
217
|
+
const components = readComponentsConfig(themePath);
|
|
92
218
|
|
|
93
219
|
return {
|
|
94
|
-
...
|
|
95
|
-
|
|
96
|
-
...defaultConfig.widgets,
|
|
97
|
-
...loadConfig(themeWidgetsV1Config),
|
|
98
|
-
},
|
|
99
|
-
widgetsV2: {
|
|
100
|
-
// To enable backwards compatibility for custom legacy widgets that are provided by
|
|
101
|
-
// extensions we include them in the list of V2 widgets.
|
|
102
|
-
...defaultConfig.widgets,
|
|
103
|
-
...defaultConfig.widgetsV2,
|
|
104
|
-
...loadConfig(themeWidgetsV2Config),
|
|
105
|
-
},
|
|
220
|
+
...components,
|
|
221
|
+
widgetsV2: withWidgetConfigs(components.widgetsV2, themePath),
|
|
106
222
|
};
|
|
107
223
|
} catch (e) {
|
|
224
|
+
if (e instanceof WidgetConfigError) {
|
|
225
|
+
throw e;
|
|
226
|
+
}
|
|
227
|
+
|
|
108
228
|
return {};
|
|
109
229
|
}
|
|
110
230
|
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Returns the widget config files the component settings are built from, so that a build can watch
|
|
234
|
+
* them. Files that do not exist yet are reported separately, because webpack needs to know about
|
|
235
|
+
* them to notice when a widget gets a config for the first time.
|
|
236
|
+
*
|
|
237
|
+
* The configs themselves are not read here. A broken one is supposed to surface while a module is
|
|
238
|
+
* built, not while the webpack config is put together.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} themePath The path of the theme.
|
|
241
|
+
* @returns {{ fileDependencies: string[], missingDependencies: string[] }}
|
|
242
|
+
*/
|
|
243
|
+
module.exports.getWidgetConfigDependencies = (themePath) => {
|
|
244
|
+
try {
|
|
245
|
+
const { widgetsV2 = {} } = readComponentsConfig(themePath);
|
|
246
|
+
|
|
247
|
+
const configPaths = Object.keys(widgetsV2).reduce((acc, id) => {
|
|
248
|
+
const { path: componentPath } = widgetsV2[id] || {};
|
|
249
|
+
|
|
250
|
+
if (!componentPath) {
|
|
251
|
+
return acc;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return [...acc, ...getWidgetConfigPaths(componentPath, themePath)];
|
|
255
|
+
}, [path.resolve(themePath, 'config', 'components.json')]);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
fileDependencies: configPaths.filter(configPath => fs.existsSync(configPath)),
|
|
259
|
+
missingDependencies: configPaths.filter(configPath => !fs.existsSync(configPath)),
|
|
260
|
+
};
|
|
261
|
+
} catch (e) {
|
|
262
|
+
return {
|
|
263
|
+
fileDependencies: [],
|
|
264
|
+
missingDependencies: [],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const getComponentsSettings = require('./getComponentsSettings');
|
|
3
|
+
|
|
4
|
+
const fixtureTheme = name => path.resolve(__dirname, '__fixtures__', 'app', 'themes', name);
|
|
5
|
+
|
|
6
|
+
// Paths reach the assertions with the separator of the platform the tests run on.
|
|
7
|
+
const escapeForRegExp = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
|
|
9
|
+
const THEME_PATH = fixtureTheme('theme-test');
|
|
10
|
+
|
|
11
|
+
describe('getComponentsSettings', () => {
|
|
12
|
+
describe('widget configs', () => {
|
|
13
|
+
it('reads the config file that belongs to a widget', () => {
|
|
14
|
+
const { widgetsV2 } = getComponentsSettings(THEME_PATH);
|
|
15
|
+
|
|
16
|
+
expect(widgetsV2['@fixture/ext/AlphaWidget']).toEqual({
|
|
17
|
+
path: 'ext-fixture/frontend/widgets/AlphaWidget',
|
|
18
|
+
config: {
|
|
19
|
+
layout: {
|
|
20
|
+
applyMediaMargins: {
|
|
21
|
+
top: true,
|
|
22
|
+
bottom: true,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('keeps sections it does not know out of the config', () => {
|
|
30
|
+
const { widgetsV2 } = getComponentsSettings(THEME_PATH);
|
|
31
|
+
|
|
32
|
+
// The fixture declares a "$schema" next to its layout, which is an editor helper that has no
|
|
33
|
+
// business in the config that gets injected into the app.
|
|
34
|
+
expect(widgetsV2['@fixture/ext/AlphaWidget']).toHaveProperty('config.layout');
|
|
35
|
+
expect(widgetsV2['@fixture/ext/AlphaWidget'].config).not.toHaveProperty('$schema');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('leaves a widget without a config file alone', () => {
|
|
39
|
+
const { widgetsV2 } = getComponentsSettings(THEME_PATH);
|
|
40
|
+
|
|
41
|
+
expect(widgetsV2['@fixture/ext/BetaWidget']).toEqual({
|
|
42
|
+
path: 'ext-fixture/frontend/widgets/BetaWidget',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('fails on a config file that cannot be parsed', () => {
|
|
47
|
+
expect(() => getComponentsSettings(fixtureTheme('theme-unparsable')))
|
|
48
|
+
.toThrow(new RegExp(`${escapeForRegExp(path.join('UnparsableWidget', 'config.json'))}" could not be read`));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('fails on a config file that holds no object', () => {
|
|
52
|
+
expect(() => getComponentsSettings(fixtureTheme('theme-empty')))
|
|
53
|
+
.toThrow(/must be object/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('fails on a config file that does not match the schema', () => {
|
|
57
|
+
expect(() => getComponentsSettings(fixtureTheme('theme-invalid')))
|
|
58
|
+
.toThrow(/must NOT have additional properties/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('names the widget of an invalid config', () => {
|
|
62
|
+
expect(() => getComponentsSettings(fixtureTheme('theme-invalid')))
|
|
63
|
+
.toThrow(/ext-broken\/frontend\/widgets\/InvalidWidget/);
|
|
64
|
+
// The widget is named by its entry from the components config, which always uses forward
|
|
65
|
+
// slashes, so this one is not affected by the separator of the platform.
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('reads the config of widgets the PWA provides', () => {
|
|
69
|
+
const { widgetsV2 } = getComponentsSettings(THEME_PATH);
|
|
70
|
+
|
|
71
|
+
expect(widgetsV2['@shopgate/widgets/imageWidget'].config).toEqual({
|
|
72
|
+
layout: { applyMediaMargins: true },
|
|
73
|
+
});
|
|
74
|
+
expect(widgetsV2['@shopgate/widgets/richTextWidget']).not.toHaveProperty('config');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('does not add configs to the widgets of the v1 system', () => {
|
|
78
|
+
const { widgets } = getComponentsSettings(THEME_PATH);
|
|
79
|
+
|
|
80
|
+
expect(widgets['@fixture/ext/LegacyWidget']).toEqual({
|
|
81
|
+
path: 'ext-fixture/frontend/widgets/LegacyWidget',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('adds configs to v1 extension widgets that the v2 system renders', () => {
|
|
86
|
+
const { widgetsV2 } = getComponentsSettings(THEME_PATH);
|
|
87
|
+
|
|
88
|
+
expect(widgetsV2['@fixture/ext/LegacyWidget'].config).toEqual({
|
|
89
|
+
layout: { applyMediaMargins: true },
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('development builds', () => {
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
jest.resetModules();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('prefers the sources of an extension that is attached as a build', () => {
|
|
100
|
+
jest.resetModules();
|
|
101
|
+
jest.doMock('./variables', () => ({ isDev: true }));
|
|
102
|
+
|
|
103
|
+
// eslint-disable-next-line global-require
|
|
104
|
+
const { widgetsV2 } = require('./getComponentsSettings')(THEME_PATH);
|
|
105
|
+
|
|
106
|
+
expect(widgetsV2['@fixture/ext/DistWidget'].config).toEqual({
|
|
107
|
+
layout: { applyMediaMargins: { left: true } },
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('ignores the sources outside of development', () => {
|
|
112
|
+
const { widgetsV2 } = getComponentsSettings(THEME_PATH);
|
|
113
|
+
|
|
114
|
+
expect(widgetsV2['@fixture/ext/DistWidget']).not.toHaveProperty('config');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('getWidgetConfigDependencies', () => {
|
|
119
|
+
const { getWidgetConfigDependencies } = getComponentsSettings;
|
|
120
|
+
|
|
121
|
+
it('lists the config files that exist', () => {
|
|
122
|
+
const { fileDependencies } = getWidgetConfigDependencies(THEME_PATH);
|
|
123
|
+
|
|
124
|
+
expect(fileDependencies).toContain(path.join(
|
|
125
|
+
__dirname,
|
|
126
|
+
'__fixtures__/app/extensions/ext-fixture/frontend/widgets/AlphaWidget/config.json'
|
|
127
|
+
));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('lists the config files a widget could still get', () => {
|
|
131
|
+
const { missingDependencies } = getWidgetConfigDependencies(THEME_PATH);
|
|
132
|
+
|
|
133
|
+
expect(missingDependencies).toContain(path.join(
|
|
134
|
+
__dirname,
|
|
135
|
+
'__fixtures__/app/extensions/ext-fixture/frontend/widgets/BetaWidget/config.json'
|
|
136
|
+
));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('watches the components config of the theme', () => {
|
|
140
|
+
const { fileDependencies } = getWidgetConfigDependencies(THEME_PATH);
|
|
141
|
+
|
|
142
|
+
expect(fileDependencies).toContain(path.join(THEME_PATH, 'config', 'components.json'));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('reports absolute paths without duplicates', () => {
|
|
146
|
+
const { fileDependencies, missingDependencies } = getWidgetConfigDependencies(THEME_PATH);
|
|
147
|
+
const all = [...fileDependencies, ...missingDependencies];
|
|
148
|
+
|
|
149
|
+
expect(all.every(dependency => path.isAbsolute(dependency))).toBe(true);
|
|
150
|
+
expect(all).toHaveLength(new Set(all).size);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('does not read the configs, so a broken one does not break the build setup', () => {
|
|
154
|
+
expect(() => getWidgetConfigDependencies(fixtureTheme('theme-invalid'))).not.toThrow();
|
|
155
|
+
expect(getWidgetConfigDependencies(fixtureTheme('theme-invalid')).fileDependencies)
|
|
156
|
+
.toContain(path.join(
|
|
157
|
+
__dirname,
|
|
158
|
+
'__fixtures__/app/extensions/ext-broken/frontend/widgets/InvalidWidget/config.json'
|
|
159
|
+
));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('reports nothing for a theme without a components config', () => {
|
|
163
|
+
expect(getWidgetConfigDependencies(path.resolve(THEME_PATH, 'nope'))).toEqual({
|
|
164
|
+
fileDependencies: [],
|
|
165
|
+
missingDependencies: [],
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe('component mapping', () => {
|
|
171
|
+
it('merges the widgets of the theme into the v1 widgets', () => {
|
|
172
|
+
const { widgets } = getComponentsSettings(THEME_PATH);
|
|
173
|
+
|
|
174
|
+
expect(widgets['@fixture/theme/ThemeWidget']).toEqual({ path: 'ThemeWidget' });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('merges the widgets of the PWA into the v2 widgets', () => {
|
|
178
|
+
const { widgetsV2 } = getComponentsSettings(THEME_PATH);
|
|
179
|
+
|
|
180
|
+
expect(widgetsV2['@shopgate/widgets/htmlWidget']).toEqual({
|
|
181
|
+
path: '@shopgate/engage/page/widgets/HTML',
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('passes other component types through', () => {
|
|
186
|
+
const { portals } = getComponentsSettings(THEME_PATH);
|
|
187
|
+
|
|
188
|
+
expect(portals['@fixture/ext/SomePortal']).toEqual({
|
|
189
|
+
path: 'ext-fixture/frontend/portals/Some',
|
|
190
|
+
target: 'app.routes',
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('drops the entries of ignored extensions', () => {
|
|
195
|
+
const { reducers } = getComponentsSettings(THEME_PATH);
|
|
196
|
+
|
|
197
|
+
expect(reducers).toEqual({});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('returns nothing for a theme without a components config', () => {
|
|
201
|
+
expect(getComponentsSettings(path.resolve(THEME_PATH, 'does-not-exist'))).toEqual({});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
package/package.json
CHANGED
package/templates/default.ejs
CHANGED
|
@@ -16,6 +16,21 @@
|
|
|
16
16
|
<link rel="stylesheet" href="<%= css %>">
|
|
17
17
|
<% }) %>
|
|
18
18
|
<% } %>
|
|
19
|
+
<%
|
|
20
|
+
// Anchors for the new theming system. Layers must apply in a fixed order:
|
|
21
|
+
// emotion-insertion-point (all tss styles) -> font css -> theme css -> preview css.
|
|
22
|
+
//
|
|
23
|
+
// TSS styles carry defaults generated during theme initialization.
|
|
24
|
+
// The font css files (the typography app settings, loaded by loadFontCss) override them.
|
|
25
|
+
// The theme css file (the themeCssUrl setting, loaded by loadThemeCss) overrides both.
|
|
26
|
+
// The admin preview (FrontendSettingsPreviewBridge) overrides all for live customizations.
|
|
27
|
+
//
|
|
28
|
+
// All of them have the same specificity, so document order is decisive.
|
|
29
|
+
// Declaring the insertion positions here makes the order structural instead of timing-based.
|
|
30
|
+
%>
|
|
31
|
+
<meta name="font-css-insertion-point" content="" />
|
|
32
|
+
<meta name="theme-css-insertion-point" content="" />
|
|
33
|
+
<meta name="preview-css-insertion-point" content="" />
|
|
19
34
|
<% const scripts = htmlWebpackPlugin.files.js; %>
|
|
20
35
|
<% const vendor = scripts.find(s => s.includes('vendor')); %>
|
|
21
36
|
<% if (vendor) { %><script src="<%= vendor %>"></script><% } %>
|
package/webpack.config.js
CHANGED
|
@@ -148,7 +148,13 @@ const config = {
|
|
|
148
148
|
'process.env': {
|
|
149
149
|
NODE_ENV: JSON.stringify(ENV),
|
|
150
150
|
APP_CONFIG: JSON.stringify(appConfig),
|
|
151
|
-
|
|
151
|
+
// A runtime value, so that a change to a widget config is picked up by a running build.
|
|
152
|
+
// The dependencies make webpack watch the config files, including the ones that do not
|
|
153
|
+
// exist yet.
|
|
154
|
+
COMPONENTS_CONFIG: webpack.DefinePlugin.runtimeValue(
|
|
155
|
+
() => JSON.stringify(getComponentsSettings(themePath)),
|
|
156
|
+
getComponentsSettings.getWidgetConfigDependencies(themePath)
|
|
157
|
+
),
|
|
152
158
|
THEME_CONFIG: JSON.stringify(themeConfig),
|
|
153
159
|
THEME: JSON.stringify(process.env.theme),
|
|
154
160
|
THEME_PATH: JSON.stringify(themePath),
|