slicejs-web-framework 3.1.4 → 3.2.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.
|
@@ -17,6 +17,7 @@ export default class Controller {
|
|
|
17
17
|
this.bundleConfig = null;
|
|
18
18
|
this.criticalBundleLoaded = false;
|
|
19
19
|
this.bundleImportPromises = new Map();
|
|
20
|
+
this.bundleLoadPromises = new Map();
|
|
20
21
|
|
|
21
22
|
this.idCounter = 0;
|
|
22
23
|
}
|
|
@@ -88,51 +89,204 @@ export default class Controller {
|
|
|
88
89
|
* 📦 Loads a bundle by name or category
|
|
89
90
|
*/
|
|
90
91
|
async loadBundle(bundleName) {
|
|
91
|
-
|
|
92
|
+
const resolvedBundleName = this.resolveBundleName(bundleName);
|
|
93
|
+
if (this.loadedBundles.has(resolvedBundleName)) {
|
|
92
94
|
return; // Already loaded
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return this.loadBundleWithDependencies(resolvedBundleName, new Set());
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
async loadBundleWithDependencies(bundleName, loadingStack = new Set()) {
|
|
101
|
+
const resolvedBundleName = this.resolveBundleName(bundleName);
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} else {
|
|
100
|
-
bundleInfo = this.bundleConfig?.bundles?.routes?.[bundleName];
|
|
103
|
+
if (this.loadedBundles.has(resolvedBundleName)) {
|
|
104
|
+
return;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
if (loadingStack.has(resolvedBundleName)) {
|
|
108
|
+
throw new Error(`Circular bundle dependency detected: ${Array.from(loadingStack).join(' -> ')} -> ${resolvedBundleName}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.bundleLoadPromises.has(resolvedBundleName)) {
|
|
112
|
+
return this.bundleLoadPromises.get(resolvedBundleName);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const loadPromise = (async () => {
|
|
116
|
+
loadingStack.add(resolvedBundleName);
|
|
117
|
+
try {
|
|
118
|
+
const bundleInfo = this.getBundleInfo(resolvedBundleName);
|
|
119
|
+
if (!bundleInfo) {
|
|
120
|
+
console.warn(`Bundle ${resolvedBundleName} not found in configuration`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dependencies = this.getBundleDependencies(bundleInfo);
|
|
125
|
+
for (const dependencyName of dependencies) {
|
|
126
|
+
await this.loadBundleWithDependencies(dependencyName, loadingStack);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const bundlePath = `/bundles/${bundleInfo.file}`;
|
|
130
|
+
const bundleModule = await this.importBundleOnce(bundlePath);
|
|
131
|
+
const { metadata, registerAll } = await this.validateBundleModule(bundleModule, resolvedBundleName);
|
|
132
|
+
|
|
133
|
+
const registerResult = await registerAll(this, slice.stylesManager);
|
|
134
|
+
this.registerVendorSharedDependencies(bundleModule, metadata, resolvedBundleName, registerResult);
|
|
135
|
+
|
|
136
|
+
this.loadedBundles.add(resolvedBundleName);
|
|
137
|
+
const loadedBundleKey = metadata.bundleKey;
|
|
138
|
+
if (loadedBundleKey && loadedBundleKey !== resolvedBundleName) {
|
|
139
|
+
this.loadedBundles.add(loadedBundleKey);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (metadata.type === 'critical' || resolvedBundleName === 'critical') {
|
|
143
|
+
this.criticalBundleLoaded = true;
|
|
144
|
+
}
|
|
145
|
+
} finally {
|
|
146
|
+
loadingStack.delete(resolvedBundleName);
|
|
110
147
|
}
|
|
148
|
+
})();
|
|
149
|
+
|
|
150
|
+
this.bundleLoadPromises.set(resolvedBundleName, loadPromise);
|
|
151
|
+
try {
|
|
152
|
+
return await loadPromise;
|
|
153
|
+
} finally {
|
|
154
|
+
this.bundleLoadPromises.delete(resolvedBundleName);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
resolveBundleName(bundleName) {
|
|
159
|
+
if (typeof bundleName !== 'string' || bundleName.length === 0) {
|
|
160
|
+
return bundleName;
|
|
111
161
|
}
|
|
112
162
|
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
163
|
+
if (bundleName.toLowerCase() === 'critical') {
|
|
164
|
+
return 'critical';
|
|
165
|
+
}
|
|
117
166
|
|
|
118
|
-
|
|
167
|
+
if (this.isVendorSharedAlias(bundleName) && this.getVendorSharedBundleInfo()) {
|
|
168
|
+
return 'vendor-shared';
|
|
169
|
+
}
|
|
119
170
|
|
|
120
|
-
|
|
121
|
-
|
|
171
|
+
const routeBundleName = this.findBundleNameByAlias(this.bundleConfig?.bundles?.routes, bundleName);
|
|
172
|
+
if (routeBundleName) {
|
|
173
|
+
return routeBundleName;
|
|
174
|
+
}
|
|
122
175
|
|
|
123
|
-
|
|
176
|
+
const sharedBundleName = this.findBundleNameByAlias(this.bundleConfig?.bundles?.shared, bundleName);
|
|
177
|
+
if (sharedBundleName) {
|
|
178
|
+
return sharedBundleName;
|
|
179
|
+
}
|
|
124
180
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (loadedBundleKey && loadedBundleKey !== bundleName) {
|
|
128
|
-
this.loadedBundles.add(loadedBundleKey);
|
|
129
|
-
}
|
|
181
|
+
return bundleName;
|
|
182
|
+
}
|
|
130
183
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
findBundleNameByAlias(bundleRegistry, bundleName) {
|
|
185
|
+
if (!bundleRegistry || typeof bundleRegistry !== 'object') {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (bundleRegistry[bundleName]) {
|
|
190
|
+
return bundleName;
|
|
134
191
|
}
|
|
135
192
|
|
|
193
|
+
const normalizedName = bundleName?.toLowerCase();
|
|
194
|
+
if (!normalizedName) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return Object.keys(bundleRegistry).find((key) => key.toLowerCase() === normalizedName) || null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getBundleDependencies(bundleInfo) {
|
|
202
|
+
if (!bundleInfo || !Array.isArray(bundleInfo.dependencies)) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return bundleInfo.dependencies.filter((dependency) => typeof dependency === 'string' && dependency.length > 0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
findBundleEntryByName(bundleRegistry, bundleName) {
|
|
210
|
+
if (!bundleRegistry || typeof bundleRegistry !== 'object') {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (bundleRegistry[bundleName]) {
|
|
215
|
+
return bundleRegistry[bundleName];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const normalizedName = bundleName?.toLowerCase();
|
|
219
|
+
if (!normalizedName) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const matchedKey = Object.keys(bundleRegistry).find((key) => key.toLowerCase() === normalizedName);
|
|
224
|
+
return matchedKey ? bundleRegistry[matchedKey] : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getBundleInfo(bundleName) {
|
|
228
|
+
if (bundleName === 'critical') {
|
|
229
|
+
return this.bundleConfig?.bundles?.critical || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (this.isVendorSharedAlias(bundleName)) {
|
|
233
|
+
return this.getVendorSharedBundleInfo();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
this.findBundleEntryByName(this.bundleConfig?.bundles?.routes, bundleName)
|
|
238
|
+
|| this.findBundleEntryByName(this.bundleConfig?.bundles?.shared, bundleName)
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getVendorSharedBundleInfo() {
|
|
243
|
+
if (this.bundleConfig?.bundles?.vendorShared && typeof this.bundleConfig.bundles.vendorShared === 'object') {
|
|
244
|
+
return this.bundleConfig.bundles.vendorShared;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return this.findBundleEntryByName(this.bundleConfig?.bundles?.shared, 'vendor-shared');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
isVendorSharedAlias(bundleName) {
|
|
251
|
+
if (typeof bundleName !== 'string') {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const normalized = bundleName.toLowerCase();
|
|
256
|
+
return normalized === 'vendor-shared' || normalized === 'vendorshared';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
registerVendorSharedDependencies(bundleModule, metadata, bundleName, registerResult) {
|
|
260
|
+
const isVendorShared = this.isVendorSharedBundleName(metadata?.bundleKey)
|
|
261
|
+
|| this.isVendorSharedBundleName(bundleName)
|
|
262
|
+
|| metadata?.registerVendorSharedDependencies === true;
|
|
263
|
+
|
|
264
|
+
if (!isVendorShared) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let sharedDeps = bundleModule?.SLICE_SHARED_DEPS;
|
|
269
|
+
if (!sharedDeps && registerResult && typeof registerResult === 'object') {
|
|
270
|
+
sharedDeps = registerResult.SLICE_SHARED_DEPS
|
|
271
|
+
|| registerResult.SLICE_BUNDLE_DEPENDENCIES
|
|
272
|
+
|| registerResult;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!sharedDeps || typeof sharedDeps !== 'object') {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
|
|
280
|
+
window.__SLICE_SHARED_DEPS__ = {};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
Object.assign(window.__SLICE_SHARED_DEPS__, sharedDeps);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
isVendorSharedBundleName(bundleName) {
|
|
287
|
+
return typeof bundleName === 'string' && bundleName.toLowerCase() === 'vendor-shared';
|
|
288
|
+
}
|
|
289
|
+
|
|
136
290
|
/**
|
|
137
291
|
* 📦 Registers a bundle's components (called automatically by bundle files)
|
|
138
292
|
*/
|
package/Slice/Slice.js
CHANGED
|
@@ -364,6 +364,9 @@ async function init() {
|
|
|
364
364
|
|
|
365
365
|
if (resolvedMode === 'production' && window.slice.controller.bundleConfig) {
|
|
366
366
|
const config = window.slice.controller.bundleConfig;
|
|
367
|
+
if (!window.__SLICE_SHARED_DEPS__ || typeof window.__SLICE_SHARED_DEPS__ !== 'object') {
|
|
368
|
+
window.__SLICE_SHARED_DEPS__ = {};
|
|
369
|
+
}
|
|
367
370
|
const criticalFile = config?.bundles?.critical?.file;
|
|
368
371
|
if (criticalFile) {
|
|
369
372
|
try {
|
|
@@ -112,6 +112,406 @@ test('loadBundle marks requested bundle key and metadata bundleKey as loaded', a
|
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
+
test('loadBundle resolves dependencies first and registers vendor-shared exports once', async () => {
|
|
116
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'slice-controller-loader-'));
|
|
117
|
+
const loaderPath = path.join(tempDir, 'components-alias-loader.mjs');
|
|
118
|
+
await writeFile(
|
|
119
|
+
loaderPath,
|
|
120
|
+
`export async function resolve(specifier, context, nextResolve) {
|
|
121
|
+
if (specifier === '/Components/components.js') {
|
|
122
|
+
return {
|
|
123
|
+
shortCircuit: true,
|
|
124
|
+
url: 'data:text/javascript,export default {};',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return nextResolve(specifier, context);
|
|
128
|
+
}
|
|
129
|
+
`,
|
|
130
|
+
'utf8'
|
|
131
|
+
);
|
|
132
|
+
register(pathToFileURL(loaderPath).href);
|
|
133
|
+
|
|
134
|
+
const controllerModuleUrl = new URL('../Components/Structural/Controller/Controller.js', import.meta.url).href;
|
|
135
|
+
const { default: Controller } = await import(controllerModuleUrl);
|
|
136
|
+
const controller = new Controller();
|
|
137
|
+
const originalSlice = globalThis.slice;
|
|
138
|
+
const originalWindow = globalThis.window;
|
|
139
|
+
|
|
140
|
+
controller.bundleConfig = {
|
|
141
|
+
bundles: {
|
|
142
|
+
shared: {
|
|
143
|
+
'vendor-shared': {
|
|
144
|
+
file: 'slice-bundle.vendor-shared.js',
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
routes: {
|
|
148
|
+
dashboard: {
|
|
149
|
+
file: 'slice-bundle.dashboard.js',
|
|
150
|
+
dependencies: ['vendor-shared'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const importOrder = [];
|
|
157
|
+
const registerOrder = [];
|
|
158
|
+
const importsByPath = {
|
|
159
|
+
'/bundles/slice-bundle.vendor-shared.js': {
|
|
160
|
+
SLICE_BUNDLE_META: {
|
|
161
|
+
bundleKey: 'vendor-shared',
|
|
162
|
+
type: 'shared',
|
|
163
|
+
},
|
|
164
|
+
SLICE_SHARED_DEPS: {
|
|
165
|
+
'vendors/dompurify': {
|
|
166
|
+
sanitize: () => 'ok',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
registerAll: async () => {
|
|
170
|
+
registerOrder.push('vendor-shared');
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
'/bundles/slice-bundle.dashboard.js': {
|
|
174
|
+
SLICE_BUNDLE_META: {
|
|
175
|
+
bundleKey: 'dashboard',
|
|
176
|
+
type: 'route',
|
|
177
|
+
},
|
|
178
|
+
registerAll: async () => {
|
|
179
|
+
registerOrder.push('dashboard');
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
controller.importBundleOnce = async (bundlePath) => {
|
|
185
|
+
importOrder.push(bundlePath);
|
|
186
|
+
return importsByPath[bundlePath];
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
globalThis.slice = {
|
|
191
|
+
stylesManager: {},
|
|
192
|
+
};
|
|
193
|
+
globalThis.window = {
|
|
194
|
+
__SLICE_SHARED_DEPS__: {},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
await controller.loadBundle('dashboard');
|
|
198
|
+
await controller.loadBundle('dashboard');
|
|
199
|
+
|
|
200
|
+
assert.deepEqual(importOrder, ['/bundles/slice-bundle.vendor-shared.js', '/bundles/slice-bundle.dashboard.js']);
|
|
201
|
+
assert.deepEqual(registerOrder, ['vendor-shared', 'dashboard']);
|
|
202
|
+
assert.equal(typeof globalThis.window.__SLICE_SHARED_DEPS__['vendors/dompurify']?.sanitize, 'function');
|
|
203
|
+
} finally {
|
|
204
|
+
globalThis.slice = originalSlice;
|
|
205
|
+
globalThis.window = originalWindow;
|
|
206
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('loadBundle resolves vendor-shared dependency from bundles.vendorShared config shape', async () => {
|
|
211
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'slice-controller-loader-'));
|
|
212
|
+
const loaderPath = path.join(tempDir, 'components-alias-loader.mjs');
|
|
213
|
+
await writeFile(
|
|
214
|
+
loaderPath,
|
|
215
|
+
`export async function resolve(specifier, context, nextResolve) {
|
|
216
|
+
if (specifier === '/Components/components.js') {
|
|
217
|
+
return {
|
|
218
|
+
shortCircuit: true,
|
|
219
|
+
url: 'data:text/javascript,export default {};',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return nextResolve(specifier, context);
|
|
223
|
+
}
|
|
224
|
+
`,
|
|
225
|
+
'utf8'
|
|
226
|
+
);
|
|
227
|
+
register(pathToFileURL(loaderPath).href);
|
|
228
|
+
|
|
229
|
+
const controllerModuleUrl = new URL('../Components/Structural/Controller/Controller.js', import.meta.url).href;
|
|
230
|
+
const { default: Controller } = await import(controllerModuleUrl);
|
|
231
|
+
const controller = new Controller();
|
|
232
|
+
const originalSlice = globalThis.slice;
|
|
233
|
+
const originalWindow = globalThis.window;
|
|
234
|
+
|
|
235
|
+
controller.bundleConfig = {
|
|
236
|
+
bundles: {
|
|
237
|
+
vendorShared: {
|
|
238
|
+
file: 'slice-bundle.vendor-shared.js',
|
|
239
|
+
},
|
|
240
|
+
routes: {
|
|
241
|
+
dashboard: {
|
|
242
|
+
file: 'slice-bundle.dashboard.js',
|
|
243
|
+
dependencies: ['vendor-shared'],
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const importOrder = [];
|
|
250
|
+
const registerOrder = [];
|
|
251
|
+
const importsByPath = {
|
|
252
|
+
'/bundles/slice-bundle.vendor-shared.js': {
|
|
253
|
+
SLICE_BUNDLE_META: {
|
|
254
|
+
bundleKey: 'vendor-shared',
|
|
255
|
+
type: 'shared',
|
|
256
|
+
},
|
|
257
|
+
SLICE_SHARED_DEPS: {
|
|
258
|
+
'vendors/dompurify': {
|
|
259
|
+
sanitize: () => 'ok',
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
registerAll: async () => {
|
|
263
|
+
registerOrder.push('vendor-shared');
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
'/bundles/slice-bundle.dashboard.js': {
|
|
267
|
+
SLICE_BUNDLE_META: {
|
|
268
|
+
bundleKey: 'dashboard',
|
|
269
|
+
type: 'route',
|
|
270
|
+
},
|
|
271
|
+
registerAll: async () => {
|
|
272
|
+
registerOrder.push('dashboard');
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
controller.importBundleOnce = async (bundlePath) => {
|
|
278
|
+
importOrder.push(bundlePath);
|
|
279
|
+
return importsByPath[bundlePath];
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
globalThis.slice = {
|
|
284
|
+
stylesManager: {},
|
|
285
|
+
};
|
|
286
|
+
globalThis.window = {
|
|
287
|
+
__SLICE_SHARED_DEPS__: {},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
await controller.loadBundle('dashboard');
|
|
291
|
+
|
|
292
|
+
assert.deepEqual(importOrder, ['/bundles/slice-bundle.vendor-shared.js', '/bundles/slice-bundle.dashboard.js']);
|
|
293
|
+
assert.deepEqual(registerOrder, ['vendor-shared', 'dashboard']);
|
|
294
|
+
assert.equal(typeof globalThis.window.__SLICE_SHARED_DEPS__['vendors/dompurify']?.sanitize, 'function');
|
|
295
|
+
} finally {
|
|
296
|
+
globalThis.slice = originalSlice;
|
|
297
|
+
globalThis.window = originalWindow;
|
|
298
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('loadBundle registers vendor-shared dependencies from registerAll return when SLICE_SHARED_DEPS export is absent', async () => {
|
|
303
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'slice-controller-loader-'));
|
|
304
|
+
const loaderPath = path.join(tempDir, 'components-alias-loader.mjs');
|
|
305
|
+
await writeFile(
|
|
306
|
+
loaderPath,
|
|
307
|
+
`export async function resolve(specifier, context, nextResolve) {
|
|
308
|
+
if (specifier === '/Components/components.js') {
|
|
309
|
+
return {
|
|
310
|
+
shortCircuit: true,
|
|
311
|
+
url: 'data:text/javascript,export default {};',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return nextResolve(specifier, context);
|
|
315
|
+
}
|
|
316
|
+
`,
|
|
317
|
+
'utf8'
|
|
318
|
+
);
|
|
319
|
+
register(pathToFileURL(loaderPath).href);
|
|
320
|
+
|
|
321
|
+
const controllerModuleUrl = new URL('../Components/Structural/Controller/Controller.js', import.meta.url).href;
|
|
322
|
+
const { default: Controller } = await import(controllerModuleUrl);
|
|
323
|
+
const controller = new Controller();
|
|
324
|
+
const originalSlice = globalThis.slice;
|
|
325
|
+
const originalWindow = globalThis.window;
|
|
326
|
+
|
|
327
|
+
controller.bundleConfig = {
|
|
328
|
+
bundles: {
|
|
329
|
+
vendorShared: {
|
|
330
|
+
file: 'slice-bundle.vendor-shared.js',
|
|
331
|
+
},
|
|
332
|
+
routes: {
|
|
333
|
+
dashboard: {
|
|
334
|
+
file: 'slice-bundle.dashboard.js',
|
|
335
|
+
dependencies: ['vendor-shared'],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const importsByPath = {
|
|
342
|
+
'/bundles/slice-bundle.vendor-shared.js': {
|
|
343
|
+
SLICE_BUNDLE_META: {
|
|
344
|
+
bundleKey: 'vendor-shared',
|
|
345
|
+
type: 'shared',
|
|
346
|
+
},
|
|
347
|
+
registerAll: async () => ({
|
|
348
|
+
'vendors/dompurify': {
|
|
349
|
+
sanitize: () => 'ok',
|
|
350
|
+
},
|
|
351
|
+
}),
|
|
352
|
+
},
|
|
353
|
+
'/bundles/slice-bundle.dashboard.js': {
|
|
354
|
+
SLICE_BUNDLE_META: {
|
|
355
|
+
bundleKey: 'dashboard',
|
|
356
|
+
type: 'route',
|
|
357
|
+
},
|
|
358
|
+
registerAll: async () => {},
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
controller.importBundleOnce = async (bundlePath) => importsByPath[bundlePath];
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
globalThis.slice = {
|
|
366
|
+
stylesManager: {},
|
|
367
|
+
};
|
|
368
|
+
globalThis.window = {
|
|
369
|
+
__SLICE_SHARED_DEPS__: {},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
await controller.loadBundle('dashboard');
|
|
373
|
+
|
|
374
|
+
assert.equal(typeof globalThis.window.__SLICE_SHARED_DEPS__['vendors/dompurify']?.sanitize, 'function');
|
|
375
|
+
} finally {
|
|
376
|
+
globalThis.slice = originalSlice;
|
|
377
|
+
globalThis.window = originalWindow;
|
|
378
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('loadBundle dedupes concurrent and repeated alias/case requests using canonical bundle key', async () => {
|
|
383
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'slice-controller-loader-'));
|
|
384
|
+
const loaderPath = path.join(tempDir, 'components-alias-loader.mjs');
|
|
385
|
+
await writeFile(
|
|
386
|
+
loaderPath,
|
|
387
|
+
`export async function resolve(specifier, context, nextResolve) {
|
|
388
|
+
if (specifier === '/Components/components.js') {
|
|
389
|
+
return {
|
|
390
|
+
shortCircuit: true,
|
|
391
|
+
url: 'data:text/javascript,export default {};',
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return nextResolve(specifier, context);
|
|
395
|
+
}
|
|
396
|
+
`,
|
|
397
|
+
'utf8'
|
|
398
|
+
);
|
|
399
|
+
register(pathToFileURL(loaderPath).href);
|
|
400
|
+
|
|
401
|
+
const controllerModuleUrl = new URL('../Components/Structural/Controller/Controller.js', import.meta.url).href;
|
|
402
|
+
const { default: Controller } = await import(controllerModuleUrl);
|
|
403
|
+
const controller = new Controller();
|
|
404
|
+
const originalSlice = globalThis.slice;
|
|
405
|
+
|
|
406
|
+
controller.bundleConfig = {
|
|
407
|
+
bundles: {
|
|
408
|
+
routes: {
|
|
409
|
+
dashboard: {
|
|
410
|
+
file: 'slice-bundle.dashboard.js',
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
let registerCallCount = 0;
|
|
417
|
+
controller.importBundleOnce = async () => ({
|
|
418
|
+
SLICE_BUNDLE_META: {
|
|
419
|
+
bundleKey: 'routes.dashboard.v2',
|
|
420
|
+
type: 'route',
|
|
421
|
+
},
|
|
422
|
+
registerAll: async () => {
|
|
423
|
+
registerCallCount += 1;
|
|
424
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
425
|
+
},
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
globalThis.slice = {
|
|
430
|
+
stylesManager: {},
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
await Promise.all([
|
|
434
|
+
controller.loadBundle('dashboard'),
|
|
435
|
+
controller.loadBundle('DASHBOARD'),
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
await controller.loadBundle('Dashboard');
|
|
439
|
+
|
|
440
|
+
assert.equal(registerCallCount, 1);
|
|
441
|
+
assert.equal(controller.loadedBundles.has('dashboard'), true);
|
|
442
|
+
assert.equal(controller.loadedBundles.has('routes.dashboard.v2'), true);
|
|
443
|
+
assert.equal(controller.bundleLoadPromises.size, 0);
|
|
444
|
+
} finally {
|
|
445
|
+
globalThis.slice = originalSlice;
|
|
446
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('registerVendorSharedDependencies only runs for vendor-shared canonical bundle or explicit metadata flag', async () => {
|
|
451
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), 'slice-controller-loader-'));
|
|
452
|
+
const loaderPath = path.join(tempDir, 'components-alias-loader.mjs');
|
|
453
|
+
await writeFile(
|
|
454
|
+
loaderPath,
|
|
455
|
+
`export async function resolve(specifier, context, nextResolve) {
|
|
456
|
+
if (specifier === '/Components/components.js') {
|
|
457
|
+
return {
|
|
458
|
+
shortCircuit: true,
|
|
459
|
+
url: 'data:text/javascript,export default {};',
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return nextResolve(specifier, context);
|
|
463
|
+
}
|
|
464
|
+
`,
|
|
465
|
+
'utf8'
|
|
466
|
+
);
|
|
467
|
+
register(pathToFileURL(loaderPath).href);
|
|
468
|
+
|
|
469
|
+
const controllerModuleUrl = new URL('../Components/Structural/Controller/Controller.js', import.meta.url).href;
|
|
470
|
+
const { default: Controller } = await import(controllerModuleUrl);
|
|
471
|
+
const controller = new Controller();
|
|
472
|
+
const originalWindow = globalThis.window;
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
globalThis.window = {
|
|
476
|
+
__SLICE_SHARED_DEPS__: {},
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
controller.registerVendorSharedDependencies(
|
|
480
|
+
{
|
|
481
|
+
SLICE_SHARED_DEPS: {
|
|
482
|
+
shouldNotLoad: true,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
type: 'shared',
|
|
487
|
+
bundleKey: 'app-shared',
|
|
488
|
+
},
|
|
489
|
+
'app-shared'
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
assert.equal(globalThis.window.__SLICE_SHARED_DEPS__.shouldNotLoad, undefined);
|
|
493
|
+
|
|
494
|
+
controller.registerVendorSharedDependencies(
|
|
495
|
+
{
|
|
496
|
+
SLICE_SHARED_DEPS: {
|
|
497
|
+
explicitFlagDependency: true,
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
type: 'shared',
|
|
502
|
+
bundleKey: 'app-shared',
|
|
503
|
+
registerVendorSharedDependencies: true,
|
|
504
|
+
},
|
|
505
|
+
'app-shared'
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
assert.equal(globalThis.window.__SLICE_SHARED_DEPS__.explicitFlagDependency, true);
|
|
509
|
+
} finally {
|
|
510
|
+
globalThis.window = originalWindow;
|
|
511
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
115
515
|
test('Slice init fails fast with contextual error on invalid Bundling V2 contract path', async () => {
|
|
116
516
|
const tempDir = await mkdtemp(path.join(tmpdir(), 'slice-init-loader-'));
|
|
117
517
|
const loaderPath = path.join(tempDir, 'components-alias-loader.mjs');
|