slicejs-cli 3.6.2 → 3.6.4
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/LICENSE +21 -21
- package/README.md +226 -226
- package/client.js +744 -744
- package/commands/Print.js +163 -163
- package/commands/Validations.js +92 -92
- package/commands/build/build.js +40 -40
- package/commands/buildProduction/buildProduction.js +577 -579
- package/commands/bundle/bundle.js +234 -234
- package/commands/createComponent/VisualComponentTemplate.js +55 -55
- package/commands/createComponent/createComponent.js +128 -128
- package/commands/deleteComponent/deleteComponent.js +81 -81
- package/commands/doctor/doctor.js +440 -440
- package/commands/getComponent/getComponent.js +701 -692
- package/commands/init/init.js +467 -463
- package/commands/listComponents/listComponents.js +172 -172
- package/commands/startServer/startServer.js +261 -261
- package/commands/startServer/watchServer.js +66 -79
- package/commands/types/types.js +580 -580
- package/commands/utils/LocalCliDelegation.js +53 -53
- package/commands/utils/PackageManager.js +148 -148
- package/commands/utils/PathHelper.js +75 -75
- package/commands/utils/VersionChecker.js +169 -169
- package/commands/utils/bundling/BundleGenerator.js +2525 -2525
- package/commands/utils/bundling/DependencyAnalyzer.js +925 -925
- package/commands/utils/loadConfig.js +31 -31
- package/commands/utils/sliceScripts.js +23 -23
- package/commands/utils/updateManager.js +471 -471
- package/package.json +73 -73
- package/post.js +60 -60
|
@@ -1,2525 +1,2525 @@
|
|
|
1
|
-
// cli/utils/bundling/BundleGenerator.js
|
|
2
|
-
import fs from 'fs-extra';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import crypto from 'crypto';
|
|
5
|
-
import { parse } from '@babel/parser';
|
|
6
|
-
import traverse from '@babel/traverse';
|
|
7
|
-
import { minify as terserMinify } from 'terser';
|
|
8
|
-
import { getSrcPath, getComponentsJsPath, getDistPath, getConfigPath } from '../PathHelper.js';
|
|
9
|
-
|
|
10
|
-
export default class BundleGenerator {
|
|
11
|
-
constructor(moduleUrl, analysisData, options = {}) {
|
|
12
|
-
this.moduleUrl = moduleUrl;
|
|
13
|
-
this.analysisData = analysisData || { components: [], routes: [], metrics: {} };
|
|
14
|
-
this.srcPath = getSrcPath(moduleUrl);
|
|
15
|
-
this.distPath = getDistPath(moduleUrl);
|
|
16
|
-
this.output = options.output || 'src';
|
|
17
|
-
this.bundlesPath = this.output === 'dist'
|
|
18
|
-
? path.join(this.distPath, 'bundles')
|
|
19
|
-
: path.join(this.srcPath, 'bundles');
|
|
20
|
-
this.componentsPath = path.dirname(getComponentsJsPath(moduleUrl));
|
|
21
|
-
this.options = {
|
|
22
|
-
minify: !!options.minify,
|
|
23
|
-
obfuscate: !!options.obfuscate
|
|
24
|
-
};
|
|
25
|
-
this.format = 'v2';
|
|
26
|
-
this.sliceConfig = this.resolveSliceConfig();
|
|
27
|
-
this.loadingPolicy = this.resolveLoadingPolicy();
|
|
28
|
-
|
|
29
|
-
// Configuration
|
|
30
|
-
this.config = {
|
|
31
|
-
maxCriticalSize: 50 * 1024, // 50KB
|
|
32
|
-
maxCriticalComponents: 15,
|
|
33
|
-
minSharedUsage: 3, // Minimum routes to be considered "shared"
|
|
34
|
-
minVendorSharedUsage: 2,
|
|
35
|
-
minVendorSharedTransformedSize: 2 * 1024,
|
|
36
|
-
maxRouteBundleSize: 120 * 1024,
|
|
37
|
-
maxRouteRequests: 12,
|
|
38
|
-
strategy: 'hybrid' // 'global', 'hybrid', 'per-route'
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
this.vendorShared = {
|
|
42
|
-
file: 'slice-bundle.vendor-shared.js',
|
|
43
|
-
dependencyModules: new Map(),
|
|
44
|
-
dependencyUsage: new Map(),
|
|
45
|
-
sharedDependencySet: new Set(),
|
|
46
|
-
bundleKeysUsingSharedDependencies: new Set(),
|
|
47
|
-
bundle: null
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
this.bundles = {
|
|
51
|
-
critical: {
|
|
52
|
-
components: [],
|
|
53
|
-
size: 0,
|
|
54
|
-
file: 'slice-bundle.critical.js'
|
|
55
|
-
},
|
|
56
|
-
routes: {}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
resolveSliceConfig() {
|
|
61
|
-
if (this.analysisData?.sliceConfig && typeof this.analysisData.sliceConfig === 'object') {
|
|
62
|
-
return this.analysisData.sliceConfig;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const configPath = getConfigPath(this.moduleUrl);
|
|
67
|
-
if (fs.existsSync(configPath)) {
|
|
68
|
-
return fs.readJsonSync(configPath);
|
|
69
|
-
}
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.warn('Warning: Could not read sliceConfig.json for loading policy:', error.message);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
resolveLoadingPolicy() {
|
|
78
|
-
return this.sliceConfig?.loading?.enabled ? 'enabled' : 'disabled';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Computes deterministic integrity hash for bundle metadata.
|
|
83
|
-
* @param {Array} components
|
|
84
|
-
* @param {string} type
|
|
85
|
-
* @param {string|null} routePath
|
|
86
|
-
* @param {string} bundleKey
|
|
87
|
-
* @param {string} fileName
|
|
88
|
-
* @returns {string}
|
|
89
|
-
*/
|
|
90
|
-
computeBundleIntegrity(components, type, routePath, bundleKey, fileName) {
|
|
91
|
-
const metadata = {
|
|
92
|
-
version: '2.0.0',
|
|
93
|
-
type,
|
|
94
|
-
route: routePath,
|
|
95
|
-
bundleKey,
|
|
96
|
-
file: fileName,
|
|
97
|
-
generated: 'static',
|
|
98
|
-
totalSize: components.reduce((sum, c) => sum + c.size, 0),
|
|
99
|
-
componentCount: components.length,
|
|
100
|
-
strategy: this.config.strategy
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const payload = {
|
|
104
|
-
metadata,
|
|
105
|
-
components: components.reduce((acc, comp) => {
|
|
106
|
-
acc[comp.name] = {
|
|
107
|
-
name: comp.name,
|
|
108
|
-
category: comp.category,
|
|
109
|
-
categoryType: comp.categoryType,
|
|
110
|
-
componentDependencies: Array.from(comp.dependencies)
|
|
111
|
-
};
|
|
112
|
-
return acc;
|
|
113
|
-
}, {})
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
return `sha256:${crypto.createHash('sha256')
|
|
117
|
-
.update(JSON.stringify(payload))
|
|
118
|
-
.digest('hex')}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Generates all bundles
|
|
123
|
-
*/
|
|
124
|
-
async generate() {
|
|
125
|
-
console.log('🔨 Generating bundles...');
|
|
126
|
-
|
|
127
|
-
// 0. Create bundles directory
|
|
128
|
-
await fs.ensureDir(this.bundlesPath);
|
|
129
|
-
if (this.output === 'dist') {
|
|
130
|
-
await fs.ensureDir(this.distPath);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 1. Determine optimal strategy
|
|
134
|
-
this.determineStrategy();
|
|
135
|
-
|
|
136
|
-
// 2. Identify critical components
|
|
137
|
-
this.identifyCriticalComponents();
|
|
138
|
-
|
|
139
|
-
// 3. Assign components to routes
|
|
140
|
-
this.assignRouteComponents();
|
|
141
|
-
|
|
142
|
-
// 4. Generate bundle files
|
|
143
|
-
const files = await this.generateBundleFiles();
|
|
144
|
-
|
|
145
|
-
// 5. Generate framework bundle (structural)
|
|
146
|
-
const frameworkComponents = this.collectFrameworkComponents();
|
|
147
|
-
let frameworkBundle = null;
|
|
148
|
-
if (frameworkComponents.length > 0) {
|
|
149
|
-
frameworkBundle = await this.createFrameworkBundle(frameworkComponents);
|
|
150
|
-
files.push(frameworkBundle);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 6. Generate configuration
|
|
154
|
-
const config = this.generateBundleConfig(frameworkBundle);
|
|
155
|
-
|
|
156
|
-
console.log('✅ Bundles generated successfully');
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
bundles: this.bundles,
|
|
160
|
-
config,
|
|
161
|
-
files
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Determines the optimal bundling strategy
|
|
167
|
-
*/
|
|
168
|
-
determineStrategy() {
|
|
169
|
-
const { metrics } = this.analysisData;
|
|
170
|
-
const { totalComponents, sharedPercentage } = metrics;
|
|
171
|
-
|
|
172
|
-
// Strategy based on size and usage pattern
|
|
173
|
-
if (totalComponents < 20 || sharedPercentage > 60) {
|
|
174
|
-
this.config.strategy = 'global';
|
|
175
|
-
console.log('📦 Strategy: Global Bundle (small project or highly shared)');
|
|
176
|
-
} else if (totalComponents < 100) {
|
|
177
|
-
this.config.strategy = 'hybrid';
|
|
178
|
-
console.log('📦 Strategy: Hybrid (critical + grouped routes)');
|
|
179
|
-
} else {
|
|
180
|
-
this.config.strategy = 'per-route';
|
|
181
|
-
console.log('📦 Strategy: Per Route (large project)');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Identifies critical components for the initial bundle
|
|
187
|
-
*/
|
|
188
|
-
identifyCriticalComponents() {
|
|
189
|
-
const { components } = this.analysisData;
|
|
190
|
-
|
|
191
|
-
// Filter critical candidates
|
|
192
|
-
const candidates = components
|
|
193
|
-
.filter(comp => {
|
|
194
|
-
if (!this.isComponentAllowedByLoadingPolicy(comp)) return false;
|
|
195
|
-
// Shared components (used in 3+ routes)
|
|
196
|
-
const isShared = comp.routes.size >= this.config.minSharedUsage;
|
|
197
|
-
|
|
198
|
-
// Structural components (Navbar, Footer, etc.)
|
|
199
|
-
const isStructural = comp.categoryType === 'Structural' ||
|
|
200
|
-
['Navbar', 'Footer', 'Layout'].includes(comp.name);
|
|
201
|
-
|
|
202
|
-
// Small and highly used components (only if used in 3+ routes)
|
|
203
|
-
const isSmallAndUseful = comp.size < 2000 && comp.routes.size >= 3;
|
|
204
|
-
|
|
205
|
-
return isShared || isStructural || isSmallAndUseful;
|
|
206
|
-
})
|
|
207
|
-
.sort((a, b) => {
|
|
208
|
-
// Prioritize by: (usage * 10) - size
|
|
209
|
-
const priorityA = (a.routes.size * 10) - (a.size / 1000);
|
|
210
|
-
const priorityB = (b.routes.size * 10) - (b.size / 1000);
|
|
211
|
-
return priorityB - priorityA;
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const loadingComponent = components.find((comp) => comp.name === 'Loading' && this.isComponentAllowedByLoadingPolicy(comp));
|
|
215
|
-
if (this.loadingPolicy === 'enabled' && loadingComponent && !candidates.includes(loadingComponent)) {
|
|
216
|
-
candidates.unshift(loadingComponent);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Fill critical bundle up to limit
|
|
220
|
-
for (const comp of candidates) {
|
|
221
|
-
const dependencies = this.getComponentDependencies(comp);
|
|
222
|
-
const totalSize = comp.size + dependencies.reduce((sum, dep) => sum + dep.size, 0);
|
|
223
|
-
const totalCount = 1 + dependencies.length;
|
|
224
|
-
|
|
225
|
-
const wouldExceedSize = this.bundles.critical.size + totalSize > this.config.maxCriticalSize;
|
|
226
|
-
const wouldExceedCount = this.bundles.critical.components.length + totalCount > this.config.maxCriticalComponents;
|
|
227
|
-
|
|
228
|
-
if ((wouldExceedSize || wouldExceedCount) && comp.name !== 'Loading') continue;
|
|
229
|
-
|
|
230
|
-
// Add component and its dependencies
|
|
231
|
-
if (!this.bundles.critical.components.find(c => c.name === comp.name)) {
|
|
232
|
-
this.bundles.critical.components.push(comp);
|
|
233
|
-
this.bundles.critical.size += comp.size;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
for (const dep of dependencies) {
|
|
237
|
-
if (!this.bundles.critical.components.find(c => c.name === dep.name)) {
|
|
238
|
-
this.bundles.critical.components.push(dep);
|
|
239
|
-
this.bundles.critical.size += dep.size;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (this.loadingPolicy === 'disabled') {
|
|
245
|
-
this.bundles.critical.components = this.bundles.critical.components.filter((comp) => comp.name !== 'Loading');
|
|
246
|
-
this.bundles.critical.size = this.bundles.critical.components.reduce((sum, comp) => sum + comp.size, 0);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
console.log(`✓ Critical bundle: ${this.bundles.critical.components.length} components, ${(this.bundles.critical.size / 1024).toFixed(1)} KB`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Assigns remaining components to route bundles
|
|
254
|
-
*/
|
|
255
|
-
assignRouteComponents() {
|
|
256
|
-
const criticalNames = new Set(this.bundles.critical.components.map(c => c.name));
|
|
257
|
-
|
|
258
|
-
if (this.config.strategy === 'hybrid') {
|
|
259
|
-
this.assignHybridBundles(criticalNames);
|
|
260
|
-
} else {
|
|
261
|
-
this.assignPerRouteBundles(criticalNames);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
this.extractSharedComponents(criticalNames);
|
|
265
|
-
this.rebalanceBundlesByBudget(this.bundles.routes, {
|
|
266
|
-
maxBundleSize: this.config.maxRouteBundleSize,
|
|
267
|
-
maxRequests: this.config.maxRouteRequests
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Assigns components to per-route bundles
|
|
273
|
-
*/
|
|
274
|
-
assignPerRouteBundles(criticalNames) {
|
|
275
|
-
for (const route of this.analysisData.routes) {
|
|
276
|
-
const routePath = route.path;
|
|
277
|
-
// Get all route dependencies
|
|
278
|
-
const routeComponents = this.getRouteComponents(route.component);
|
|
279
|
-
|
|
280
|
-
// Include dependencies for all route components
|
|
281
|
-
const allComponents = new Set();
|
|
282
|
-
for (const comp of routeComponents) {
|
|
283
|
-
allComponents.add(comp);
|
|
284
|
-
const dependencies = this.getComponentDependencies(comp);
|
|
285
|
-
for (const dep of dependencies) {
|
|
286
|
-
allComponents.add(dep);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Filter those already in critical
|
|
291
|
-
const uniqueComponents = Array.from(allComponents).filter(comp =>
|
|
292
|
-
!criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
if (uniqueComponents.length === 0) continue;
|
|
296
|
-
|
|
297
|
-
const routeKey = this.routeToFileName(routePath);
|
|
298
|
-
const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
|
|
299
|
-
|
|
300
|
-
this.bundles.routes[routeKey] = {
|
|
301
|
-
path: routePath,
|
|
302
|
-
components: this.sortComponentsByName(uniqueComponents),
|
|
303
|
-
size: totalSize,
|
|
304
|
-
file: `slice-bundle.${routeKey}.js`
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
console.log(`✓ Bundle ${routeKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Gets all component dependencies transitively
|
|
313
|
-
*/
|
|
314
|
-
getComponentDependencies(component, visited = new Set()) {
|
|
315
|
-
if (visited.has(component.name)) return [];
|
|
316
|
-
visited.add(component.name);
|
|
317
|
-
|
|
318
|
-
const dependencies = [];
|
|
319
|
-
|
|
320
|
-
// Add direct dependencies
|
|
321
|
-
for (const depName of component.dependencies) {
|
|
322
|
-
const depComp = this.analysisData.components.find(c => c.name === depName);
|
|
323
|
-
if (depComp && !visited.has(depName)) {
|
|
324
|
-
dependencies.push(depComp);
|
|
325
|
-
// Add transitive dependencies
|
|
326
|
-
dependencies.push(...this.getComponentDependencies(depComp, visited));
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return dependencies;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Assigns components to hybrid bundles (grouped by category)
|
|
335
|
-
*/
|
|
336
|
-
assignHybridBundles(criticalNames) {
|
|
337
|
-
const routeGroups = new Map();
|
|
338
|
-
|
|
339
|
-
// First, handle MultiRoute groups
|
|
340
|
-
if (this.analysisData.routeGroups) {
|
|
341
|
-
for (const [groupKey, groupData] of this.analysisData.routeGroups) {
|
|
342
|
-
if (groupData.type === 'multiroute') {
|
|
343
|
-
// Create a bundle for this MultiRoute group
|
|
344
|
-
const allComponents = new Set();
|
|
345
|
-
|
|
346
|
-
// Add the main component (MultiRoute handler)
|
|
347
|
-
const mainComponent = this.analysisData.components.find(c => c.name === groupData.component);
|
|
348
|
-
if (mainComponent) {
|
|
349
|
-
allComponents.add(mainComponent);
|
|
350
|
-
|
|
351
|
-
// Add all components used by this MultiRoute
|
|
352
|
-
const routeComponents = this.getRouteComponents(mainComponent.name);
|
|
353
|
-
for (const comp of routeComponents) {
|
|
354
|
-
allComponents.add(comp);
|
|
355
|
-
// Add transitive dependencies
|
|
356
|
-
const dependencies = this.getComponentDependencies(comp);
|
|
357
|
-
for (const dep of dependencies) {
|
|
358
|
-
allComponents.add(dep);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Filter those already in critical
|
|
364
|
-
const uniqueComponents = Array.from(allComponents).filter(comp =>
|
|
365
|
-
!criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
if (uniqueComponents.length > 0) {
|
|
369
|
-
const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
|
|
370
|
-
|
|
371
|
-
this.bundles.routes[groupKey] = {
|
|
372
|
-
paths: groupData.routes,
|
|
373
|
-
components: this.sortComponentsByName(uniqueComponents),
|
|
374
|
-
size: totalSize,
|
|
375
|
-
file: `slice-bundle.${this.routeToFileName(groupKey)}.js`
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
console.log(`✓ Bundle ${groupKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${groupData.routes.length} routes)`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Group remaining routes by category (skip those already handled by MultiRoute)
|
|
385
|
-
for (const route of this.analysisData.routes) {
|
|
386
|
-
// Check if this route is already handled by a MultiRoute group
|
|
387
|
-
const isHandledByMultiRoute = this.analysisData.routeGroups &&
|
|
388
|
-
Array.from(this.analysisData.routeGroups.values()).some(group =>
|
|
389
|
-
group.type === 'multiroute' && group.routes.includes(route.path)
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
if (!isHandledByMultiRoute) {
|
|
393
|
-
const category = this.categorizeRoute(route.path);
|
|
394
|
-
if (!routeGroups.has(category)) {
|
|
395
|
-
routeGroups.set(category, []);
|
|
396
|
-
}
|
|
397
|
-
routeGroups.get(category).push(route);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Create bundles for each group
|
|
402
|
-
for (const [category, routes] of routeGroups) {
|
|
403
|
-
const allComponents = new Set();
|
|
404
|
-
|
|
405
|
-
// Collect all unique components for this category (including dependencies)
|
|
406
|
-
for (const route of routes) {
|
|
407
|
-
const routeComponents = this.getRouteComponents(route.component);
|
|
408
|
-
for (const comp of routeComponents) {
|
|
409
|
-
allComponents.add(comp);
|
|
410
|
-
// Add transitive dependencies
|
|
411
|
-
const dependencies = this.getComponentDependencies(comp);
|
|
412
|
-
for (const dep of dependencies) {
|
|
413
|
-
allComponents.add(dep);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Filter those already in critical
|
|
419
|
-
const uniqueComponents = Array.from(allComponents).filter(comp =>
|
|
420
|
-
!criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
if (uniqueComponents.length === 0) continue;
|
|
424
|
-
|
|
425
|
-
const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
|
|
426
|
-
const routePaths = routes.map(r => r.path);
|
|
427
|
-
|
|
428
|
-
this.bundles.routes[category] = {
|
|
429
|
-
paths: routePaths,
|
|
430
|
-
components: this.sortComponentsByName(uniqueComponents),
|
|
431
|
-
size: totalSize,
|
|
432
|
-
file: `slice-bundle.${this.routeToFileName(category)}.js`
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
console.log(`✓ Bundle ${category}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${routes.length} routes)`);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
isComponentAllowedByLoadingPolicy(component) {
|
|
440
|
-
if (!component) return false;
|
|
441
|
-
if (this.loadingPolicy === 'disabled' && component.name === 'Loading') {
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
return true;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
sortComponentsByName(components) {
|
|
448
|
-
return [...components].sort((a, b) => a.name.localeCompare(b.name));
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
dedupeComponentsByName(components = []) {
|
|
452
|
-
const byName = new Map();
|
|
453
|
-
for (const component of components) {
|
|
454
|
-
if (!component?.name) continue;
|
|
455
|
-
if (!byName.has(component.name)) {
|
|
456
|
-
byName.set(component.name, component);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return this.sortComponentsByName(Array.from(byName.values()));
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
getBundlePaths(bundle = {}) {
|
|
463
|
-
const raw = Array.isArray(bundle.paths)
|
|
464
|
-
? bundle.paths
|
|
465
|
-
: Array.isArray(bundle.path)
|
|
466
|
-
? bundle.path
|
|
467
|
-
: bundle.path
|
|
468
|
-
? [bundle.path]
|
|
469
|
-
: [];
|
|
470
|
-
|
|
471
|
-
return Array.from(new Set(raw.filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
setBundlePaths(bundle, paths = []) {
|
|
475
|
-
const mergedPaths = Array.from(new Set((paths || []).filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
476
|
-
if (mergedPaths.length === 0) {
|
|
477
|
-
delete bundle.path;
|
|
478
|
-
delete bundle.paths;
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
if (mergedPaths.length === 1) {
|
|
482
|
-
bundle.path = mergedPaths[0];
|
|
483
|
-
delete bundle.paths;
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
bundle.paths = mergedPaths;
|
|
487
|
-
delete bundle.path;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
mergeBundleDependencies(...dependencyLists) {
|
|
491
|
-
const merged = [];
|
|
492
|
-
const append = (dep) => {
|
|
493
|
-
if (!dep || merged.includes(dep)) return;
|
|
494
|
-
if (dep === 'critical') {
|
|
495
|
-
merged.unshift(dep);
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
merged.push(dep);
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
dependencyLists
|
|
502
|
-
.flat()
|
|
503
|
-
.forEach(append);
|
|
504
|
-
|
|
505
|
-
if (!merged.includes('critical')) {
|
|
506
|
-
merged.unshift('critical');
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const rest = merged
|
|
510
|
-
.filter((dep) => dep !== 'critical')
|
|
511
|
-
.sort((a, b) => a.localeCompare(b));
|
|
512
|
-
return ['critical', ...rest];
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
extractSharedComponents(criticalNames) {
|
|
516
|
-
const usage = new Map();
|
|
517
|
-
|
|
518
|
-
for (const bundle of Object.values(this.bundles.routes)) {
|
|
519
|
-
const seenInBundle = new Set();
|
|
520
|
-
for (const component of this.dedupeComponentsByName(bundle.components || [])) {
|
|
521
|
-
if (criticalNames.has(component.name)) continue;
|
|
522
|
-
if (!this.isComponentAllowedByLoadingPolicy(component)) continue;
|
|
523
|
-
if (seenInBundle.has(component.name)) continue;
|
|
524
|
-
seenInBundle.add(component.name);
|
|
525
|
-
if (!usage.has(component.name)) {
|
|
526
|
-
usage.set(component.name, { component, count: 0 });
|
|
527
|
-
}
|
|
528
|
-
usage.get(component.name).count += 1;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const sharedComponents = Array.from(usage.values())
|
|
533
|
-
.filter((entry) => entry.count >= this.config.minSharedUsage)
|
|
534
|
-
.map((entry) => entry.component);
|
|
535
|
-
|
|
536
|
-
if (sharedComponents.length === 0) {
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const sharedSet = new Set(sharedComponents.map((component) => component.name));
|
|
541
|
-
const orderedShared = this.sortComponentsByName(sharedComponents);
|
|
542
|
-
|
|
543
|
-
for (const bundle of Object.values(this.bundles.routes)) {
|
|
544
|
-
const original = this.dedupeComponentsByName(bundle.components || []);
|
|
545
|
-
const filtered = original.filter((component) => !sharedSet.has(component.name));
|
|
546
|
-
const removedShared = original.length - filtered.length;
|
|
547
|
-
bundle.components = this.sortComponentsByName(filtered);
|
|
548
|
-
bundle.size = bundle.components.reduce((sum, component) => sum + component.size, 0);
|
|
549
|
-
if (removedShared > 0) {
|
|
550
|
-
bundle.dependencies = this.mergeBundleDependencies(bundle.dependencies || [], ['shared-core']);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
this.bundles.routes['shared-core'] = {
|
|
555
|
-
paths: [],
|
|
556
|
-
components: orderedShared,
|
|
557
|
-
size: orderedShared.reduce((sum, component) => sum + component.size, 0),
|
|
558
|
-
file: `slice-bundle.${this.routeToFileName('shared-core')}.js`
|
|
559
|
-
};
|
|
560
|
-
|
|
561
|
-
for (const [key, bundle] of Object.entries(this.bundles.routes)) {
|
|
562
|
-
if (key === 'shared-core') continue;
|
|
563
|
-
if ((bundle.components || []).length === 0) {
|
|
564
|
-
delete this.bundles.routes[key];
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
rebalanceBundlesByBudget(bundles, limits = {}) {
|
|
570
|
-
const maxBundleSize = limits.maxBundleSize || this.config.maxRouteBundleSize;
|
|
571
|
-
const maxRequests = limits.maxRequests || this.config.maxRouteRequests;
|
|
572
|
-
const orderedEntries = Object.entries(bundles)
|
|
573
|
-
.sort(([a], [b]) => a.localeCompare(b));
|
|
574
|
-
const rebalanced = {};
|
|
575
|
-
|
|
576
|
-
for (const [key, bundle] of orderedEntries) {
|
|
577
|
-
const sortedComponents = this.dedupeComponentsByName(bundle.components || []);
|
|
578
|
-
const totalSize = sortedComponents.reduce((sum, component) => sum + component.size, 0);
|
|
579
|
-
if (totalSize <= maxBundleSize || sortedComponents.length <= 1) {
|
|
580
|
-
rebalanced[key] = {
|
|
581
|
-
...bundle,
|
|
582
|
-
components: sortedComponents,
|
|
583
|
-
size: totalSize,
|
|
584
|
-
file: `slice-bundle.${this.routeToFileName(key)}.js`
|
|
585
|
-
};
|
|
586
|
-
continue;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
let partIndex = 1;
|
|
590
|
-
let currentChunk = [];
|
|
591
|
-
let currentSize = 0;
|
|
592
|
-
|
|
593
|
-
for (const component of sortedComponents) {
|
|
594
|
-
const nextSize = currentSize + component.size;
|
|
595
|
-
const shouldFlush = currentChunk.length > 0 && nextSize > maxBundleSize;
|
|
596
|
-
|
|
597
|
-
if (shouldFlush) {
|
|
598
|
-
const partKey = `${key}--p${partIndex}`;
|
|
599
|
-
rebalanced[partKey] = {
|
|
600
|
-
...bundle,
|
|
601
|
-
components: currentChunk,
|
|
602
|
-
size: currentSize,
|
|
603
|
-
file: `slice-bundle.${this.routeToFileName(partKey)}.js`
|
|
604
|
-
};
|
|
605
|
-
partIndex += 1;
|
|
606
|
-
currentChunk = [];
|
|
607
|
-
currentSize = 0;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
currentChunk.push(component);
|
|
611
|
-
currentSize += component.size;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
if (currentChunk.length > 0) {
|
|
615
|
-
const partKey = `${key}--p${partIndex}`;
|
|
616
|
-
rebalanced[partKey] = {
|
|
617
|
-
...bundle,
|
|
618
|
-
components: currentChunk,
|
|
619
|
-
size: currentSize,
|
|
620
|
-
file: `slice-bundle.${this.routeToFileName(partKey)}.js`
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const keys = Object.keys(rebalanced).sort((a, b) => a.localeCompare(b));
|
|
626
|
-
while (keys.length > maxRequests) {
|
|
627
|
-
const lastKey = keys.pop();
|
|
628
|
-
const targetKey = keys[keys.length - 1];
|
|
629
|
-
if (!lastKey || !targetKey) break;
|
|
630
|
-
const mergedComponents = this.dedupeComponentsByName([
|
|
631
|
-
...(rebalanced[targetKey].components || []),
|
|
632
|
-
...(rebalanced[lastKey].components || [])
|
|
633
|
-
]);
|
|
634
|
-
const mergedPaths = [
|
|
635
|
-
...this.getBundlePaths(rebalanced[targetKey]),
|
|
636
|
-
...this.getBundlePaths(rebalanced[lastKey])
|
|
637
|
-
];
|
|
638
|
-
const mergedDependencies = this.mergeBundleDependencies(
|
|
639
|
-
rebalanced[targetKey].dependencies || [],
|
|
640
|
-
rebalanced[lastKey].dependencies || []
|
|
641
|
-
);
|
|
642
|
-
|
|
643
|
-
rebalanced[targetKey].components = mergedComponents;
|
|
644
|
-
rebalanced[targetKey].size = mergedComponents.reduce((sum, component) => sum + component.size, 0);
|
|
645
|
-
rebalanced[targetKey].dependencies = mergedDependencies;
|
|
646
|
-
this.setBundlePaths(rebalanced[targetKey], mergedPaths);
|
|
647
|
-
delete rebalanced[lastKey];
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
Object.keys(bundles).forEach((key) => delete bundles[key]);
|
|
651
|
-
for (const [key, bundle] of Object.entries(rebalanced).sort(([a], [b]) => a.localeCompare(b))) {
|
|
652
|
-
bundles[key] = bundle;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return bundles;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Categorizes a route path for grouping, considering MultiRoute context
|
|
660
|
-
*/
|
|
661
|
-
categorizeRoute(routePath) {
|
|
662
|
-
// Check if this route belongs to a MultiRoute handler
|
|
663
|
-
if (this.analysisData.routeGroups) {
|
|
664
|
-
for (const [groupKey, groupData] of this.analysisData.routeGroups) {
|
|
665
|
-
if (groupData.type === 'multiroute' && groupData.routes.includes(routePath)) {
|
|
666
|
-
return groupKey; // Return the MultiRoute group key
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Default categorization
|
|
672
|
-
const path = routePath.toLowerCase();
|
|
673
|
-
|
|
674
|
-
if (path === '/' || path === '/home') return 'home';
|
|
675
|
-
if (path.includes('docum') || path.includes('documentation')) return 'documentation';
|
|
676
|
-
if (path.includes('component') || path.includes('visual') || path.includes('card') ||
|
|
677
|
-
path.includes('button') || path.includes('input') || path.includes('switch') ||
|
|
678
|
-
path.includes('checkbox') || path.includes('select') || path.includes('details') ||
|
|
679
|
-
path.includes('grid') || path.includes('loading') || path.includes('layout') ||
|
|
680
|
-
path.includes('navbar') || path.includes('treeview') || path.includes('multiroute')) return 'components';
|
|
681
|
-
if (path.includes('theme') || path.includes('slice') || path.includes('config')) return 'configuration';
|
|
682
|
-
if (path.includes('routing') || path.includes('guard')) return 'routing';
|
|
683
|
-
if (path.includes('service') || path.includes('command')) return 'services';
|
|
684
|
-
if (path.includes('structural') || path.includes('lifecycle') || path.includes('static') ||
|
|
685
|
-
path.includes('build')) return 'advanced';
|
|
686
|
-
if (path.includes('playground') || path.includes('creator')) return 'tools';
|
|
687
|
-
if (path.includes('about') || path.includes('404')) return 'misc';
|
|
688
|
-
|
|
689
|
-
return 'general';
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Gets all components needed for a route
|
|
694
|
-
*/
|
|
695
|
-
getRouteComponents(componentName) {
|
|
696
|
-
const result = [];
|
|
697
|
-
const visited = new Set();
|
|
698
|
-
|
|
699
|
-
const traverse = (name) => {
|
|
700
|
-
if (visited.has(name)) return;
|
|
701
|
-
visited.add(name);
|
|
702
|
-
|
|
703
|
-
const component = this.analysisData.components.find(c => c.name === name);
|
|
704
|
-
if (!component) return;
|
|
705
|
-
|
|
706
|
-
result.push(component);
|
|
707
|
-
|
|
708
|
-
// Add dependencies recursively
|
|
709
|
-
for (const dep of component.dependencies) {
|
|
710
|
-
traverse(dep);
|
|
711
|
-
}
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
traverse(componentName);
|
|
715
|
-
return result;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/**
|
|
719
|
-
* Generates the physical bundle files
|
|
720
|
-
*/
|
|
721
|
-
async generateBundleFiles() {
|
|
722
|
-
const files = [];
|
|
723
|
-
|
|
724
|
-
await this.prepareVendorSharedDependencies();
|
|
725
|
-
|
|
726
|
-
if (this.vendorShared.sharedDependencySet.size > 0) {
|
|
727
|
-
const vendorSharedFile = await this.createVendorSharedDependencyBundleFile(this.vendorShared.sharedDependencySet);
|
|
728
|
-
this.vendorShared.bundle = vendorSharedFile;
|
|
729
|
-
files.push(vendorSharedFile);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// 1. Critical bundle
|
|
733
|
-
if (this.bundles.critical.components.length > 0) {
|
|
734
|
-
const criticalFile = await this.createBundleFile(
|
|
735
|
-
this.bundles.critical.components,
|
|
736
|
-
'critical',
|
|
737
|
-
null
|
|
738
|
-
);
|
|
739
|
-
const criticalIntegrity = this.computeBundleIntegrity(
|
|
740
|
-
this.bundles.critical.components,
|
|
741
|
-
'critical',
|
|
742
|
-
null,
|
|
743
|
-
'critical',
|
|
744
|
-
criticalFile.file
|
|
745
|
-
);
|
|
746
|
-
this.bundles.critical.integrity = `sha256:${criticalFile.hash}`;
|
|
747
|
-
this.bundles.critical.hash = criticalFile.hash;
|
|
748
|
-
files.push(criticalFile);
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// 2. Route bundles
|
|
752
|
-
for (const [routeKey, bundle] of Object.entries(this.bundles.routes)) {
|
|
753
|
-
const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
|
|
754
|
-
? routeKey
|
|
755
|
-
: (bundle.path || bundle.paths || routeKey);
|
|
756
|
-
|
|
757
|
-
const routeFile = await this.createBundleFile(
|
|
758
|
-
bundle.components,
|
|
759
|
-
'route',
|
|
760
|
-
routeIdentifier
|
|
761
|
-
);
|
|
762
|
-
const routeIntegrity = `sha256:${routeFile.hash}`;
|
|
763
|
-
const matchingBundle = Object.values(this.bundles.routes)
|
|
764
|
-
.find((entry) => entry.file === routeFile.file);
|
|
765
|
-
if (matchingBundle) {
|
|
766
|
-
matchingBundle.hash = routeFile.hash;
|
|
767
|
-
matchingBundle.integrity = routeIntegrity;
|
|
768
|
-
}
|
|
769
|
-
files.push(routeFile);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
return files;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
async prepareVendorSharedDependencies() {
|
|
776
|
-
const routeDependencyIndex = await this.collectRouteExternalDependencyIndex();
|
|
777
|
-
const usageIndex = this.indexExternalDependencyUsage(routeDependencyIndex);
|
|
778
|
-
const sharedDependencySet = this.computeSharedDependencySet(usageIndex);
|
|
779
|
-
|
|
780
|
-
this.vendorShared.dependencyUsage = usageIndex;
|
|
781
|
-
this.vendorShared.sharedDependencySet = sharedDependencySet;
|
|
782
|
-
this.vendorShared.bundleKeysUsingSharedDependencies = new Set();
|
|
783
|
-
|
|
784
|
-
if (sharedDependencySet.size === 0) {
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
for (const dependencyName of sharedDependencySet) {
|
|
789
|
-
const usageEntry = usageIndex.get(dependencyName);
|
|
790
|
-
for (const bundleKey of usageEntry?.bundleKeys || []) {
|
|
791
|
-
this.vendorShared.bundleKeysUsingSharedDependencies.add(bundleKey);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
for (const bundleKey of this.vendorShared.bundleKeysUsingSharedDependencies) {
|
|
796
|
-
const bundle = this.bundles.routes[bundleKey];
|
|
797
|
-
if (!bundle) continue;
|
|
798
|
-
bundle.dependencies = this.mergeBundleDependencies(bundle.dependencies || [], ['vendor-shared']);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
async collectRouteExternalDependencyIndex() {
|
|
803
|
-
const routeDependencyIndex = {};
|
|
804
|
-
|
|
805
|
-
for (const [bundleKey, bundle] of Object.entries(this.bundles.routes)) {
|
|
806
|
-
routeDependencyIndex[bundleKey] = {};
|
|
807
|
-
const uniqueComponents = this.dedupeComponentsByName(bundle.components || []);
|
|
808
|
-
|
|
809
|
-
for (const comp of uniqueComponents) {
|
|
810
|
-
const fileBaseName = comp.fileName || comp.name;
|
|
811
|
-
const jsPath = path.join(comp.path, `${fileBaseName}.js`);
|
|
812
|
-
if (!await fs.pathExists(jsPath)) continue;
|
|
813
|
-
const jsContent = await fs.readFile(jsPath, 'utf-8');
|
|
814
|
-
const dependencies = await this.buildDependencyContents(jsContent, comp.path);
|
|
815
|
-
for (const [depName, depEntry] of Object.entries(dependencies || {})) {
|
|
816
|
-
if (!routeDependencyIndex[bundleKey][depName]) {
|
|
817
|
-
routeDependencyIndex[bundleKey][depName] = depEntry;
|
|
818
|
-
}
|
|
819
|
-
if (!this.vendorShared.dependencyModules.has(depName)) {
|
|
820
|
-
this.vendorShared.dependencyModules.set(depName, {
|
|
821
|
-
name: depName,
|
|
822
|
-
content: depEntry?.content || ''
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
return routeDependencyIndex;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
indexExternalDependencyUsage(routeDependencyIndex = {}) {
|
|
833
|
-
const usage = new Map();
|
|
834
|
-
|
|
835
|
-
for (const [bundleKey, dependencies] of Object.entries(routeDependencyIndex || {})) {
|
|
836
|
-
for (const [dependencyName, dependencyEntry] of Object.entries(dependencies || {})) {
|
|
837
|
-
if (!usage.has(dependencyName)) {
|
|
838
|
-
usage.set(dependencyName, {
|
|
839
|
-
name: dependencyName,
|
|
840
|
-
bundleKeys: new Set(),
|
|
841
|
-
bundleCount: 0,
|
|
842
|
-
content: dependencyEntry?.content || ''
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
const entry = usage.get(dependencyName);
|
|
846
|
-
entry.bundleKeys.add(bundleKey);
|
|
847
|
-
entry.bundleCount = entry.bundleKeys.size;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
return usage;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
computeSharedDependencySet(usageIndex = new Map()) {
|
|
855
|
-
const shared = new Set();
|
|
856
|
-
|
|
857
|
-
for (const [dependencyName, entry] of usageIndex.entries()) {
|
|
858
|
-
if ((entry?.bundleCount || 0) < this.config.minVendorSharedUsage) continue;
|
|
859
|
-
const transformedContent = this.transformDependencyContent(
|
|
860
|
-
entry?.content || '',
|
|
861
|
-
'__sliceVendorSharedProbe',
|
|
862
|
-
dependencyName
|
|
863
|
-
);
|
|
864
|
-
const transformedSize = Buffer.byteLength(transformedContent, 'utf-8');
|
|
865
|
-
if (transformedSize < this.config.minVendorSharedTransformedSize) continue;
|
|
866
|
-
shared.add(dependencyName);
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
return shared;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
generateVendorSharedDependencyBundleContent(sharedDependencySet = new Set()) {
|
|
873
|
-
const selectedModules = Array.from(sharedDependencySet)
|
|
874
|
-
.sort((a, b) => a.localeCompare(b))
|
|
875
|
-
.map((dependencyName) => {
|
|
876
|
-
const fromUsage = this.vendorShared.dependencyUsage.get(dependencyName)?.content;
|
|
877
|
-
const fromCollected = this.vendorShared.dependencyModules.get(dependencyName)?.content;
|
|
878
|
-
const content = fromUsage || fromCollected || '';
|
|
879
|
-
return { name: dependencyName, content };
|
|
880
|
-
})
|
|
881
|
-
.filter((entry) => !!entry.content);
|
|
882
|
-
|
|
883
|
-
const dependencyModuleBlock = this.buildV2DependencyModuleBlockFromModules(selectedModules);
|
|
884
|
-
const metadata = {
|
|
885
|
-
version: '2',
|
|
886
|
-
bundleKey: 'vendor-shared',
|
|
887
|
-
type: 'vendor-shared',
|
|
888
|
-
routes: [],
|
|
889
|
-
componentCount: 0,
|
|
890
|
-
dependencyCount: selectedModules.length
|
|
891
|
-
};
|
|
892
|
-
|
|
893
|
-
return `export const SLICE_BUNDLE_META = ${JSON.stringify(metadata, null, 2)};\n\n${dependencyModuleBlock}\n\nexport async function registerAll() {\n return SLICE_BUNDLE_DEPENDENCIES;\n}\n`;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
async createVendorSharedDependencyBundleFile(sharedDependencySet) {
|
|
897
|
-
const fileName = this.vendorShared.file;
|
|
898
|
-
const filePath = path.join(this.bundlesPath, fileName);
|
|
899
|
-
const bundleContent = this.generateVendorSharedDependencyBundleContent(sharedDependencySet);
|
|
900
|
-
const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
|
|
901
|
-
await fs.ensureDir(path.dirname(filePath));
|
|
902
|
-
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
903
|
-
|
|
904
|
-
const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
|
|
905
|
-
|
|
906
|
-
return {
|
|
907
|
-
name: 'vendor-shared',
|
|
908
|
-
file: fileName,
|
|
909
|
-
path: filePath,
|
|
910
|
-
size: Buffer.byteLength(bundleContent, 'utf-8'),
|
|
911
|
-
hash,
|
|
912
|
-
integrity: `sha256:${hash}`,
|
|
913
|
-
componentCount: 0
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
/**
|
|
918
|
-
* Creates a bundle file
|
|
919
|
-
*/
|
|
920
|
-
async createBundleFile(components, type, routePath) {
|
|
921
|
-
const routeKey = routePath ? this.routeToFileName(routePath) : 'critical';
|
|
922
|
-
const fileName = `slice-bundle.${routeKey}.js`;
|
|
923
|
-
const filePath = path.join(this.bundlesPath, fileName);
|
|
924
|
-
|
|
925
|
-
const bundleContent = await this.generateBundleContent(
|
|
926
|
-
components,
|
|
927
|
-
type,
|
|
928
|
-
routePath,
|
|
929
|
-
routeKey,
|
|
930
|
-
fileName
|
|
931
|
-
);
|
|
932
|
-
|
|
933
|
-
const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
|
|
934
|
-
|
|
935
|
-
await fs.ensureDir(path.dirname(filePath));
|
|
936
|
-
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
937
|
-
|
|
938
|
-
const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
|
|
939
|
-
|
|
940
|
-
return {
|
|
941
|
-
name: routeKey,
|
|
942
|
-
file: fileName,
|
|
943
|
-
path: filePath,
|
|
944
|
-
size: Buffer.byteLength(bundleContent, 'utf-8'),
|
|
945
|
-
hash,
|
|
946
|
-
componentCount: components.length
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
async applyBundleTransforms(bundleContent, fileName) {
|
|
951
|
-
if (!this.options.minify && !this.options.obfuscate) {
|
|
952
|
-
return bundleContent;
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
const options = {
|
|
956
|
-
parse: {
|
|
957
|
-
ecma: 2022
|
|
958
|
-
},
|
|
959
|
-
ecma: 2022,
|
|
960
|
-
compress: this.options.minify ? {
|
|
961
|
-
drop_console: false,
|
|
962
|
-
drop_debugger: true,
|
|
963
|
-
passes: 1
|
|
964
|
-
} : false,
|
|
965
|
-
mangle: this.options.obfuscate ? {
|
|
966
|
-
properties:
|
|
967
|
-
} : false,
|
|
968
|
-
keep_fnames: true,
|
|
969
|
-
keep_classnames: true,
|
|
970
|
-
format: {
|
|
971
|
-
comments: false,
|
|
972
|
-
ecma: 2022
|
|
973
|
-
}
|
|
974
|
-
};
|
|
975
|
-
|
|
976
|
-
let result;
|
|
977
|
-
try {
|
|
978
|
-
result = await terserMinify(bundleContent, options);
|
|
979
|
-
} catch (error) {
|
|
980
|
-
const tmpDir = path.resolve(process.cwd(), '.tmp');
|
|
981
|
-
const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
982
|
-
const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
|
|
983
|
-
try {
|
|
984
|
-
await fs.ensureDir(tmpDir);
|
|
985
|
-
await fs.writeFile(tmpPath, bundleContent, 'utf-8');
|
|
986
|
-
} catch (writeError) {
|
|
987
|
-
console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
|
|
988
|
-
}
|
|
989
|
-
const message = error?.message ? `${error.message}.` : 'Unknown Terser error.';
|
|
990
|
-
throw new Error(`Terser failed for ${fileName}: ${message} Saved bundle to ${tmpPath}`);
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
if (result.error) {
|
|
994
|
-
const tmpDir = path.resolve(process.cwd(), '.tmp');
|
|
995
|
-
const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
996
|
-
const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
|
|
997
|
-
try {
|
|
998
|
-
await fs.ensureDir(tmpDir);
|
|
999
|
-
await fs.writeFile(tmpPath, bundleContent, 'utf-8');
|
|
1000
|
-
} catch (writeError) {
|
|
1001
|
-
console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
|
|
1002
|
-
}
|
|
1003
|
-
throw new Error(`Terser failed for ${fileName}: ${result.error.message}. Saved bundle to ${tmpPath}`);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
return result.code || bundleContent;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
/**
|
|
1011
|
-
* Analyzes dependencies of a JavaScript file using simple regex
|
|
1012
|
-
*/
|
|
1013
|
-
analyzeDependencies(jsContent, componentPath) {
|
|
1014
|
-
const dependencies = [];
|
|
1015
|
-
|
|
1016
|
-
const resolveImportPath = (importPath) => {
|
|
1017
|
-
const resolvedPath = path.resolve(componentPath, importPath);
|
|
1018
|
-
let finalPath = resolvedPath;
|
|
1019
|
-
const ext = path.extname(resolvedPath);
|
|
1020
|
-
if (!ext) {
|
|
1021
|
-
const extensions = ['.js', '.json', '.mjs'];
|
|
1022
|
-
for (const extension of extensions) {
|
|
1023
|
-
if (fs.existsSync(resolvedPath + extension)) {
|
|
1024
|
-
finalPath = resolvedPath + extension;
|
|
1025
|
-
break;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
return fs.existsSync(finalPath) ? finalPath : null;
|
|
1031
|
-
};
|
|
1032
|
-
|
|
1033
|
-
try {
|
|
1034
|
-
const ast = parse(jsContent, {
|
|
1035
|
-
sourceType: 'module',
|
|
1036
|
-
plugins: ['jsx']
|
|
1037
|
-
});
|
|
1038
|
-
|
|
1039
|
-
traverse.default(ast, {
|
|
1040
|
-
ImportDeclaration(pathNode) {
|
|
1041
|
-
const importPath = pathNode.node.source.value;
|
|
1042
|
-
if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
const resolvedPath = resolveImportPath(importPath);
|
|
1047
|
-
if (!resolvedPath) {
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
const bindings = pathNode.node.specifiers.map(spec => {
|
|
1052
|
-
if (spec.type === 'ImportDefaultSpecifier') {
|
|
1053
|
-
return {
|
|
1054
|
-
type: 'default',
|
|
1055
|
-
importedName: 'default',
|
|
1056
|
-
localName: spec.local.name
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
if (spec.type === 'ImportSpecifier') {
|
|
1061
|
-
return {
|
|
1062
|
-
type: 'named',
|
|
1063
|
-
importedName: spec.imported.name,
|
|
1064
|
-
localName: spec.local.name
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
1069
|
-
return {
|
|
1070
|
-
type: 'namespace',
|
|
1071
|
-
localName: spec.local.name
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
return null;
|
|
1076
|
-
}).filter(Boolean);
|
|
1077
|
-
|
|
1078
|
-
dependencies.push({
|
|
1079
|
-
path: resolvedPath,
|
|
1080
|
-
bindings
|
|
1081
|
-
});
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
} catch (error) {
|
|
1085
|
-
console.warn(`Warning: Could not analyze dependencies for ${componentPath}:`, error.message);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
return dependencies;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
/**
|
|
1092
|
-
* Generates the content of a bundle
|
|
1093
|
-
*/
|
|
1094
|
-
async generateBundleContent(components, type, routePath, bundleKey, fileName) {
|
|
1095
|
-
const bundleComponents = [];
|
|
1096
|
-
const uniqueComponents = this.dedupeComponentsByName(components || []);
|
|
1097
|
-
|
|
1098
|
-
for (const comp of uniqueComponents) {
|
|
1099
|
-
const fileBaseName = comp.fileName || comp.name;
|
|
1100
|
-
const jsPath = path.join(comp.path, `${fileBaseName}.js`);
|
|
1101
|
-
const jsContent = await fs.readFile(jsPath, 'utf-8');
|
|
1102
|
-
|
|
1103
|
-
let htmlContent = null;
|
|
1104
|
-
let cssContent = null;
|
|
1105
|
-
|
|
1106
|
-
const htmlPath = path.join(comp.path, `${fileBaseName}.html`);
|
|
1107
|
-
const cssPath = path.join(comp.path, `${fileBaseName}.css`);
|
|
1108
|
-
|
|
1109
|
-
if (await fs.pathExists(htmlPath)) {
|
|
1110
|
-
htmlContent = await fs.readFile(htmlPath, 'utf-8');
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
if (await fs.pathExists(cssPath)) {
|
|
1114
|
-
cssContent = await fs.readFile(cssPath, 'utf-8');
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
|
|
1118
|
-
|
|
1119
|
-
bundleComponents.push({
|
|
1120
|
-
name: comp.name,
|
|
1121
|
-
category: comp.category,
|
|
1122
|
-
categoryType: comp.categoryType,
|
|
1123
|
-
js: cleanedJavaScript.code,
|
|
1124
|
-
hoistedImports: cleanedJavaScript.hoistedImports,
|
|
1125
|
-
html: htmlContent,
|
|
1126
|
-
css: cssContent,
|
|
1127
|
-
externalDependencies: await this.buildDependencyContents(jsContent, comp.path),
|
|
1128
|
-
size: comp.size
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
return this.generateBundleFileContent(fileName, type, this.sortComponentsByName(bundleComponents), routePath);
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
classFactoryName(componentName) {
|
|
1136
|
-
return `SLICE_CLASS_FACTORY_${this.toSafeIdentifier(componentName)}`;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
indentCodeBlock(code, spaces = 2) {
|
|
1140
|
-
const indentation = ' '.repeat(spaces);
|
|
1141
|
-
return String(code)
|
|
1142
|
-
.split('\n')
|
|
1143
|
-
.map((line) => `${indentation}${line}`)
|
|
1144
|
-
.join('\n');
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
generateBundleFileContent(fileName, type, components, routePath = null) {
|
|
1148
|
-
const uniqueComponents = this.dedupeComponentsByName(components || []);
|
|
1149
|
-
const bundleKey = type === 'critical'
|
|
1150
|
-
? 'critical'
|
|
1151
|
-
: type === 'framework'
|
|
1152
|
-
? 'framework'
|
|
1153
|
-
: this.routeToFileName(routePath || fileName.replace('slice-bundle.', '').replace('.js', ''));
|
|
1154
|
-
|
|
1155
|
-
const dependencyModules = this.collectDependencyModulesFromComponents(uniqueComponents);
|
|
1156
|
-
const isRouteBundle = type === 'route';
|
|
1157
|
-
const dependencyModuleBlock = this.buildV2DependencyModuleBlock(uniqueComponents, {
|
|
1158
|
-
includeSharedResolver: isRouteBundle,
|
|
1159
|
-
omittedDependencies: isRouteBundle ? this.vendorShared.sharedDependencySet : null
|
|
1160
|
-
});
|
|
1161
|
-
const rawHoistedImports = uniqueComponents
|
|
1162
|
-
.flatMap((component) => component.hoistedImports || [])
|
|
1163
|
-
.map((statement) => String(statement).trim())
|
|
1164
|
-
.filter(Boolean);
|
|
1165
|
-
const reservedIdentifiers = new Set([
|
|
1166
|
-
'SLICE_BUNDLE_META',
|
|
1167
|
-
'SLICE_BUNDLE_DEPENDENCIES',
|
|
1168
|
-
...uniqueComponents.map((component) => this.classFactoryName(component.name)),
|
|
1169
|
-
...uniqueComponents.map((component) => `__templateElement_${this.toSafeIdentifier(component.name)}`),
|
|
1170
|
-
...this.getDependencyExportVariableNames(dependencyModules)
|
|
1171
|
-
]);
|
|
1172
|
-
this.validateHoistedImportCollisions(rawHoistedImports, reservedIdentifiers);
|
|
1173
|
-
const hoistedImports = Array.from(new Set(rawHoistedImports));
|
|
1174
|
-
const hoistedImportBlock = hoistedImports.join('\n');
|
|
1175
|
-
|
|
1176
|
-
const classFactoryDefinitions = uniqueComponents
|
|
1177
|
-
.map((component) => {
|
|
1178
|
-
const factoryName = this.classFactoryName(component.name);
|
|
1179
|
-
const dependencyBindings = this.buildDependencyBindings(component.externalDependencies || {}, {
|
|
1180
|
-
preferShared: isRouteBundle
|
|
1181
|
-
});
|
|
1182
|
-
const body = component.js && component.js.trim()
|
|
1183
|
-
? component.js
|
|
1184
|
-
: `return window.${component.name};`;
|
|
1185
|
-
const bodyWithBindings = dependencyBindings
|
|
1186
|
-
? `${dependencyBindings}\n${body}`
|
|
1187
|
-
: body;
|
|
1188
|
-
return `const ${factoryName} = () => {\n${this.indentCodeBlock(bodyWithBindings, 2)}\n};`;
|
|
1189
|
-
})
|
|
1190
|
-
.join('\n\n');
|
|
1191
|
-
|
|
1192
|
-
const templateDeclarations = uniqueComponents
|
|
1193
|
-
.map((component) => {
|
|
1194
|
-
const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
|
|
1195
|
-
return `const ${templateVarName} = document.createElement('template');\n${templateVarName}.innerHTML = ${JSON.stringify(component.html || '')};`;
|
|
1196
|
-
})
|
|
1197
|
-
.join('\n');
|
|
1198
|
-
|
|
1199
|
-
const classRegistrations = uniqueComponents
|
|
1200
|
-
.map((component) => {
|
|
1201
|
-
const componentName = JSON.stringify(component.name);
|
|
1202
|
-
return ` if (!controller.classes.has(${componentName})) {\n controller.classes.set(${componentName}, ${this.classFactoryName(component.name)}());\n }`;
|
|
1203
|
-
})
|
|
1204
|
-
.join('\n');
|
|
1205
|
-
|
|
1206
|
-
const templateRegistrations = uniqueComponents
|
|
1207
|
-
.map((component) => {
|
|
1208
|
-
const componentName = JSON.stringify(component.name);
|
|
1209
|
-
const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
|
|
1210
|
-
return ` if (!controller.templates.has(${componentName})) {\n controller.templates.set(${componentName}, ${templateVarName});\n }`;
|
|
1211
|
-
})
|
|
1212
|
-
.join('\n');
|
|
1213
|
-
|
|
1214
|
-
const cssRegistrationInit = uniqueComponents.length
|
|
1215
|
-
? ` if (!stylesManager.__sliceRegisteredComponentStyles) {\n stylesManager.__sliceRegisteredComponentStyles = new Set();\n }`
|
|
1216
|
-
: '';
|
|
1217
|
-
|
|
1218
|
-
const cssRegistrations = uniqueComponents
|
|
1219
|
-
.map((component) => {
|
|
1220
|
-
const componentName = JSON.stringify(component.name);
|
|
1221
|
-
return ` if (!stylesManager.__sliceRegisteredComponentStyles.has(${componentName})) {\n stylesManager.registerComponentStyles(${componentName}, ${JSON.stringify(component.css || '')});\n stylesManager.__sliceRegisteredComponentStyles.add(${componentName});\n }`;
|
|
1222
|
-
})
|
|
1223
|
-
.join('\n');
|
|
1224
|
-
|
|
1225
|
-
const categoryRegistrations = uniqueComponents
|
|
1226
|
-
.map((component) => {
|
|
1227
|
-
const componentName = JSON.stringify(component.name);
|
|
1228
|
-
return ` if (!controller.componentCategories.has(${componentName})) {\n controller.componentCategories.set(${componentName}, ${JSON.stringify(component.category)});\n }`;
|
|
1229
|
-
})
|
|
1230
|
-
.join('\n');
|
|
1231
|
-
|
|
1232
|
-
const metadata = {
|
|
1233
|
-
version: '2',
|
|
1234
|
-
bundleKey,
|
|
1235
|
-
type,
|
|
1236
|
-
routes: routePath ? [routePath] : [],
|
|
1237
|
-
componentCount: uniqueComponents.length
|
|
1238
|
-
};
|
|
1239
|
-
|
|
1240
|
-
return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}export const SLICE_BUNDLE_META = ${JSON.stringify(metadata, null, 2)};\n\n${dependencyModuleBlock}\n\n${classFactoryDefinitions}\n\n${templateDeclarations}\n\nexport async function registerAll(controller, stylesManager) {\n${classRegistrations}\n${templateRegistrations}\n${cssRegistrationInit}${cssRegistrationInit ? '\n' : ''}${cssRegistrations}\n${categoryRegistrations}\n}\n`;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
buildV2DependencyModuleBlock(components, options = {}) {
|
|
1244
|
-
const modules = this.collectDependencyModulesFromComponents(components);
|
|
1245
|
-
return this.buildV2DependencyModuleBlockFromModules(modules, options);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
buildV2DependencyModuleBlockFromModules(modules = [], options = {}) {
|
|
1249
|
-
const omittedDependencies = options.omittedDependencies instanceof Set
|
|
1250
|
-
? options.omittedDependencies
|
|
1251
|
-
: new Set(options.omittedDependencies || []);
|
|
1252
|
-
// Emit in topological order so a module is registered before any module
|
|
1253
|
-
// that depends on it (its transitive imports resolve at IIFE-eval time).
|
|
1254
|
-
const filteredModules = this.sortDependencyModulesTopologically(
|
|
1255
|
-
modules.filter((module) => !omittedDependencies.has(module.name))
|
|
1256
|
-
);
|
|
1257
|
-
|
|
1258
|
-
const lines = [
|
|
1259
|
-
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1260
|
-
...this.getDefaultExportResolverLines()
|
|
1261
|
-
];
|
|
1262
|
-
if (options.includeSharedResolver) {
|
|
1263
|
-
lines.push(...this.getBundleDependencyResolverLines());
|
|
1264
|
-
}
|
|
1265
|
-
filteredModules.forEach((module, index) => {
|
|
1266
|
-
const exportVar = `__sliceDepExports${index}`;
|
|
1267
|
-
// Evaluate each dependency inside its own IIFE so its private,
|
|
1268
|
-
// non-exported top-level bindings stay local and cannot collide with
|
|
1269
|
-
// another dependency's (or the bundle's) identifiers. Only the exports
|
|
1270
|
-
// object escapes the closure.
|
|
1271
|
-
const transformedContent = this.transformDependencyContent(module.content, '__sliceExports', module.name);
|
|
1272
|
-
// Bind this module's own (transitive) imports inside its IIFE — they were
|
|
1273
|
-
// registered by earlier modules in the topological order.
|
|
1274
|
-
const importBindings = this.buildDependencyBindings(
|
|
1275
|
-
Object.fromEntries(
|
|
1276
|
-
(module.moduleImports || [])
|
|
1277
|
-
.filter((mi) => mi.bindings && mi.bindings.length)
|
|
1278
|
-
.map((mi) => [mi.depName, { bindings: mi.bindings }])
|
|
1279
|
-
),
|
|
1280
|
-
{ preferShared: !!options.includeSharedResolver }
|
|
1281
|
-
);
|
|
1282
|
-
const body = transformedContent.trim();
|
|
1283
|
-
lines.push(`const ${exportVar} = (() => {`);
|
|
1284
|
-
lines.push('const __sliceExports = {};');
|
|
1285
|
-
if (importBindings) lines.push(importBindings);
|
|
1286
|
-
if (body) lines.push(body);
|
|
1287
|
-
lines.push('return __sliceExports;');
|
|
1288
|
-
lines.push('})();');
|
|
1289
|
-
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1290
|
-
});
|
|
1291
|
-
|
|
1292
|
-
return lines.join('\n');
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
sortDependencyModulesTopologically(modules = []) {
|
|
1296
|
-
const byName = new Map(modules.map((module) => [module.name, module]));
|
|
1297
|
-
const visited = new Set();
|
|
1298
|
-
const ordered = [];
|
|
1299
|
-
const visit = (module, stack) => {
|
|
1300
|
-
if (visited.has(module.name) || stack.has(module.name)) return;
|
|
1301
|
-
stack.add(module.name);
|
|
1302
|
-
for (const imp of module.moduleImports || []) {
|
|
1303
|
-
const dependency = byName.get(imp.depName);
|
|
1304
|
-
if (dependency) visit(dependency, stack);
|
|
1305
|
-
}
|
|
1306
|
-
stack.delete(module.name);
|
|
1307
|
-
visited.add(module.name);
|
|
1308
|
-
ordered.push(module);
|
|
1309
|
-
};
|
|
1310
|
-
for (const module of modules) visit(module, new Set());
|
|
1311
|
-
return ordered;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
async buildDependencyContents(jsContent, componentPath) {
|
|
1315
|
-
const dependencyContents = {};
|
|
1316
|
-
const visited = new Set();
|
|
1317
|
-
|
|
1318
|
-
// Recursively resolve the relative-import graph rooted at `content`, so a
|
|
1319
|
-
// dependency module's OWN (transitive) imports are inlined too. Returns the
|
|
1320
|
-
// consumer's direct imports as [{ depName, bindings }].
|
|
1321
|
-
const resolveModule = async (content, basePath) => {
|
|
1322
|
-
const consumerImports = [];
|
|
1323
|
-
|
|
1324
|
-
for (const dep of this.analyzeDependencies(content, basePath)) {
|
|
1325
|
-
const depName = path.relative(this.srcPath, dep.path).replace(/\\/g, '/');
|
|
1326
|
-
consumerImports.push({ depName, bindings: dep.bindings || [] });
|
|
1327
|
-
|
|
1328
|
-
if (visited.has(depName)) continue;
|
|
1329
|
-
visited.add(depName);
|
|
1330
|
-
|
|
1331
|
-
try {
|
|
1332
|
-
const depContent = await fs.readFile(dep.path, 'utf-8');
|
|
1333
|
-
// Resolve this module's own transitive imports first.
|
|
1334
|
-
const moduleImports = await resolveModule(depContent, path.dirname(dep.path));
|
|
1335
|
-
dependencyContents[depName] = { content: depContent, bindings: [], moduleImports };
|
|
1336
|
-
} catch (error) {
|
|
1337
|
-
console.warn(`Warning: Could not read dependency ${dep.path}:`, error.message);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
return consumerImports;
|
|
1342
|
-
};
|
|
1343
|
-
|
|
1344
|
-
const directImports = await resolveModule(jsContent, componentPath);
|
|
1345
|
-
// The component's direct imports drive its class-factory bindings.
|
|
1346
|
-
for (const { depName, bindings } of directImports) {
|
|
1347
|
-
if (dependencyContents[depName]) {
|
|
1348
|
-
dependencyContents[depName].bindings = bindings;
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
return dependencyContents;
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
/**
|
|
1356
|
-
* Cleans JavaScript code by removing imports/exports and ensuring class is available globally
|
|
1357
|
-
*/
|
|
1358
|
-
cleanJavaScript(code, componentName, sourceContext = componentName) {
|
|
1359
|
-
// Remove export default
|
|
1360
|
-
code = code.replace(/export\s+default\s+/g, '');
|
|
1361
|
-
|
|
1362
|
-
// Remove only unsupported imports (relative always removed, allowed absolute kept)
|
|
1363
|
-
const stripped = this.stripImports(code, {
|
|
1364
|
-
sourceContext,
|
|
1365
|
-
collectHoistedImports: true
|
|
1366
|
-
});
|
|
1367
|
-
const hoistedImports = stripped.hoistedImports || [];
|
|
1368
|
-
code = stripped.code;
|
|
1369
|
-
|
|
1370
|
-
// Guard customElements.define to avoid duplicate registrations
|
|
1371
|
-
code = code.replace(
|
|
1372
|
-
/customElements\.define\(([^)]+)\);?/g,
|
|
1373
|
-
(match, args) => {
|
|
1374
|
-
const firstArg = args.split(',')[0]?.trim() || '';
|
|
1375
|
-
if (!/^['"][^'"]+['"]$/.test(firstArg)) {
|
|
1376
|
-
return match;
|
|
1377
|
-
}
|
|
1378
|
-
return `if (!customElements.get(${firstArg})) { customElements.define(${args}); }`;
|
|
1379
|
-
}
|
|
1380
|
-
);
|
|
1381
|
-
|
|
1382
|
-
// Make sure the class is available globally for bundle evaluation
|
|
1383
|
-
// Preserve original customElements.define if it exists
|
|
1384
|
-
if (code.includes('customElements.define')) {
|
|
1385
|
-
// Add global assignment before guarded or direct customElements.define
|
|
1386
|
-
const globalAssignment = `window.${componentName} = ${componentName};\n`;
|
|
1387
|
-
const guardedDefineRegex = /if\s*\(\s*!\s*customElements\.get\([^)]*\)\s*\)\s*\{\s*customElements\.define\([^;]+\);?\s*\}\s*$/;
|
|
1388
|
-
const directDefineRegex = /customElements\.define\([^;]+\);?\s*$/;
|
|
1389
|
-
if (guardedDefineRegex.test(code)) {
|
|
1390
|
-
code = code.replace(guardedDefineRegex, `${globalAssignment}$&`);
|
|
1391
|
-
} else {
|
|
1392
|
-
code = code.replace(directDefineRegex, `${globalAssignment}$&`);
|
|
1393
|
-
}
|
|
1394
|
-
} else {
|
|
1395
|
-
// If no customElements.define found, just assign to global
|
|
1396
|
-
code += `\nwindow.${componentName} = ${componentName};`;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
// Add return statement for bundle evaluation compatibility
|
|
1400
|
-
code += `\nreturn ${componentName};`;
|
|
1401
|
-
|
|
1402
|
-
return {
|
|
1403
|
-
code,
|
|
1404
|
-
hoistedImports
|
|
1405
|
-
};
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
/**
|
|
1409
|
-
* Formats the bundle file
|
|
1410
|
-
*/
|
|
1411
|
-
formatBundleFile(componentsData, metadata) {
|
|
1412
|
-
const integrityPayload = {
|
|
1413
|
-
metadata: {
|
|
1414
|
-
...metadata,
|
|
1415
|
-
generated: 'static'
|
|
1416
|
-
},
|
|
1417
|
-
components: Object.fromEntries(
|
|
1418
|
-
Object.entries(componentsData).map(([name, data]) => [
|
|
1419
|
-
name,
|
|
1420
|
-
{
|
|
1421
|
-
name: data.name,
|
|
1422
|
-
category: data.category,
|
|
1423
|
-
categoryType: data.categoryType,
|
|
1424
|
-
componentDependencies: data.componentDependencies
|
|
1425
|
-
}
|
|
1426
|
-
])
|
|
1427
|
-
)
|
|
1428
|
-
};
|
|
1429
|
-
const integrity = `sha256:${crypto
|
|
1430
|
-
.createHash('sha256')
|
|
1431
|
-
.update(JSON.stringify(integrityPayload))
|
|
1432
|
-
.digest('hex')}`;
|
|
1433
|
-
|
|
1434
|
-
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1435
|
-
const frameworkComponentKeys = Object.keys(componentsData || {});
|
|
1436
|
-
const frameworkClassIdentifiers = frameworkComponentKeys.map((key) => this.toSafeIdentifier(key));
|
|
1437
|
-
const frameworkReservedIdentifiers = new Set([
|
|
1438
|
-
'SLICE_BUNDLE',
|
|
1439
|
-
'SLICE_BUNDLE_COMPONENTS',
|
|
1440
|
-
'SLICE_BUNDLE_DEPENDENCIES',
|
|
1441
|
-
'SLICE_FRAMEWORK_CLASSES',
|
|
1442
|
-
...frameworkClassIdentifiers,
|
|
1443
|
-
...this.getDependencyExportVariableNames(dependencyModules)
|
|
1444
|
-
]);
|
|
1445
|
-
const rawHoistedImports = Object.values(componentsData || {})
|
|
1446
|
-
.flatMap((component) => component?.hoistedImports || [])
|
|
1447
|
-
.map((statement) => String(statement).trim())
|
|
1448
|
-
.filter(Boolean);
|
|
1449
|
-
this.validateHoistedImportCollisions(rawHoistedImports, frameworkReservedIdentifiers);
|
|
1450
|
-
const hoistedImportBlock = Array.from(new Set(rawHoistedImports)).join('\n');
|
|
1451
|
-
|
|
1452
|
-
const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
|
|
1453
|
-
const componentBlock = this.buildComponentBundleBlock(componentsData);
|
|
1454
|
-
|
|
1455
|
-
return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}/**
|
|
1456
|
-
* Slice.js Bundle
|
|
1457
|
-
* Type: ${metadata.type}
|
|
1458
|
-
* Generated: ${metadata.generated}
|
|
1459
|
-
* Strategy: ${metadata.strategy}
|
|
1460
|
-
* Components: ${metadata.componentCount}
|
|
1461
|
-
* Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
|
|
1462
|
-
*/
|
|
1463
|
-
|
|
1464
|
-
${dependencyBlock}
|
|
1465
|
-
${componentBlock}
|
|
1466
|
-
|
|
1467
|
-
export const SLICE_BUNDLE = {
|
|
1468
|
-
metadata: ${JSON.stringify({ ...metadata, integrity }, null, 2)},
|
|
1469
|
-
components: SLICE_BUNDLE_COMPONENTS
|
|
1470
|
-
};
|
|
1471
|
-
|
|
1472
|
-
// Auto-registration of components
|
|
1473
|
-
if (window.slice && window.slice.controller) {
|
|
1474
|
-
slice.controller.registerBundle(SLICE_BUNDLE);
|
|
1475
|
-
}
|
|
1476
|
-
`;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
buildDependencyModuleBlock(componentsData) {
|
|
1480
|
-
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1481
|
-
const lines = [
|
|
1482
|
-
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1483
|
-
...this.getDefaultExportResolverLines()
|
|
1484
|
-
];
|
|
1485
|
-
if (dependencyModules.length === 0) {
|
|
1486
|
-
return `${lines.join('\n')}`;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
dependencyModules.forEach((module, index) => {
|
|
1490
|
-
const exportVar = `__sliceDepExports${index}`;
|
|
1491
|
-
// Each dependency lives in its own IIFE scope (see
|
|
1492
|
-
// buildV2DependencyModuleBlockFromModules) so private helpers cannot
|
|
1493
|
-
// collide across modules.
|
|
1494
|
-
const content = this.transformDependencyContent(module.content, '__sliceExports', module.name);
|
|
1495
|
-
const body = content.trim();
|
|
1496
|
-
lines.push(`// Dependency: ${module.name}`);
|
|
1497
|
-
lines.push(`const ${exportVar} = (() => {`);
|
|
1498
|
-
lines.push('const __sliceExports = {};');
|
|
1499
|
-
if (body) lines.push(body);
|
|
1500
|
-
lines.push('return __sliceExports;');
|
|
1501
|
-
lines.push('})();');
|
|
1502
|
-
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1503
|
-
});
|
|
1504
|
-
|
|
1505
|
-
return `${lines.join('\n')}`;
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
collectDependencyModules(componentsData) {
|
|
1509
|
-
const modules = new Map();
|
|
1510
|
-
Object.values(componentsData).forEach((component) => {
|
|
1511
|
-
Object.entries(component.externalDependencies || {}).forEach(([name, entry]) => {
|
|
1512
|
-
if (modules.has(name)) return;
|
|
1513
|
-
const content = typeof entry === 'string' ? entry : entry.content;
|
|
1514
|
-
modules.set(name, { name, content });
|
|
1515
|
-
});
|
|
1516
|
-
});
|
|
1517
|
-
return Array.from(modules.values());
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
collectDependencyModulesFromComponents(components = []) {
|
|
1521
|
-
const modules = new Map();
|
|
1522
|
-
for (const component of components || []) {
|
|
1523
|
-
const externalDependencies = component.externalDependencies || {};
|
|
1524
|
-
for (const [moduleName, entry] of Object.entries(externalDependencies)) {
|
|
1525
|
-
if (modules.has(moduleName)) continue;
|
|
1526
|
-
const content = typeof entry === 'string' ? entry : entry?.content;
|
|
1527
|
-
if (!content) continue;
|
|
1528
|
-
const moduleImports = (entry && typeof entry === 'object' ? entry.moduleImports : null) || [];
|
|
1529
|
-
modules.set(moduleName, { name: moduleName, content, moduleImports });
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
return Array.from(modules.values());
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
getDependencyExportVariableNames(dependencyModules = []) {
|
|
1536
|
-
return (dependencyModules || []).map((_, index) => `__sliceDepExports${index}`);
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
transformDependencyContent(content, exportVar, moduleName) {
|
|
1540
|
-
let ast;
|
|
1541
|
-
try {
|
|
1542
|
-
ast = parse(content, { sourceType: 'module', plugins: ['jsx'] });
|
|
1543
|
-
} catch (error) {
|
|
1544
|
-
// Unparseable content (e.g. TS syntax): fall back to the regex transform
|
|
1545
|
-
// so we never lose a dependency entirely.
|
|
1546
|
-
return this.transformDependencyContentRegexFallback(content, exportVar, moduleName);
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
const fallbackKey = this.getDependencyDefaultFallbackKey(moduleName);
|
|
1550
|
-
const statements = ast.program.body
|
|
1551
|
-
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
|
|
1552
|
-
.sort((a, b) => a.start - b.start);
|
|
1553
|
-
|
|
1554
|
-
let cursor = 0;
|
|
1555
|
-
let output = '';
|
|
1556
|
-
for (const node of statements) {
|
|
1557
|
-
output += content.slice(cursor, node.start);
|
|
1558
|
-
output += this.transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey);
|
|
1559
|
-
cursor = node.end;
|
|
1560
|
-
}
|
|
1561
|
-
output += content.slice(cursor);
|
|
1562
|
-
return output;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
describeExportTarget(exportVar, name) {
|
|
1566
|
-
return /^[A-Za-z_$][\w$]*$/.test(name)
|
|
1567
|
-
? `${exportVar}.${name}`
|
|
1568
|
-
: `${exportVar}[${JSON.stringify(name)}]`;
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
collectPatternIdentifiers(node, acc = []) {
|
|
1572
|
-
if (!node) return acc;
|
|
1573
|
-
switch (node.type) {
|
|
1574
|
-
case 'Identifier':
|
|
1575
|
-
acc.push(node.name);
|
|
1576
|
-
break;
|
|
1577
|
-
case 'ObjectPattern':
|
|
1578
|
-
for (const prop of node.properties) {
|
|
1579
|
-
if (prop.type === 'RestElement') {
|
|
1580
|
-
this.collectPatternIdentifiers(prop.argument, acc);
|
|
1581
|
-
} else {
|
|
1582
|
-
this.collectPatternIdentifiers(prop.value, acc);
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
break;
|
|
1586
|
-
case 'ArrayPattern':
|
|
1587
|
-
for (const element of node.elements) {
|
|
1588
|
-
if (element) this.collectPatternIdentifiers(element, acc);
|
|
1589
|
-
}
|
|
1590
|
-
break;
|
|
1591
|
-
case 'AssignmentPattern':
|
|
1592
|
-
this.collectPatternIdentifiers(node.left, acc);
|
|
1593
|
-
break;
|
|
1594
|
-
case 'RestElement':
|
|
1595
|
-
this.collectPatternIdentifiers(node.argument, acc);
|
|
1596
|
-
break;
|
|
1597
|
-
default:
|
|
1598
|
-
break;
|
|
1599
|
-
}
|
|
1600
|
-
return acc;
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
transformExportedDeclaration(decl, content, exportVar) {
|
|
1604
|
-
const sourceOf = (n) => content.slice(n.start, n.end);
|
|
1605
|
-
|
|
1606
|
-
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
|
|
1607
|
-
const name = decl.id?.name;
|
|
1608
|
-
if (!name) return sourceOf(decl);
|
|
1609
|
-
// Keep the declaration so other code in the module can still reference the
|
|
1610
|
-
// name (intra-module references), then mirror it onto the exports object.
|
|
1611
|
-
// Each dependency is IIFE-scoped, so this local binding can't collide.
|
|
1612
|
-
return `${sourceOf(decl)}\n${this.describeExportTarget(exportVar, name)} = ${name};`;
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
if (decl.type === 'VariableDeclaration') {
|
|
1616
|
-
// Keep the declaration verbatim — preserving intra-module references and
|
|
1617
|
-
// initializer evaluation — then export every bound name.
|
|
1618
|
-
const names = [];
|
|
1619
|
-
for (const declarator of decl.declarations) {
|
|
1620
|
-
names.push(...this.collectPatternIdentifiers(declarator.id, []));
|
|
1621
|
-
}
|
|
1622
|
-
const assigns = names
|
|
1623
|
-
.map((n) => `${this.describeExportTarget(exportVar, n)} = ${n};`)
|
|
1624
|
-
.join('\n');
|
|
1625
|
-
return `${sourceOf(decl)}\n${assigns}`;
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
return sourceOf(decl);
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey) {
|
|
1632
|
-
const sourceOf = (n) => content.slice(n.start, n.end);
|
|
1633
|
-
|
|
1634
|
-
if (node.type === 'ImportDeclaration') {
|
|
1635
|
-
// Transitive imports of a bundled dependency cannot be resolved at
|
|
1636
|
-
// runtime; strip them so they never leak into the emitted bundle.
|
|
1637
|
-
console.warn(this.buildImportWarningMessage(
|
|
1638
|
-
`Warning: Stripping unsupported import inside bundled dependency: ${node.source?.value}`,
|
|
1639
|
-
moduleName
|
|
1640
|
-
));
|
|
1641
|
-
return '';
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
if (node.type === 'ExportAllDeclaration') {
|
|
1645
|
-
console.warn(this.buildImportWarningMessage(
|
|
1646
|
-
`Warning: Dropping unsupported 'export *' inside bundled dependency: ${node.source?.value}`,
|
|
1647
|
-
moduleName
|
|
1648
|
-
));
|
|
1649
|
-
return '';
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
if (node.type === 'ExportDefaultDeclaration') {
|
|
1653
|
-
const declSource = sourceOf(node.declaration);
|
|
1654
|
-
const lines = [`${exportVar}.default = (${declSource});`];
|
|
1655
|
-
if (fallbackKey && fallbackKey !== 'default') {
|
|
1656
|
-
// Preserve the historical `<basename>Data` key so existing default
|
|
1657
|
-
// bindings (which pass it as the preferred key) keep resolving.
|
|
1658
|
-
lines.push(`${this.describeExportTarget(exportVar, fallbackKey)} = ${exportVar}.default;`);
|
|
1659
|
-
}
|
|
1660
|
-
return lines.join('\n');
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
if (node.type === 'ExportNamedDeclaration') {
|
|
1664
|
-
if (node.source) {
|
|
1665
|
-
console.warn(this.buildImportWarningMessage(
|
|
1666
|
-
`Warning: Dropping unsupported re-export inside bundled dependency: ${node.source.value}`,
|
|
1667
|
-
moduleName
|
|
1668
|
-
));
|
|
1669
|
-
return '';
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
if (node.declaration) {
|
|
1673
|
-
return this.transformExportedDeclaration(node.declaration, content, exportVar);
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
// `export { local as exported, ... }` — key the exports object by the
|
|
1677
|
-
// PUBLIC (exported) name, mapped to the local binding's value.
|
|
1678
|
-
return node.specifiers
|
|
1679
|
-
.map((spec) => {
|
|
1680
|
-
const localName = spec.local.name;
|
|
1681
|
-
const exportedName = spec.exported.name ?? spec.exported.value;
|
|
1682
|
-
return `${this.describeExportTarget(exportVar, exportedName)} = ${localName};`;
|
|
1683
|
-
})
|
|
1684
|
-
.join('\n');
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
// Any other top-level statement is kept verbatim.
|
|
1688
|
-
return sourceOf(node);
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
transformDependencyContentRegexFallback(content, exportVar, moduleName) {
|
|
1692
|
-
const dataName = this.getDependencyDefaultFallbackKey(moduleName);
|
|
1693
|
-
const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
|
|
1694
|
-
|
|
1695
|
-
return content
|
|
1696
|
-
.replace(/export\s+const\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1697
|
-
.replace(/export\s+let\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1698
|
-
.replace(/export\s+var\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1699
|
-
.replace(/export\s+function\s+(\w+)/g, `${exportVar}.$1 = function`)
|
|
1700
|
-
.replace(/export\s+default\s+/g, exportPrefix)
|
|
1701
|
-
.replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
|
|
1702
|
-
return exportsStr
|
|
1703
|
-
.split(',')
|
|
1704
|
-
.map((exp) => {
|
|
1705
|
-
const cleanExp = exp.trim();
|
|
1706
|
-
const varName = cleanExp.split(' as ')[0].trim();
|
|
1707
|
-
return `${exportVar}.${varName} = ${varName};`;
|
|
1708
|
-
})
|
|
1709
|
-
.join('\n');
|
|
1710
|
-
})
|
|
1711
|
-
.replace(/^\s*export\s+/gm, '');
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
getDependencyDefaultFallbackKey(moduleName) {
|
|
1715
|
-
const baseName = moduleName?.split('/').pop()?.replace(/\.[^.]+$/, '');
|
|
1716
|
-
return baseName ? `${baseName}Data` : null;
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
buildComponentBundleBlock(componentsData) {
|
|
1720
|
-
const componentEntries = [];
|
|
1721
|
-
const componentDefs = [];
|
|
1722
|
-
const frameworkEntries = [];
|
|
1723
|
-
|
|
1724
|
-
Object.entries(componentsData).forEach(([name, data]) => {
|
|
1725
|
-
const classVar = this.toSafeIdentifier(name);
|
|
1726
|
-
const bindings = this.buildDependencyBindings(data.externalDependencies || {});
|
|
1727
|
-
|
|
1728
|
-
componentDefs.push(`const ${classVar} = (() => {\n${bindings}\n${data.js}\nreturn ${name};\n})();`);
|
|
1729
|
-
|
|
1730
|
-
if (data.isFramework) {
|
|
1731
|
-
frameworkEntries.push(`${JSON.stringify(data.name)}: ${classVar}`);
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
componentEntries.push(
|
|
1735
|
-
`${JSON.stringify(name)}: {\n` +
|
|
1736
|
-
` name: ${JSON.stringify(data.name)},\n` +
|
|
1737
|
-
` category: ${JSON.stringify(data.category)},\n` +
|
|
1738
|
-
` categoryType: ${JSON.stringify(data.categoryType)},\n` +
|
|
1739
|
-
` componentDependencies: ${JSON.stringify(data.componentDependencies)},\n` +
|
|
1740
|
-
` html: ${JSON.stringify(data.html)},\n` +
|
|
1741
|
-
` css: ${JSON.stringify(data.css)},\n` +
|
|
1742
|
-
` size: ${JSON.stringify(data.size)},\n` +
|
|
1743
|
-
` class: ${classVar}\n` +
|
|
1744
|
-
`}`
|
|
1745
|
-
);
|
|
1746
|
-
});
|
|
1747
|
-
|
|
1748
|
-
const frameworkBlock = frameworkEntries.length > 0
|
|
1749
|
-
? `const SLICE_FRAMEWORK_CLASSES = {\n${frameworkEntries.join(',\n')}\n};\nwindow.SLICE_FRAMEWORK_CLASSES = SLICE_FRAMEWORK_CLASSES;`
|
|
1750
|
-
: '';
|
|
1751
|
-
|
|
1752
|
-
return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
buildDependencyBindings(externalDependencies, options = {}) {
|
|
1756
|
-
const lines = [];
|
|
1757
|
-
Object.entries(externalDependencies).forEach(([name, entry]) => {
|
|
1758
|
-
const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
|
|
1759
|
-
const depVar = options.preferShared
|
|
1760
|
-
? `__sliceResolveBundleDependency(${JSON.stringify(name)})`
|
|
1761
|
-
: `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
|
|
1762
|
-
|
|
1763
|
-
bindings.forEach((binding) => {
|
|
1764
|
-
if (!binding?.localName) return;
|
|
1765
|
-
if (binding.type === 'default') {
|
|
1766
|
-
const preferredKey = this.getDependencyDefaultFallbackKey(name);
|
|
1767
|
-
lines.push(`const ${binding.localName} = __sliceResolveDefaultExport(${depVar}, ${JSON.stringify(name)}, ${JSON.stringify(preferredKey)});`);
|
|
1768
|
-
}
|
|
1769
|
-
if (binding.type === 'named') {
|
|
1770
|
-
lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
|
|
1771
|
-
}
|
|
1772
|
-
if (binding.type === 'namespace') {
|
|
1773
|
-
lines.push(`const ${binding.localName} = ${depVar};`);
|
|
1774
|
-
}
|
|
1775
|
-
});
|
|
1776
|
-
});
|
|
1777
|
-
|
|
1778
|
-
return lines.join('\n');
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
getBundleDependencyResolverLines() {
|
|
1782
|
-
return [
|
|
1783
|
-
"const __sliceSharedDeps = typeof window !== 'undefined' ? (window.__SLICE_SHARED_DEPS__ || {}) : {};",
|
|
1784
|
-
'const __sliceResolveBundleDependency = (depName) => Object.prototype.hasOwnProperty.call(__sliceSharedDeps, depName) ? __sliceSharedDeps[depName] : SLICE_BUNDLE_DEPENDENCIES[depName];'
|
|
1785
|
-
];
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
getDefaultExportResolverLines() {
|
|
1789
|
-
return [
|
|
1790
|
-
'const __sliceDefaultExportWarningDeps = new Set();',
|
|
1791
|
-
"const __sliceDefaultExportPreferredKeys = ['module', 'exports', 'purify'];",
|
|
1792
|
-
'const __sliceDeterministicKeyCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);',
|
|
1793
|
-
'function __sliceResolveDefaultExport(dep, depName, preferredKey) {',
|
|
1794
|
-
' if (dep?.default !== undefined) return dep.default;',
|
|
1795
|
-
" if (dep === null || (typeof dep !== 'object' && typeof dep !== 'function')) return dep;",
|
|
1796
|
-
" if (preferredKey && preferredKey !== 'default' && preferredKey !== '__esModule' && Object.prototype.hasOwnProperty.call(dep, preferredKey)) return dep[preferredKey];",
|
|
1797
|
-
" const keys = Object.keys(dep).filter((key) => key !== 'default' && key !== '__esModule');",
|
|
1798
|
-
' if (keys.length === 1) return dep[keys[0]];',
|
|
1799
|
-
' if (keys.length > 1) {',
|
|
1800
|
-
' const preferredMatches = __sliceDefaultExportPreferredKeys.filter((key) => keys.includes(key));',
|
|
1801
|
-
' if (preferredMatches.length === 1) return dep[preferredMatches[0]];',
|
|
1802
|
-
' const sortedKeys = [...keys].sort(__sliceDeterministicKeyCompare);',
|
|
1803
|
-
' const fallbackKey = sortedKeys[0];',
|
|
1804
|
-
' const warningDepName = depName || "<unknown dependency>";',
|
|
1805
|
-
' if (!__sliceDefaultExportWarningDeps.has(warningDepName)) {',
|
|
1806
|
-
' __sliceDefaultExportWarningDeps.add(warningDepName);',
|
|
1807
|
-
' console.warn(`[Slice.js bundler] Ambiguous default export resolution for "${warningDepName}". Falling back to "${fallbackKey}". Keys: ${sortedKeys.join(\', \')}`);',
|
|
1808
|
-
' }',
|
|
1809
|
-
' return dep[fallbackKey];',
|
|
1810
|
-
' }',
|
|
1811
|
-
' return dep;',
|
|
1812
|
-
'}'
|
|
1813
|
-
];
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
toSafeIdentifier(name) {
|
|
1817
|
-
// Injective encoding: every character outside [A-Za-z0-9] (including '_')
|
|
1818
|
-
// is escaped to `_<hex>_`. This guarantees that two distinct component
|
|
1819
|
-
// names can never collapse to the same identifier (e.g. "my-btn" and
|
|
1820
|
-
// "my_btn" used to both yield "SliceComponent_my_btn", emitting duplicate
|
|
1821
|
-
// `const` declarations and producing invalid bundle JS). The leading
|
|
1822
|
-
// `SliceComponent_` prefix keeps the result a valid identifier even when
|
|
1823
|
-
// the name starts with a digit.
|
|
1824
|
-
const encoded = String(name).replace(
|
|
1825
|
-
/[^a-zA-Z0-9]/g,
|
|
1826
|
-
(char) => `_${char.charCodeAt(0).toString(16)}_`
|
|
1827
|
-
);
|
|
1828
|
-
return `SliceComponent_${encoded}`;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
/**
|
|
1832
|
-
* Generates the bundle configuration
|
|
1833
|
-
*/
|
|
1834
|
-
generateBundleConfig(frameworkBundle = null) {
|
|
1835
|
-
const metrics = this.analysisData.metrics || {};
|
|
1836
|
-
const config = {
|
|
1837
|
-
version: '2.0.0',
|
|
1838
|
-
format: this.format,
|
|
1839
|
-
loadingPolicy: this.loadingPolicy,
|
|
1840
|
-
strategy: this.config.strategy,
|
|
1841
|
-
minified: this.options.minify,
|
|
1842
|
-
obfuscated: this.options.obfuscate,
|
|
1843
|
-
production: true,
|
|
1844
|
-
generated: new Date().toISOString(),
|
|
1845
|
-
|
|
1846
|
-
stats: {
|
|
1847
|
-
totalComponents: metrics.totalComponents || 0,
|
|
1848
|
-
totalRoutes: metrics.totalRoutes || 0,
|
|
1849
|
-
sharedComponents: this.bundles.critical.components.length,
|
|
1850
|
-
sharedPercentage: metrics.sharedPercentage || 0,
|
|
1851
|
-
totalSize: metrics.totalSize || 0,
|
|
1852
|
-
criticalSize: this.bundles.critical.size
|
|
1853
|
-
},
|
|
1854
|
-
|
|
1855
|
-
bundles: {
|
|
1856
|
-
framework: {
|
|
1857
|
-
file: 'slice-bundle.framework.js',
|
|
1858
|
-
size: 0,
|
|
1859
|
-
hash: null,
|
|
1860
|
-
integrity: null,
|
|
1861
|
-
components: []
|
|
1862
|
-
},
|
|
1863
|
-
// Only advertise the vendor-shared bundle when it was actually emitted
|
|
1864
|
-
// (i.e. there were shared dependencies). Otherwise the config would
|
|
1865
|
-
// reference a file that does not exist on disk -> 404 for any runtime
|
|
1866
|
-
// that resolves it.
|
|
1867
|
-
vendorShared: this.vendorShared.bundle
|
|
1868
|
-
? {
|
|
1869
|
-
bundleKey: 'vendor-shared',
|
|
1870
|
-
type: 'vendor-shared',
|
|
1871
|
-
file: this.vendorShared.file,
|
|
1872
|
-
size: this.vendorShared.bundle?.size || 0,
|
|
1873
|
-
hash: this.vendorShared.bundle?.hash || null,
|
|
1874
|
-
integrity: this.vendorShared.bundle?.integrity || null,
|
|
1875
|
-
dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
|
|
1876
|
-
dependencyCount: this.vendorShared.sharedDependencySet.size,
|
|
1877
|
-
routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
|
|
1878
|
-
}
|
|
1879
|
-
: null,
|
|
1880
|
-
critical: {
|
|
1881
|
-
file: this.bundles.critical.file,
|
|
1882
|
-
size: this.bundles.critical.size,
|
|
1883
|
-
hash: this.bundles.critical.hash || null,
|
|
1884
|
-
integrity: this.bundles.critical.integrity || null,
|
|
1885
|
-
components: this.bundles.critical.components.map(c => c.name)
|
|
1886
|
-
},
|
|
1887
|
-
routes: {}
|
|
1888
|
-
},
|
|
1889
|
-
routeBundles: {},
|
|
1890
|
-
routeDependencyGraph: {}
|
|
1891
|
-
};
|
|
1892
|
-
|
|
1893
|
-
for (const [key, bundle] of Object.entries(this.bundles.routes)) {
|
|
1894
|
-
const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
|
|
1895
|
-
? key
|
|
1896
|
-
: (bundle.path || bundle.paths || key);
|
|
1897
|
-
const usesVendorShared = this.vendorShared.bundleKeysUsingSharedDependencies.has(key)
|
|
1898
|
-
|| (bundle.dependencies || []).includes('vendor-shared');
|
|
1899
|
-
const dependencies = this.mergeBundleDependencies(
|
|
1900
|
-
bundle.dependencies || [],
|
|
1901
|
-
usesVendorShared ? ['vendor-shared'] : []
|
|
1902
|
-
);
|
|
1903
|
-
|
|
1904
|
-
config.bundles.routes[key] = {
|
|
1905
|
-
path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
|
|
1906
|
-
file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
|
|
1907
|
-
size: bundle.size,
|
|
1908
|
-
hash: bundle.hash || null,
|
|
1909
|
-
integrity: bundle.integrity || null,
|
|
1910
|
-
components: bundle.components.map(c => c.name),
|
|
1911
|
-
dependencies
|
|
1912
|
-
};
|
|
1913
|
-
|
|
1914
|
-
const paths = Array.isArray(config.bundles.routes[key].path)
|
|
1915
|
-
? config.bundles.routes[key].path
|
|
1916
|
-
: [config.bundles.routes[key].path];
|
|
1917
|
-
|
|
1918
|
-
for (const routePath of paths) {
|
|
1919
|
-
if (!config.routeBundles[routePath]) {
|
|
1920
|
-
config.routeBundles[routePath] = ['critical'];
|
|
1921
|
-
}
|
|
1922
|
-
for (const dependency of dependencies.filter((dep) => dep !== 'critical')) {
|
|
1923
|
-
if (!config.routeBundles[routePath].includes(dependency)) {
|
|
1924
|
-
config.routeBundles[routePath].push(dependency);
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
if (!config.routeBundles[routePath].includes(key)) {
|
|
1928
|
-
config.routeBundles[routePath].push(key);
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
const graphEntry = config.routeDependencyGraph[routePath] || {
|
|
1932
|
-
bundles: [],
|
|
1933
|
-
edges: []
|
|
1934
|
-
};
|
|
1935
|
-
if (!graphEntry.bundles.includes(key)) {
|
|
1936
|
-
graphEntry.bundles.push(key);
|
|
1937
|
-
graphEntry.bundles.sort((a, b) => a.localeCompare(b));
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
|
-
const edgeKeys = new Set(graphEntry.edges.map((edge) => `${edge.from}->${edge.to}`));
|
|
1941
|
-
const orderedEdgeSources = ['critical', ...dependencies.filter((dependency) => dependency !== 'critical')];
|
|
1942
|
-
for (const source of orderedEdgeSources) {
|
|
1943
|
-
const edgeKey = `${source}->${key}`;
|
|
1944
|
-
if (!edgeKeys.has(edgeKey)) {
|
|
1945
|
-
graphEntry.edges.push({ from: source, to: key });
|
|
1946
|
-
edgeKeys.add(edgeKey);
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
config.routeDependencyGraph[routePath] = graphEntry;
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
if (frameworkBundle) {
|
|
1955
|
-
config.bundles.framework = {
|
|
1956
|
-
file: frameworkBundle.file,
|
|
1957
|
-
size: frameworkBundle.size,
|
|
1958
|
-
hash: frameworkBundle.hash,
|
|
1959
|
-
integrity: frameworkBundle.integrity,
|
|
1960
|
-
components: frameworkBundle.components || []
|
|
1961
|
-
};
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
return config;
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
collectFrameworkComponents() {
|
|
1968
|
-
return this.analysisData.components.filter((comp) => comp.isFramework);
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
async createFrameworkBundle(components) {
|
|
1972
|
-
const fileName = 'slice-bundle.framework.js';
|
|
1973
|
-
const filePath = path.join(this.bundlesPath, fileName);
|
|
1974
|
-
return this.generateFrameworkBundleFile(components, fileName, filePath);
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
async generateFrameworkBundleFile(components, fileName, filePath) {
|
|
1978
|
-
const componentsData = {};
|
|
1979
|
-
const componentsMap = await this.loadComponentsMap();
|
|
1980
|
-
const metadata = {
|
|
1981
|
-
version: '2.0.0',
|
|
1982
|
-
type: 'framework',
|
|
1983
|
-
route: null,
|
|
1984
|
-
bundleKey: 'framework',
|
|
1985
|
-
file: fileName,
|
|
1986
|
-
generated: new Date().toISOString(),
|
|
1987
|
-
totalSize: components.reduce((sum, c) => sum + c.size, 0),
|
|
1988
|
-
componentCount: components.length,
|
|
1989
|
-
strategy: this.config.strategy,
|
|
1990
|
-
minified: this.options.minify,
|
|
1991
|
-
obfuscated: this.options.obfuscate
|
|
1992
|
-
};
|
|
1993
|
-
|
|
1994
|
-
components.forEach((comp) => {
|
|
1995
|
-
const componentKey = `Framework/Structural/${comp.name}`;
|
|
1996
|
-
const fileBaseName = comp.fileName || comp.name;
|
|
1997
|
-
const jsPath = path.join(comp.path, `${fileBaseName}.js`);
|
|
1998
|
-
const jsContent = fs.readFileSync(jsPath, 'utf-8');
|
|
1999
|
-
const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
|
|
2000
|
-
const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
|
|
2001
|
-
|
|
2002
|
-
componentsData[componentKey] = {
|
|
2003
|
-
name: comp.name,
|
|
2004
|
-
category: comp.category,
|
|
2005
|
-
categoryType: comp.categoryType,
|
|
2006
|
-
isFramework: true,
|
|
2007
|
-
js: cleanedJavaScript.code,
|
|
2008
|
-
hoistedImports: cleanedJavaScript.hoistedImports,
|
|
2009
|
-
externalDependencies: dependencyContents,
|
|
2010
|
-
componentDependencies: Array.from(comp.dependencies),
|
|
2011
|
-
html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
|
|
2012
|
-
? fs.readFileSync(path.join(comp.path, `${fileBaseName}.html`), 'utf-8')
|
|
2013
|
-
: null,
|
|
2014
|
-
css: fs.existsSync(path.join(comp.path, `${fileBaseName}.css`))
|
|
2015
|
-
? fs.readFileSync(path.join(comp.path, `${fileBaseName}.css`), 'utf-8')
|
|
2016
|
-
: null,
|
|
2017
|
-
size: comp.size
|
|
2018
|
-
};
|
|
2019
|
-
});
|
|
2020
|
-
|
|
2021
|
-
const prelude = `const components = ${JSON.stringify(componentsMap)};`;
|
|
2022
|
-
const bundleContent = `${prelude}\n${this.formatBundleFile(componentsData, metadata)}`;
|
|
2023
|
-
const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
|
|
2024
|
-
await fs.ensureDir(path.dirname(filePath));
|
|
2025
|
-
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
2026
|
-
|
|
2027
|
-
const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
|
|
2028
|
-
const integrity = `sha256:${hash}`;
|
|
2029
|
-
|
|
2030
|
-
return {
|
|
2031
|
-
name: 'framework',
|
|
2032
|
-
file: fileName,
|
|
2033
|
-
size: Buffer.byteLength(bundleContent, 'utf-8'),
|
|
2034
|
-
hash,
|
|
2035
|
-
integrity,
|
|
2036
|
-
componentCount: components.length,
|
|
2037
|
-
components: components.map((comp) => `Framework/Structural/${comp.name}`)
|
|
2038
|
-
};
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
buildDependencyContentsSync(jsContent, componentPath) {
|
|
2042
|
-
const dependencies = this.analyzeDependencies(jsContent, componentPath);
|
|
2043
|
-
const dependencyContents = {};
|
|
2044
|
-
|
|
2045
|
-
for (const dep of dependencies) {
|
|
2046
|
-
const depPath = dep.path;
|
|
2047
|
-
try {
|
|
2048
|
-
const depContent = fs.readFileSync(depPath, 'utf-8');
|
|
2049
|
-
const depName = path
|
|
2050
|
-
.relative(this.srcPath, depPath)
|
|
2051
|
-
.replace(/\\/g, '/');
|
|
2052
|
-
dependencyContents[depName] = {
|
|
2053
|
-
content: depContent,
|
|
2054
|
-
bindings: dep.bindings || []
|
|
2055
|
-
};
|
|
2056
|
-
} catch (error) {
|
|
2057
|
-
console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
return dependencyContents;
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
getConfiguredPublicFolders() {
|
|
2065
|
-
const publicFolders = Array.isArray(this.sliceConfig?.publicFolders)
|
|
2066
|
-
? this.sliceConfig.publicFolders
|
|
2067
|
-
: [];
|
|
2068
|
-
|
|
2069
|
-
return publicFolders
|
|
2070
|
-
.map((folder) => this.normalizePublicFolder(folder))
|
|
2071
|
-
.filter(Boolean);
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
normalizePublicFolder(folder) {
|
|
2075
|
-
if (typeof folder !== 'string') return null;
|
|
2076
|
-
let normalized = folder.trim();
|
|
2077
|
-
if (!normalized) return null;
|
|
2078
|
-
|
|
2079
|
-
if (!normalized.startsWith('/')) {
|
|
2080
|
-
normalized = `/${normalized}`;
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
normalized = normalized.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
2084
|
-
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
2085
|
-
normalized = normalized.slice(0, -1);
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
return normalized;
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
normalizeImportPath(importPath) {
|
|
2092
|
-
if (typeof importPath !== 'string') return '';
|
|
2093
|
-
const cleanPath = importPath.split(/[?#]/)[0];
|
|
2094
|
-
return cleanPath.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
isRelativeImport(importPath) {
|
|
2098
|
-
return importPath.startsWith('./') || importPath.startsWith('../');
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
isAbsoluteImport(importPath) {
|
|
2102
|
-
return importPath.startsWith('/');
|
|
2103
|
-
}
|
|
2104
|
-
|
|
2105
|
-
isImportInPublicFolders(importPath, publicFolders) {
|
|
2106
|
-
const normalizedImport = this.normalizeImportPath(importPath);
|
|
2107
|
-
return publicFolders.some((folder) => normalizedImport === folder || normalizedImport.startsWith(`${folder}/`));
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
classifyImport(importPath, publicFolders) {
|
|
2111
|
-
if (typeof importPath !== 'string' || !importPath) {
|
|
2112
|
-
return { keep: false, warning: 'Warning: Removing bare import: <unknown>' };
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
if (this.isRelativeImport(importPath)) {
|
|
2116
|
-
return { keep: false, warning: null };
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
if (this.isAbsoluteImport(importPath)) {
|
|
2120
|
-
if (this.isImportInPublicFolders(importPath, publicFolders)) {
|
|
2121
|
-
return { keep: true, warning: null };
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
return {
|
|
2125
|
-
keep: false,
|
|
2126
|
-
warning: `Warning: Removing absolute import outside publicFolders: ${importPath}`
|
|
2127
|
-
};
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
return {
|
|
2131
|
-
keep: false,
|
|
2132
|
-
warning: `Warning: Removing bare import: ${importPath}`
|
|
2133
|
-
};
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
buildImportWarningMessage(baseMessage, sourceContext) {
|
|
2137
|
-
if (!sourceContext) return baseMessage;
|
|
2138
|
-
return `${baseMessage} [${sourceContext}]`;
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
extractLocalBindingsFromImportStatement(statement) {
|
|
2142
|
-
const source = String(statement || '').trim();
|
|
2143
|
-
if (!source.startsWith('import ')) return [];
|
|
2144
|
-
if (/^import\s+['"][^'"]+['"]\s*;?$/.test(source)) return [];
|
|
2145
|
-
|
|
2146
|
-
const bindings = [];
|
|
2147
|
-
|
|
2148
|
-
const defaultMatch = source.match(/^import\s+([A-Za-z_$][\w$]*)\s*(,|\s+from\s+)/);
|
|
2149
|
-
if (defaultMatch && defaultMatch[1] !== '*') {
|
|
2150
|
-
bindings.push(defaultMatch[1]);
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
const namespaceMatch = source.match(/,?\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]/);
|
|
2154
|
-
if (namespaceMatch) {
|
|
2155
|
-
bindings.push(namespaceMatch[1]);
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
const namedMatch = source.match(/\{([\s\S]*?)\}\s*from\s*['"]/);
|
|
2159
|
-
if (namedMatch) {
|
|
2160
|
-
const namedSection = namedMatch[1];
|
|
2161
|
-
for (const part of namedSection.split(',')) {
|
|
2162
|
-
const cleanPart = part.trim();
|
|
2163
|
-
if (!cleanPart) continue;
|
|
2164
|
-
const aliasParts = cleanPart.split(/\s+as\s+/i).map((v) => v.trim()).filter(Boolean);
|
|
2165
|
-
const localName = aliasParts.length > 1 ? aliasParts[1] : aliasParts[0];
|
|
2166
|
-
if (/^[A-Za-z_$][\w$]*$/.test(localName)) {
|
|
2167
|
-
bindings.push(localName);
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
return Array.from(new Set(bindings));
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
validateHoistedImportCollisions(importStatements, reservedIdentifiers = new Set()) {
|
|
2176
|
-
const reserved = reservedIdentifiers instanceof Set
|
|
2177
|
-
? reservedIdentifiers
|
|
2178
|
-
: new Set(reservedIdentifiers || []);
|
|
2179
|
-
const bindingToStatement = new Map();
|
|
2180
|
-
|
|
2181
|
-
for (const statement of importStatements || []) {
|
|
2182
|
-
const normalizedStatement = String(statement || '').trim();
|
|
2183
|
-
if (!normalizedStatement) continue;
|
|
2184
|
-
const localBindings = this.extractLocalBindingsFromImportStatement(normalizedStatement);
|
|
2185
|
-
|
|
2186
|
-
for (const localBinding of localBindings) {
|
|
2187
|
-
if (reserved.has(localBinding)) {
|
|
2188
|
-
throw new Error(`Hoisted import reserved identifier collision: ${localBinding}`);
|
|
2189
|
-
}
|
|
2190
|
-
const previousStatement = bindingToStatement.get(localBinding);
|
|
2191
|
-
if (previousStatement && previousStatement !== normalizedStatement) {
|
|
2192
|
-
throw new Error(`Hoisted import binding collision: ${localBinding}`);
|
|
2193
|
-
}
|
|
2194
|
-
bindingToStatement.set(localBinding, normalizedStatement);
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
parseImportsFromCode(code) {
|
|
2200
|
-
const ast = parse(code, {
|
|
2201
|
-
sourceType: 'module',
|
|
2202
|
-
plugins: ['jsx']
|
|
2203
|
-
});
|
|
2204
|
-
|
|
2205
|
-
const importNodes = [];
|
|
2206
|
-
traverse.default(ast, {
|
|
2207
|
-
ImportDeclaration(pathNode) {
|
|
2208
|
-
importNodes.push(pathNode.node);
|
|
2209
|
-
}
|
|
2210
|
-
});
|
|
2211
|
-
|
|
2212
|
-
return importNodes
|
|
2213
|
-
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
|
|
2214
|
-
.sort((a, b) => a.start - b.start);
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
parseImportsWithFallbackScanner(code) {
|
|
2218
|
-
const entries = [];
|
|
2219
|
-
const importRegex = /\bimport\b/g;
|
|
2220
|
-
let match = null;
|
|
2221
|
-
|
|
2222
|
-
while ((match = importRegex.exec(code)) !== null) {
|
|
2223
|
-
const start = match.index;
|
|
2224
|
-
const nextChar = code[start + 'import'.length];
|
|
2225
|
-
if (nextChar === '(') {
|
|
2226
|
-
continue;
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
let index = start + 'import'.length;
|
|
2230
|
-
let quote = null;
|
|
2231
|
-
let escaped = false;
|
|
2232
|
-
|
|
2233
|
-
while (index < code.length) {
|
|
2234
|
-
const char = code[index];
|
|
2235
|
-
|
|
2236
|
-
if (quote) {
|
|
2237
|
-
if (escaped) {
|
|
2238
|
-
escaped = false;
|
|
2239
|
-
} else if (char === '\\') {
|
|
2240
|
-
escaped = true;
|
|
2241
|
-
} else if (char === quote) {
|
|
2242
|
-
quote = null;
|
|
2243
|
-
}
|
|
2244
|
-
index += 1;
|
|
2245
|
-
continue;
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
if (char === '\'' || char === '"' || char === '`') {
|
|
2249
|
-
quote = char;
|
|
2250
|
-
index += 1;
|
|
2251
|
-
continue;
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
if (char === ';') {
|
|
2255
|
-
index += 1;
|
|
2256
|
-
break;
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
index += 1;
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
const end = index;
|
|
2263
|
-
const statement = code.slice(start, end);
|
|
2264
|
-
const fromMatch = statement.match(/\bfrom\s+['"]([^'"]+)['"]/);
|
|
2265
|
-
const sideEffectMatch = statement.match(/\bimport\s+['"]([^'"]+)['"]/);
|
|
2266
|
-
const importPath = fromMatch?.[1] || sideEffectMatch?.[1] || null;
|
|
2267
|
-
|
|
2268
|
-
if (!importPath) {
|
|
2269
|
-
continue;
|
|
2270
|
-
}
|
|
2271
|
-
|
|
2272
|
-
entries.push({ start, end, statement, importPath });
|
|
2273
|
-
importRegex.lastIndex = end;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
return entries;
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports) {
|
|
2280
|
-
const hoistedImports = [];
|
|
2281
|
-
const importEntries = this.parseImportsWithFallbackScanner(code);
|
|
2282
|
-
if (importEntries.length === 0) {
|
|
2283
|
-
return { code, hoistedImports };
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
let cleanedCode = '';
|
|
2287
|
-
let cursor = 0;
|
|
2288
|
-
for (const entry of importEntries) {
|
|
2289
|
-
const { start, end, statement, importPath } = entry;
|
|
2290
|
-
const classification = this.classifyImport(importPath, publicFolders);
|
|
2291
|
-
cleanedCode += code.slice(cursor, start);
|
|
2292
|
-
if (classification.keep) {
|
|
2293
|
-
if (collectHoistedImports) {
|
|
2294
|
-
hoistedImports.push(statement.trim());
|
|
2295
|
-
} else {
|
|
2296
|
-
cleanedCode += statement;
|
|
2297
|
-
}
|
|
2298
|
-
} else if (classification.warning) {
|
|
2299
|
-
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
2300
|
-
}
|
|
2301
|
-
cursor = end;
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
cleanedCode += code.slice(cursor);
|
|
2305
|
-
|
|
2306
|
-
return { code: cleanedCode, hoistedImports };
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
stripImports(code, options = {}) {
|
|
2310
|
-
const { sourceContext = null, collectHoistedImports = false } = options;
|
|
2311
|
-
const publicFolders = this.getConfiguredPublicFolders();
|
|
2312
|
-
const hoistedImports = [];
|
|
2313
|
-
|
|
2314
|
-
try {
|
|
2315
|
-
const importNodes = this.parseImportsFromCode(code);
|
|
2316
|
-
|
|
2317
|
-
if (importNodes.length === 0) {
|
|
2318
|
-
return collectHoistedImports ? { code, hoistedImports } : code;
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
let cleaned = '';
|
|
2322
|
-
let cursor = 0;
|
|
2323
|
-
|
|
2324
|
-
for (const node of importNodes) {
|
|
2325
|
-
const importPath = node.source?.value;
|
|
2326
|
-
const classification = this.classifyImport(importPath, publicFolders);
|
|
2327
|
-
const statement = code.slice(node.start, node.end);
|
|
2328
|
-
|
|
2329
|
-
cleaned += code.slice(cursor, node.start);
|
|
2330
|
-
if (classification.keep) {
|
|
2331
|
-
if (collectHoistedImports) {
|
|
2332
|
-
hoistedImports.push(statement.trim());
|
|
2333
|
-
} else {
|
|
2334
|
-
cleaned += statement;
|
|
2335
|
-
}
|
|
2336
|
-
} else if (classification.warning) {
|
|
2337
|
-
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
cursor = node.end;
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
|
-
cleaned += code.slice(cursor);
|
|
2344
|
-
return collectHoistedImports
|
|
2345
|
-
? { code: cleaned, hoistedImports }
|
|
2346
|
-
: cleaned;
|
|
2347
|
-
} catch (error) {
|
|
2348
|
-
const fallback = this.stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports);
|
|
2349
|
-
return collectHoistedImports ? fallback : fallback.code;
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
async loadComponentsMap() {
|
|
2354
|
-
const componentsConfigPath = path.join(this.componentsPath, 'components.js');
|
|
2355
|
-
if (!await fs.pathExists(componentsConfigPath)) {
|
|
2356
|
-
return {};
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
const content = await fs.readFile(componentsConfigPath, 'utf-8');
|
|
2360
|
-
return this.parseComponentsConfig(content);
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
parseComponentsConfig(content) {
|
|
2364
|
-
try {
|
|
2365
|
-
const ast = parse(content, {
|
|
2366
|
-
sourceType: 'module',
|
|
2367
|
-
plugins: ['jsx']
|
|
2368
|
-
});
|
|
2369
|
-
|
|
2370
|
-
let componentsNode = null;
|
|
2371
|
-
|
|
2372
|
-
traverse.default(ast, {
|
|
2373
|
-
VariableDeclarator(path) {
|
|
2374
|
-
if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
|
|
2375
|
-
componentsNode = path.node.init;
|
|
2376
|
-
path.stop();
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
});
|
|
2380
|
-
|
|
2381
|
-
if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
|
|
2382
|
-
throw new Error('components object not found');
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
const config = {};
|
|
2386
|
-
for (const prop of componentsNode.properties) {
|
|
2387
|
-
if (prop.type !== 'ObjectProperty') continue;
|
|
2388
|
-
|
|
2389
|
-
const key = this.extractStringValue(prop.key);
|
|
2390
|
-
const value = this.extractStringValue(prop.value);
|
|
2391
|
-
|
|
2392
|
-
if (!key || !value) {
|
|
2393
|
-
throw new Error('Invalid components entry');
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
config[key] = value;
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
return config;
|
|
2400
|
-
} catch (error) {
|
|
2401
|
-
console.warn(`Could not parse components.js: ${error.message}`);
|
|
2402
|
-
return {};
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
extractStringValue(node) {
|
|
2407
|
-
if (!node) return null;
|
|
2408
|
-
|
|
2409
|
-
if (node.type === 'StringLiteral') {
|
|
2410
|
-
return node.value;
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
if (node.type === 'Identifier') {
|
|
2414
|
-
return node.name;
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
2418
|
-
return node.quasis.map((q) => q.value.cooked).join('');
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
return null;
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
/**
|
|
2425
|
-
* Converts a route to filename
|
|
2426
|
-
*/
|
|
2427
|
-
routeToFileName(routePath) {
|
|
2428
|
-
if (routePath === '/') return 'home';
|
|
2429
|
-
return routePath
|
|
2430
|
-
.replace(/^\//, '')
|
|
2431
|
-
.replace(/\//g, '-')
|
|
2432
|
-
.replace(/[^a-zA-Z0-9-]/g, '')
|
|
2433
|
-
.toLowerCase();
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
/**
|
|
2437
|
-
* Saves the configuration to file
|
|
2438
|
-
*/
|
|
2439
|
-
async saveBundleConfig(config) {
|
|
2440
|
-
// Ensure bundles directory exists
|
|
2441
|
-
await fs.ensureDir(this.bundlesPath);
|
|
2442
|
-
|
|
2443
|
-
// Save JSON config
|
|
2444
|
-
const configPath = path.join(this.bundlesPath, 'bundle.config.json');
|
|
2445
|
-
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
2446
|
-
|
|
2447
|
-
// Generate JavaScript module for direct import
|
|
2448
|
-
const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
|
|
2449
|
-
const jsConfig = this.generateBundleConfigJS(config);
|
|
2450
|
-
await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
|
|
2451
|
-
|
|
2452
|
-
console.log(`✓ Configuration saved to ${configPath}`);
|
|
2453
|
-
console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
|
-
/**
|
|
2457
|
-
* Creates a default bundle config file if none exists
|
|
2458
|
-
*/
|
|
2459
|
-
async createDefaultBundleConfig() {
|
|
2460
|
-
const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
|
|
2461
|
-
|
|
2462
|
-
// Only create if it doesn't exist
|
|
2463
|
-
if (await fs.pathExists(defaultConfigPath)) {
|
|
2464
|
-
return;
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
await fs.ensureDir(path.dirname(defaultConfigPath));
|
|
2468
|
-
|
|
2469
|
-
const defaultConfig = `/**
|
|
2470
|
-
* Slice.js Bundle Configuration
|
|
2471
|
-
* Default empty configuration - no bundles available
|
|
2472
|
-
* Run 'slice build' to generate optimized bundles
|
|
2473
|
-
*/
|
|
2474
|
-
|
|
2475
|
-
// No bundles available - using individual component loading
|
|
2476
|
-
export const SLICE_BUNDLE_CONFIG = null;
|
|
2477
|
-
|
|
2478
|
-
// No auto-initialization needed for default config
|
|
2479
|
-
`;
|
|
2480
|
-
|
|
2481
|
-
await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
|
|
2482
|
-
console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
/**
|
|
2486
|
-
* Generates JavaScript module for direct import
|
|
2487
|
-
*/
|
|
2488
|
-
generateBundleConfigJS(config) {
|
|
2489
|
-
return `/**
|
|
2490
|
-
* Slice.js Bundle Configuration
|
|
2491
|
-
* Generated: ${new Date().toISOString()}
|
|
2492
|
-
* Strategy: ${config.strategy}
|
|
2493
|
-
*/
|
|
2494
|
-
|
|
2495
|
-
// Direct bundle configuration (no fetch required)
|
|
2496
|
-
export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
|
|
2497
|
-
|
|
2498
|
-
// Auto-initialization if slice is available
|
|
2499
|
-
if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
|
|
2500
|
-
window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
|
|
2501
|
-
|
|
2502
|
-
// Load critical bundle automatically
|
|
2503
|
-
if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
|
|
2504
|
-
(async () => {
|
|
2505
|
-
const bundlePath = "/bundles/" + SLICE_BUNDLE_CONFIG.bundles.critical.file;
|
|
2506
|
-
const integrity = SLICE_BUNDLE_CONFIG.bundles.critical.integrity;
|
|
2507
|
-
|
|
2508
|
-
if (typeof window.slice.controller.verifyBundleIntegrity === 'function') {
|
|
2509
|
-
const ok = await window.slice.controller.verifyBundleIntegrity(bundlePath, integrity);
|
|
2510
|
-
if (!ok) {
|
|
2511
|
-
console.warn('Failed to load critical bundle: integrity check failed');
|
|
2512
|
-
return;
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
import('./slice-bundle.critical.js').catch(err =>
|
|
2517
|
-
console.warn('Failed to load critical bundle:', err)
|
|
2518
|
-
);
|
|
2519
|
-
window.slice.controller.criticalBundleLoaded = true;
|
|
2520
|
-
})();
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
2523
|
-
`;
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
1
|
+
// cli/utils/bundling/BundleGenerator.js
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { parse } from '@babel/parser';
|
|
6
|
+
import traverse from '@babel/traverse';
|
|
7
|
+
import { minify as terserMinify } from 'terser';
|
|
8
|
+
import { getSrcPath, getComponentsJsPath, getDistPath, getConfigPath } from '../PathHelper.js';
|
|
9
|
+
|
|
10
|
+
export default class BundleGenerator {
|
|
11
|
+
constructor(moduleUrl, analysisData, options = {}) {
|
|
12
|
+
this.moduleUrl = moduleUrl;
|
|
13
|
+
this.analysisData = analysisData || { components: [], routes: [], metrics: {} };
|
|
14
|
+
this.srcPath = getSrcPath(moduleUrl);
|
|
15
|
+
this.distPath = getDistPath(moduleUrl);
|
|
16
|
+
this.output = options.output || 'src';
|
|
17
|
+
this.bundlesPath = this.output === 'dist'
|
|
18
|
+
? path.join(this.distPath, 'bundles')
|
|
19
|
+
: path.join(this.srcPath, 'bundles');
|
|
20
|
+
this.componentsPath = path.dirname(getComponentsJsPath(moduleUrl));
|
|
21
|
+
this.options = {
|
|
22
|
+
minify: !!options.minify,
|
|
23
|
+
obfuscate: !!options.obfuscate
|
|
24
|
+
};
|
|
25
|
+
this.format = 'v2';
|
|
26
|
+
this.sliceConfig = this.resolveSliceConfig();
|
|
27
|
+
this.loadingPolicy = this.resolveLoadingPolicy();
|
|
28
|
+
|
|
29
|
+
// Configuration
|
|
30
|
+
this.config = {
|
|
31
|
+
maxCriticalSize: 50 * 1024, // 50KB
|
|
32
|
+
maxCriticalComponents: 15,
|
|
33
|
+
minSharedUsage: 3, // Minimum routes to be considered "shared"
|
|
34
|
+
minVendorSharedUsage: 2,
|
|
35
|
+
minVendorSharedTransformedSize: 2 * 1024,
|
|
36
|
+
maxRouteBundleSize: 120 * 1024,
|
|
37
|
+
maxRouteRequests: 12,
|
|
38
|
+
strategy: 'hybrid' // 'global', 'hybrid', 'per-route'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
this.vendorShared = {
|
|
42
|
+
file: 'slice-bundle.vendor-shared.js',
|
|
43
|
+
dependencyModules: new Map(),
|
|
44
|
+
dependencyUsage: new Map(),
|
|
45
|
+
sharedDependencySet: new Set(),
|
|
46
|
+
bundleKeysUsingSharedDependencies: new Set(),
|
|
47
|
+
bundle: null
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
this.bundles = {
|
|
51
|
+
critical: {
|
|
52
|
+
components: [],
|
|
53
|
+
size: 0,
|
|
54
|
+
file: 'slice-bundle.critical.js'
|
|
55
|
+
},
|
|
56
|
+
routes: {}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
resolveSliceConfig() {
|
|
61
|
+
if (this.analysisData?.sliceConfig && typeof this.analysisData.sliceConfig === 'object') {
|
|
62
|
+
return this.analysisData.sliceConfig;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const configPath = getConfigPath(this.moduleUrl);
|
|
67
|
+
if (fs.existsSync(configPath)) {
|
|
68
|
+
return fs.readJsonSync(configPath);
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.warn('Warning: Could not read sliceConfig.json for loading policy:', error.message);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
resolveLoadingPolicy() {
|
|
78
|
+
return this.sliceConfig?.loading?.enabled ? 'enabled' : 'disabled';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Computes deterministic integrity hash for bundle metadata.
|
|
83
|
+
* @param {Array} components
|
|
84
|
+
* @param {string} type
|
|
85
|
+
* @param {string|null} routePath
|
|
86
|
+
* @param {string} bundleKey
|
|
87
|
+
* @param {string} fileName
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
computeBundleIntegrity(components, type, routePath, bundleKey, fileName) {
|
|
91
|
+
const metadata = {
|
|
92
|
+
version: '2.0.0',
|
|
93
|
+
type,
|
|
94
|
+
route: routePath,
|
|
95
|
+
bundleKey,
|
|
96
|
+
file: fileName,
|
|
97
|
+
generated: 'static',
|
|
98
|
+
totalSize: components.reduce((sum, c) => sum + c.size, 0),
|
|
99
|
+
componentCount: components.length,
|
|
100
|
+
strategy: this.config.strategy
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const payload = {
|
|
104
|
+
metadata,
|
|
105
|
+
components: components.reduce((acc, comp) => {
|
|
106
|
+
acc[comp.name] = {
|
|
107
|
+
name: comp.name,
|
|
108
|
+
category: comp.category,
|
|
109
|
+
categoryType: comp.categoryType,
|
|
110
|
+
componentDependencies: Array.from(comp.dependencies)
|
|
111
|
+
};
|
|
112
|
+
return acc;
|
|
113
|
+
}, {})
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return `sha256:${crypto.createHash('sha256')
|
|
117
|
+
.update(JSON.stringify(payload))
|
|
118
|
+
.digest('hex')}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Generates all bundles
|
|
123
|
+
*/
|
|
124
|
+
async generate() {
|
|
125
|
+
console.log('🔨 Generating bundles...');
|
|
126
|
+
|
|
127
|
+
// 0. Create bundles directory
|
|
128
|
+
await fs.ensureDir(this.bundlesPath);
|
|
129
|
+
if (this.output === 'dist') {
|
|
130
|
+
await fs.ensureDir(this.distPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 1. Determine optimal strategy
|
|
134
|
+
this.determineStrategy();
|
|
135
|
+
|
|
136
|
+
// 2. Identify critical components
|
|
137
|
+
this.identifyCriticalComponents();
|
|
138
|
+
|
|
139
|
+
// 3. Assign components to routes
|
|
140
|
+
this.assignRouteComponents();
|
|
141
|
+
|
|
142
|
+
// 4. Generate bundle files
|
|
143
|
+
const files = await this.generateBundleFiles();
|
|
144
|
+
|
|
145
|
+
// 5. Generate framework bundle (structural)
|
|
146
|
+
const frameworkComponents = this.collectFrameworkComponents();
|
|
147
|
+
let frameworkBundle = null;
|
|
148
|
+
if (frameworkComponents.length > 0) {
|
|
149
|
+
frameworkBundle = await this.createFrameworkBundle(frameworkComponents);
|
|
150
|
+
files.push(frameworkBundle);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 6. Generate configuration
|
|
154
|
+
const config = this.generateBundleConfig(frameworkBundle);
|
|
155
|
+
|
|
156
|
+
console.log('✅ Bundles generated successfully');
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
bundles: this.bundles,
|
|
160
|
+
config,
|
|
161
|
+
files
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Determines the optimal bundling strategy
|
|
167
|
+
*/
|
|
168
|
+
determineStrategy() {
|
|
169
|
+
const { metrics } = this.analysisData;
|
|
170
|
+
const { totalComponents, sharedPercentage } = metrics;
|
|
171
|
+
|
|
172
|
+
// Strategy based on size and usage pattern
|
|
173
|
+
if (totalComponents < 20 || sharedPercentage > 60) {
|
|
174
|
+
this.config.strategy = 'global';
|
|
175
|
+
console.log('📦 Strategy: Global Bundle (small project or highly shared)');
|
|
176
|
+
} else if (totalComponents < 100) {
|
|
177
|
+
this.config.strategy = 'hybrid';
|
|
178
|
+
console.log('📦 Strategy: Hybrid (critical + grouped routes)');
|
|
179
|
+
} else {
|
|
180
|
+
this.config.strategy = 'per-route';
|
|
181
|
+
console.log('📦 Strategy: Per Route (large project)');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Identifies critical components for the initial bundle
|
|
187
|
+
*/
|
|
188
|
+
identifyCriticalComponents() {
|
|
189
|
+
const { components } = this.analysisData;
|
|
190
|
+
|
|
191
|
+
// Filter critical candidates
|
|
192
|
+
const candidates = components
|
|
193
|
+
.filter(comp => {
|
|
194
|
+
if (!this.isComponentAllowedByLoadingPolicy(comp)) return false;
|
|
195
|
+
// Shared components (used in 3+ routes)
|
|
196
|
+
const isShared = comp.routes.size >= this.config.minSharedUsage;
|
|
197
|
+
|
|
198
|
+
// Structural components (Navbar, Footer, etc.)
|
|
199
|
+
const isStructural = comp.categoryType === 'Structural' ||
|
|
200
|
+
['Navbar', 'Footer', 'Layout'].includes(comp.name);
|
|
201
|
+
|
|
202
|
+
// Small and highly used components (only if used in 3+ routes)
|
|
203
|
+
const isSmallAndUseful = comp.size < 2000 && comp.routes.size >= 3;
|
|
204
|
+
|
|
205
|
+
return isShared || isStructural || isSmallAndUseful;
|
|
206
|
+
})
|
|
207
|
+
.sort((a, b) => {
|
|
208
|
+
// Prioritize by: (usage * 10) - size
|
|
209
|
+
const priorityA = (a.routes.size * 10) - (a.size / 1000);
|
|
210
|
+
const priorityB = (b.routes.size * 10) - (b.size / 1000);
|
|
211
|
+
return priorityB - priorityA;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const loadingComponent = components.find((comp) => comp.name === 'Loading' && this.isComponentAllowedByLoadingPolicy(comp));
|
|
215
|
+
if (this.loadingPolicy === 'enabled' && loadingComponent && !candidates.includes(loadingComponent)) {
|
|
216
|
+
candidates.unshift(loadingComponent);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Fill critical bundle up to limit
|
|
220
|
+
for (const comp of candidates) {
|
|
221
|
+
const dependencies = this.getComponentDependencies(comp);
|
|
222
|
+
const totalSize = comp.size + dependencies.reduce((sum, dep) => sum + dep.size, 0);
|
|
223
|
+
const totalCount = 1 + dependencies.length;
|
|
224
|
+
|
|
225
|
+
const wouldExceedSize = this.bundles.critical.size + totalSize > this.config.maxCriticalSize;
|
|
226
|
+
const wouldExceedCount = this.bundles.critical.components.length + totalCount > this.config.maxCriticalComponents;
|
|
227
|
+
|
|
228
|
+
if ((wouldExceedSize || wouldExceedCount) && comp.name !== 'Loading') continue;
|
|
229
|
+
|
|
230
|
+
// Add component and its dependencies
|
|
231
|
+
if (!this.bundles.critical.components.find(c => c.name === comp.name)) {
|
|
232
|
+
this.bundles.critical.components.push(comp);
|
|
233
|
+
this.bundles.critical.size += comp.size;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const dep of dependencies) {
|
|
237
|
+
if (!this.bundles.critical.components.find(c => c.name === dep.name)) {
|
|
238
|
+
this.bundles.critical.components.push(dep);
|
|
239
|
+
this.bundles.critical.size += dep.size;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (this.loadingPolicy === 'disabled') {
|
|
245
|
+
this.bundles.critical.components = this.bundles.critical.components.filter((comp) => comp.name !== 'Loading');
|
|
246
|
+
this.bundles.critical.size = this.bundles.critical.components.reduce((sum, comp) => sum + comp.size, 0);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(`✓ Critical bundle: ${this.bundles.critical.components.length} components, ${(this.bundles.critical.size / 1024).toFixed(1)} KB`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Assigns remaining components to route bundles
|
|
254
|
+
*/
|
|
255
|
+
assignRouteComponents() {
|
|
256
|
+
const criticalNames = new Set(this.bundles.critical.components.map(c => c.name));
|
|
257
|
+
|
|
258
|
+
if (this.config.strategy === 'hybrid') {
|
|
259
|
+
this.assignHybridBundles(criticalNames);
|
|
260
|
+
} else {
|
|
261
|
+
this.assignPerRouteBundles(criticalNames);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.extractSharedComponents(criticalNames);
|
|
265
|
+
this.rebalanceBundlesByBudget(this.bundles.routes, {
|
|
266
|
+
maxBundleSize: this.config.maxRouteBundleSize,
|
|
267
|
+
maxRequests: this.config.maxRouteRequests
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Assigns components to per-route bundles
|
|
273
|
+
*/
|
|
274
|
+
assignPerRouteBundles(criticalNames) {
|
|
275
|
+
for (const route of this.analysisData.routes) {
|
|
276
|
+
const routePath = route.path;
|
|
277
|
+
// Get all route dependencies
|
|
278
|
+
const routeComponents = this.getRouteComponents(route.component);
|
|
279
|
+
|
|
280
|
+
// Include dependencies for all route components
|
|
281
|
+
const allComponents = new Set();
|
|
282
|
+
for (const comp of routeComponents) {
|
|
283
|
+
allComponents.add(comp);
|
|
284
|
+
const dependencies = this.getComponentDependencies(comp);
|
|
285
|
+
for (const dep of dependencies) {
|
|
286
|
+
allComponents.add(dep);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Filter those already in critical
|
|
291
|
+
const uniqueComponents = Array.from(allComponents).filter(comp =>
|
|
292
|
+
!criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (uniqueComponents.length === 0) continue;
|
|
296
|
+
|
|
297
|
+
const routeKey = this.routeToFileName(routePath);
|
|
298
|
+
const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
|
|
299
|
+
|
|
300
|
+
this.bundles.routes[routeKey] = {
|
|
301
|
+
path: routePath,
|
|
302
|
+
components: this.sortComponentsByName(uniqueComponents),
|
|
303
|
+
size: totalSize,
|
|
304
|
+
file: `slice-bundle.${routeKey}.js`
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
console.log(`✓ Bundle ${routeKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Gets all component dependencies transitively
|
|
313
|
+
*/
|
|
314
|
+
getComponentDependencies(component, visited = new Set()) {
|
|
315
|
+
if (visited.has(component.name)) return [];
|
|
316
|
+
visited.add(component.name);
|
|
317
|
+
|
|
318
|
+
const dependencies = [];
|
|
319
|
+
|
|
320
|
+
// Add direct dependencies
|
|
321
|
+
for (const depName of component.dependencies) {
|
|
322
|
+
const depComp = this.analysisData.components.find(c => c.name === depName);
|
|
323
|
+
if (depComp && !visited.has(depName)) {
|
|
324
|
+
dependencies.push(depComp);
|
|
325
|
+
// Add transitive dependencies
|
|
326
|
+
dependencies.push(...this.getComponentDependencies(depComp, visited));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return dependencies;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Assigns components to hybrid bundles (grouped by category)
|
|
335
|
+
*/
|
|
336
|
+
assignHybridBundles(criticalNames) {
|
|
337
|
+
const routeGroups = new Map();
|
|
338
|
+
|
|
339
|
+
// First, handle MultiRoute groups
|
|
340
|
+
if (this.analysisData.routeGroups) {
|
|
341
|
+
for (const [groupKey, groupData] of this.analysisData.routeGroups) {
|
|
342
|
+
if (groupData.type === 'multiroute') {
|
|
343
|
+
// Create a bundle for this MultiRoute group
|
|
344
|
+
const allComponents = new Set();
|
|
345
|
+
|
|
346
|
+
// Add the main component (MultiRoute handler)
|
|
347
|
+
const mainComponent = this.analysisData.components.find(c => c.name === groupData.component);
|
|
348
|
+
if (mainComponent) {
|
|
349
|
+
allComponents.add(mainComponent);
|
|
350
|
+
|
|
351
|
+
// Add all components used by this MultiRoute
|
|
352
|
+
const routeComponents = this.getRouteComponents(mainComponent.name);
|
|
353
|
+
for (const comp of routeComponents) {
|
|
354
|
+
allComponents.add(comp);
|
|
355
|
+
// Add transitive dependencies
|
|
356
|
+
const dependencies = this.getComponentDependencies(comp);
|
|
357
|
+
for (const dep of dependencies) {
|
|
358
|
+
allComponents.add(dep);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Filter those already in critical
|
|
364
|
+
const uniqueComponents = Array.from(allComponents).filter(comp =>
|
|
365
|
+
!criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
if (uniqueComponents.length > 0) {
|
|
369
|
+
const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
|
|
370
|
+
|
|
371
|
+
this.bundles.routes[groupKey] = {
|
|
372
|
+
paths: groupData.routes,
|
|
373
|
+
components: this.sortComponentsByName(uniqueComponents),
|
|
374
|
+
size: totalSize,
|
|
375
|
+
file: `slice-bundle.${this.routeToFileName(groupKey)}.js`
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
console.log(`✓ Bundle ${groupKey}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${groupData.routes.length} routes)`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Group remaining routes by category (skip those already handled by MultiRoute)
|
|
385
|
+
for (const route of this.analysisData.routes) {
|
|
386
|
+
// Check if this route is already handled by a MultiRoute group
|
|
387
|
+
const isHandledByMultiRoute = this.analysisData.routeGroups &&
|
|
388
|
+
Array.from(this.analysisData.routeGroups.values()).some(group =>
|
|
389
|
+
group.type === 'multiroute' && group.routes.includes(route.path)
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (!isHandledByMultiRoute) {
|
|
393
|
+
const category = this.categorizeRoute(route.path);
|
|
394
|
+
if (!routeGroups.has(category)) {
|
|
395
|
+
routeGroups.set(category, []);
|
|
396
|
+
}
|
|
397
|
+
routeGroups.get(category).push(route);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Create bundles for each group
|
|
402
|
+
for (const [category, routes] of routeGroups) {
|
|
403
|
+
const allComponents = new Set();
|
|
404
|
+
|
|
405
|
+
// Collect all unique components for this category (including dependencies)
|
|
406
|
+
for (const route of routes) {
|
|
407
|
+
const routeComponents = this.getRouteComponents(route.component);
|
|
408
|
+
for (const comp of routeComponents) {
|
|
409
|
+
allComponents.add(comp);
|
|
410
|
+
// Add transitive dependencies
|
|
411
|
+
const dependencies = this.getComponentDependencies(comp);
|
|
412
|
+
for (const dep of dependencies) {
|
|
413
|
+
allComponents.add(dep);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Filter those already in critical
|
|
419
|
+
const uniqueComponents = Array.from(allComponents).filter(comp =>
|
|
420
|
+
!criticalNames.has(comp.name) && this.isComponentAllowedByLoadingPolicy(comp)
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
if (uniqueComponents.length === 0) continue;
|
|
424
|
+
|
|
425
|
+
const totalSize = uniqueComponents.reduce((sum, c) => sum + c.size, 0);
|
|
426
|
+
const routePaths = routes.map(r => r.path);
|
|
427
|
+
|
|
428
|
+
this.bundles.routes[category] = {
|
|
429
|
+
paths: routePaths,
|
|
430
|
+
components: this.sortComponentsByName(uniqueComponents),
|
|
431
|
+
size: totalSize,
|
|
432
|
+
file: `slice-bundle.${this.routeToFileName(category)}.js`
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
console.log(`✓ Bundle ${category}: ${uniqueComponents.length} components, ${(totalSize / 1024).toFixed(1)} KB (${routes.length} routes)`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
isComponentAllowedByLoadingPolicy(component) {
|
|
440
|
+
if (!component) return false;
|
|
441
|
+
if (this.loadingPolicy === 'disabled' && component.name === 'Loading') {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
sortComponentsByName(components) {
|
|
448
|
+
return [...components].sort((a, b) => a.name.localeCompare(b.name));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
dedupeComponentsByName(components = []) {
|
|
452
|
+
const byName = new Map();
|
|
453
|
+
for (const component of components) {
|
|
454
|
+
if (!component?.name) continue;
|
|
455
|
+
if (!byName.has(component.name)) {
|
|
456
|
+
byName.set(component.name, component);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return this.sortComponentsByName(Array.from(byName.values()));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
getBundlePaths(bundle = {}) {
|
|
463
|
+
const raw = Array.isArray(bundle.paths)
|
|
464
|
+
? bundle.paths
|
|
465
|
+
: Array.isArray(bundle.path)
|
|
466
|
+
? bundle.path
|
|
467
|
+
: bundle.path
|
|
468
|
+
? [bundle.path]
|
|
469
|
+
: [];
|
|
470
|
+
|
|
471
|
+
return Array.from(new Set(raw.filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
setBundlePaths(bundle, paths = []) {
|
|
475
|
+
const mergedPaths = Array.from(new Set((paths || []).filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
476
|
+
if (mergedPaths.length === 0) {
|
|
477
|
+
delete bundle.path;
|
|
478
|
+
delete bundle.paths;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (mergedPaths.length === 1) {
|
|
482
|
+
bundle.path = mergedPaths[0];
|
|
483
|
+
delete bundle.paths;
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
bundle.paths = mergedPaths;
|
|
487
|
+
delete bundle.path;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
mergeBundleDependencies(...dependencyLists) {
|
|
491
|
+
const merged = [];
|
|
492
|
+
const append = (dep) => {
|
|
493
|
+
if (!dep || merged.includes(dep)) return;
|
|
494
|
+
if (dep === 'critical') {
|
|
495
|
+
merged.unshift(dep);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
merged.push(dep);
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
dependencyLists
|
|
502
|
+
.flat()
|
|
503
|
+
.forEach(append);
|
|
504
|
+
|
|
505
|
+
if (!merged.includes('critical')) {
|
|
506
|
+
merged.unshift('critical');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const rest = merged
|
|
510
|
+
.filter((dep) => dep !== 'critical')
|
|
511
|
+
.sort((a, b) => a.localeCompare(b));
|
|
512
|
+
return ['critical', ...rest];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
extractSharedComponents(criticalNames) {
|
|
516
|
+
const usage = new Map();
|
|
517
|
+
|
|
518
|
+
for (const bundle of Object.values(this.bundles.routes)) {
|
|
519
|
+
const seenInBundle = new Set();
|
|
520
|
+
for (const component of this.dedupeComponentsByName(bundle.components || [])) {
|
|
521
|
+
if (criticalNames.has(component.name)) continue;
|
|
522
|
+
if (!this.isComponentAllowedByLoadingPolicy(component)) continue;
|
|
523
|
+
if (seenInBundle.has(component.name)) continue;
|
|
524
|
+
seenInBundle.add(component.name);
|
|
525
|
+
if (!usage.has(component.name)) {
|
|
526
|
+
usage.set(component.name, { component, count: 0 });
|
|
527
|
+
}
|
|
528
|
+
usage.get(component.name).count += 1;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const sharedComponents = Array.from(usage.values())
|
|
533
|
+
.filter((entry) => entry.count >= this.config.minSharedUsage)
|
|
534
|
+
.map((entry) => entry.component);
|
|
535
|
+
|
|
536
|
+
if (sharedComponents.length === 0) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const sharedSet = new Set(sharedComponents.map((component) => component.name));
|
|
541
|
+
const orderedShared = this.sortComponentsByName(sharedComponents);
|
|
542
|
+
|
|
543
|
+
for (const bundle of Object.values(this.bundles.routes)) {
|
|
544
|
+
const original = this.dedupeComponentsByName(bundle.components || []);
|
|
545
|
+
const filtered = original.filter((component) => !sharedSet.has(component.name));
|
|
546
|
+
const removedShared = original.length - filtered.length;
|
|
547
|
+
bundle.components = this.sortComponentsByName(filtered);
|
|
548
|
+
bundle.size = bundle.components.reduce((sum, component) => sum + component.size, 0);
|
|
549
|
+
if (removedShared > 0) {
|
|
550
|
+
bundle.dependencies = this.mergeBundleDependencies(bundle.dependencies || [], ['shared-core']);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
this.bundles.routes['shared-core'] = {
|
|
555
|
+
paths: [],
|
|
556
|
+
components: orderedShared,
|
|
557
|
+
size: orderedShared.reduce((sum, component) => sum + component.size, 0),
|
|
558
|
+
file: `slice-bundle.${this.routeToFileName('shared-core')}.js`
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
for (const [key, bundle] of Object.entries(this.bundles.routes)) {
|
|
562
|
+
if (key === 'shared-core') continue;
|
|
563
|
+
if ((bundle.components || []).length === 0) {
|
|
564
|
+
delete this.bundles.routes[key];
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
rebalanceBundlesByBudget(bundles, limits = {}) {
|
|
570
|
+
const maxBundleSize = limits.maxBundleSize || this.config.maxRouteBundleSize;
|
|
571
|
+
const maxRequests = limits.maxRequests || this.config.maxRouteRequests;
|
|
572
|
+
const orderedEntries = Object.entries(bundles)
|
|
573
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
574
|
+
const rebalanced = {};
|
|
575
|
+
|
|
576
|
+
for (const [key, bundle] of orderedEntries) {
|
|
577
|
+
const sortedComponents = this.dedupeComponentsByName(bundle.components || []);
|
|
578
|
+
const totalSize = sortedComponents.reduce((sum, component) => sum + component.size, 0);
|
|
579
|
+
if (totalSize <= maxBundleSize || sortedComponents.length <= 1) {
|
|
580
|
+
rebalanced[key] = {
|
|
581
|
+
...bundle,
|
|
582
|
+
components: sortedComponents,
|
|
583
|
+
size: totalSize,
|
|
584
|
+
file: `slice-bundle.${this.routeToFileName(key)}.js`
|
|
585
|
+
};
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let partIndex = 1;
|
|
590
|
+
let currentChunk = [];
|
|
591
|
+
let currentSize = 0;
|
|
592
|
+
|
|
593
|
+
for (const component of sortedComponents) {
|
|
594
|
+
const nextSize = currentSize + component.size;
|
|
595
|
+
const shouldFlush = currentChunk.length > 0 && nextSize > maxBundleSize;
|
|
596
|
+
|
|
597
|
+
if (shouldFlush) {
|
|
598
|
+
const partKey = `${key}--p${partIndex}`;
|
|
599
|
+
rebalanced[partKey] = {
|
|
600
|
+
...bundle,
|
|
601
|
+
components: currentChunk,
|
|
602
|
+
size: currentSize,
|
|
603
|
+
file: `slice-bundle.${this.routeToFileName(partKey)}.js`
|
|
604
|
+
};
|
|
605
|
+
partIndex += 1;
|
|
606
|
+
currentChunk = [];
|
|
607
|
+
currentSize = 0;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
currentChunk.push(component);
|
|
611
|
+
currentSize += component.size;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (currentChunk.length > 0) {
|
|
615
|
+
const partKey = `${key}--p${partIndex}`;
|
|
616
|
+
rebalanced[partKey] = {
|
|
617
|
+
...bundle,
|
|
618
|
+
components: currentChunk,
|
|
619
|
+
size: currentSize,
|
|
620
|
+
file: `slice-bundle.${this.routeToFileName(partKey)}.js`
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const keys = Object.keys(rebalanced).sort((a, b) => a.localeCompare(b));
|
|
626
|
+
while (keys.length > maxRequests) {
|
|
627
|
+
const lastKey = keys.pop();
|
|
628
|
+
const targetKey = keys[keys.length - 1];
|
|
629
|
+
if (!lastKey || !targetKey) break;
|
|
630
|
+
const mergedComponents = this.dedupeComponentsByName([
|
|
631
|
+
...(rebalanced[targetKey].components || []),
|
|
632
|
+
...(rebalanced[lastKey].components || [])
|
|
633
|
+
]);
|
|
634
|
+
const mergedPaths = [
|
|
635
|
+
...this.getBundlePaths(rebalanced[targetKey]),
|
|
636
|
+
...this.getBundlePaths(rebalanced[lastKey])
|
|
637
|
+
];
|
|
638
|
+
const mergedDependencies = this.mergeBundleDependencies(
|
|
639
|
+
rebalanced[targetKey].dependencies || [],
|
|
640
|
+
rebalanced[lastKey].dependencies || []
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
rebalanced[targetKey].components = mergedComponents;
|
|
644
|
+
rebalanced[targetKey].size = mergedComponents.reduce((sum, component) => sum + component.size, 0);
|
|
645
|
+
rebalanced[targetKey].dependencies = mergedDependencies;
|
|
646
|
+
this.setBundlePaths(rebalanced[targetKey], mergedPaths);
|
|
647
|
+
delete rebalanced[lastKey];
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
Object.keys(bundles).forEach((key) => delete bundles[key]);
|
|
651
|
+
for (const [key, bundle] of Object.entries(rebalanced).sort(([a], [b]) => a.localeCompare(b))) {
|
|
652
|
+
bundles[key] = bundle;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return bundles;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Categorizes a route path for grouping, considering MultiRoute context
|
|
660
|
+
*/
|
|
661
|
+
categorizeRoute(routePath) {
|
|
662
|
+
// Check if this route belongs to a MultiRoute handler
|
|
663
|
+
if (this.analysisData.routeGroups) {
|
|
664
|
+
for (const [groupKey, groupData] of this.analysisData.routeGroups) {
|
|
665
|
+
if (groupData.type === 'multiroute' && groupData.routes.includes(routePath)) {
|
|
666
|
+
return groupKey; // Return the MultiRoute group key
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Default categorization
|
|
672
|
+
const path = routePath.toLowerCase();
|
|
673
|
+
|
|
674
|
+
if (path === '/' || path === '/home') return 'home';
|
|
675
|
+
if (path.includes('docum') || path.includes('documentation')) return 'documentation';
|
|
676
|
+
if (path.includes('component') || path.includes('visual') || path.includes('card') ||
|
|
677
|
+
path.includes('button') || path.includes('input') || path.includes('switch') ||
|
|
678
|
+
path.includes('checkbox') || path.includes('select') || path.includes('details') ||
|
|
679
|
+
path.includes('grid') || path.includes('loading') || path.includes('layout') ||
|
|
680
|
+
path.includes('navbar') || path.includes('treeview') || path.includes('multiroute')) return 'components';
|
|
681
|
+
if (path.includes('theme') || path.includes('slice') || path.includes('config')) return 'configuration';
|
|
682
|
+
if (path.includes('routing') || path.includes('guard')) return 'routing';
|
|
683
|
+
if (path.includes('service') || path.includes('command')) return 'services';
|
|
684
|
+
if (path.includes('structural') || path.includes('lifecycle') || path.includes('static') ||
|
|
685
|
+
path.includes('build')) return 'advanced';
|
|
686
|
+
if (path.includes('playground') || path.includes('creator')) return 'tools';
|
|
687
|
+
if (path.includes('about') || path.includes('404')) return 'misc';
|
|
688
|
+
|
|
689
|
+
return 'general';
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Gets all components needed for a route
|
|
694
|
+
*/
|
|
695
|
+
getRouteComponents(componentName) {
|
|
696
|
+
const result = [];
|
|
697
|
+
const visited = new Set();
|
|
698
|
+
|
|
699
|
+
const traverse = (name) => {
|
|
700
|
+
if (visited.has(name)) return;
|
|
701
|
+
visited.add(name);
|
|
702
|
+
|
|
703
|
+
const component = this.analysisData.components.find(c => c.name === name);
|
|
704
|
+
if (!component) return;
|
|
705
|
+
|
|
706
|
+
result.push(component);
|
|
707
|
+
|
|
708
|
+
// Add dependencies recursively
|
|
709
|
+
for (const dep of component.dependencies) {
|
|
710
|
+
traverse(dep);
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
traverse(componentName);
|
|
715
|
+
return result;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Generates the physical bundle files
|
|
720
|
+
*/
|
|
721
|
+
async generateBundleFiles() {
|
|
722
|
+
const files = [];
|
|
723
|
+
|
|
724
|
+
await this.prepareVendorSharedDependencies();
|
|
725
|
+
|
|
726
|
+
if (this.vendorShared.sharedDependencySet.size > 0) {
|
|
727
|
+
const vendorSharedFile = await this.createVendorSharedDependencyBundleFile(this.vendorShared.sharedDependencySet);
|
|
728
|
+
this.vendorShared.bundle = vendorSharedFile;
|
|
729
|
+
files.push(vendorSharedFile);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// 1. Critical bundle
|
|
733
|
+
if (this.bundles.critical.components.length > 0) {
|
|
734
|
+
const criticalFile = await this.createBundleFile(
|
|
735
|
+
this.bundles.critical.components,
|
|
736
|
+
'critical',
|
|
737
|
+
null
|
|
738
|
+
);
|
|
739
|
+
const criticalIntegrity = this.computeBundleIntegrity(
|
|
740
|
+
this.bundles.critical.components,
|
|
741
|
+
'critical',
|
|
742
|
+
null,
|
|
743
|
+
'critical',
|
|
744
|
+
criticalFile.file
|
|
745
|
+
);
|
|
746
|
+
this.bundles.critical.integrity = `sha256:${criticalFile.hash}`;
|
|
747
|
+
this.bundles.critical.hash = criticalFile.hash;
|
|
748
|
+
files.push(criticalFile);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// 2. Route bundles
|
|
752
|
+
for (const [routeKey, bundle] of Object.entries(this.bundles.routes)) {
|
|
753
|
+
const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
|
|
754
|
+
? routeKey
|
|
755
|
+
: (bundle.path || bundle.paths || routeKey);
|
|
756
|
+
|
|
757
|
+
const routeFile = await this.createBundleFile(
|
|
758
|
+
bundle.components,
|
|
759
|
+
'route',
|
|
760
|
+
routeIdentifier
|
|
761
|
+
);
|
|
762
|
+
const routeIntegrity = `sha256:${routeFile.hash}`;
|
|
763
|
+
const matchingBundle = Object.values(this.bundles.routes)
|
|
764
|
+
.find((entry) => entry.file === routeFile.file);
|
|
765
|
+
if (matchingBundle) {
|
|
766
|
+
matchingBundle.hash = routeFile.hash;
|
|
767
|
+
matchingBundle.integrity = routeIntegrity;
|
|
768
|
+
}
|
|
769
|
+
files.push(routeFile);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return files;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
async prepareVendorSharedDependencies() {
|
|
776
|
+
const routeDependencyIndex = await this.collectRouteExternalDependencyIndex();
|
|
777
|
+
const usageIndex = this.indexExternalDependencyUsage(routeDependencyIndex);
|
|
778
|
+
const sharedDependencySet = this.computeSharedDependencySet(usageIndex);
|
|
779
|
+
|
|
780
|
+
this.vendorShared.dependencyUsage = usageIndex;
|
|
781
|
+
this.vendorShared.sharedDependencySet = sharedDependencySet;
|
|
782
|
+
this.vendorShared.bundleKeysUsingSharedDependencies = new Set();
|
|
783
|
+
|
|
784
|
+
if (sharedDependencySet.size === 0) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
for (const dependencyName of sharedDependencySet) {
|
|
789
|
+
const usageEntry = usageIndex.get(dependencyName);
|
|
790
|
+
for (const bundleKey of usageEntry?.bundleKeys || []) {
|
|
791
|
+
this.vendorShared.bundleKeysUsingSharedDependencies.add(bundleKey);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
for (const bundleKey of this.vendorShared.bundleKeysUsingSharedDependencies) {
|
|
796
|
+
const bundle = this.bundles.routes[bundleKey];
|
|
797
|
+
if (!bundle) continue;
|
|
798
|
+
bundle.dependencies = this.mergeBundleDependencies(bundle.dependencies || [], ['vendor-shared']);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async collectRouteExternalDependencyIndex() {
|
|
803
|
+
const routeDependencyIndex = {};
|
|
804
|
+
|
|
805
|
+
for (const [bundleKey, bundle] of Object.entries(this.bundles.routes)) {
|
|
806
|
+
routeDependencyIndex[bundleKey] = {};
|
|
807
|
+
const uniqueComponents = this.dedupeComponentsByName(bundle.components || []);
|
|
808
|
+
|
|
809
|
+
for (const comp of uniqueComponents) {
|
|
810
|
+
const fileBaseName = comp.fileName || comp.name;
|
|
811
|
+
const jsPath = path.join(comp.path, `${fileBaseName}.js`);
|
|
812
|
+
if (!await fs.pathExists(jsPath)) continue;
|
|
813
|
+
const jsContent = await fs.readFile(jsPath, 'utf-8');
|
|
814
|
+
const dependencies = await this.buildDependencyContents(jsContent, comp.path);
|
|
815
|
+
for (const [depName, depEntry] of Object.entries(dependencies || {})) {
|
|
816
|
+
if (!routeDependencyIndex[bundleKey][depName]) {
|
|
817
|
+
routeDependencyIndex[bundleKey][depName] = depEntry;
|
|
818
|
+
}
|
|
819
|
+
if (!this.vendorShared.dependencyModules.has(depName)) {
|
|
820
|
+
this.vendorShared.dependencyModules.set(depName, {
|
|
821
|
+
name: depName,
|
|
822
|
+
content: depEntry?.content || ''
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return routeDependencyIndex;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
indexExternalDependencyUsage(routeDependencyIndex = {}) {
|
|
833
|
+
const usage = new Map();
|
|
834
|
+
|
|
835
|
+
for (const [bundleKey, dependencies] of Object.entries(routeDependencyIndex || {})) {
|
|
836
|
+
for (const [dependencyName, dependencyEntry] of Object.entries(dependencies || {})) {
|
|
837
|
+
if (!usage.has(dependencyName)) {
|
|
838
|
+
usage.set(dependencyName, {
|
|
839
|
+
name: dependencyName,
|
|
840
|
+
bundleKeys: new Set(),
|
|
841
|
+
bundleCount: 0,
|
|
842
|
+
content: dependencyEntry?.content || ''
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
const entry = usage.get(dependencyName);
|
|
846
|
+
entry.bundleKeys.add(bundleKey);
|
|
847
|
+
entry.bundleCount = entry.bundleKeys.size;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return usage;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
computeSharedDependencySet(usageIndex = new Map()) {
|
|
855
|
+
const shared = new Set();
|
|
856
|
+
|
|
857
|
+
for (const [dependencyName, entry] of usageIndex.entries()) {
|
|
858
|
+
if ((entry?.bundleCount || 0) < this.config.minVendorSharedUsage) continue;
|
|
859
|
+
const transformedContent = this.transformDependencyContent(
|
|
860
|
+
entry?.content || '',
|
|
861
|
+
'__sliceVendorSharedProbe',
|
|
862
|
+
dependencyName
|
|
863
|
+
);
|
|
864
|
+
const transformedSize = Buffer.byteLength(transformedContent, 'utf-8');
|
|
865
|
+
if (transformedSize < this.config.minVendorSharedTransformedSize) continue;
|
|
866
|
+
shared.add(dependencyName);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return shared;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
generateVendorSharedDependencyBundleContent(sharedDependencySet = new Set()) {
|
|
873
|
+
const selectedModules = Array.from(sharedDependencySet)
|
|
874
|
+
.sort((a, b) => a.localeCompare(b))
|
|
875
|
+
.map((dependencyName) => {
|
|
876
|
+
const fromUsage = this.vendorShared.dependencyUsage.get(dependencyName)?.content;
|
|
877
|
+
const fromCollected = this.vendorShared.dependencyModules.get(dependencyName)?.content;
|
|
878
|
+
const content = fromUsage || fromCollected || '';
|
|
879
|
+
return { name: dependencyName, content };
|
|
880
|
+
})
|
|
881
|
+
.filter((entry) => !!entry.content);
|
|
882
|
+
|
|
883
|
+
const dependencyModuleBlock = this.buildV2DependencyModuleBlockFromModules(selectedModules);
|
|
884
|
+
const metadata = {
|
|
885
|
+
version: '2',
|
|
886
|
+
bundleKey: 'vendor-shared',
|
|
887
|
+
type: 'vendor-shared',
|
|
888
|
+
routes: [],
|
|
889
|
+
componentCount: 0,
|
|
890
|
+
dependencyCount: selectedModules.length
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
return `export const SLICE_BUNDLE_META = ${JSON.stringify(metadata, null, 2)};\n\n${dependencyModuleBlock}\n\nexport async function registerAll() {\n return SLICE_BUNDLE_DEPENDENCIES;\n}\n`;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
async createVendorSharedDependencyBundleFile(sharedDependencySet) {
|
|
897
|
+
const fileName = this.vendorShared.file;
|
|
898
|
+
const filePath = path.join(this.bundlesPath, fileName);
|
|
899
|
+
const bundleContent = this.generateVendorSharedDependencyBundleContent(sharedDependencySet);
|
|
900
|
+
const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
|
|
901
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
902
|
+
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
903
|
+
|
|
904
|
+
const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
|
|
905
|
+
|
|
906
|
+
return {
|
|
907
|
+
name: 'vendor-shared',
|
|
908
|
+
file: fileName,
|
|
909
|
+
path: filePath,
|
|
910
|
+
size: Buffer.byteLength(bundleContent, 'utf-8'),
|
|
911
|
+
hash,
|
|
912
|
+
integrity: `sha256:${hash}`,
|
|
913
|
+
componentCount: 0
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Creates a bundle file
|
|
919
|
+
*/
|
|
920
|
+
async createBundleFile(components, type, routePath) {
|
|
921
|
+
const routeKey = routePath ? this.routeToFileName(routePath) : 'critical';
|
|
922
|
+
const fileName = `slice-bundle.${routeKey}.js`;
|
|
923
|
+
const filePath = path.join(this.bundlesPath, fileName);
|
|
924
|
+
|
|
925
|
+
const bundleContent = await this.generateBundleContent(
|
|
926
|
+
components,
|
|
927
|
+
type,
|
|
928
|
+
routePath,
|
|
929
|
+
routeKey,
|
|
930
|
+
fileName
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
|
|
934
|
+
|
|
935
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
936
|
+
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
937
|
+
|
|
938
|
+
const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
|
|
939
|
+
|
|
940
|
+
return {
|
|
941
|
+
name: routeKey,
|
|
942
|
+
file: fileName,
|
|
943
|
+
path: filePath,
|
|
944
|
+
size: Buffer.byteLength(bundleContent, 'utf-8'),
|
|
945
|
+
hash,
|
|
946
|
+
componentCount: components.length
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async applyBundleTransforms(bundleContent, fileName) {
|
|
951
|
+
if (!this.options.minify && !this.options.obfuscate) {
|
|
952
|
+
return bundleContent;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const options = {
|
|
956
|
+
parse: {
|
|
957
|
+
ecma: 2022
|
|
958
|
+
},
|
|
959
|
+
ecma: 2022,
|
|
960
|
+
compress: this.options.minify ? {
|
|
961
|
+
drop_console: false,
|
|
962
|
+
drop_debugger: true,
|
|
963
|
+
passes: 1
|
|
964
|
+
} : false,
|
|
965
|
+
mangle: this.options.obfuscate ? {
|
|
966
|
+
properties: { regex: /^_/ }
|
|
967
|
+
} : false,
|
|
968
|
+
keep_fnames: true,
|
|
969
|
+
keep_classnames: true,
|
|
970
|
+
format: {
|
|
971
|
+
comments: false,
|
|
972
|
+
ecma: 2022
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
let result;
|
|
977
|
+
try {
|
|
978
|
+
result = await terserMinify(bundleContent, options);
|
|
979
|
+
} catch (error) {
|
|
980
|
+
const tmpDir = path.resolve(process.cwd(), '.tmp');
|
|
981
|
+
const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
982
|
+
const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
|
|
983
|
+
try {
|
|
984
|
+
await fs.ensureDir(tmpDir);
|
|
985
|
+
await fs.writeFile(tmpPath, bundleContent, 'utf-8');
|
|
986
|
+
} catch (writeError) {
|
|
987
|
+
console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
|
|
988
|
+
}
|
|
989
|
+
const message = error?.message ? `${error.message}.` : 'Unknown Terser error.';
|
|
990
|
+
throw new Error(`Terser failed for ${fileName}: ${message} Saved bundle to ${tmpPath}`);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (result.error) {
|
|
994
|
+
const tmpDir = path.resolve(process.cwd(), '.tmp');
|
|
995
|
+
const safeName = fileName.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
996
|
+
const tmpPath = path.join(tmpDir, `terser-fail-${safeName}`);
|
|
997
|
+
try {
|
|
998
|
+
await fs.ensureDir(tmpDir);
|
|
999
|
+
await fs.writeFile(tmpPath, bundleContent, 'utf-8');
|
|
1000
|
+
} catch (writeError) {
|
|
1001
|
+
console.warn(`Warning: Failed to write ${tmpPath}:`, writeError.message);
|
|
1002
|
+
}
|
|
1003
|
+
throw new Error(`Terser failed for ${fileName}: ${result.error.message}. Saved bundle to ${tmpPath}`);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return result.code || bundleContent;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Analyzes dependencies of a JavaScript file using simple regex
|
|
1012
|
+
*/
|
|
1013
|
+
analyzeDependencies(jsContent, componentPath) {
|
|
1014
|
+
const dependencies = [];
|
|
1015
|
+
|
|
1016
|
+
const resolveImportPath = (importPath) => {
|
|
1017
|
+
const resolvedPath = path.resolve(componentPath, importPath);
|
|
1018
|
+
let finalPath = resolvedPath;
|
|
1019
|
+
const ext = path.extname(resolvedPath);
|
|
1020
|
+
if (!ext) {
|
|
1021
|
+
const extensions = ['.js', '.json', '.mjs'];
|
|
1022
|
+
for (const extension of extensions) {
|
|
1023
|
+
if (fs.existsSync(resolvedPath + extension)) {
|
|
1024
|
+
finalPath = resolvedPath + extension;
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
return fs.existsSync(finalPath) ? finalPath : null;
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
try {
|
|
1034
|
+
const ast = parse(jsContent, {
|
|
1035
|
+
sourceType: 'module',
|
|
1036
|
+
plugins: ['jsx']
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
traverse.default(ast, {
|
|
1040
|
+
ImportDeclaration(pathNode) {
|
|
1041
|
+
const importPath = pathNode.node.source.value;
|
|
1042
|
+
if (!importPath.startsWith('./') && !importPath.startsWith('../')) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const resolvedPath = resolveImportPath(importPath);
|
|
1047
|
+
if (!resolvedPath) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const bindings = pathNode.node.specifiers.map(spec => {
|
|
1052
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
1053
|
+
return {
|
|
1054
|
+
type: 'default',
|
|
1055
|
+
importedName: 'default',
|
|
1056
|
+
localName: spec.local.name
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (spec.type === 'ImportSpecifier') {
|
|
1061
|
+
return {
|
|
1062
|
+
type: 'named',
|
|
1063
|
+
importedName: spec.imported.name,
|
|
1064
|
+
localName: spec.local.name
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
1069
|
+
return {
|
|
1070
|
+
type: 'namespace',
|
|
1071
|
+
localName: spec.local.name
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return null;
|
|
1076
|
+
}).filter(Boolean);
|
|
1077
|
+
|
|
1078
|
+
dependencies.push({
|
|
1079
|
+
path: resolvedPath,
|
|
1080
|
+
bindings
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
console.warn(`Warning: Could not analyze dependencies for ${componentPath}:`, error.message);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return dependencies;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Generates the content of a bundle
|
|
1093
|
+
*/
|
|
1094
|
+
async generateBundleContent(components, type, routePath, bundleKey, fileName) {
|
|
1095
|
+
const bundleComponents = [];
|
|
1096
|
+
const uniqueComponents = this.dedupeComponentsByName(components || []);
|
|
1097
|
+
|
|
1098
|
+
for (const comp of uniqueComponents) {
|
|
1099
|
+
const fileBaseName = comp.fileName || comp.name;
|
|
1100
|
+
const jsPath = path.join(comp.path, `${fileBaseName}.js`);
|
|
1101
|
+
const jsContent = await fs.readFile(jsPath, 'utf-8');
|
|
1102
|
+
|
|
1103
|
+
let htmlContent = null;
|
|
1104
|
+
let cssContent = null;
|
|
1105
|
+
|
|
1106
|
+
const htmlPath = path.join(comp.path, `${fileBaseName}.html`);
|
|
1107
|
+
const cssPath = path.join(comp.path, `${fileBaseName}.css`);
|
|
1108
|
+
|
|
1109
|
+
if (await fs.pathExists(htmlPath)) {
|
|
1110
|
+
htmlContent = await fs.readFile(htmlPath, 'utf-8');
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (await fs.pathExists(cssPath)) {
|
|
1114
|
+
cssContent = await fs.readFile(cssPath, 'utf-8');
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
|
|
1118
|
+
|
|
1119
|
+
bundleComponents.push({
|
|
1120
|
+
name: comp.name,
|
|
1121
|
+
category: comp.category,
|
|
1122
|
+
categoryType: comp.categoryType,
|
|
1123
|
+
js: cleanedJavaScript.code,
|
|
1124
|
+
hoistedImports: cleanedJavaScript.hoistedImports,
|
|
1125
|
+
html: htmlContent,
|
|
1126
|
+
css: cssContent,
|
|
1127
|
+
externalDependencies: await this.buildDependencyContents(jsContent, comp.path),
|
|
1128
|
+
size: comp.size
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return this.generateBundleFileContent(fileName, type, this.sortComponentsByName(bundleComponents), routePath);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
classFactoryName(componentName) {
|
|
1136
|
+
return `SLICE_CLASS_FACTORY_${this.toSafeIdentifier(componentName)}`;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
indentCodeBlock(code, spaces = 2) {
|
|
1140
|
+
const indentation = ' '.repeat(spaces);
|
|
1141
|
+
return String(code)
|
|
1142
|
+
.split('\n')
|
|
1143
|
+
.map((line) => `${indentation}${line}`)
|
|
1144
|
+
.join('\n');
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
generateBundleFileContent(fileName, type, components, routePath = null) {
|
|
1148
|
+
const uniqueComponents = this.dedupeComponentsByName(components || []);
|
|
1149
|
+
const bundleKey = type === 'critical'
|
|
1150
|
+
? 'critical'
|
|
1151
|
+
: type === 'framework'
|
|
1152
|
+
? 'framework'
|
|
1153
|
+
: this.routeToFileName(routePath || fileName.replace('slice-bundle.', '').replace('.js', ''));
|
|
1154
|
+
|
|
1155
|
+
const dependencyModules = this.collectDependencyModulesFromComponents(uniqueComponents);
|
|
1156
|
+
const isRouteBundle = type === 'route';
|
|
1157
|
+
const dependencyModuleBlock = this.buildV2DependencyModuleBlock(uniqueComponents, {
|
|
1158
|
+
includeSharedResolver: isRouteBundle,
|
|
1159
|
+
omittedDependencies: isRouteBundle ? this.vendorShared.sharedDependencySet : null
|
|
1160
|
+
});
|
|
1161
|
+
const rawHoistedImports = uniqueComponents
|
|
1162
|
+
.flatMap((component) => component.hoistedImports || [])
|
|
1163
|
+
.map((statement) => String(statement).trim())
|
|
1164
|
+
.filter(Boolean);
|
|
1165
|
+
const reservedIdentifiers = new Set([
|
|
1166
|
+
'SLICE_BUNDLE_META',
|
|
1167
|
+
'SLICE_BUNDLE_DEPENDENCIES',
|
|
1168
|
+
...uniqueComponents.map((component) => this.classFactoryName(component.name)),
|
|
1169
|
+
...uniqueComponents.map((component) => `__templateElement_${this.toSafeIdentifier(component.name)}`),
|
|
1170
|
+
...this.getDependencyExportVariableNames(dependencyModules)
|
|
1171
|
+
]);
|
|
1172
|
+
this.validateHoistedImportCollisions(rawHoistedImports, reservedIdentifiers);
|
|
1173
|
+
const hoistedImports = Array.from(new Set(rawHoistedImports));
|
|
1174
|
+
const hoistedImportBlock = hoistedImports.join('\n');
|
|
1175
|
+
|
|
1176
|
+
const classFactoryDefinitions = uniqueComponents
|
|
1177
|
+
.map((component) => {
|
|
1178
|
+
const factoryName = this.classFactoryName(component.name);
|
|
1179
|
+
const dependencyBindings = this.buildDependencyBindings(component.externalDependencies || {}, {
|
|
1180
|
+
preferShared: isRouteBundle
|
|
1181
|
+
});
|
|
1182
|
+
const body = component.js && component.js.trim()
|
|
1183
|
+
? component.js
|
|
1184
|
+
: `return window.${component.name};`;
|
|
1185
|
+
const bodyWithBindings = dependencyBindings
|
|
1186
|
+
? `${dependencyBindings}\n${body}`
|
|
1187
|
+
: body;
|
|
1188
|
+
return `const ${factoryName} = () => {\n${this.indentCodeBlock(bodyWithBindings, 2)}\n};`;
|
|
1189
|
+
})
|
|
1190
|
+
.join('\n\n');
|
|
1191
|
+
|
|
1192
|
+
const templateDeclarations = uniqueComponents
|
|
1193
|
+
.map((component) => {
|
|
1194
|
+
const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
|
|
1195
|
+
return `const ${templateVarName} = document.createElement('template');\n${templateVarName}.innerHTML = ${JSON.stringify(component.html || '')};`;
|
|
1196
|
+
})
|
|
1197
|
+
.join('\n');
|
|
1198
|
+
|
|
1199
|
+
const classRegistrations = uniqueComponents
|
|
1200
|
+
.map((component) => {
|
|
1201
|
+
const componentName = JSON.stringify(component.name);
|
|
1202
|
+
return ` if (!controller.classes.has(${componentName})) {\n controller.classes.set(${componentName}, ${this.classFactoryName(component.name)}());\n }`;
|
|
1203
|
+
})
|
|
1204
|
+
.join('\n');
|
|
1205
|
+
|
|
1206
|
+
const templateRegistrations = uniqueComponents
|
|
1207
|
+
.map((component) => {
|
|
1208
|
+
const componentName = JSON.stringify(component.name);
|
|
1209
|
+
const templateVarName = `__templateElement_${this.toSafeIdentifier(component.name)}`;
|
|
1210
|
+
return ` if (!controller.templates.has(${componentName})) {\n controller.templates.set(${componentName}, ${templateVarName});\n }`;
|
|
1211
|
+
})
|
|
1212
|
+
.join('\n');
|
|
1213
|
+
|
|
1214
|
+
const cssRegistrationInit = uniqueComponents.length
|
|
1215
|
+
? ` if (!stylesManager.__sliceRegisteredComponentStyles) {\n stylesManager.__sliceRegisteredComponentStyles = new Set();\n }`
|
|
1216
|
+
: '';
|
|
1217
|
+
|
|
1218
|
+
const cssRegistrations = uniqueComponents
|
|
1219
|
+
.map((component) => {
|
|
1220
|
+
const componentName = JSON.stringify(component.name);
|
|
1221
|
+
return ` if (!stylesManager.__sliceRegisteredComponentStyles.has(${componentName})) {\n stylesManager.registerComponentStyles(${componentName}, ${JSON.stringify(component.css || '')});\n stylesManager.__sliceRegisteredComponentStyles.add(${componentName});\n }`;
|
|
1222
|
+
})
|
|
1223
|
+
.join('\n');
|
|
1224
|
+
|
|
1225
|
+
const categoryRegistrations = uniqueComponents
|
|
1226
|
+
.map((component) => {
|
|
1227
|
+
const componentName = JSON.stringify(component.name);
|
|
1228
|
+
return ` if (!controller.componentCategories.has(${componentName})) {\n controller.componentCategories.set(${componentName}, ${JSON.stringify(component.category)});\n }`;
|
|
1229
|
+
})
|
|
1230
|
+
.join('\n');
|
|
1231
|
+
|
|
1232
|
+
const metadata = {
|
|
1233
|
+
version: '2',
|
|
1234
|
+
bundleKey,
|
|
1235
|
+
type,
|
|
1236
|
+
routes: routePath ? [routePath] : [],
|
|
1237
|
+
componentCount: uniqueComponents.length
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}export const SLICE_BUNDLE_META = ${JSON.stringify(metadata, null, 2)};\n\n${dependencyModuleBlock}\n\n${classFactoryDefinitions}\n\n${templateDeclarations}\n\nexport async function registerAll(controller, stylesManager) {\n${classRegistrations}\n${templateRegistrations}\n${cssRegistrationInit}${cssRegistrationInit ? '\n' : ''}${cssRegistrations}\n${categoryRegistrations}\n}\n`;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
buildV2DependencyModuleBlock(components, options = {}) {
|
|
1244
|
+
const modules = this.collectDependencyModulesFromComponents(components);
|
|
1245
|
+
return this.buildV2DependencyModuleBlockFromModules(modules, options);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
buildV2DependencyModuleBlockFromModules(modules = [], options = {}) {
|
|
1249
|
+
const omittedDependencies = options.omittedDependencies instanceof Set
|
|
1250
|
+
? options.omittedDependencies
|
|
1251
|
+
: new Set(options.omittedDependencies || []);
|
|
1252
|
+
// Emit in topological order so a module is registered before any module
|
|
1253
|
+
// that depends on it (its transitive imports resolve at IIFE-eval time).
|
|
1254
|
+
const filteredModules = this.sortDependencyModulesTopologically(
|
|
1255
|
+
modules.filter((module) => !omittedDependencies.has(module.name))
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
const lines = [
|
|
1259
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1260
|
+
...this.getDefaultExportResolverLines()
|
|
1261
|
+
];
|
|
1262
|
+
if (options.includeSharedResolver) {
|
|
1263
|
+
lines.push(...this.getBundleDependencyResolverLines());
|
|
1264
|
+
}
|
|
1265
|
+
filteredModules.forEach((module, index) => {
|
|
1266
|
+
const exportVar = `__sliceDepExports${index}`;
|
|
1267
|
+
// Evaluate each dependency inside its own IIFE so its private,
|
|
1268
|
+
// non-exported top-level bindings stay local and cannot collide with
|
|
1269
|
+
// another dependency's (or the bundle's) identifiers. Only the exports
|
|
1270
|
+
// object escapes the closure.
|
|
1271
|
+
const transformedContent = this.transformDependencyContent(module.content, '__sliceExports', module.name);
|
|
1272
|
+
// Bind this module's own (transitive) imports inside its IIFE — they were
|
|
1273
|
+
// registered by earlier modules in the topological order.
|
|
1274
|
+
const importBindings = this.buildDependencyBindings(
|
|
1275
|
+
Object.fromEntries(
|
|
1276
|
+
(module.moduleImports || [])
|
|
1277
|
+
.filter((mi) => mi.bindings && mi.bindings.length)
|
|
1278
|
+
.map((mi) => [mi.depName, { bindings: mi.bindings }])
|
|
1279
|
+
),
|
|
1280
|
+
{ preferShared: !!options.includeSharedResolver }
|
|
1281
|
+
);
|
|
1282
|
+
const body = transformedContent.trim();
|
|
1283
|
+
lines.push(`const ${exportVar} = (() => {`);
|
|
1284
|
+
lines.push('const __sliceExports = {};');
|
|
1285
|
+
if (importBindings) lines.push(importBindings);
|
|
1286
|
+
if (body) lines.push(body);
|
|
1287
|
+
lines.push('return __sliceExports;');
|
|
1288
|
+
lines.push('})();');
|
|
1289
|
+
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
return lines.join('\n');
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
sortDependencyModulesTopologically(modules = []) {
|
|
1296
|
+
const byName = new Map(modules.map((module) => [module.name, module]));
|
|
1297
|
+
const visited = new Set();
|
|
1298
|
+
const ordered = [];
|
|
1299
|
+
const visit = (module, stack) => {
|
|
1300
|
+
if (visited.has(module.name) || stack.has(module.name)) return;
|
|
1301
|
+
stack.add(module.name);
|
|
1302
|
+
for (const imp of module.moduleImports || []) {
|
|
1303
|
+
const dependency = byName.get(imp.depName);
|
|
1304
|
+
if (dependency) visit(dependency, stack);
|
|
1305
|
+
}
|
|
1306
|
+
stack.delete(module.name);
|
|
1307
|
+
visited.add(module.name);
|
|
1308
|
+
ordered.push(module);
|
|
1309
|
+
};
|
|
1310
|
+
for (const module of modules) visit(module, new Set());
|
|
1311
|
+
return ordered;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async buildDependencyContents(jsContent, componentPath) {
|
|
1315
|
+
const dependencyContents = {};
|
|
1316
|
+
const visited = new Set();
|
|
1317
|
+
|
|
1318
|
+
// Recursively resolve the relative-import graph rooted at `content`, so a
|
|
1319
|
+
// dependency module's OWN (transitive) imports are inlined too. Returns the
|
|
1320
|
+
// consumer's direct imports as [{ depName, bindings }].
|
|
1321
|
+
const resolveModule = async (content, basePath) => {
|
|
1322
|
+
const consumerImports = [];
|
|
1323
|
+
|
|
1324
|
+
for (const dep of this.analyzeDependencies(content, basePath)) {
|
|
1325
|
+
const depName = path.relative(this.srcPath, dep.path).replace(/\\/g, '/');
|
|
1326
|
+
consumerImports.push({ depName, bindings: dep.bindings || [] });
|
|
1327
|
+
|
|
1328
|
+
if (visited.has(depName)) continue;
|
|
1329
|
+
visited.add(depName);
|
|
1330
|
+
|
|
1331
|
+
try {
|
|
1332
|
+
const depContent = await fs.readFile(dep.path, 'utf-8');
|
|
1333
|
+
// Resolve this module's own transitive imports first.
|
|
1334
|
+
const moduleImports = await resolveModule(depContent, path.dirname(dep.path));
|
|
1335
|
+
dependencyContents[depName] = { content: depContent, bindings: [], moduleImports };
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
console.warn(`Warning: Could not read dependency ${dep.path}:`, error.message);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
return consumerImports;
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
const directImports = await resolveModule(jsContent, componentPath);
|
|
1345
|
+
// The component's direct imports drive its class-factory bindings.
|
|
1346
|
+
for (const { depName, bindings } of directImports) {
|
|
1347
|
+
if (dependencyContents[depName]) {
|
|
1348
|
+
dependencyContents[depName].bindings = bindings;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
return dependencyContents;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Cleans JavaScript code by removing imports/exports and ensuring class is available globally
|
|
1357
|
+
*/
|
|
1358
|
+
cleanJavaScript(code, componentName, sourceContext = componentName) {
|
|
1359
|
+
// Remove export default
|
|
1360
|
+
code = code.replace(/export\s+default\s+/g, '');
|
|
1361
|
+
|
|
1362
|
+
// Remove only unsupported imports (relative always removed, allowed absolute kept)
|
|
1363
|
+
const stripped = this.stripImports(code, {
|
|
1364
|
+
sourceContext,
|
|
1365
|
+
collectHoistedImports: true
|
|
1366
|
+
});
|
|
1367
|
+
const hoistedImports = stripped.hoistedImports || [];
|
|
1368
|
+
code = stripped.code;
|
|
1369
|
+
|
|
1370
|
+
// Guard customElements.define to avoid duplicate registrations
|
|
1371
|
+
code = code.replace(
|
|
1372
|
+
/customElements\.define\(([^)]+)\);?/g,
|
|
1373
|
+
(match, args) => {
|
|
1374
|
+
const firstArg = args.split(',')[0]?.trim() || '';
|
|
1375
|
+
if (!/^['"][^'"]+['"]$/.test(firstArg)) {
|
|
1376
|
+
return match;
|
|
1377
|
+
}
|
|
1378
|
+
return `if (!customElements.get(${firstArg})) { customElements.define(${args}); }`;
|
|
1379
|
+
}
|
|
1380
|
+
);
|
|
1381
|
+
|
|
1382
|
+
// Make sure the class is available globally for bundle evaluation
|
|
1383
|
+
// Preserve original customElements.define if it exists
|
|
1384
|
+
if (code.includes('customElements.define')) {
|
|
1385
|
+
// Add global assignment before guarded or direct customElements.define
|
|
1386
|
+
const globalAssignment = `window.${componentName} = ${componentName};\n`;
|
|
1387
|
+
const guardedDefineRegex = /if\s*\(\s*!\s*customElements\.get\([^)]*\)\s*\)\s*\{\s*customElements\.define\([^;]+\);?\s*\}\s*$/;
|
|
1388
|
+
const directDefineRegex = /customElements\.define\([^;]+\);?\s*$/;
|
|
1389
|
+
if (guardedDefineRegex.test(code)) {
|
|
1390
|
+
code = code.replace(guardedDefineRegex, `${globalAssignment}$&`);
|
|
1391
|
+
} else {
|
|
1392
|
+
code = code.replace(directDefineRegex, `${globalAssignment}$&`);
|
|
1393
|
+
}
|
|
1394
|
+
} else {
|
|
1395
|
+
// If no customElements.define found, just assign to global
|
|
1396
|
+
code += `\nwindow.${componentName} = ${componentName};`;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Add return statement for bundle evaluation compatibility
|
|
1400
|
+
code += `\nreturn ${componentName};`;
|
|
1401
|
+
|
|
1402
|
+
return {
|
|
1403
|
+
code,
|
|
1404
|
+
hoistedImports
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Formats the bundle file
|
|
1410
|
+
*/
|
|
1411
|
+
formatBundleFile(componentsData, metadata) {
|
|
1412
|
+
const integrityPayload = {
|
|
1413
|
+
metadata: {
|
|
1414
|
+
...metadata,
|
|
1415
|
+
generated: 'static'
|
|
1416
|
+
},
|
|
1417
|
+
components: Object.fromEntries(
|
|
1418
|
+
Object.entries(componentsData).map(([name, data]) => [
|
|
1419
|
+
name,
|
|
1420
|
+
{
|
|
1421
|
+
name: data.name,
|
|
1422
|
+
category: data.category,
|
|
1423
|
+
categoryType: data.categoryType,
|
|
1424
|
+
componentDependencies: data.componentDependencies
|
|
1425
|
+
}
|
|
1426
|
+
])
|
|
1427
|
+
)
|
|
1428
|
+
};
|
|
1429
|
+
const integrity = `sha256:${crypto
|
|
1430
|
+
.createHash('sha256')
|
|
1431
|
+
.update(JSON.stringify(integrityPayload))
|
|
1432
|
+
.digest('hex')}`;
|
|
1433
|
+
|
|
1434
|
+
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1435
|
+
const frameworkComponentKeys = Object.keys(componentsData || {});
|
|
1436
|
+
const frameworkClassIdentifiers = frameworkComponentKeys.map((key) => this.toSafeIdentifier(key));
|
|
1437
|
+
const frameworkReservedIdentifiers = new Set([
|
|
1438
|
+
'SLICE_BUNDLE',
|
|
1439
|
+
'SLICE_BUNDLE_COMPONENTS',
|
|
1440
|
+
'SLICE_BUNDLE_DEPENDENCIES',
|
|
1441
|
+
'SLICE_FRAMEWORK_CLASSES',
|
|
1442
|
+
...frameworkClassIdentifiers,
|
|
1443
|
+
...this.getDependencyExportVariableNames(dependencyModules)
|
|
1444
|
+
]);
|
|
1445
|
+
const rawHoistedImports = Object.values(componentsData || {})
|
|
1446
|
+
.flatMap((component) => component?.hoistedImports || [])
|
|
1447
|
+
.map((statement) => String(statement).trim())
|
|
1448
|
+
.filter(Boolean);
|
|
1449
|
+
this.validateHoistedImportCollisions(rawHoistedImports, frameworkReservedIdentifiers);
|
|
1450
|
+
const hoistedImportBlock = Array.from(new Set(rawHoistedImports)).join('\n');
|
|
1451
|
+
|
|
1452
|
+
const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
|
|
1453
|
+
const componentBlock = this.buildComponentBundleBlock(componentsData);
|
|
1454
|
+
|
|
1455
|
+
return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}/**
|
|
1456
|
+
* Slice.js Bundle
|
|
1457
|
+
* Type: ${metadata.type}
|
|
1458
|
+
* Generated: ${metadata.generated}
|
|
1459
|
+
* Strategy: ${metadata.strategy}
|
|
1460
|
+
* Components: ${metadata.componentCount}
|
|
1461
|
+
* Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
|
|
1462
|
+
*/
|
|
1463
|
+
|
|
1464
|
+
${dependencyBlock}
|
|
1465
|
+
${componentBlock}
|
|
1466
|
+
|
|
1467
|
+
export const SLICE_BUNDLE = {
|
|
1468
|
+
metadata: ${JSON.stringify({ ...metadata, integrity }, null, 2)},
|
|
1469
|
+
components: SLICE_BUNDLE_COMPONENTS
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// Auto-registration of components
|
|
1473
|
+
if (window.slice && window.slice.controller) {
|
|
1474
|
+
slice.controller.registerBundle(SLICE_BUNDLE);
|
|
1475
|
+
}
|
|
1476
|
+
`;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
buildDependencyModuleBlock(componentsData) {
|
|
1480
|
+
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1481
|
+
const lines = [
|
|
1482
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1483
|
+
...this.getDefaultExportResolverLines()
|
|
1484
|
+
];
|
|
1485
|
+
if (dependencyModules.length === 0) {
|
|
1486
|
+
return `${lines.join('\n')}`;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
dependencyModules.forEach((module, index) => {
|
|
1490
|
+
const exportVar = `__sliceDepExports${index}`;
|
|
1491
|
+
// Each dependency lives in its own IIFE scope (see
|
|
1492
|
+
// buildV2DependencyModuleBlockFromModules) so private helpers cannot
|
|
1493
|
+
// collide across modules.
|
|
1494
|
+
const content = this.transformDependencyContent(module.content, '__sliceExports', module.name);
|
|
1495
|
+
const body = content.trim();
|
|
1496
|
+
lines.push(`// Dependency: ${module.name}`);
|
|
1497
|
+
lines.push(`const ${exportVar} = (() => {`);
|
|
1498
|
+
lines.push('const __sliceExports = {};');
|
|
1499
|
+
if (body) lines.push(body);
|
|
1500
|
+
lines.push('return __sliceExports;');
|
|
1501
|
+
lines.push('})();');
|
|
1502
|
+
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
return `${lines.join('\n')}`;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
collectDependencyModules(componentsData) {
|
|
1509
|
+
const modules = new Map();
|
|
1510
|
+
Object.values(componentsData).forEach((component) => {
|
|
1511
|
+
Object.entries(component.externalDependencies || {}).forEach(([name, entry]) => {
|
|
1512
|
+
if (modules.has(name)) return;
|
|
1513
|
+
const content = typeof entry === 'string' ? entry : entry.content;
|
|
1514
|
+
modules.set(name, { name, content });
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
return Array.from(modules.values());
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
collectDependencyModulesFromComponents(components = []) {
|
|
1521
|
+
const modules = new Map();
|
|
1522
|
+
for (const component of components || []) {
|
|
1523
|
+
const externalDependencies = component.externalDependencies || {};
|
|
1524
|
+
for (const [moduleName, entry] of Object.entries(externalDependencies)) {
|
|
1525
|
+
if (modules.has(moduleName)) continue;
|
|
1526
|
+
const content = typeof entry === 'string' ? entry : entry?.content;
|
|
1527
|
+
if (!content) continue;
|
|
1528
|
+
const moduleImports = (entry && typeof entry === 'object' ? entry.moduleImports : null) || [];
|
|
1529
|
+
modules.set(moduleName, { name: moduleName, content, moduleImports });
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return Array.from(modules.values());
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
getDependencyExportVariableNames(dependencyModules = []) {
|
|
1536
|
+
return (dependencyModules || []).map((_, index) => `__sliceDepExports${index}`);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
transformDependencyContent(content, exportVar, moduleName) {
|
|
1540
|
+
let ast;
|
|
1541
|
+
try {
|
|
1542
|
+
ast = parse(content, { sourceType: 'module', plugins: ['jsx'] });
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
// Unparseable content (e.g. TS syntax): fall back to the regex transform
|
|
1545
|
+
// so we never lose a dependency entirely.
|
|
1546
|
+
return this.transformDependencyContentRegexFallback(content, exportVar, moduleName);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const fallbackKey = this.getDependencyDefaultFallbackKey(moduleName);
|
|
1550
|
+
const statements = ast.program.body
|
|
1551
|
+
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
|
|
1552
|
+
.sort((a, b) => a.start - b.start);
|
|
1553
|
+
|
|
1554
|
+
let cursor = 0;
|
|
1555
|
+
let output = '';
|
|
1556
|
+
for (const node of statements) {
|
|
1557
|
+
output += content.slice(cursor, node.start);
|
|
1558
|
+
output += this.transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey);
|
|
1559
|
+
cursor = node.end;
|
|
1560
|
+
}
|
|
1561
|
+
output += content.slice(cursor);
|
|
1562
|
+
return output;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
describeExportTarget(exportVar, name) {
|
|
1566
|
+
return /^[A-Za-z_$][\w$]*$/.test(name)
|
|
1567
|
+
? `${exportVar}.${name}`
|
|
1568
|
+
: `${exportVar}[${JSON.stringify(name)}]`;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
collectPatternIdentifiers(node, acc = []) {
|
|
1572
|
+
if (!node) return acc;
|
|
1573
|
+
switch (node.type) {
|
|
1574
|
+
case 'Identifier':
|
|
1575
|
+
acc.push(node.name);
|
|
1576
|
+
break;
|
|
1577
|
+
case 'ObjectPattern':
|
|
1578
|
+
for (const prop of node.properties) {
|
|
1579
|
+
if (prop.type === 'RestElement') {
|
|
1580
|
+
this.collectPatternIdentifiers(prop.argument, acc);
|
|
1581
|
+
} else {
|
|
1582
|
+
this.collectPatternIdentifiers(prop.value, acc);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
break;
|
|
1586
|
+
case 'ArrayPattern':
|
|
1587
|
+
for (const element of node.elements) {
|
|
1588
|
+
if (element) this.collectPatternIdentifiers(element, acc);
|
|
1589
|
+
}
|
|
1590
|
+
break;
|
|
1591
|
+
case 'AssignmentPattern':
|
|
1592
|
+
this.collectPatternIdentifiers(node.left, acc);
|
|
1593
|
+
break;
|
|
1594
|
+
case 'RestElement':
|
|
1595
|
+
this.collectPatternIdentifiers(node.argument, acc);
|
|
1596
|
+
break;
|
|
1597
|
+
default:
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
return acc;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
transformExportedDeclaration(decl, content, exportVar) {
|
|
1604
|
+
const sourceOf = (n) => content.slice(n.start, n.end);
|
|
1605
|
+
|
|
1606
|
+
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
|
|
1607
|
+
const name = decl.id?.name;
|
|
1608
|
+
if (!name) return sourceOf(decl);
|
|
1609
|
+
// Keep the declaration so other code in the module can still reference the
|
|
1610
|
+
// name (intra-module references), then mirror it onto the exports object.
|
|
1611
|
+
// Each dependency is IIFE-scoped, so this local binding can't collide.
|
|
1612
|
+
return `${sourceOf(decl)}\n${this.describeExportTarget(exportVar, name)} = ${name};`;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
if (decl.type === 'VariableDeclaration') {
|
|
1616
|
+
// Keep the declaration verbatim — preserving intra-module references and
|
|
1617
|
+
// initializer evaluation — then export every bound name.
|
|
1618
|
+
const names = [];
|
|
1619
|
+
for (const declarator of decl.declarations) {
|
|
1620
|
+
names.push(...this.collectPatternIdentifiers(declarator.id, []));
|
|
1621
|
+
}
|
|
1622
|
+
const assigns = names
|
|
1623
|
+
.map((n) => `${this.describeExportTarget(exportVar, n)} = ${n};`)
|
|
1624
|
+
.join('\n');
|
|
1625
|
+
return `${sourceOf(decl)}\n${assigns}`;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return sourceOf(decl);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey) {
|
|
1632
|
+
const sourceOf = (n) => content.slice(n.start, n.end);
|
|
1633
|
+
|
|
1634
|
+
if (node.type === 'ImportDeclaration') {
|
|
1635
|
+
// Transitive imports of a bundled dependency cannot be resolved at
|
|
1636
|
+
// runtime; strip them so they never leak into the emitted bundle.
|
|
1637
|
+
console.warn(this.buildImportWarningMessage(
|
|
1638
|
+
`Warning: Stripping unsupported import inside bundled dependency: ${node.source?.value}`,
|
|
1639
|
+
moduleName
|
|
1640
|
+
));
|
|
1641
|
+
return '';
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (node.type === 'ExportAllDeclaration') {
|
|
1645
|
+
console.warn(this.buildImportWarningMessage(
|
|
1646
|
+
`Warning: Dropping unsupported 'export *' inside bundled dependency: ${node.source?.value}`,
|
|
1647
|
+
moduleName
|
|
1648
|
+
));
|
|
1649
|
+
return '';
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
if (node.type === 'ExportDefaultDeclaration') {
|
|
1653
|
+
const declSource = sourceOf(node.declaration);
|
|
1654
|
+
const lines = [`${exportVar}.default = (${declSource});`];
|
|
1655
|
+
if (fallbackKey && fallbackKey !== 'default') {
|
|
1656
|
+
// Preserve the historical `<basename>Data` key so existing default
|
|
1657
|
+
// bindings (which pass it as the preferred key) keep resolving.
|
|
1658
|
+
lines.push(`${this.describeExportTarget(exportVar, fallbackKey)} = ${exportVar}.default;`);
|
|
1659
|
+
}
|
|
1660
|
+
return lines.join('\n');
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if (node.type === 'ExportNamedDeclaration') {
|
|
1664
|
+
if (node.source) {
|
|
1665
|
+
console.warn(this.buildImportWarningMessage(
|
|
1666
|
+
`Warning: Dropping unsupported re-export inside bundled dependency: ${node.source.value}`,
|
|
1667
|
+
moduleName
|
|
1668
|
+
));
|
|
1669
|
+
return '';
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (node.declaration) {
|
|
1673
|
+
return this.transformExportedDeclaration(node.declaration, content, exportVar);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// `export { local as exported, ... }` — key the exports object by the
|
|
1677
|
+
// PUBLIC (exported) name, mapped to the local binding's value.
|
|
1678
|
+
return node.specifiers
|
|
1679
|
+
.map((spec) => {
|
|
1680
|
+
const localName = spec.local.name;
|
|
1681
|
+
const exportedName = spec.exported.name ?? spec.exported.value;
|
|
1682
|
+
return `${this.describeExportTarget(exportVar, exportedName)} = ${localName};`;
|
|
1683
|
+
})
|
|
1684
|
+
.join('\n');
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// Any other top-level statement is kept verbatim.
|
|
1688
|
+
return sourceOf(node);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
transformDependencyContentRegexFallback(content, exportVar, moduleName) {
|
|
1692
|
+
const dataName = this.getDependencyDefaultFallbackKey(moduleName);
|
|
1693
|
+
const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
|
|
1694
|
+
|
|
1695
|
+
return content
|
|
1696
|
+
.replace(/export\s+const\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1697
|
+
.replace(/export\s+let\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1698
|
+
.replace(/export\s+var\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1699
|
+
.replace(/export\s+function\s+(\w+)/g, `${exportVar}.$1 = function`)
|
|
1700
|
+
.replace(/export\s+default\s+/g, exportPrefix)
|
|
1701
|
+
.replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
|
|
1702
|
+
return exportsStr
|
|
1703
|
+
.split(',')
|
|
1704
|
+
.map((exp) => {
|
|
1705
|
+
const cleanExp = exp.trim();
|
|
1706
|
+
const varName = cleanExp.split(' as ')[0].trim();
|
|
1707
|
+
return `${exportVar}.${varName} = ${varName};`;
|
|
1708
|
+
})
|
|
1709
|
+
.join('\n');
|
|
1710
|
+
})
|
|
1711
|
+
.replace(/^\s*export\s+/gm, '');
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
getDependencyDefaultFallbackKey(moduleName) {
|
|
1715
|
+
const baseName = moduleName?.split('/').pop()?.replace(/\.[^.]+$/, '');
|
|
1716
|
+
return baseName ? `${baseName}Data` : null;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
buildComponentBundleBlock(componentsData) {
|
|
1720
|
+
const componentEntries = [];
|
|
1721
|
+
const componentDefs = [];
|
|
1722
|
+
const frameworkEntries = [];
|
|
1723
|
+
|
|
1724
|
+
Object.entries(componentsData).forEach(([name, data]) => {
|
|
1725
|
+
const classVar = this.toSafeIdentifier(name);
|
|
1726
|
+
const bindings = this.buildDependencyBindings(data.externalDependencies || {});
|
|
1727
|
+
|
|
1728
|
+
componentDefs.push(`const ${classVar} = (() => {\n${bindings}\n${data.js}\nreturn ${name};\n})();`);
|
|
1729
|
+
|
|
1730
|
+
if (data.isFramework) {
|
|
1731
|
+
frameworkEntries.push(`${JSON.stringify(data.name)}: ${classVar}`);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
componentEntries.push(
|
|
1735
|
+
`${JSON.stringify(name)}: {\n` +
|
|
1736
|
+
` name: ${JSON.stringify(data.name)},\n` +
|
|
1737
|
+
` category: ${JSON.stringify(data.category)},\n` +
|
|
1738
|
+
` categoryType: ${JSON.stringify(data.categoryType)},\n` +
|
|
1739
|
+
` componentDependencies: ${JSON.stringify(data.componentDependencies)},\n` +
|
|
1740
|
+
` html: ${JSON.stringify(data.html)},\n` +
|
|
1741
|
+
` css: ${JSON.stringify(data.css)},\n` +
|
|
1742
|
+
` size: ${JSON.stringify(data.size)},\n` +
|
|
1743
|
+
` class: ${classVar}\n` +
|
|
1744
|
+
`}`
|
|
1745
|
+
);
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
const frameworkBlock = frameworkEntries.length > 0
|
|
1749
|
+
? `const SLICE_FRAMEWORK_CLASSES = {\n${frameworkEntries.join(',\n')}\n};\nwindow.SLICE_FRAMEWORK_CLASSES = SLICE_FRAMEWORK_CLASSES;`
|
|
1750
|
+
: '';
|
|
1751
|
+
|
|
1752
|
+
return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
buildDependencyBindings(externalDependencies, options = {}) {
|
|
1756
|
+
const lines = [];
|
|
1757
|
+
Object.entries(externalDependencies).forEach(([name, entry]) => {
|
|
1758
|
+
const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
|
|
1759
|
+
const depVar = options.preferShared
|
|
1760
|
+
? `__sliceResolveBundleDependency(${JSON.stringify(name)})`
|
|
1761
|
+
: `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
|
|
1762
|
+
|
|
1763
|
+
bindings.forEach((binding) => {
|
|
1764
|
+
if (!binding?.localName) return;
|
|
1765
|
+
if (binding.type === 'default') {
|
|
1766
|
+
const preferredKey = this.getDependencyDefaultFallbackKey(name);
|
|
1767
|
+
lines.push(`const ${binding.localName} = __sliceResolveDefaultExport(${depVar}, ${JSON.stringify(name)}, ${JSON.stringify(preferredKey)});`);
|
|
1768
|
+
}
|
|
1769
|
+
if (binding.type === 'named') {
|
|
1770
|
+
lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
|
|
1771
|
+
}
|
|
1772
|
+
if (binding.type === 'namespace') {
|
|
1773
|
+
lines.push(`const ${binding.localName} = ${depVar};`);
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
});
|
|
1777
|
+
|
|
1778
|
+
return lines.join('\n');
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
getBundleDependencyResolverLines() {
|
|
1782
|
+
return [
|
|
1783
|
+
"const __sliceSharedDeps = typeof window !== 'undefined' ? (window.__SLICE_SHARED_DEPS__ || {}) : {};",
|
|
1784
|
+
'const __sliceResolveBundleDependency = (depName) => Object.prototype.hasOwnProperty.call(__sliceSharedDeps, depName) ? __sliceSharedDeps[depName] : SLICE_BUNDLE_DEPENDENCIES[depName];'
|
|
1785
|
+
];
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
getDefaultExportResolverLines() {
|
|
1789
|
+
return [
|
|
1790
|
+
'const __sliceDefaultExportWarningDeps = new Set();',
|
|
1791
|
+
"const __sliceDefaultExportPreferredKeys = ['module', 'exports', 'purify'];",
|
|
1792
|
+
'const __sliceDeterministicKeyCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);',
|
|
1793
|
+
'function __sliceResolveDefaultExport(dep, depName, preferredKey) {',
|
|
1794
|
+
' if (dep?.default !== undefined) return dep.default;',
|
|
1795
|
+
" if (dep === null || (typeof dep !== 'object' && typeof dep !== 'function')) return dep;",
|
|
1796
|
+
" if (preferredKey && preferredKey !== 'default' && preferredKey !== '__esModule' && Object.prototype.hasOwnProperty.call(dep, preferredKey)) return dep[preferredKey];",
|
|
1797
|
+
" const keys = Object.keys(dep).filter((key) => key !== 'default' && key !== '__esModule');",
|
|
1798
|
+
' if (keys.length === 1) return dep[keys[0]];',
|
|
1799
|
+
' if (keys.length > 1) {',
|
|
1800
|
+
' const preferredMatches = __sliceDefaultExportPreferredKeys.filter((key) => keys.includes(key));',
|
|
1801
|
+
' if (preferredMatches.length === 1) return dep[preferredMatches[0]];',
|
|
1802
|
+
' const sortedKeys = [...keys].sort(__sliceDeterministicKeyCompare);',
|
|
1803
|
+
' const fallbackKey = sortedKeys[0];',
|
|
1804
|
+
' const warningDepName = depName || "<unknown dependency>";',
|
|
1805
|
+
' if (!__sliceDefaultExportWarningDeps.has(warningDepName)) {',
|
|
1806
|
+
' __sliceDefaultExportWarningDeps.add(warningDepName);',
|
|
1807
|
+
' console.warn(`[Slice.js bundler] Ambiguous default export resolution for "${warningDepName}". Falling back to "${fallbackKey}". Keys: ${sortedKeys.join(\', \')}`);',
|
|
1808
|
+
' }',
|
|
1809
|
+
' return dep[fallbackKey];',
|
|
1810
|
+
' }',
|
|
1811
|
+
' return dep;',
|
|
1812
|
+
'}'
|
|
1813
|
+
];
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
toSafeIdentifier(name) {
|
|
1817
|
+
// Injective encoding: every character outside [A-Za-z0-9] (including '_')
|
|
1818
|
+
// is escaped to `_<hex>_`. This guarantees that two distinct component
|
|
1819
|
+
// names can never collapse to the same identifier (e.g. "my-btn" and
|
|
1820
|
+
// "my_btn" used to both yield "SliceComponent_my_btn", emitting duplicate
|
|
1821
|
+
// `const` declarations and producing invalid bundle JS). The leading
|
|
1822
|
+
// `SliceComponent_` prefix keeps the result a valid identifier even when
|
|
1823
|
+
// the name starts with a digit.
|
|
1824
|
+
const encoded = String(name).replace(
|
|
1825
|
+
/[^a-zA-Z0-9]/g,
|
|
1826
|
+
(char) => `_${char.charCodeAt(0).toString(16)}_`
|
|
1827
|
+
);
|
|
1828
|
+
return `SliceComponent_${encoded}`;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
/**
|
|
1832
|
+
* Generates the bundle configuration
|
|
1833
|
+
*/
|
|
1834
|
+
generateBundleConfig(frameworkBundle = null) {
|
|
1835
|
+
const metrics = this.analysisData.metrics || {};
|
|
1836
|
+
const config = {
|
|
1837
|
+
version: '2.0.0',
|
|
1838
|
+
format: this.format,
|
|
1839
|
+
loadingPolicy: this.loadingPolicy,
|
|
1840
|
+
strategy: this.config.strategy,
|
|
1841
|
+
minified: this.options.minify,
|
|
1842
|
+
obfuscated: this.options.obfuscate,
|
|
1843
|
+
production: true,
|
|
1844
|
+
generated: new Date().toISOString(),
|
|
1845
|
+
|
|
1846
|
+
stats: {
|
|
1847
|
+
totalComponents: metrics.totalComponents || 0,
|
|
1848
|
+
totalRoutes: metrics.totalRoutes || 0,
|
|
1849
|
+
sharedComponents: this.bundles.critical.components.length,
|
|
1850
|
+
sharedPercentage: metrics.sharedPercentage || 0,
|
|
1851
|
+
totalSize: metrics.totalSize || 0,
|
|
1852
|
+
criticalSize: this.bundles.critical.size
|
|
1853
|
+
},
|
|
1854
|
+
|
|
1855
|
+
bundles: {
|
|
1856
|
+
framework: {
|
|
1857
|
+
file: 'slice-bundle.framework.js',
|
|
1858
|
+
size: 0,
|
|
1859
|
+
hash: null,
|
|
1860
|
+
integrity: null,
|
|
1861
|
+
components: []
|
|
1862
|
+
},
|
|
1863
|
+
// Only advertise the vendor-shared bundle when it was actually emitted
|
|
1864
|
+
// (i.e. there were shared dependencies). Otherwise the config would
|
|
1865
|
+
// reference a file that does not exist on disk -> 404 for any runtime
|
|
1866
|
+
// that resolves it.
|
|
1867
|
+
vendorShared: this.vendorShared.bundle
|
|
1868
|
+
? {
|
|
1869
|
+
bundleKey: 'vendor-shared',
|
|
1870
|
+
type: 'vendor-shared',
|
|
1871
|
+
file: this.vendorShared.file,
|
|
1872
|
+
size: this.vendorShared.bundle?.size || 0,
|
|
1873
|
+
hash: this.vendorShared.bundle?.hash || null,
|
|
1874
|
+
integrity: this.vendorShared.bundle?.integrity || null,
|
|
1875
|
+
dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
|
|
1876
|
+
dependencyCount: this.vendorShared.sharedDependencySet.size,
|
|
1877
|
+
routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
|
|
1878
|
+
}
|
|
1879
|
+
: null,
|
|
1880
|
+
critical: {
|
|
1881
|
+
file: this.bundles.critical.file,
|
|
1882
|
+
size: this.bundles.critical.size,
|
|
1883
|
+
hash: this.bundles.critical.hash || null,
|
|
1884
|
+
integrity: this.bundles.critical.integrity || null,
|
|
1885
|
+
components: this.bundles.critical.components.map(c => c.name)
|
|
1886
|
+
},
|
|
1887
|
+
routes: {}
|
|
1888
|
+
},
|
|
1889
|
+
routeBundles: {},
|
|
1890
|
+
routeDependencyGraph: {}
|
|
1891
|
+
};
|
|
1892
|
+
|
|
1893
|
+
for (const [key, bundle] of Object.entries(this.bundles.routes)) {
|
|
1894
|
+
const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
|
|
1895
|
+
? key
|
|
1896
|
+
: (bundle.path || bundle.paths || key);
|
|
1897
|
+
const usesVendorShared = this.vendorShared.bundleKeysUsingSharedDependencies.has(key)
|
|
1898
|
+
|| (bundle.dependencies || []).includes('vendor-shared');
|
|
1899
|
+
const dependencies = this.mergeBundleDependencies(
|
|
1900
|
+
bundle.dependencies || [],
|
|
1901
|
+
usesVendorShared ? ['vendor-shared'] : []
|
|
1902
|
+
);
|
|
1903
|
+
|
|
1904
|
+
config.bundles.routes[key] = {
|
|
1905
|
+
path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
|
|
1906
|
+
file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
|
|
1907
|
+
size: bundle.size,
|
|
1908
|
+
hash: bundle.hash || null,
|
|
1909
|
+
integrity: bundle.integrity || null,
|
|
1910
|
+
components: bundle.components.map(c => c.name),
|
|
1911
|
+
dependencies
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
const paths = Array.isArray(config.bundles.routes[key].path)
|
|
1915
|
+
? config.bundles.routes[key].path
|
|
1916
|
+
: [config.bundles.routes[key].path];
|
|
1917
|
+
|
|
1918
|
+
for (const routePath of paths) {
|
|
1919
|
+
if (!config.routeBundles[routePath]) {
|
|
1920
|
+
config.routeBundles[routePath] = ['critical'];
|
|
1921
|
+
}
|
|
1922
|
+
for (const dependency of dependencies.filter((dep) => dep !== 'critical')) {
|
|
1923
|
+
if (!config.routeBundles[routePath].includes(dependency)) {
|
|
1924
|
+
config.routeBundles[routePath].push(dependency);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
if (!config.routeBundles[routePath].includes(key)) {
|
|
1928
|
+
config.routeBundles[routePath].push(key);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
const graphEntry = config.routeDependencyGraph[routePath] || {
|
|
1932
|
+
bundles: [],
|
|
1933
|
+
edges: []
|
|
1934
|
+
};
|
|
1935
|
+
if (!graphEntry.bundles.includes(key)) {
|
|
1936
|
+
graphEntry.bundles.push(key);
|
|
1937
|
+
graphEntry.bundles.sort((a, b) => a.localeCompare(b));
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
const edgeKeys = new Set(graphEntry.edges.map((edge) => `${edge.from}->${edge.to}`));
|
|
1941
|
+
const orderedEdgeSources = ['critical', ...dependencies.filter((dependency) => dependency !== 'critical')];
|
|
1942
|
+
for (const source of orderedEdgeSources) {
|
|
1943
|
+
const edgeKey = `${source}->${key}`;
|
|
1944
|
+
if (!edgeKeys.has(edgeKey)) {
|
|
1945
|
+
graphEntry.edges.push({ from: source, to: key });
|
|
1946
|
+
edgeKeys.add(edgeKey);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
config.routeDependencyGraph[routePath] = graphEntry;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
if (frameworkBundle) {
|
|
1955
|
+
config.bundles.framework = {
|
|
1956
|
+
file: frameworkBundle.file,
|
|
1957
|
+
size: frameworkBundle.size,
|
|
1958
|
+
hash: frameworkBundle.hash,
|
|
1959
|
+
integrity: frameworkBundle.integrity,
|
|
1960
|
+
components: frameworkBundle.components || []
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
return config;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
collectFrameworkComponents() {
|
|
1968
|
+
return this.analysisData.components.filter((comp) => comp.isFramework);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
async createFrameworkBundle(components) {
|
|
1972
|
+
const fileName = 'slice-bundle.framework.js';
|
|
1973
|
+
const filePath = path.join(this.bundlesPath, fileName);
|
|
1974
|
+
return this.generateFrameworkBundleFile(components, fileName, filePath);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
async generateFrameworkBundleFile(components, fileName, filePath) {
|
|
1978
|
+
const componentsData = {};
|
|
1979
|
+
const componentsMap = await this.loadComponentsMap();
|
|
1980
|
+
const metadata = {
|
|
1981
|
+
version: '2.0.0',
|
|
1982
|
+
type: 'framework',
|
|
1983
|
+
route: null,
|
|
1984
|
+
bundleKey: 'framework',
|
|
1985
|
+
file: fileName,
|
|
1986
|
+
generated: new Date().toISOString(),
|
|
1987
|
+
totalSize: components.reduce((sum, c) => sum + c.size, 0),
|
|
1988
|
+
componentCount: components.length,
|
|
1989
|
+
strategy: this.config.strategy,
|
|
1990
|
+
minified: this.options.minify,
|
|
1991
|
+
obfuscated: this.options.obfuscate
|
|
1992
|
+
};
|
|
1993
|
+
|
|
1994
|
+
components.forEach((comp) => {
|
|
1995
|
+
const componentKey = `Framework/Structural/${comp.name}`;
|
|
1996
|
+
const fileBaseName = comp.fileName || comp.name;
|
|
1997
|
+
const jsPath = path.join(comp.path, `${fileBaseName}.js`);
|
|
1998
|
+
const jsContent = fs.readFileSync(jsPath, 'utf-8');
|
|
1999
|
+
const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
|
|
2000
|
+
const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
|
|
2001
|
+
|
|
2002
|
+
componentsData[componentKey] = {
|
|
2003
|
+
name: comp.name,
|
|
2004
|
+
category: comp.category,
|
|
2005
|
+
categoryType: comp.categoryType,
|
|
2006
|
+
isFramework: true,
|
|
2007
|
+
js: cleanedJavaScript.code,
|
|
2008
|
+
hoistedImports: cleanedJavaScript.hoistedImports,
|
|
2009
|
+
externalDependencies: dependencyContents,
|
|
2010
|
+
componentDependencies: Array.from(comp.dependencies),
|
|
2011
|
+
html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
|
|
2012
|
+
? fs.readFileSync(path.join(comp.path, `${fileBaseName}.html`), 'utf-8')
|
|
2013
|
+
: null,
|
|
2014
|
+
css: fs.existsSync(path.join(comp.path, `${fileBaseName}.css`))
|
|
2015
|
+
? fs.readFileSync(path.join(comp.path, `${fileBaseName}.css`), 'utf-8')
|
|
2016
|
+
: null,
|
|
2017
|
+
size: comp.size
|
|
2018
|
+
};
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
const prelude = `const components = ${JSON.stringify(componentsMap)};`;
|
|
2022
|
+
const bundleContent = `${prelude}\n${this.formatBundleFile(componentsData, metadata)}`;
|
|
2023
|
+
const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
|
|
2024
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
2025
|
+
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
2026
|
+
|
|
2027
|
+
const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
|
|
2028
|
+
const integrity = `sha256:${hash}`;
|
|
2029
|
+
|
|
2030
|
+
return {
|
|
2031
|
+
name: 'framework',
|
|
2032
|
+
file: fileName,
|
|
2033
|
+
size: Buffer.byteLength(bundleContent, 'utf-8'),
|
|
2034
|
+
hash,
|
|
2035
|
+
integrity,
|
|
2036
|
+
componentCount: components.length,
|
|
2037
|
+
components: components.map((comp) => `Framework/Structural/${comp.name}`)
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
buildDependencyContentsSync(jsContent, componentPath) {
|
|
2042
|
+
const dependencies = this.analyzeDependencies(jsContent, componentPath);
|
|
2043
|
+
const dependencyContents = {};
|
|
2044
|
+
|
|
2045
|
+
for (const dep of dependencies) {
|
|
2046
|
+
const depPath = dep.path;
|
|
2047
|
+
try {
|
|
2048
|
+
const depContent = fs.readFileSync(depPath, 'utf-8');
|
|
2049
|
+
const depName = path
|
|
2050
|
+
.relative(this.srcPath, depPath)
|
|
2051
|
+
.replace(/\\/g, '/');
|
|
2052
|
+
dependencyContents[depName] = {
|
|
2053
|
+
content: depContent,
|
|
2054
|
+
bindings: dep.bindings || []
|
|
2055
|
+
};
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
return dependencyContents;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
getConfiguredPublicFolders() {
|
|
2065
|
+
const publicFolders = Array.isArray(this.sliceConfig?.publicFolders)
|
|
2066
|
+
? this.sliceConfig.publicFolders
|
|
2067
|
+
: [];
|
|
2068
|
+
|
|
2069
|
+
return publicFolders
|
|
2070
|
+
.map((folder) => this.normalizePublicFolder(folder))
|
|
2071
|
+
.filter(Boolean);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
normalizePublicFolder(folder) {
|
|
2075
|
+
if (typeof folder !== 'string') return null;
|
|
2076
|
+
let normalized = folder.trim();
|
|
2077
|
+
if (!normalized) return null;
|
|
2078
|
+
|
|
2079
|
+
if (!normalized.startsWith('/')) {
|
|
2080
|
+
normalized = `/${normalized}`;
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
normalized = normalized.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
2084
|
+
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
2085
|
+
normalized = normalized.slice(0, -1);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
return normalized;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
normalizeImportPath(importPath) {
|
|
2092
|
+
if (typeof importPath !== 'string') return '';
|
|
2093
|
+
const cleanPath = importPath.split(/[?#]/)[0];
|
|
2094
|
+
return cleanPath.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
isRelativeImport(importPath) {
|
|
2098
|
+
return importPath.startsWith('./') || importPath.startsWith('../');
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
isAbsoluteImport(importPath) {
|
|
2102
|
+
return importPath.startsWith('/');
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
isImportInPublicFolders(importPath, publicFolders) {
|
|
2106
|
+
const normalizedImport = this.normalizeImportPath(importPath);
|
|
2107
|
+
return publicFolders.some((folder) => normalizedImport === folder || normalizedImport.startsWith(`${folder}/`));
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
classifyImport(importPath, publicFolders) {
|
|
2111
|
+
if (typeof importPath !== 'string' || !importPath) {
|
|
2112
|
+
return { keep: false, warning: 'Warning: Removing bare import: <unknown>' };
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
if (this.isRelativeImport(importPath)) {
|
|
2116
|
+
return { keep: false, warning: null };
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
if (this.isAbsoluteImport(importPath)) {
|
|
2120
|
+
if (this.isImportInPublicFolders(importPath, publicFolders)) {
|
|
2121
|
+
return { keep: true, warning: null };
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
return {
|
|
2125
|
+
keep: false,
|
|
2126
|
+
warning: `Warning: Removing absolute import outside publicFolders: ${importPath}`
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
return {
|
|
2131
|
+
keep: false,
|
|
2132
|
+
warning: `Warning: Removing bare import: ${importPath}`
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
buildImportWarningMessage(baseMessage, sourceContext) {
|
|
2137
|
+
if (!sourceContext) return baseMessage;
|
|
2138
|
+
return `${baseMessage} [${sourceContext}]`;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
extractLocalBindingsFromImportStatement(statement) {
|
|
2142
|
+
const source = String(statement || '').trim();
|
|
2143
|
+
if (!source.startsWith('import ')) return [];
|
|
2144
|
+
if (/^import\s+['"][^'"]+['"]\s*;?$/.test(source)) return [];
|
|
2145
|
+
|
|
2146
|
+
const bindings = [];
|
|
2147
|
+
|
|
2148
|
+
const defaultMatch = source.match(/^import\s+([A-Za-z_$][\w$]*)\s*(,|\s+from\s+)/);
|
|
2149
|
+
if (defaultMatch && defaultMatch[1] !== '*') {
|
|
2150
|
+
bindings.push(defaultMatch[1]);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const namespaceMatch = source.match(/,?\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]/);
|
|
2154
|
+
if (namespaceMatch) {
|
|
2155
|
+
bindings.push(namespaceMatch[1]);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
const namedMatch = source.match(/\{([\s\S]*?)\}\s*from\s*['"]/);
|
|
2159
|
+
if (namedMatch) {
|
|
2160
|
+
const namedSection = namedMatch[1];
|
|
2161
|
+
for (const part of namedSection.split(',')) {
|
|
2162
|
+
const cleanPart = part.trim();
|
|
2163
|
+
if (!cleanPart) continue;
|
|
2164
|
+
const aliasParts = cleanPart.split(/\s+as\s+/i).map((v) => v.trim()).filter(Boolean);
|
|
2165
|
+
const localName = aliasParts.length > 1 ? aliasParts[1] : aliasParts[0];
|
|
2166
|
+
if (/^[A-Za-z_$][\w$]*$/.test(localName)) {
|
|
2167
|
+
bindings.push(localName);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
return Array.from(new Set(bindings));
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
validateHoistedImportCollisions(importStatements, reservedIdentifiers = new Set()) {
|
|
2176
|
+
const reserved = reservedIdentifiers instanceof Set
|
|
2177
|
+
? reservedIdentifiers
|
|
2178
|
+
: new Set(reservedIdentifiers || []);
|
|
2179
|
+
const bindingToStatement = new Map();
|
|
2180
|
+
|
|
2181
|
+
for (const statement of importStatements || []) {
|
|
2182
|
+
const normalizedStatement = String(statement || '').trim();
|
|
2183
|
+
if (!normalizedStatement) continue;
|
|
2184
|
+
const localBindings = this.extractLocalBindingsFromImportStatement(normalizedStatement);
|
|
2185
|
+
|
|
2186
|
+
for (const localBinding of localBindings) {
|
|
2187
|
+
if (reserved.has(localBinding)) {
|
|
2188
|
+
throw new Error(`Hoisted import reserved identifier collision: ${localBinding}`);
|
|
2189
|
+
}
|
|
2190
|
+
const previousStatement = bindingToStatement.get(localBinding);
|
|
2191
|
+
if (previousStatement && previousStatement !== normalizedStatement) {
|
|
2192
|
+
throw new Error(`Hoisted import binding collision: ${localBinding}`);
|
|
2193
|
+
}
|
|
2194
|
+
bindingToStatement.set(localBinding, normalizedStatement);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
parseImportsFromCode(code) {
|
|
2200
|
+
const ast = parse(code, {
|
|
2201
|
+
sourceType: 'module',
|
|
2202
|
+
plugins: ['jsx']
|
|
2203
|
+
});
|
|
2204
|
+
|
|
2205
|
+
const importNodes = [];
|
|
2206
|
+
traverse.default(ast, {
|
|
2207
|
+
ImportDeclaration(pathNode) {
|
|
2208
|
+
importNodes.push(pathNode.node);
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2211
|
+
|
|
2212
|
+
return importNodes
|
|
2213
|
+
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
|
|
2214
|
+
.sort((a, b) => a.start - b.start);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
parseImportsWithFallbackScanner(code) {
|
|
2218
|
+
const entries = [];
|
|
2219
|
+
const importRegex = /\bimport\b/g;
|
|
2220
|
+
let match = null;
|
|
2221
|
+
|
|
2222
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
2223
|
+
const start = match.index;
|
|
2224
|
+
const nextChar = code[start + 'import'.length];
|
|
2225
|
+
if (nextChar === '(') {
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
let index = start + 'import'.length;
|
|
2230
|
+
let quote = null;
|
|
2231
|
+
let escaped = false;
|
|
2232
|
+
|
|
2233
|
+
while (index < code.length) {
|
|
2234
|
+
const char = code[index];
|
|
2235
|
+
|
|
2236
|
+
if (quote) {
|
|
2237
|
+
if (escaped) {
|
|
2238
|
+
escaped = false;
|
|
2239
|
+
} else if (char === '\\') {
|
|
2240
|
+
escaped = true;
|
|
2241
|
+
} else if (char === quote) {
|
|
2242
|
+
quote = null;
|
|
2243
|
+
}
|
|
2244
|
+
index += 1;
|
|
2245
|
+
continue;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
if (char === '\'' || char === '"' || char === '`') {
|
|
2249
|
+
quote = char;
|
|
2250
|
+
index += 1;
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
if (char === ';') {
|
|
2255
|
+
index += 1;
|
|
2256
|
+
break;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
index += 1;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
const end = index;
|
|
2263
|
+
const statement = code.slice(start, end);
|
|
2264
|
+
const fromMatch = statement.match(/\bfrom\s+['"]([^'"]+)['"]/);
|
|
2265
|
+
const sideEffectMatch = statement.match(/\bimport\s+['"]([^'"]+)['"]/);
|
|
2266
|
+
const importPath = fromMatch?.[1] || sideEffectMatch?.[1] || null;
|
|
2267
|
+
|
|
2268
|
+
if (!importPath) {
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
entries.push({ start, end, statement, importPath });
|
|
2273
|
+
importRegex.lastIndex = end;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
return entries;
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports) {
|
|
2280
|
+
const hoistedImports = [];
|
|
2281
|
+
const importEntries = this.parseImportsWithFallbackScanner(code);
|
|
2282
|
+
if (importEntries.length === 0) {
|
|
2283
|
+
return { code, hoistedImports };
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
let cleanedCode = '';
|
|
2287
|
+
let cursor = 0;
|
|
2288
|
+
for (const entry of importEntries) {
|
|
2289
|
+
const { start, end, statement, importPath } = entry;
|
|
2290
|
+
const classification = this.classifyImport(importPath, publicFolders);
|
|
2291
|
+
cleanedCode += code.slice(cursor, start);
|
|
2292
|
+
if (classification.keep) {
|
|
2293
|
+
if (collectHoistedImports) {
|
|
2294
|
+
hoistedImports.push(statement.trim());
|
|
2295
|
+
} else {
|
|
2296
|
+
cleanedCode += statement;
|
|
2297
|
+
}
|
|
2298
|
+
} else if (classification.warning) {
|
|
2299
|
+
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
2300
|
+
}
|
|
2301
|
+
cursor = end;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
cleanedCode += code.slice(cursor);
|
|
2305
|
+
|
|
2306
|
+
return { code: cleanedCode, hoistedImports };
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
stripImports(code, options = {}) {
|
|
2310
|
+
const { sourceContext = null, collectHoistedImports = false } = options;
|
|
2311
|
+
const publicFolders = this.getConfiguredPublicFolders();
|
|
2312
|
+
const hoistedImports = [];
|
|
2313
|
+
|
|
2314
|
+
try {
|
|
2315
|
+
const importNodes = this.parseImportsFromCode(code);
|
|
2316
|
+
|
|
2317
|
+
if (importNodes.length === 0) {
|
|
2318
|
+
return collectHoistedImports ? { code, hoistedImports } : code;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
let cleaned = '';
|
|
2322
|
+
let cursor = 0;
|
|
2323
|
+
|
|
2324
|
+
for (const node of importNodes) {
|
|
2325
|
+
const importPath = node.source?.value;
|
|
2326
|
+
const classification = this.classifyImport(importPath, publicFolders);
|
|
2327
|
+
const statement = code.slice(node.start, node.end);
|
|
2328
|
+
|
|
2329
|
+
cleaned += code.slice(cursor, node.start);
|
|
2330
|
+
if (classification.keep) {
|
|
2331
|
+
if (collectHoistedImports) {
|
|
2332
|
+
hoistedImports.push(statement.trim());
|
|
2333
|
+
} else {
|
|
2334
|
+
cleaned += statement;
|
|
2335
|
+
}
|
|
2336
|
+
} else if (classification.warning) {
|
|
2337
|
+
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
cursor = node.end;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
cleaned += code.slice(cursor);
|
|
2344
|
+
return collectHoistedImports
|
|
2345
|
+
? { code: cleaned, hoistedImports }
|
|
2346
|
+
: cleaned;
|
|
2347
|
+
} catch (error) {
|
|
2348
|
+
const fallback = this.stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports);
|
|
2349
|
+
return collectHoistedImports ? fallback : fallback.code;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
async loadComponentsMap() {
|
|
2354
|
+
const componentsConfigPath = path.join(this.componentsPath, 'components.js');
|
|
2355
|
+
if (!await fs.pathExists(componentsConfigPath)) {
|
|
2356
|
+
return {};
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
const content = await fs.readFile(componentsConfigPath, 'utf-8');
|
|
2360
|
+
return this.parseComponentsConfig(content);
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
parseComponentsConfig(content) {
|
|
2364
|
+
try {
|
|
2365
|
+
const ast = parse(content, {
|
|
2366
|
+
sourceType: 'module',
|
|
2367
|
+
plugins: ['jsx']
|
|
2368
|
+
});
|
|
2369
|
+
|
|
2370
|
+
let componentsNode = null;
|
|
2371
|
+
|
|
2372
|
+
traverse.default(ast, {
|
|
2373
|
+
VariableDeclarator(path) {
|
|
2374
|
+
if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
|
|
2375
|
+
componentsNode = path.node.init;
|
|
2376
|
+
path.stop();
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
|
|
2382
|
+
throw new Error('components object not found');
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
const config = {};
|
|
2386
|
+
for (const prop of componentsNode.properties) {
|
|
2387
|
+
if (prop.type !== 'ObjectProperty') continue;
|
|
2388
|
+
|
|
2389
|
+
const key = this.extractStringValue(prop.key);
|
|
2390
|
+
const value = this.extractStringValue(prop.value);
|
|
2391
|
+
|
|
2392
|
+
if (!key || !value) {
|
|
2393
|
+
throw new Error('Invalid components entry');
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
config[key] = value;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
return config;
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
console.warn(`Could not parse components.js: ${error.message}`);
|
|
2402
|
+
return {};
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
extractStringValue(node) {
|
|
2407
|
+
if (!node) return null;
|
|
2408
|
+
|
|
2409
|
+
if (node.type === 'StringLiteral') {
|
|
2410
|
+
return node.value;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (node.type === 'Identifier') {
|
|
2414
|
+
return node.name;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
2418
|
+
return node.quasis.map((q) => q.value.cooked).join('');
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
return null;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
/**
|
|
2425
|
+
* Converts a route to filename
|
|
2426
|
+
*/
|
|
2427
|
+
routeToFileName(routePath) {
|
|
2428
|
+
if (routePath === '/') return 'home';
|
|
2429
|
+
return routePath
|
|
2430
|
+
.replace(/^\//, '')
|
|
2431
|
+
.replace(/\//g, '-')
|
|
2432
|
+
.replace(/[^a-zA-Z0-9-]/g, '')
|
|
2433
|
+
.toLowerCase();
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
/**
|
|
2437
|
+
* Saves the configuration to file
|
|
2438
|
+
*/
|
|
2439
|
+
async saveBundleConfig(config) {
|
|
2440
|
+
// Ensure bundles directory exists
|
|
2441
|
+
await fs.ensureDir(this.bundlesPath);
|
|
2442
|
+
|
|
2443
|
+
// Save JSON config
|
|
2444
|
+
const configPath = path.join(this.bundlesPath, 'bundle.config.json');
|
|
2445
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
2446
|
+
|
|
2447
|
+
// Generate JavaScript module for direct import
|
|
2448
|
+
const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
|
|
2449
|
+
const jsConfig = this.generateBundleConfigJS(config);
|
|
2450
|
+
await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
|
|
2451
|
+
|
|
2452
|
+
console.log(`✓ Configuration saved to ${configPath}`);
|
|
2453
|
+
console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
/**
|
|
2457
|
+
* Creates a default bundle config file if none exists
|
|
2458
|
+
*/
|
|
2459
|
+
async createDefaultBundleConfig() {
|
|
2460
|
+
const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
|
|
2461
|
+
|
|
2462
|
+
// Only create if it doesn't exist
|
|
2463
|
+
if (await fs.pathExists(defaultConfigPath)) {
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
await fs.ensureDir(path.dirname(defaultConfigPath));
|
|
2468
|
+
|
|
2469
|
+
const defaultConfig = `/**
|
|
2470
|
+
* Slice.js Bundle Configuration
|
|
2471
|
+
* Default empty configuration - no bundles available
|
|
2472
|
+
* Run 'slice build' to generate optimized bundles
|
|
2473
|
+
*/
|
|
2474
|
+
|
|
2475
|
+
// No bundles available - using individual component loading
|
|
2476
|
+
export const SLICE_BUNDLE_CONFIG = null;
|
|
2477
|
+
|
|
2478
|
+
// No auto-initialization needed for default config
|
|
2479
|
+
`;
|
|
2480
|
+
|
|
2481
|
+
await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
|
|
2482
|
+
console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
/**
|
|
2486
|
+
* Generates JavaScript module for direct import
|
|
2487
|
+
*/
|
|
2488
|
+
generateBundleConfigJS(config) {
|
|
2489
|
+
return `/**
|
|
2490
|
+
* Slice.js Bundle Configuration
|
|
2491
|
+
* Generated: ${new Date().toISOString()}
|
|
2492
|
+
* Strategy: ${config.strategy}
|
|
2493
|
+
*/
|
|
2494
|
+
|
|
2495
|
+
// Direct bundle configuration (no fetch required)
|
|
2496
|
+
export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
|
|
2497
|
+
|
|
2498
|
+
// Auto-initialization if slice is available
|
|
2499
|
+
if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
|
|
2500
|
+
window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
|
|
2501
|
+
|
|
2502
|
+
// Load critical bundle automatically
|
|
2503
|
+
if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
|
|
2504
|
+
(async () => {
|
|
2505
|
+
const bundlePath = "/bundles/" + SLICE_BUNDLE_CONFIG.bundles.critical.file;
|
|
2506
|
+
const integrity = SLICE_BUNDLE_CONFIG.bundles.critical.integrity;
|
|
2507
|
+
|
|
2508
|
+
if (typeof window.slice.controller.verifyBundleIntegrity === 'function') {
|
|
2509
|
+
const ok = await window.slice.controller.verifyBundleIntegrity(bundlePath, integrity);
|
|
2510
|
+
if (!ok) {
|
|
2511
|
+
console.warn('Failed to load critical bundle: integrity check failed');
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
import('./slice-bundle.critical.js').catch(err =>
|
|
2517
|
+
console.warn('Failed to load critical bundle:', err)
|
|
2518
|
+
);
|
|
2519
|
+
window.slice.controller.criticalBundleLoaded = true;
|
|
2520
|
+
})();
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
`;
|
|
2524
|
+
}
|
|
2525
|
+
}
|