slicejs-cli 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +29 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +25 -0
- package/.github/pull_request_template.md +22 -0
- package/.github/workflows/docs-render-cicd.yml +65 -0
- package/CODE_OF_CONDUCT.md +126 -0
- package/ECOSYSTEM.md +9 -0
- package/LICENSE +21 -0
- package/README.md +104 -308
- package/client.js +644 -557
- package/commands/Print.js +167 -167
- package/commands/Validations.js +103 -103
- package/commands/build/build.js +40 -40
- package/commands/buildProduction/buildProduction.js +579 -579
- package/commands/bundle/bundle.js +235 -235
- package/commands/createComponent/VisualComponentTemplate.js +55 -55
- package/commands/createComponent/createComponent.js +126 -126
- package/commands/deleteComponent/deleteComponent.js +77 -77
- package/commands/doctor/doctor.js +369 -369
- package/commands/getComponent/getComponent.js +747 -747
- package/commands/init/init.js +261 -261
- package/commands/listComponents/listComponents.js +175 -175
- package/commands/startServer/startServer.js +264 -264
- package/commands/startServer/watchServer.js +79 -79
- package/commands/types/types.js +538 -0
- package/commands/utils/LocalCliDelegation.js +53 -53
- package/commands/utils/PathHelper.js +68 -68
- package/commands/utils/VersionChecker.js +167 -167
- package/commands/utils/bundling/BundleGenerator.js +2292 -2292
- package/commands/utils/bundling/DependencyAnalyzer.js +933 -933
- package/commands/utils/updateManager.js +453 -453
- package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +182 -0
- package/package.json +46 -46
- package/post.js +65 -25
- package/tests/bundle-generator.test.js +708 -708
- package/tests/bundle-v2-register-output.test.js +470 -470
- package/tests/client-launcher-contract.test.js +211 -211
- package/tests/client-update-flow-contract.test.js +272 -272
- package/tests/dependency-analyzer.test.js +24 -24
- package/tests/local-cli-delegation.test.js +79 -79
- package/tests/postinstall-command.test.js +72 -0
- package/tests/types-generator.test.js +356 -0
- package/tests/update-manager-notifications.test.js +88 -88
- package/refactor.md +0 -271
|
@@ -1,2292 +1,2292 @@
|
|
|
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: false
|
|
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
|
-
const filteredModules = modules.filter((module) => !omittedDependencies.has(module.name));
|
|
1253
|
-
|
|
1254
|
-
const lines = [
|
|
1255
|
-
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1256
|
-
...this.getDefaultExportResolverLines()
|
|
1257
|
-
];
|
|
1258
|
-
if (options.includeSharedResolver) {
|
|
1259
|
-
lines.push(...this.getBundleDependencyResolverLines());
|
|
1260
|
-
}
|
|
1261
|
-
filteredModules.forEach((module, index) => {
|
|
1262
|
-
const exportVar = `__sliceDepExports${index}`;
|
|
1263
|
-
const transformedContent = this.transformDependencyContent(module.content, exportVar, module.name);
|
|
1264
|
-
lines.push(`const ${exportVar} = {};`);
|
|
1265
|
-
lines.push(transformedContent.trim());
|
|
1266
|
-
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1267
|
-
});
|
|
1268
|
-
|
|
1269
|
-
return lines.join('\n');
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
async buildDependencyContents(jsContent, componentPath) {
|
|
1273
|
-
const dependencies = this.analyzeDependencies(jsContent, componentPath);
|
|
1274
|
-
const dependencyContents = {};
|
|
1275
|
-
|
|
1276
|
-
for (const dep of dependencies) {
|
|
1277
|
-
const depPath = dep.path;
|
|
1278
|
-
try {
|
|
1279
|
-
const depContent = await fs.readFile(depPath, 'utf-8');
|
|
1280
|
-
const depName = path
|
|
1281
|
-
.relative(this.srcPath, depPath)
|
|
1282
|
-
.replace(/\\/g, '/');
|
|
1283
|
-
dependencyContents[depName] = {
|
|
1284
|
-
content: depContent,
|
|
1285
|
-
bindings: dep.bindings || []
|
|
1286
|
-
};
|
|
1287
|
-
} catch (error) {
|
|
1288
|
-
console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
return dependencyContents;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
/**
|
|
1296
|
-
* Cleans JavaScript code by removing imports/exports and ensuring class is available globally
|
|
1297
|
-
*/
|
|
1298
|
-
cleanJavaScript(code, componentName, sourceContext = componentName) {
|
|
1299
|
-
// Remove export default
|
|
1300
|
-
code = code.replace(/export\s+default\s+/g, '');
|
|
1301
|
-
|
|
1302
|
-
// Remove only unsupported imports (relative always removed, allowed absolute kept)
|
|
1303
|
-
const stripped = this.stripImports(code, {
|
|
1304
|
-
sourceContext,
|
|
1305
|
-
collectHoistedImports: true
|
|
1306
|
-
});
|
|
1307
|
-
const hoistedImports = stripped.hoistedImports || [];
|
|
1308
|
-
code = stripped.code;
|
|
1309
|
-
|
|
1310
|
-
// Guard customElements.define to avoid duplicate registrations
|
|
1311
|
-
code = code.replace(
|
|
1312
|
-
/customElements\.define\(([^)]+)\);?/g,
|
|
1313
|
-
(match, args) => {
|
|
1314
|
-
const firstArg = args.split(',')[0]?.trim() || '';
|
|
1315
|
-
if (!/^['"][^'"]+['"]$/.test(firstArg)) {
|
|
1316
|
-
return match;
|
|
1317
|
-
}
|
|
1318
|
-
return `if (!customElements.get(${firstArg})) { customElements.define(${args}); }`;
|
|
1319
|
-
}
|
|
1320
|
-
);
|
|
1321
|
-
|
|
1322
|
-
// Make sure the class is available globally for bundle evaluation
|
|
1323
|
-
// Preserve original customElements.define if it exists
|
|
1324
|
-
if (code.includes('customElements.define')) {
|
|
1325
|
-
// Add global assignment before guarded or direct customElements.define
|
|
1326
|
-
const globalAssignment = `window.${componentName} = ${componentName};\n`;
|
|
1327
|
-
const guardedDefineRegex = /if\s*\(\s*!\s*customElements\.get\([^)]*\)\s*\)\s*\{\s*customElements\.define\([^;]+\);?\s*\}\s*$/;
|
|
1328
|
-
const directDefineRegex = /customElements\.define\([^;]+\);?\s*$/;
|
|
1329
|
-
if (guardedDefineRegex.test(code)) {
|
|
1330
|
-
code = code.replace(guardedDefineRegex, `${globalAssignment}$&`);
|
|
1331
|
-
} else {
|
|
1332
|
-
code = code.replace(directDefineRegex, `${globalAssignment}$&`);
|
|
1333
|
-
}
|
|
1334
|
-
} else {
|
|
1335
|
-
// If no customElements.define found, just assign to global
|
|
1336
|
-
code += `\nwindow.${componentName} = ${componentName};`;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
// Add return statement for bundle evaluation compatibility
|
|
1340
|
-
code += `\nreturn ${componentName};`;
|
|
1341
|
-
|
|
1342
|
-
return {
|
|
1343
|
-
code,
|
|
1344
|
-
hoistedImports
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
/**
|
|
1349
|
-
* Formats the bundle file
|
|
1350
|
-
*/
|
|
1351
|
-
formatBundleFile(componentsData, metadata) {
|
|
1352
|
-
const integrityPayload = {
|
|
1353
|
-
metadata: {
|
|
1354
|
-
...metadata,
|
|
1355
|
-
generated: 'static'
|
|
1356
|
-
},
|
|
1357
|
-
components: Object.fromEntries(
|
|
1358
|
-
Object.entries(componentsData).map(([name, data]) => [
|
|
1359
|
-
name,
|
|
1360
|
-
{
|
|
1361
|
-
name: data.name,
|
|
1362
|
-
category: data.category,
|
|
1363
|
-
categoryType: data.categoryType,
|
|
1364
|
-
componentDependencies: data.componentDependencies
|
|
1365
|
-
}
|
|
1366
|
-
])
|
|
1367
|
-
)
|
|
1368
|
-
};
|
|
1369
|
-
const integrity = `sha256:${crypto
|
|
1370
|
-
.createHash('sha256')
|
|
1371
|
-
.update(JSON.stringify(integrityPayload))
|
|
1372
|
-
.digest('hex')}`;
|
|
1373
|
-
|
|
1374
|
-
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1375
|
-
const frameworkComponentKeys = Object.keys(componentsData || {});
|
|
1376
|
-
const frameworkClassIdentifiers = frameworkComponentKeys.map((key) => this.toSafeIdentifier(key));
|
|
1377
|
-
const frameworkReservedIdentifiers = new Set([
|
|
1378
|
-
'SLICE_BUNDLE',
|
|
1379
|
-
'SLICE_BUNDLE_COMPONENTS',
|
|
1380
|
-
'SLICE_BUNDLE_DEPENDENCIES',
|
|
1381
|
-
'SLICE_FRAMEWORK_CLASSES',
|
|
1382
|
-
...frameworkClassIdentifiers,
|
|
1383
|
-
...this.getDependencyExportVariableNames(dependencyModules)
|
|
1384
|
-
]);
|
|
1385
|
-
const rawHoistedImports = Object.values(componentsData || {})
|
|
1386
|
-
.flatMap((component) => component?.hoistedImports || [])
|
|
1387
|
-
.map((statement) => String(statement).trim())
|
|
1388
|
-
.filter(Boolean);
|
|
1389
|
-
this.validateHoistedImportCollisions(rawHoistedImports, frameworkReservedIdentifiers);
|
|
1390
|
-
const hoistedImportBlock = Array.from(new Set(rawHoistedImports)).join('\n');
|
|
1391
|
-
|
|
1392
|
-
const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
|
|
1393
|
-
const componentBlock = this.buildComponentBundleBlock(componentsData);
|
|
1394
|
-
|
|
1395
|
-
return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}/**
|
|
1396
|
-
* Slice.js Bundle
|
|
1397
|
-
* Type: ${metadata.type}
|
|
1398
|
-
* Generated: ${metadata.generated}
|
|
1399
|
-
* Strategy: ${metadata.strategy}
|
|
1400
|
-
* Components: ${metadata.componentCount}
|
|
1401
|
-
* Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
|
|
1402
|
-
*/
|
|
1403
|
-
|
|
1404
|
-
${dependencyBlock}
|
|
1405
|
-
${componentBlock}
|
|
1406
|
-
|
|
1407
|
-
export const SLICE_BUNDLE = {
|
|
1408
|
-
metadata: ${JSON.stringify({ ...metadata, integrity }, null, 2)},
|
|
1409
|
-
components: SLICE_BUNDLE_COMPONENTS
|
|
1410
|
-
};
|
|
1411
|
-
|
|
1412
|
-
// Auto-registration of components
|
|
1413
|
-
if (window.slice && window.slice.controller) {
|
|
1414
|
-
slice.controller.registerBundle(SLICE_BUNDLE);
|
|
1415
|
-
}
|
|
1416
|
-
`;
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
buildDependencyModuleBlock(componentsData) {
|
|
1420
|
-
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1421
|
-
const lines = [
|
|
1422
|
-
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1423
|
-
...this.getDefaultExportResolverLines()
|
|
1424
|
-
];
|
|
1425
|
-
if (dependencyModules.length === 0) {
|
|
1426
|
-
return `${lines.join('\n')}`;
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
dependencyModules.forEach((module, index) => {
|
|
1430
|
-
const exportVar = `__sliceDepExports${index}`;
|
|
1431
|
-
const content = this.transformDependencyContent(module.content, exportVar, module.name);
|
|
1432
|
-
lines.push(`// Dependency: ${module.name}`);
|
|
1433
|
-
lines.push(`const ${exportVar} = {};`);
|
|
1434
|
-
lines.push(content.trim());
|
|
1435
|
-
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1436
|
-
});
|
|
1437
|
-
|
|
1438
|
-
return `${lines.join('\n')}`;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
collectDependencyModules(componentsData) {
|
|
1442
|
-
const modules = new Map();
|
|
1443
|
-
Object.values(componentsData).forEach((component) => {
|
|
1444
|
-
Object.entries(component.externalDependencies || {}).forEach(([name, entry]) => {
|
|
1445
|
-
if (modules.has(name)) return;
|
|
1446
|
-
const content = typeof entry === 'string' ? entry : entry.content;
|
|
1447
|
-
modules.set(name, { name, content });
|
|
1448
|
-
});
|
|
1449
|
-
});
|
|
1450
|
-
return Array.from(modules.values());
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
collectDependencyModulesFromComponents(components = []) {
|
|
1454
|
-
const modules = new Map();
|
|
1455
|
-
for (const component of components || []) {
|
|
1456
|
-
const externalDependencies = component.externalDependencies || {};
|
|
1457
|
-
for (const [moduleName, entry] of Object.entries(externalDependencies)) {
|
|
1458
|
-
if (modules.has(moduleName)) continue;
|
|
1459
|
-
const content = typeof entry === 'string' ? entry : entry?.content;
|
|
1460
|
-
if (!content) continue;
|
|
1461
|
-
modules.set(moduleName, { name: moduleName, content });
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
return Array.from(modules.values());
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
getDependencyExportVariableNames(dependencyModules = []) {
|
|
1468
|
-
return (dependencyModules || []).map((_, index) => `__sliceDepExports${index}`);
|
|
1469
|
-
}
|
|
1470
|
-
|
|
1471
|
-
transformDependencyContent(content, exportVar, moduleName) {
|
|
1472
|
-
const dataName = this.getDependencyDefaultFallbackKey(moduleName);
|
|
1473
|
-
const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
|
|
1474
|
-
|
|
1475
|
-
return content
|
|
1476
|
-
.replace(/export\s+const\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1477
|
-
.replace(/export\s+let\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1478
|
-
.replace(/export\s+var\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1479
|
-
.replace(/export\s+function\s+(\w+)/g, `${exportVar}.$1 = function`)
|
|
1480
|
-
.replace(/export\s+default\s+/g, exportPrefix)
|
|
1481
|
-
.replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
|
|
1482
|
-
return exportsStr
|
|
1483
|
-
.split(',')
|
|
1484
|
-
.map((exp) => {
|
|
1485
|
-
const cleanExp = exp.trim();
|
|
1486
|
-
const varName = cleanExp.split(' as ')[0].trim();
|
|
1487
|
-
return `${exportVar}.${varName} = ${varName};`;
|
|
1488
|
-
})
|
|
1489
|
-
.join('\n');
|
|
1490
|
-
})
|
|
1491
|
-
.replace(/^\s*export\s+/gm, '');
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
getDependencyDefaultFallbackKey(moduleName) {
|
|
1495
|
-
const baseName = moduleName?.split('/').pop()?.replace(/\.[^.]+$/, '');
|
|
1496
|
-
return baseName ? `${baseName}Data` : null;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
buildComponentBundleBlock(componentsData) {
|
|
1500
|
-
const componentEntries = [];
|
|
1501
|
-
const componentDefs = [];
|
|
1502
|
-
const frameworkEntries = [];
|
|
1503
|
-
|
|
1504
|
-
Object.entries(componentsData).forEach(([name, data]) => {
|
|
1505
|
-
const classVar = this.toSafeIdentifier(name);
|
|
1506
|
-
const bindings = this.buildDependencyBindings(data.externalDependencies || {});
|
|
1507
|
-
|
|
1508
|
-
componentDefs.push(`const ${classVar} = (() => {\n${bindings}\n${data.js}\nreturn ${name};\n})();`);
|
|
1509
|
-
|
|
1510
|
-
if (data.isFramework) {
|
|
1511
|
-
frameworkEntries.push(`${JSON.stringify(data.name)}: ${classVar}`);
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
componentEntries.push(
|
|
1515
|
-
`${JSON.stringify(name)}: {\n` +
|
|
1516
|
-
` name: ${JSON.stringify(data.name)},\n` +
|
|
1517
|
-
` category: ${JSON.stringify(data.category)},\n` +
|
|
1518
|
-
` categoryType: ${JSON.stringify(data.categoryType)},\n` +
|
|
1519
|
-
` componentDependencies: ${JSON.stringify(data.componentDependencies)},\n` +
|
|
1520
|
-
` html: ${JSON.stringify(data.html)},\n` +
|
|
1521
|
-
` css: ${JSON.stringify(data.css)},\n` +
|
|
1522
|
-
` size: ${JSON.stringify(data.size)},\n` +
|
|
1523
|
-
` class: ${classVar}\n` +
|
|
1524
|
-
`}`
|
|
1525
|
-
);
|
|
1526
|
-
});
|
|
1527
|
-
|
|
1528
|
-
const frameworkBlock = frameworkEntries.length > 0
|
|
1529
|
-
? `const SLICE_FRAMEWORK_CLASSES = {\n${frameworkEntries.join(',\n')}\n};\nwindow.SLICE_FRAMEWORK_CLASSES = SLICE_FRAMEWORK_CLASSES;`
|
|
1530
|
-
: '';
|
|
1531
|
-
|
|
1532
|
-
return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
buildDependencyBindings(externalDependencies, options = {}) {
|
|
1536
|
-
const lines = [];
|
|
1537
|
-
Object.entries(externalDependencies).forEach(([name, entry]) => {
|
|
1538
|
-
const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
|
|
1539
|
-
const depVar = options.preferShared
|
|
1540
|
-
? `__sliceResolveBundleDependency(${JSON.stringify(name)})`
|
|
1541
|
-
: `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
|
|
1542
|
-
|
|
1543
|
-
bindings.forEach((binding) => {
|
|
1544
|
-
if (!binding?.localName) return;
|
|
1545
|
-
if (binding.type === 'default') {
|
|
1546
|
-
const preferredKey = this.getDependencyDefaultFallbackKey(name);
|
|
1547
|
-
lines.push(`const ${binding.localName} = __sliceResolveDefaultExport(${depVar}, ${JSON.stringify(name)}, ${JSON.stringify(preferredKey)});`);
|
|
1548
|
-
}
|
|
1549
|
-
if (binding.type === 'named') {
|
|
1550
|
-
lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
|
|
1551
|
-
}
|
|
1552
|
-
if (binding.type === 'namespace') {
|
|
1553
|
-
lines.push(`const ${binding.localName} = ${depVar};`);
|
|
1554
|
-
}
|
|
1555
|
-
});
|
|
1556
|
-
});
|
|
1557
|
-
|
|
1558
|
-
return lines.join('\n');
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
getBundleDependencyResolverLines() {
|
|
1562
|
-
return [
|
|
1563
|
-
"const __sliceSharedDeps = typeof window !== 'undefined' ? (window.__SLICE_SHARED_DEPS__ || {}) : {};",
|
|
1564
|
-
'const __sliceResolveBundleDependency = (depName) => Object.prototype.hasOwnProperty.call(__sliceSharedDeps, depName) ? __sliceSharedDeps[depName] : SLICE_BUNDLE_DEPENDENCIES[depName];'
|
|
1565
|
-
];
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
getDefaultExportResolverLines() {
|
|
1569
|
-
return [
|
|
1570
|
-
'const __sliceDefaultExportWarningDeps = new Set();',
|
|
1571
|
-
"const __sliceDefaultExportPreferredKeys = ['module', 'exports', 'purify'];",
|
|
1572
|
-
'const __sliceDeterministicKeyCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);',
|
|
1573
|
-
'function __sliceResolveDefaultExport(dep, depName, preferredKey) {',
|
|
1574
|
-
' if (dep?.default !== undefined) return dep.default;',
|
|
1575
|
-
" if (dep === null || (typeof dep !== 'object' && typeof dep !== 'function')) return dep;",
|
|
1576
|
-
" if (preferredKey && preferredKey !== 'default' && preferredKey !== '__esModule' && Object.prototype.hasOwnProperty.call(dep, preferredKey)) return dep[preferredKey];",
|
|
1577
|
-
" const keys = Object.keys(dep).filter((key) => key !== 'default' && key !== '__esModule');",
|
|
1578
|
-
' if (keys.length === 1) return dep[keys[0]];',
|
|
1579
|
-
' if (keys.length > 1) {',
|
|
1580
|
-
' const preferredMatches = __sliceDefaultExportPreferredKeys.filter((key) => keys.includes(key));',
|
|
1581
|
-
' if (preferredMatches.length === 1) return dep[preferredMatches[0]];',
|
|
1582
|
-
' const sortedKeys = [...keys].sort(__sliceDeterministicKeyCompare);',
|
|
1583
|
-
' const fallbackKey = sortedKeys[0];',
|
|
1584
|
-
' const warningDepName = depName || "<unknown dependency>";',
|
|
1585
|
-
' if (!__sliceDefaultExportWarningDeps.has(warningDepName)) {',
|
|
1586
|
-
' __sliceDefaultExportWarningDeps.add(warningDepName);',
|
|
1587
|
-
' console.warn(`[Slice.js bundler] Ambiguous default export resolution for "${warningDepName}". Falling back to "${fallbackKey}". Keys: ${sortedKeys.join(\', \')}`);',
|
|
1588
|
-
' }',
|
|
1589
|
-
' return dep[fallbackKey];',
|
|
1590
|
-
' }',
|
|
1591
|
-
' return dep;',
|
|
1592
|
-
'}'
|
|
1593
|
-
];
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
toSafeIdentifier(name) {
|
|
1597
|
-
const cleaned = name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
1598
|
-
if (/^\d/.test(cleaned)) {
|
|
1599
|
-
return `SliceComponent_${cleaned}`;
|
|
1600
|
-
}
|
|
1601
|
-
return `SliceComponent_${cleaned}`;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
/**
|
|
1605
|
-
* Generates the bundle configuration
|
|
1606
|
-
*/
|
|
1607
|
-
generateBundleConfig(frameworkBundle = null) {
|
|
1608
|
-
const metrics = this.analysisData.metrics || {};
|
|
1609
|
-
const config = {
|
|
1610
|
-
version: '2.0.0',
|
|
1611
|
-
format: this.format,
|
|
1612
|
-
loadingPolicy: this.loadingPolicy,
|
|
1613
|
-
strategy: this.config.strategy,
|
|
1614
|
-
minified: this.options.minify,
|
|
1615
|
-
obfuscated: this.options.obfuscate,
|
|
1616
|
-
production: true,
|
|
1617
|
-
generated: new Date().toISOString(),
|
|
1618
|
-
|
|
1619
|
-
stats: {
|
|
1620
|
-
totalComponents: metrics.totalComponents || 0,
|
|
1621
|
-
totalRoutes: metrics.totalRoutes || 0,
|
|
1622
|
-
sharedComponents: this.bundles.critical.components.length,
|
|
1623
|
-
sharedPercentage: metrics.sharedPercentage || 0,
|
|
1624
|
-
totalSize: metrics.totalSize || 0,
|
|
1625
|
-
criticalSize: this.bundles.critical.size
|
|
1626
|
-
},
|
|
1627
|
-
|
|
1628
|
-
bundles: {
|
|
1629
|
-
framework: {
|
|
1630
|
-
file: 'slice-bundle.framework.js',
|
|
1631
|
-
size: 0,
|
|
1632
|
-
hash: null,
|
|
1633
|
-
integrity: null,
|
|
1634
|
-
components: []
|
|
1635
|
-
},
|
|
1636
|
-
vendorShared: {
|
|
1637
|
-
bundleKey: 'vendor-shared',
|
|
1638
|
-
type: 'vendor-shared',
|
|
1639
|
-
file: this.vendorShared.file,
|
|
1640
|
-
size: this.vendorShared.bundle?.size || 0,
|
|
1641
|
-
hash: this.vendorShared.bundle?.hash || null,
|
|
1642
|
-
integrity: this.vendorShared.bundle?.integrity || null,
|
|
1643
|
-
dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
|
|
1644
|
-
dependencyCount: this.vendorShared.sharedDependencySet.size,
|
|
1645
|
-
routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
|
|
1646
|
-
},
|
|
1647
|
-
critical: {
|
|
1648
|
-
file: this.bundles.critical.file,
|
|
1649
|
-
size: this.bundles.critical.size,
|
|
1650
|
-
hash: this.bundles.critical.hash || null,
|
|
1651
|
-
integrity: this.bundles.critical.integrity || null,
|
|
1652
|
-
components: this.bundles.critical.components.map(c => c.name)
|
|
1653
|
-
},
|
|
1654
|
-
routes: {}
|
|
1655
|
-
},
|
|
1656
|
-
routeBundles: {},
|
|
1657
|
-
routeDependencyGraph: {}
|
|
1658
|
-
};
|
|
1659
|
-
|
|
1660
|
-
for (const [key, bundle] of Object.entries(this.bundles.routes)) {
|
|
1661
|
-
const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
|
|
1662
|
-
? key
|
|
1663
|
-
: (bundle.path || bundle.paths || key);
|
|
1664
|
-
const usesVendorShared = this.vendorShared.bundleKeysUsingSharedDependencies.has(key)
|
|
1665
|
-
|| (bundle.dependencies || []).includes('vendor-shared');
|
|
1666
|
-
const dependencies = this.mergeBundleDependencies(
|
|
1667
|
-
bundle.dependencies || [],
|
|
1668
|
-
usesVendorShared ? ['vendor-shared'] : []
|
|
1669
|
-
);
|
|
1670
|
-
|
|
1671
|
-
config.bundles.routes[key] = {
|
|
1672
|
-
path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
|
|
1673
|
-
file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
|
|
1674
|
-
size: bundle.size,
|
|
1675
|
-
hash: bundle.hash || null,
|
|
1676
|
-
integrity: bundle.integrity || null,
|
|
1677
|
-
components: bundle.components.map(c => c.name),
|
|
1678
|
-
dependencies
|
|
1679
|
-
};
|
|
1680
|
-
|
|
1681
|
-
const paths = Array.isArray(config.bundles.routes[key].path)
|
|
1682
|
-
? config.bundles.routes[key].path
|
|
1683
|
-
: [config.bundles.routes[key].path];
|
|
1684
|
-
|
|
1685
|
-
for (const routePath of paths) {
|
|
1686
|
-
if (!config.routeBundles[routePath]) {
|
|
1687
|
-
config.routeBundles[routePath] = ['critical'];
|
|
1688
|
-
}
|
|
1689
|
-
for (const dependency of dependencies.filter((dep) => dep !== 'critical')) {
|
|
1690
|
-
if (!config.routeBundles[routePath].includes(dependency)) {
|
|
1691
|
-
config.routeBundles[routePath].push(dependency);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
if (!config.routeBundles[routePath].includes(key)) {
|
|
1695
|
-
config.routeBundles[routePath].push(key);
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
const graphEntry = config.routeDependencyGraph[routePath] || {
|
|
1699
|
-
bundles: [],
|
|
1700
|
-
edges: []
|
|
1701
|
-
};
|
|
1702
|
-
if (!graphEntry.bundles.includes(key)) {
|
|
1703
|
-
graphEntry.bundles.push(key);
|
|
1704
|
-
graphEntry.bundles.sort((a, b) => a.localeCompare(b));
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
const edgeKeys = new Set(graphEntry.edges.map((edge) => `${edge.from}->${edge.to}`));
|
|
1708
|
-
const orderedEdgeSources = ['critical', ...dependencies.filter((dependency) => dependency !== 'critical')];
|
|
1709
|
-
for (const source of orderedEdgeSources) {
|
|
1710
|
-
const edgeKey = `${source}->${key}`;
|
|
1711
|
-
if (!edgeKeys.has(edgeKey)) {
|
|
1712
|
-
graphEntry.edges.push({ from: source, to: key });
|
|
1713
|
-
edgeKeys.add(edgeKey);
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
config.routeDependencyGraph[routePath] = graphEntry;
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
if (frameworkBundle) {
|
|
1722
|
-
config.bundles.framework = {
|
|
1723
|
-
file: frameworkBundle.file,
|
|
1724
|
-
size: frameworkBundle.size,
|
|
1725
|
-
hash: frameworkBundle.hash,
|
|
1726
|
-
integrity: frameworkBundle.integrity,
|
|
1727
|
-
components: frameworkBundle.components || []
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
return config;
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
collectFrameworkComponents() {
|
|
1735
|
-
return this.analysisData.components.filter((comp) => comp.isFramework);
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
async createFrameworkBundle(components) {
|
|
1739
|
-
const fileName = 'slice-bundle.framework.js';
|
|
1740
|
-
const filePath = path.join(this.bundlesPath, fileName);
|
|
1741
|
-
return this.generateFrameworkBundleFile(components, fileName, filePath);
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
async generateFrameworkBundleFile(components, fileName, filePath) {
|
|
1745
|
-
const componentsData = {};
|
|
1746
|
-
const componentsMap = await this.loadComponentsMap();
|
|
1747
|
-
const metadata = {
|
|
1748
|
-
version: '2.0.0',
|
|
1749
|
-
type: 'framework',
|
|
1750
|
-
route: null,
|
|
1751
|
-
bundleKey: 'framework',
|
|
1752
|
-
file: fileName,
|
|
1753
|
-
generated: new Date().toISOString(),
|
|
1754
|
-
totalSize: components.reduce((sum, c) => sum + c.size, 0),
|
|
1755
|
-
componentCount: components.length,
|
|
1756
|
-
strategy: this.config.strategy,
|
|
1757
|
-
minified: this.options.minify,
|
|
1758
|
-
obfuscated: this.options.obfuscate
|
|
1759
|
-
};
|
|
1760
|
-
|
|
1761
|
-
components.forEach((comp) => {
|
|
1762
|
-
const componentKey = `Framework/Structural/${comp.name}`;
|
|
1763
|
-
const fileBaseName = comp.fileName || comp.name;
|
|
1764
|
-
const jsPath = path.join(comp.path, `${fileBaseName}.js`);
|
|
1765
|
-
const jsContent = fs.readFileSync(jsPath, 'utf-8');
|
|
1766
|
-
const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
|
|
1767
|
-
const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
|
|
1768
|
-
|
|
1769
|
-
componentsData[componentKey] = {
|
|
1770
|
-
name: comp.name,
|
|
1771
|
-
category: comp.category,
|
|
1772
|
-
categoryType: comp.categoryType,
|
|
1773
|
-
isFramework: true,
|
|
1774
|
-
js: cleanedJavaScript.code,
|
|
1775
|
-
hoistedImports: cleanedJavaScript.hoistedImports,
|
|
1776
|
-
externalDependencies: dependencyContents,
|
|
1777
|
-
componentDependencies: Array.from(comp.dependencies),
|
|
1778
|
-
html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
|
|
1779
|
-
? fs.readFileSync(path.join(comp.path, `${fileBaseName}.html`), 'utf-8')
|
|
1780
|
-
: null,
|
|
1781
|
-
css: fs.existsSync(path.join(comp.path, `${fileBaseName}.css`))
|
|
1782
|
-
? fs.readFileSync(path.join(comp.path, `${fileBaseName}.css`), 'utf-8')
|
|
1783
|
-
: null,
|
|
1784
|
-
size: comp.size
|
|
1785
|
-
};
|
|
1786
|
-
});
|
|
1787
|
-
|
|
1788
|
-
const prelude = `const components = ${JSON.stringify(componentsMap)};`;
|
|
1789
|
-
const bundleContent = `${prelude}\n${this.formatBundleFile(componentsData, metadata)}`;
|
|
1790
|
-
const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
|
|
1791
|
-
await fs.ensureDir(path.dirname(filePath));
|
|
1792
|
-
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
1793
|
-
|
|
1794
|
-
const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
|
|
1795
|
-
const integrity = `sha256:${hash}`;
|
|
1796
|
-
|
|
1797
|
-
return {
|
|
1798
|
-
name: 'framework',
|
|
1799
|
-
file: fileName,
|
|
1800
|
-
size: Buffer.byteLength(bundleContent, 'utf-8'),
|
|
1801
|
-
hash,
|
|
1802
|
-
integrity,
|
|
1803
|
-
componentCount: components.length,
|
|
1804
|
-
components: components.map((comp) => `Framework/Structural/${comp.name}`)
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
buildDependencyContentsSync(jsContent, componentPath) {
|
|
1809
|
-
const dependencies = this.analyzeDependencies(jsContent, componentPath);
|
|
1810
|
-
const dependencyContents = {};
|
|
1811
|
-
|
|
1812
|
-
for (const dep of dependencies) {
|
|
1813
|
-
const depPath = dep.path;
|
|
1814
|
-
try {
|
|
1815
|
-
const depContent = fs.readFileSync(depPath, 'utf-8');
|
|
1816
|
-
const depName = path
|
|
1817
|
-
.relative(this.srcPath, depPath)
|
|
1818
|
-
.replace(/\\/g, '/');
|
|
1819
|
-
dependencyContents[depName] = {
|
|
1820
|
-
content: depContent,
|
|
1821
|
-
bindings: dep.bindings || []
|
|
1822
|
-
};
|
|
1823
|
-
} catch (error) {
|
|
1824
|
-
console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
return dependencyContents;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
getConfiguredPublicFolders() {
|
|
1832
|
-
const publicFolders = Array.isArray(this.sliceConfig?.publicFolders)
|
|
1833
|
-
? this.sliceConfig.publicFolders
|
|
1834
|
-
: [];
|
|
1835
|
-
|
|
1836
|
-
return publicFolders
|
|
1837
|
-
.map((folder) => this.normalizePublicFolder(folder))
|
|
1838
|
-
.filter(Boolean);
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
normalizePublicFolder(folder) {
|
|
1842
|
-
if (typeof folder !== 'string') return null;
|
|
1843
|
-
let normalized = folder.trim();
|
|
1844
|
-
if (!normalized) return null;
|
|
1845
|
-
|
|
1846
|
-
if (!normalized.startsWith('/')) {
|
|
1847
|
-
normalized = `/${normalized}`;
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
normalized = normalized.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
1851
|
-
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
1852
|
-
normalized = normalized.slice(0, -1);
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
return normalized;
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
normalizeImportPath(importPath) {
|
|
1859
|
-
if (typeof importPath !== 'string') return '';
|
|
1860
|
-
const cleanPath = importPath.split(/[?#]/)[0];
|
|
1861
|
-
return cleanPath.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
isRelativeImport(importPath) {
|
|
1865
|
-
return importPath.startsWith('./') || importPath.startsWith('../');
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
isAbsoluteImport(importPath) {
|
|
1869
|
-
return importPath.startsWith('/');
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
isImportInPublicFolders(importPath, publicFolders) {
|
|
1873
|
-
const normalizedImport = this.normalizeImportPath(importPath);
|
|
1874
|
-
return publicFolders.some((folder) => normalizedImport === folder || normalizedImport.startsWith(`${folder}/`));
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
classifyImport(importPath, publicFolders) {
|
|
1878
|
-
if (typeof importPath !== 'string' || !importPath) {
|
|
1879
|
-
return { keep: false, warning: 'Warning: Removing bare import: <unknown>' };
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
if (this.isRelativeImport(importPath)) {
|
|
1883
|
-
return { keep: false, warning: null };
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
if (this.isAbsoluteImport(importPath)) {
|
|
1887
|
-
if (this.isImportInPublicFolders(importPath, publicFolders)) {
|
|
1888
|
-
return { keep: true, warning: null };
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
return {
|
|
1892
|
-
keep: false,
|
|
1893
|
-
warning: `Warning: Removing absolute import outside publicFolders: ${importPath}`
|
|
1894
|
-
};
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
return {
|
|
1898
|
-
keep: false,
|
|
1899
|
-
warning: `Warning: Removing bare import: ${importPath}`
|
|
1900
|
-
};
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
buildImportWarningMessage(baseMessage, sourceContext) {
|
|
1904
|
-
if (!sourceContext) return baseMessage;
|
|
1905
|
-
return `${baseMessage} [${sourceContext}]`;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
extractLocalBindingsFromImportStatement(statement) {
|
|
1909
|
-
const source = String(statement || '').trim();
|
|
1910
|
-
if (!source.startsWith('import ')) return [];
|
|
1911
|
-
if (/^import\s+['"][^'"]+['"]\s*;?$/.test(source)) return [];
|
|
1912
|
-
|
|
1913
|
-
const bindings = [];
|
|
1914
|
-
|
|
1915
|
-
const defaultMatch = source.match(/^import\s+([A-Za-z_$][\w$]*)\s*(,|\s+from\s+)/);
|
|
1916
|
-
if (defaultMatch && defaultMatch[1] !== '*') {
|
|
1917
|
-
bindings.push(defaultMatch[1]);
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
const namespaceMatch = source.match(/,?\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]/);
|
|
1921
|
-
if (namespaceMatch) {
|
|
1922
|
-
bindings.push(namespaceMatch[1]);
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
const namedMatch = source.match(/\{([\s\S]*?)\}\s*from\s*['"]/);
|
|
1926
|
-
if (namedMatch) {
|
|
1927
|
-
const namedSection = namedMatch[1];
|
|
1928
|
-
for (const part of namedSection.split(',')) {
|
|
1929
|
-
const cleanPart = part.trim();
|
|
1930
|
-
if (!cleanPart) continue;
|
|
1931
|
-
const aliasParts = cleanPart.split(/\s+as\s+/i).map((v) => v.trim()).filter(Boolean);
|
|
1932
|
-
const localName = aliasParts.length > 1 ? aliasParts[1] : aliasParts[0];
|
|
1933
|
-
if (/^[A-Za-z_$][\w$]*$/.test(localName)) {
|
|
1934
|
-
bindings.push(localName);
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
return Array.from(new Set(bindings));
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
validateHoistedImportCollisions(importStatements, reservedIdentifiers = new Set()) {
|
|
1943
|
-
const reserved = reservedIdentifiers instanceof Set
|
|
1944
|
-
? reservedIdentifiers
|
|
1945
|
-
: new Set(reservedIdentifiers || []);
|
|
1946
|
-
const bindingToStatement = new Map();
|
|
1947
|
-
|
|
1948
|
-
for (const statement of importStatements || []) {
|
|
1949
|
-
const normalizedStatement = String(statement || '').trim();
|
|
1950
|
-
if (!normalizedStatement) continue;
|
|
1951
|
-
const localBindings = this.extractLocalBindingsFromImportStatement(normalizedStatement);
|
|
1952
|
-
|
|
1953
|
-
for (const localBinding of localBindings) {
|
|
1954
|
-
if (reserved.has(localBinding)) {
|
|
1955
|
-
throw new Error(`Hoisted import reserved identifier collision: ${localBinding}`);
|
|
1956
|
-
}
|
|
1957
|
-
const previousStatement = bindingToStatement.get(localBinding);
|
|
1958
|
-
if (previousStatement && previousStatement !== normalizedStatement) {
|
|
1959
|
-
throw new Error(`Hoisted import binding collision: ${localBinding}`);
|
|
1960
|
-
}
|
|
1961
|
-
bindingToStatement.set(localBinding, normalizedStatement);
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
parseImportsFromCode(code) {
|
|
1967
|
-
const ast = parse(code, {
|
|
1968
|
-
sourceType: 'module',
|
|
1969
|
-
plugins: ['jsx']
|
|
1970
|
-
});
|
|
1971
|
-
|
|
1972
|
-
const importNodes = [];
|
|
1973
|
-
traverse.default(ast, {
|
|
1974
|
-
ImportDeclaration(pathNode) {
|
|
1975
|
-
importNodes.push(pathNode.node);
|
|
1976
|
-
}
|
|
1977
|
-
});
|
|
1978
|
-
|
|
1979
|
-
return importNodes
|
|
1980
|
-
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
|
|
1981
|
-
.sort((a, b) => a.start - b.start);
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
parseImportsWithFallbackScanner(code) {
|
|
1985
|
-
const entries = [];
|
|
1986
|
-
const importRegex = /\bimport\b/g;
|
|
1987
|
-
let match = null;
|
|
1988
|
-
|
|
1989
|
-
while ((match = importRegex.exec(code)) !== null) {
|
|
1990
|
-
const start = match.index;
|
|
1991
|
-
const nextChar = code[start + 'import'.length];
|
|
1992
|
-
if (nextChar === '(') {
|
|
1993
|
-
continue;
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
let index = start + 'import'.length;
|
|
1997
|
-
let quote = null;
|
|
1998
|
-
let escaped = false;
|
|
1999
|
-
|
|
2000
|
-
while (index < code.length) {
|
|
2001
|
-
const char = code[index];
|
|
2002
|
-
|
|
2003
|
-
if (quote) {
|
|
2004
|
-
if (escaped) {
|
|
2005
|
-
escaped = false;
|
|
2006
|
-
} else if (char === '\\') {
|
|
2007
|
-
escaped = true;
|
|
2008
|
-
} else if (char === quote) {
|
|
2009
|
-
quote = null;
|
|
2010
|
-
}
|
|
2011
|
-
index += 1;
|
|
2012
|
-
continue;
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
if (char === '\'' || char === '"' || char === '`') {
|
|
2016
|
-
quote = char;
|
|
2017
|
-
index += 1;
|
|
2018
|
-
continue;
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
if (char === ';') {
|
|
2022
|
-
index += 1;
|
|
2023
|
-
break;
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
index += 1;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
const end = index;
|
|
2030
|
-
const statement = code.slice(start, end);
|
|
2031
|
-
const fromMatch = statement.match(/\bfrom\s+['"]([^'"]+)['"]/);
|
|
2032
|
-
const sideEffectMatch = statement.match(/\bimport\s+['"]([^'"]+)['"]/);
|
|
2033
|
-
const importPath = fromMatch?.[1] || sideEffectMatch?.[1] || null;
|
|
2034
|
-
|
|
2035
|
-
if (!importPath) {
|
|
2036
|
-
continue;
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
entries.push({ start, end, statement, importPath });
|
|
2040
|
-
importRegex.lastIndex = end;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
return entries;
|
|
2044
|
-
}
|
|
2045
|
-
|
|
2046
|
-
stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports) {
|
|
2047
|
-
const hoistedImports = [];
|
|
2048
|
-
const importEntries = this.parseImportsWithFallbackScanner(code);
|
|
2049
|
-
if (importEntries.length === 0) {
|
|
2050
|
-
return { code, hoistedImports };
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
let cleanedCode = '';
|
|
2054
|
-
let cursor = 0;
|
|
2055
|
-
for (const entry of importEntries) {
|
|
2056
|
-
const { start, end, statement, importPath } = entry;
|
|
2057
|
-
const classification = this.classifyImport(importPath, publicFolders);
|
|
2058
|
-
cleanedCode += code.slice(cursor, start);
|
|
2059
|
-
if (classification.keep) {
|
|
2060
|
-
if (collectHoistedImports) {
|
|
2061
|
-
hoistedImports.push(statement.trim());
|
|
2062
|
-
} else {
|
|
2063
|
-
cleanedCode += statement;
|
|
2064
|
-
}
|
|
2065
|
-
} else if (classification.warning) {
|
|
2066
|
-
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
2067
|
-
}
|
|
2068
|
-
cursor = end;
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
cleanedCode += code.slice(cursor);
|
|
2072
|
-
|
|
2073
|
-
return { code: cleanedCode, hoistedImports };
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
stripImports(code, options = {}) {
|
|
2077
|
-
const { sourceContext = null, collectHoistedImports = false } = options;
|
|
2078
|
-
const publicFolders = this.getConfiguredPublicFolders();
|
|
2079
|
-
const hoistedImports = [];
|
|
2080
|
-
|
|
2081
|
-
try {
|
|
2082
|
-
const importNodes = this.parseImportsFromCode(code);
|
|
2083
|
-
|
|
2084
|
-
if (importNodes.length === 0) {
|
|
2085
|
-
return collectHoistedImports ? { code, hoistedImports } : code;
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
let cleaned = '';
|
|
2089
|
-
let cursor = 0;
|
|
2090
|
-
|
|
2091
|
-
for (const node of importNodes) {
|
|
2092
|
-
const importPath = node.source?.value;
|
|
2093
|
-
const classification = this.classifyImport(importPath, publicFolders);
|
|
2094
|
-
const statement = code.slice(node.start, node.end);
|
|
2095
|
-
|
|
2096
|
-
cleaned += code.slice(cursor, node.start);
|
|
2097
|
-
if (classification.keep) {
|
|
2098
|
-
if (collectHoistedImports) {
|
|
2099
|
-
hoistedImports.push(statement.trim());
|
|
2100
|
-
} else {
|
|
2101
|
-
cleaned += statement;
|
|
2102
|
-
}
|
|
2103
|
-
} else if (classification.warning) {
|
|
2104
|
-
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
cursor = node.end;
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
cleaned += code.slice(cursor);
|
|
2111
|
-
return collectHoistedImports
|
|
2112
|
-
? { code: cleaned, hoistedImports }
|
|
2113
|
-
: cleaned;
|
|
2114
|
-
} catch (error) {
|
|
2115
|
-
const fallback = this.stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports);
|
|
2116
|
-
return collectHoistedImports ? fallback : fallback.code;
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
async loadComponentsMap() {
|
|
2121
|
-
const componentsConfigPath = path.join(this.componentsPath, 'components.js');
|
|
2122
|
-
if (!await fs.pathExists(componentsConfigPath)) {
|
|
2123
|
-
return {};
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
const content = await fs.readFile(componentsConfigPath, 'utf-8');
|
|
2127
|
-
return this.parseComponentsConfig(content);
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
parseComponentsConfig(content) {
|
|
2131
|
-
try {
|
|
2132
|
-
const ast = parse(content, {
|
|
2133
|
-
sourceType: 'module',
|
|
2134
|
-
plugins: ['jsx']
|
|
2135
|
-
});
|
|
2136
|
-
|
|
2137
|
-
let componentsNode = null;
|
|
2138
|
-
|
|
2139
|
-
traverse.default(ast, {
|
|
2140
|
-
VariableDeclarator(path) {
|
|
2141
|
-
if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
|
|
2142
|
-
componentsNode = path.node.init;
|
|
2143
|
-
path.stop();
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
});
|
|
2147
|
-
|
|
2148
|
-
if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
|
|
2149
|
-
throw new Error('components object not found');
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
const config = {};
|
|
2153
|
-
for (const prop of componentsNode.properties) {
|
|
2154
|
-
if (prop.type !== 'ObjectProperty') continue;
|
|
2155
|
-
|
|
2156
|
-
const key = this.extractStringValue(prop.key);
|
|
2157
|
-
const value = this.extractStringValue(prop.value);
|
|
2158
|
-
|
|
2159
|
-
if (!key || !value) {
|
|
2160
|
-
throw new Error('Invalid components entry');
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
config[key] = value;
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
return config;
|
|
2167
|
-
} catch (error) {
|
|
2168
|
-
console.warn(`Could not parse components.js: ${error.message}`);
|
|
2169
|
-
return {};
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
extractStringValue(node) {
|
|
2174
|
-
if (!node) return null;
|
|
2175
|
-
|
|
2176
|
-
if (node.type === 'StringLiteral') {
|
|
2177
|
-
return node.value;
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
if (node.type === 'Identifier') {
|
|
2181
|
-
return node.name;
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
2185
|
-
return node.quasis.map((q) => q.value.cooked).join('');
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
return null;
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
/**
|
|
2192
|
-
* Converts a route to filename
|
|
2193
|
-
*/
|
|
2194
|
-
routeToFileName(routePath) {
|
|
2195
|
-
if (routePath === '/') return 'home';
|
|
2196
|
-
return routePath
|
|
2197
|
-
.replace(/^\//, '')
|
|
2198
|
-
.replace(/\//g, '-')
|
|
2199
|
-
.replace(/[^a-zA-Z0-9-]/g, '')
|
|
2200
|
-
.toLowerCase();
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
/**
|
|
2204
|
-
* Saves the configuration to file
|
|
2205
|
-
*/
|
|
2206
|
-
async saveBundleConfig(config) {
|
|
2207
|
-
// Ensure bundles directory exists
|
|
2208
|
-
await fs.ensureDir(this.bundlesPath);
|
|
2209
|
-
|
|
2210
|
-
// Save JSON config
|
|
2211
|
-
const configPath = path.join(this.bundlesPath, 'bundle.config.json');
|
|
2212
|
-
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
2213
|
-
|
|
2214
|
-
// Generate JavaScript module for direct import
|
|
2215
|
-
const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
|
|
2216
|
-
const jsConfig = this.generateBundleConfigJS(config);
|
|
2217
|
-
await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
|
|
2218
|
-
|
|
2219
|
-
console.log(`✓ Configuration saved to ${configPath}`);
|
|
2220
|
-
console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
|
|
2221
|
-
}
|
|
2222
|
-
|
|
2223
|
-
/**
|
|
2224
|
-
* Creates a default bundle config file if none exists
|
|
2225
|
-
*/
|
|
2226
|
-
async createDefaultBundleConfig() {
|
|
2227
|
-
const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
|
|
2228
|
-
|
|
2229
|
-
// Only create if it doesn't exist
|
|
2230
|
-
if (await fs.pathExists(defaultConfigPath)) {
|
|
2231
|
-
return;
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
await fs.ensureDir(path.dirname(defaultConfigPath));
|
|
2235
|
-
|
|
2236
|
-
const defaultConfig = `/**
|
|
2237
|
-
* Slice.js Bundle Configuration
|
|
2238
|
-
* Default empty configuration - no bundles available
|
|
2239
|
-
* Run 'slice build' to generate optimized bundles
|
|
2240
|
-
*/
|
|
2241
|
-
|
|
2242
|
-
// No bundles available - using individual component loading
|
|
2243
|
-
export const SLICE_BUNDLE_CONFIG = null;
|
|
2244
|
-
|
|
2245
|
-
// No auto-initialization needed for default config
|
|
2246
|
-
`;
|
|
2247
|
-
|
|
2248
|
-
await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
|
|
2249
|
-
console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
/**
|
|
2253
|
-
* Generates JavaScript module for direct import
|
|
2254
|
-
*/
|
|
2255
|
-
generateBundleConfigJS(config) {
|
|
2256
|
-
return `/**
|
|
2257
|
-
* Slice.js Bundle Configuration
|
|
2258
|
-
* Generated: ${new Date().toISOString()}
|
|
2259
|
-
* Strategy: ${config.strategy}
|
|
2260
|
-
*/
|
|
2261
|
-
|
|
2262
|
-
// Direct bundle configuration (no fetch required)
|
|
2263
|
-
export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
|
|
2264
|
-
|
|
2265
|
-
// Auto-initialization if slice is available
|
|
2266
|
-
if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
|
|
2267
|
-
window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
|
|
2268
|
-
|
|
2269
|
-
// Load critical bundle automatically
|
|
2270
|
-
if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
|
|
2271
|
-
(async () => {
|
|
2272
|
-
const bundlePath = "/bundles/" + SLICE_BUNDLE_CONFIG.bundles.critical.file;
|
|
2273
|
-
const integrity = SLICE_BUNDLE_CONFIG.bundles.critical.integrity;
|
|
2274
|
-
|
|
2275
|
-
if (typeof window.slice.controller.verifyBundleIntegrity === 'function') {
|
|
2276
|
-
const ok = await window.slice.controller.verifyBundleIntegrity(bundlePath, integrity);
|
|
2277
|
-
if (!ok) {
|
|
2278
|
-
console.warn('Failed to load critical bundle: integrity check failed');
|
|
2279
|
-
return;
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
import('./slice-bundle.critical.js').catch(err =>
|
|
2284
|
-
console.warn('Failed to load critical bundle:', err)
|
|
2285
|
-
);
|
|
2286
|
-
window.slice.controller.criticalBundleLoaded = true;
|
|
2287
|
-
})();
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
`;
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
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: false
|
|
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
|
+
const filteredModules = modules.filter((module) => !omittedDependencies.has(module.name));
|
|
1253
|
+
|
|
1254
|
+
const lines = [
|
|
1255
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1256
|
+
...this.getDefaultExportResolverLines()
|
|
1257
|
+
];
|
|
1258
|
+
if (options.includeSharedResolver) {
|
|
1259
|
+
lines.push(...this.getBundleDependencyResolverLines());
|
|
1260
|
+
}
|
|
1261
|
+
filteredModules.forEach((module, index) => {
|
|
1262
|
+
const exportVar = `__sliceDepExports${index}`;
|
|
1263
|
+
const transformedContent = this.transformDependencyContent(module.content, exportVar, module.name);
|
|
1264
|
+
lines.push(`const ${exportVar} = {};`);
|
|
1265
|
+
lines.push(transformedContent.trim());
|
|
1266
|
+
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
return lines.join('\n');
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
async buildDependencyContents(jsContent, componentPath) {
|
|
1273
|
+
const dependencies = this.analyzeDependencies(jsContent, componentPath);
|
|
1274
|
+
const dependencyContents = {};
|
|
1275
|
+
|
|
1276
|
+
for (const dep of dependencies) {
|
|
1277
|
+
const depPath = dep.path;
|
|
1278
|
+
try {
|
|
1279
|
+
const depContent = await fs.readFile(depPath, 'utf-8');
|
|
1280
|
+
const depName = path
|
|
1281
|
+
.relative(this.srcPath, depPath)
|
|
1282
|
+
.replace(/\\/g, '/');
|
|
1283
|
+
dependencyContents[depName] = {
|
|
1284
|
+
content: depContent,
|
|
1285
|
+
bindings: dep.bindings || []
|
|
1286
|
+
};
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return dependencyContents;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Cleans JavaScript code by removing imports/exports and ensuring class is available globally
|
|
1297
|
+
*/
|
|
1298
|
+
cleanJavaScript(code, componentName, sourceContext = componentName) {
|
|
1299
|
+
// Remove export default
|
|
1300
|
+
code = code.replace(/export\s+default\s+/g, '');
|
|
1301
|
+
|
|
1302
|
+
// Remove only unsupported imports (relative always removed, allowed absolute kept)
|
|
1303
|
+
const stripped = this.stripImports(code, {
|
|
1304
|
+
sourceContext,
|
|
1305
|
+
collectHoistedImports: true
|
|
1306
|
+
});
|
|
1307
|
+
const hoistedImports = stripped.hoistedImports || [];
|
|
1308
|
+
code = stripped.code;
|
|
1309
|
+
|
|
1310
|
+
// Guard customElements.define to avoid duplicate registrations
|
|
1311
|
+
code = code.replace(
|
|
1312
|
+
/customElements\.define\(([^)]+)\);?/g,
|
|
1313
|
+
(match, args) => {
|
|
1314
|
+
const firstArg = args.split(',')[0]?.trim() || '';
|
|
1315
|
+
if (!/^['"][^'"]+['"]$/.test(firstArg)) {
|
|
1316
|
+
return match;
|
|
1317
|
+
}
|
|
1318
|
+
return `if (!customElements.get(${firstArg})) { customElements.define(${args}); }`;
|
|
1319
|
+
}
|
|
1320
|
+
);
|
|
1321
|
+
|
|
1322
|
+
// Make sure the class is available globally for bundle evaluation
|
|
1323
|
+
// Preserve original customElements.define if it exists
|
|
1324
|
+
if (code.includes('customElements.define')) {
|
|
1325
|
+
// Add global assignment before guarded or direct customElements.define
|
|
1326
|
+
const globalAssignment = `window.${componentName} = ${componentName};\n`;
|
|
1327
|
+
const guardedDefineRegex = /if\s*\(\s*!\s*customElements\.get\([^)]*\)\s*\)\s*\{\s*customElements\.define\([^;]+\);?\s*\}\s*$/;
|
|
1328
|
+
const directDefineRegex = /customElements\.define\([^;]+\);?\s*$/;
|
|
1329
|
+
if (guardedDefineRegex.test(code)) {
|
|
1330
|
+
code = code.replace(guardedDefineRegex, `${globalAssignment}$&`);
|
|
1331
|
+
} else {
|
|
1332
|
+
code = code.replace(directDefineRegex, `${globalAssignment}$&`);
|
|
1333
|
+
}
|
|
1334
|
+
} else {
|
|
1335
|
+
// If no customElements.define found, just assign to global
|
|
1336
|
+
code += `\nwindow.${componentName} = ${componentName};`;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Add return statement for bundle evaluation compatibility
|
|
1340
|
+
code += `\nreturn ${componentName};`;
|
|
1341
|
+
|
|
1342
|
+
return {
|
|
1343
|
+
code,
|
|
1344
|
+
hoistedImports
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Formats the bundle file
|
|
1350
|
+
*/
|
|
1351
|
+
formatBundleFile(componentsData, metadata) {
|
|
1352
|
+
const integrityPayload = {
|
|
1353
|
+
metadata: {
|
|
1354
|
+
...metadata,
|
|
1355
|
+
generated: 'static'
|
|
1356
|
+
},
|
|
1357
|
+
components: Object.fromEntries(
|
|
1358
|
+
Object.entries(componentsData).map(([name, data]) => [
|
|
1359
|
+
name,
|
|
1360
|
+
{
|
|
1361
|
+
name: data.name,
|
|
1362
|
+
category: data.category,
|
|
1363
|
+
categoryType: data.categoryType,
|
|
1364
|
+
componentDependencies: data.componentDependencies
|
|
1365
|
+
}
|
|
1366
|
+
])
|
|
1367
|
+
)
|
|
1368
|
+
};
|
|
1369
|
+
const integrity = `sha256:${crypto
|
|
1370
|
+
.createHash('sha256')
|
|
1371
|
+
.update(JSON.stringify(integrityPayload))
|
|
1372
|
+
.digest('hex')}`;
|
|
1373
|
+
|
|
1374
|
+
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1375
|
+
const frameworkComponentKeys = Object.keys(componentsData || {});
|
|
1376
|
+
const frameworkClassIdentifiers = frameworkComponentKeys.map((key) => this.toSafeIdentifier(key));
|
|
1377
|
+
const frameworkReservedIdentifiers = new Set([
|
|
1378
|
+
'SLICE_BUNDLE',
|
|
1379
|
+
'SLICE_BUNDLE_COMPONENTS',
|
|
1380
|
+
'SLICE_BUNDLE_DEPENDENCIES',
|
|
1381
|
+
'SLICE_FRAMEWORK_CLASSES',
|
|
1382
|
+
...frameworkClassIdentifiers,
|
|
1383
|
+
...this.getDependencyExportVariableNames(dependencyModules)
|
|
1384
|
+
]);
|
|
1385
|
+
const rawHoistedImports = Object.values(componentsData || {})
|
|
1386
|
+
.flatMap((component) => component?.hoistedImports || [])
|
|
1387
|
+
.map((statement) => String(statement).trim())
|
|
1388
|
+
.filter(Boolean);
|
|
1389
|
+
this.validateHoistedImportCollisions(rawHoistedImports, frameworkReservedIdentifiers);
|
|
1390
|
+
const hoistedImportBlock = Array.from(new Set(rawHoistedImports)).join('\n');
|
|
1391
|
+
|
|
1392
|
+
const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
|
|
1393
|
+
const componentBlock = this.buildComponentBundleBlock(componentsData);
|
|
1394
|
+
|
|
1395
|
+
return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}/**
|
|
1396
|
+
* Slice.js Bundle
|
|
1397
|
+
* Type: ${metadata.type}
|
|
1398
|
+
* Generated: ${metadata.generated}
|
|
1399
|
+
* Strategy: ${metadata.strategy}
|
|
1400
|
+
* Components: ${metadata.componentCount}
|
|
1401
|
+
* Total Size: ${(metadata.totalSize / 1024).toFixed(1)} KB
|
|
1402
|
+
*/
|
|
1403
|
+
|
|
1404
|
+
${dependencyBlock}
|
|
1405
|
+
${componentBlock}
|
|
1406
|
+
|
|
1407
|
+
export const SLICE_BUNDLE = {
|
|
1408
|
+
metadata: ${JSON.stringify({ ...metadata, integrity }, null, 2)},
|
|
1409
|
+
components: SLICE_BUNDLE_COMPONENTS
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// Auto-registration of components
|
|
1413
|
+
if (window.slice && window.slice.controller) {
|
|
1414
|
+
slice.controller.registerBundle(SLICE_BUNDLE);
|
|
1415
|
+
}
|
|
1416
|
+
`;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
buildDependencyModuleBlock(componentsData) {
|
|
1420
|
+
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1421
|
+
const lines = [
|
|
1422
|
+
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
1423
|
+
...this.getDefaultExportResolverLines()
|
|
1424
|
+
];
|
|
1425
|
+
if (dependencyModules.length === 0) {
|
|
1426
|
+
return `${lines.join('\n')}`;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
dependencyModules.forEach((module, index) => {
|
|
1430
|
+
const exportVar = `__sliceDepExports${index}`;
|
|
1431
|
+
const content = this.transformDependencyContent(module.content, exportVar, module.name);
|
|
1432
|
+
lines.push(`// Dependency: ${module.name}`);
|
|
1433
|
+
lines.push(`const ${exportVar} = {};`);
|
|
1434
|
+
lines.push(content.trim());
|
|
1435
|
+
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
return `${lines.join('\n')}`;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
collectDependencyModules(componentsData) {
|
|
1442
|
+
const modules = new Map();
|
|
1443
|
+
Object.values(componentsData).forEach((component) => {
|
|
1444
|
+
Object.entries(component.externalDependencies || {}).forEach(([name, entry]) => {
|
|
1445
|
+
if (modules.has(name)) return;
|
|
1446
|
+
const content = typeof entry === 'string' ? entry : entry.content;
|
|
1447
|
+
modules.set(name, { name, content });
|
|
1448
|
+
});
|
|
1449
|
+
});
|
|
1450
|
+
return Array.from(modules.values());
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
collectDependencyModulesFromComponents(components = []) {
|
|
1454
|
+
const modules = new Map();
|
|
1455
|
+
for (const component of components || []) {
|
|
1456
|
+
const externalDependencies = component.externalDependencies || {};
|
|
1457
|
+
for (const [moduleName, entry] of Object.entries(externalDependencies)) {
|
|
1458
|
+
if (modules.has(moduleName)) continue;
|
|
1459
|
+
const content = typeof entry === 'string' ? entry : entry?.content;
|
|
1460
|
+
if (!content) continue;
|
|
1461
|
+
modules.set(moduleName, { name: moduleName, content });
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return Array.from(modules.values());
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
getDependencyExportVariableNames(dependencyModules = []) {
|
|
1468
|
+
return (dependencyModules || []).map((_, index) => `__sliceDepExports${index}`);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
transformDependencyContent(content, exportVar, moduleName) {
|
|
1472
|
+
const dataName = this.getDependencyDefaultFallbackKey(moduleName);
|
|
1473
|
+
const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
|
|
1474
|
+
|
|
1475
|
+
return content
|
|
1476
|
+
.replace(/export\s+const\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1477
|
+
.replace(/export\s+let\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1478
|
+
.replace(/export\s+var\s+(\w+)\s*=\s*/g, `${exportVar}.$1 = `)
|
|
1479
|
+
.replace(/export\s+function\s+(\w+)/g, `${exportVar}.$1 = function`)
|
|
1480
|
+
.replace(/export\s+default\s+/g, exportPrefix)
|
|
1481
|
+
.replace(/export\s*{\s*([^}]+)\s*}/g, (match, exportsStr) => {
|
|
1482
|
+
return exportsStr
|
|
1483
|
+
.split(',')
|
|
1484
|
+
.map((exp) => {
|
|
1485
|
+
const cleanExp = exp.trim();
|
|
1486
|
+
const varName = cleanExp.split(' as ')[0].trim();
|
|
1487
|
+
return `${exportVar}.${varName} = ${varName};`;
|
|
1488
|
+
})
|
|
1489
|
+
.join('\n');
|
|
1490
|
+
})
|
|
1491
|
+
.replace(/^\s*export\s+/gm, '');
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
getDependencyDefaultFallbackKey(moduleName) {
|
|
1495
|
+
const baseName = moduleName?.split('/').pop()?.replace(/\.[^.]+$/, '');
|
|
1496
|
+
return baseName ? `${baseName}Data` : null;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
buildComponentBundleBlock(componentsData) {
|
|
1500
|
+
const componentEntries = [];
|
|
1501
|
+
const componentDefs = [];
|
|
1502
|
+
const frameworkEntries = [];
|
|
1503
|
+
|
|
1504
|
+
Object.entries(componentsData).forEach(([name, data]) => {
|
|
1505
|
+
const classVar = this.toSafeIdentifier(name);
|
|
1506
|
+
const bindings = this.buildDependencyBindings(data.externalDependencies || {});
|
|
1507
|
+
|
|
1508
|
+
componentDefs.push(`const ${classVar} = (() => {\n${bindings}\n${data.js}\nreturn ${name};\n})();`);
|
|
1509
|
+
|
|
1510
|
+
if (data.isFramework) {
|
|
1511
|
+
frameworkEntries.push(`${JSON.stringify(data.name)}: ${classVar}`);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
componentEntries.push(
|
|
1515
|
+
`${JSON.stringify(name)}: {\n` +
|
|
1516
|
+
` name: ${JSON.stringify(data.name)},\n` +
|
|
1517
|
+
` category: ${JSON.stringify(data.category)},\n` +
|
|
1518
|
+
` categoryType: ${JSON.stringify(data.categoryType)},\n` +
|
|
1519
|
+
` componentDependencies: ${JSON.stringify(data.componentDependencies)},\n` +
|
|
1520
|
+
` html: ${JSON.stringify(data.html)},\n` +
|
|
1521
|
+
` css: ${JSON.stringify(data.css)},\n` +
|
|
1522
|
+
` size: ${JSON.stringify(data.size)},\n` +
|
|
1523
|
+
` class: ${classVar}\n` +
|
|
1524
|
+
`}`
|
|
1525
|
+
);
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
const frameworkBlock = frameworkEntries.length > 0
|
|
1529
|
+
? `const SLICE_FRAMEWORK_CLASSES = {\n${frameworkEntries.join(',\n')}\n};\nwindow.SLICE_FRAMEWORK_CLASSES = SLICE_FRAMEWORK_CLASSES;`
|
|
1530
|
+
: '';
|
|
1531
|
+
|
|
1532
|
+
return `${componentDefs.join('\n\n')}\n\nconst SLICE_BUNDLE_COMPONENTS = {\n${componentEntries.join(',\n')}\n};\n${frameworkBlock}`;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
buildDependencyBindings(externalDependencies, options = {}) {
|
|
1536
|
+
const lines = [];
|
|
1537
|
+
Object.entries(externalDependencies).forEach(([name, entry]) => {
|
|
1538
|
+
const bindings = typeof entry === 'string' ? [] : entry.bindings || [];
|
|
1539
|
+
const depVar = options.preferShared
|
|
1540
|
+
? `__sliceResolveBundleDependency(${JSON.stringify(name)})`
|
|
1541
|
+
: `SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(name)}]`;
|
|
1542
|
+
|
|
1543
|
+
bindings.forEach((binding) => {
|
|
1544
|
+
if (!binding?.localName) return;
|
|
1545
|
+
if (binding.type === 'default') {
|
|
1546
|
+
const preferredKey = this.getDependencyDefaultFallbackKey(name);
|
|
1547
|
+
lines.push(`const ${binding.localName} = __sliceResolveDefaultExport(${depVar}, ${JSON.stringify(name)}, ${JSON.stringify(preferredKey)});`);
|
|
1548
|
+
}
|
|
1549
|
+
if (binding.type === 'named') {
|
|
1550
|
+
lines.push(`const ${binding.localName} = ${depVar}.${binding.importedName};`);
|
|
1551
|
+
}
|
|
1552
|
+
if (binding.type === 'namespace') {
|
|
1553
|
+
lines.push(`const ${binding.localName} = ${depVar};`);
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
return lines.join('\n');
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
getBundleDependencyResolverLines() {
|
|
1562
|
+
return [
|
|
1563
|
+
"const __sliceSharedDeps = typeof window !== 'undefined' ? (window.__SLICE_SHARED_DEPS__ || {}) : {};",
|
|
1564
|
+
'const __sliceResolveBundleDependency = (depName) => Object.prototype.hasOwnProperty.call(__sliceSharedDeps, depName) ? __sliceSharedDeps[depName] : SLICE_BUNDLE_DEPENDENCIES[depName];'
|
|
1565
|
+
];
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
getDefaultExportResolverLines() {
|
|
1569
|
+
return [
|
|
1570
|
+
'const __sliceDefaultExportWarningDeps = new Set();',
|
|
1571
|
+
"const __sliceDefaultExportPreferredKeys = ['module', 'exports', 'purify'];",
|
|
1572
|
+
'const __sliceDeterministicKeyCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);',
|
|
1573
|
+
'function __sliceResolveDefaultExport(dep, depName, preferredKey) {',
|
|
1574
|
+
' if (dep?.default !== undefined) return dep.default;',
|
|
1575
|
+
" if (dep === null || (typeof dep !== 'object' && typeof dep !== 'function')) return dep;",
|
|
1576
|
+
" if (preferredKey && preferredKey !== 'default' && preferredKey !== '__esModule' && Object.prototype.hasOwnProperty.call(dep, preferredKey)) return dep[preferredKey];",
|
|
1577
|
+
" const keys = Object.keys(dep).filter((key) => key !== 'default' && key !== '__esModule');",
|
|
1578
|
+
' if (keys.length === 1) return dep[keys[0]];',
|
|
1579
|
+
' if (keys.length > 1) {',
|
|
1580
|
+
' const preferredMatches = __sliceDefaultExportPreferredKeys.filter((key) => keys.includes(key));',
|
|
1581
|
+
' if (preferredMatches.length === 1) return dep[preferredMatches[0]];',
|
|
1582
|
+
' const sortedKeys = [...keys].sort(__sliceDeterministicKeyCompare);',
|
|
1583
|
+
' const fallbackKey = sortedKeys[0];',
|
|
1584
|
+
' const warningDepName = depName || "<unknown dependency>";',
|
|
1585
|
+
' if (!__sliceDefaultExportWarningDeps.has(warningDepName)) {',
|
|
1586
|
+
' __sliceDefaultExportWarningDeps.add(warningDepName);',
|
|
1587
|
+
' console.warn(`[Slice.js bundler] Ambiguous default export resolution for "${warningDepName}". Falling back to "${fallbackKey}". Keys: ${sortedKeys.join(\', \')}`);',
|
|
1588
|
+
' }',
|
|
1589
|
+
' return dep[fallbackKey];',
|
|
1590
|
+
' }',
|
|
1591
|
+
' return dep;',
|
|
1592
|
+
'}'
|
|
1593
|
+
];
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
toSafeIdentifier(name) {
|
|
1597
|
+
const cleaned = name.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
1598
|
+
if (/^\d/.test(cleaned)) {
|
|
1599
|
+
return `SliceComponent_${cleaned}`;
|
|
1600
|
+
}
|
|
1601
|
+
return `SliceComponent_${cleaned}`;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Generates the bundle configuration
|
|
1606
|
+
*/
|
|
1607
|
+
generateBundleConfig(frameworkBundle = null) {
|
|
1608
|
+
const metrics = this.analysisData.metrics || {};
|
|
1609
|
+
const config = {
|
|
1610
|
+
version: '2.0.0',
|
|
1611
|
+
format: this.format,
|
|
1612
|
+
loadingPolicy: this.loadingPolicy,
|
|
1613
|
+
strategy: this.config.strategy,
|
|
1614
|
+
minified: this.options.minify,
|
|
1615
|
+
obfuscated: this.options.obfuscate,
|
|
1616
|
+
production: true,
|
|
1617
|
+
generated: new Date().toISOString(),
|
|
1618
|
+
|
|
1619
|
+
stats: {
|
|
1620
|
+
totalComponents: metrics.totalComponents || 0,
|
|
1621
|
+
totalRoutes: metrics.totalRoutes || 0,
|
|
1622
|
+
sharedComponents: this.bundles.critical.components.length,
|
|
1623
|
+
sharedPercentage: metrics.sharedPercentage || 0,
|
|
1624
|
+
totalSize: metrics.totalSize || 0,
|
|
1625
|
+
criticalSize: this.bundles.critical.size
|
|
1626
|
+
},
|
|
1627
|
+
|
|
1628
|
+
bundles: {
|
|
1629
|
+
framework: {
|
|
1630
|
+
file: 'slice-bundle.framework.js',
|
|
1631
|
+
size: 0,
|
|
1632
|
+
hash: null,
|
|
1633
|
+
integrity: null,
|
|
1634
|
+
components: []
|
|
1635
|
+
},
|
|
1636
|
+
vendorShared: {
|
|
1637
|
+
bundleKey: 'vendor-shared',
|
|
1638
|
+
type: 'vendor-shared',
|
|
1639
|
+
file: this.vendorShared.file,
|
|
1640
|
+
size: this.vendorShared.bundle?.size || 0,
|
|
1641
|
+
hash: this.vendorShared.bundle?.hash || null,
|
|
1642
|
+
integrity: this.vendorShared.bundle?.integrity || null,
|
|
1643
|
+
dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
|
|
1644
|
+
dependencyCount: this.vendorShared.sharedDependencySet.size,
|
|
1645
|
+
routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
|
|
1646
|
+
},
|
|
1647
|
+
critical: {
|
|
1648
|
+
file: this.bundles.critical.file,
|
|
1649
|
+
size: this.bundles.critical.size,
|
|
1650
|
+
hash: this.bundles.critical.hash || null,
|
|
1651
|
+
integrity: this.bundles.critical.integrity || null,
|
|
1652
|
+
components: this.bundles.critical.components.map(c => c.name)
|
|
1653
|
+
},
|
|
1654
|
+
routes: {}
|
|
1655
|
+
},
|
|
1656
|
+
routeBundles: {},
|
|
1657
|
+
routeDependencyGraph: {}
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
for (const [key, bundle] of Object.entries(this.bundles.routes)) {
|
|
1661
|
+
const routeIdentifier = Array.isArray(bundle.path || bundle.paths)
|
|
1662
|
+
? key
|
|
1663
|
+
: (bundle.path || bundle.paths || key);
|
|
1664
|
+
const usesVendorShared = this.vendorShared.bundleKeysUsingSharedDependencies.has(key)
|
|
1665
|
+
|| (bundle.dependencies || []).includes('vendor-shared');
|
|
1666
|
+
const dependencies = this.mergeBundleDependencies(
|
|
1667
|
+
bundle.dependencies || [],
|
|
1668
|
+
usesVendorShared ? ['vendor-shared'] : []
|
|
1669
|
+
);
|
|
1670
|
+
|
|
1671
|
+
config.bundles.routes[key] = {
|
|
1672
|
+
path: bundle.path || bundle.paths || key, // Support both single path and array of paths, fallback to key
|
|
1673
|
+
file: `slice-bundle.${this.routeToFileName(routeIdentifier)}.js`,
|
|
1674
|
+
size: bundle.size,
|
|
1675
|
+
hash: bundle.hash || null,
|
|
1676
|
+
integrity: bundle.integrity || null,
|
|
1677
|
+
components: bundle.components.map(c => c.name),
|
|
1678
|
+
dependencies
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
const paths = Array.isArray(config.bundles.routes[key].path)
|
|
1682
|
+
? config.bundles.routes[key].path
|
|
1683
|
+
: [config.bundles.routes[key].path];
|
|
1684
|
+
|
|
1685
|
+
for (const routePath of paths) {
|
|
1686
|
+
if (!config.routeBundles[routePath]) {
|
|
1687
|
+
config.routeBundles[routePath] = ['critical'];
|
|
1688
|
+
}
|
|
1689
|
+
for (const dependency of dependencies.filter((dep) => dep !== 'critical')) {
|
|
1690
|
+
if (!config.routeBundles[routePath].includes(dependency)) {
|
|
1691
|
+
config.routeBundles[routePath].push(dependency);
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (!config.routeBundles[routePath].includes(key)) {
|
|
1695
|
+
config.routeBundles[routePath].push(key);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
const graphEntry = config.routeDependencyGraph[routePath] || {
|
|
1699
|
+
bundles: [],
|
|
1700
|
+
edges: []
|
|
1701
|
+
};
|
|
1702
|
+
if (!graphEntry.bundles.includes(key)) {
|
|
1703
|
+
graphEntry.bundles.push(key);
|
|
1704
|
+
graphEntry.bundles.sort((a, b) => a.localeCompare(b));
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const edgeKeys = new Set(graphEntry.edges.map((edge) => `${edge.from}->${edge.to}`));
|
|
1708
|
+
const orderedEdgeSources = ['critical', ...dependencies.filter((dependency) => dependency !== 'critical')];
|
|
1709
|
+
for (const source of orderedEdgeSources) {
|
|
1710
|
+
const edgeKey = `${source}->${key}`;
|
|
1711
|
+
if (!edgeKeys.has(edgeKey)) {
|
|
1712
|
+
graphEntry.edges.push({ from: source, to: key });
|
|
1713
|
+
edgeKeys.add(edgeKey);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
config.routeDependencyGraph[routePath] = graphEntry;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
if (frameworkBundle) {
|
|
1722
|
+
config.bundles.framework = {
|
|
1723
|
+
file: frameworkBundle.file,
|
|
1724
|
+
size: frameworkBundle.size,
|
|
1725
|
+
hash: frameworkBundle.hash,
|
|
1726
|
+
integrity: frameworkBundle.integrity,
|
|
1727
|
+
components: frameworkBundle.components || []
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
return config;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
collectFrameworkComponents() {
|
|
1735
|
+
return this.analysisData.components.filter((comp) => comp.isFramework);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
async createFrameworkBundle(components) {
|
|
1739
|
+
const fileName = 'slice-bundle.framework.js';
|
|
1740
|
+
const filePath = path.join(this.bundlesPath, fileName);
|
|
1741
|
+
return this.generateFrameworkBundleFile(components, fileName, filePath);
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
async generateFrameworkBundleFile(components, fileName, filePath) {
|
|
1745
|
+
const componentsData = {};
|
|
1746
|
+
const componentsMap = await this.loadComponentsMap();
|
|
1747
|
+
const metadata = {
|
|
1748
|
+
version: '2.0.0',
|
|
1749
|
+
type: 'framework',
|
|
1750
|
+
route: null,
|
|
1751
|
+
bundleKey: 'framework',
|
|
1752
|
+
file: fileName,
|
|
1753
|
+
generated: new Date().toISOString(),
|
|
1754
|
+
totalSize: components.reduce((sum, c) => sum + c.size, 0),
|
|
1755
|
+
componentCount: components.length,
|
|
1756
|
+
strategy: this.config.strategy,
|
|
1757
|
+
minified: this.options.minify,
|
|
1758
|
+
obfuscated: this.options.obfuscate
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1761
|
+
components.forEach((comp) => {
|
|
1762
|
+
const componentKey = `Framework/Structural/${comp.name}`;
|
|
1763
|
+
const fileBaseName = comp.fileName || comp.name;
|
|
1764
|
+
const jsPath = path.join(comp.path, `${fileBaseName}.js`);
|
|
1765
|
+
const jsContent = fs.readFileSync(jsPath, 'utf-8');
|
|
1766
|
+
const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
|
|
1767
|
+
const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
|
|
1768
|
+
|
|
1769
|
+
componentsData[componentKey] = {
|
|
1770
|
+
name: comp.name,
|
|
1771
|
+
category: comp.category,
|
|
1772
|
+
categoryType: comp.categoryType,
|
|
1773
|
+
isFramework: true,
|
|
1774
|
+
js: cleanedJavaScript.code,
|
|
1775
|
+
hoistedImports: cleanedJavaScript.hoistedImports,
|
|
1776
|
+
externalDependencies: dependencyContents,
|
|
1777
|
+
componentDependencies: Array.from(comp.dependencies),
|
|
1778
|
+
html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
|
|
1779
|
+
? fs.readFileSync(path.join(comp.path, `${fileBaseName}.html`), 'utf-8')
|
|
1780
|
+
: null,
|
|
1781
|
+
css: fs.existsSync(path.join(comp.path, `${fileBaseName}.css`))
|
|
1782
|
+
? fs.readFileSync(path.join(comp.path, `${fileBaseName}.css`), 'utf-8')
|
|
1783
|
+
: null,
|
|
1784
|
+
size: comp.size
|
|
1785
|
+
};
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
const prelude = `const components = ${JSON.stringify(componentsMap)};`;
|
|
1789
|
+
const bundleContent = `${prelude}\n${this.formatBundleFile(componentsData, metadata)}`;
|
|
1790
|
+
const finalContent = await this.applyBundleTransforms(bundleContent, fileName);
|
|
1791
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
1792
|
+
await fs.writeFile(filePath, finalContent, 'utf-8');
|
|
1793
|
+
|
|
1794
|
+
const hash = crypto.createHash('sha256').update(finalContent).digest('hex');
|
|
1795
|
+
const integrity = `sha256:${hash}`;
|
|
1796
|
+
|
|
1797
|
+
return {
|
|
1798
|
+
name: 'framework',
|
|
1799
|
+
file: fileName,
|
|
1800
|
+
size: Buffer.byteLength(bundleContent, 'utf-8'),
|
|
1801
|
+
hash,
|
|
1802
|
+
integrity,
|
|
1803
|
+
componentCount: components.length,
|
|
1804
|
+
components: components.map((comp) => `Framework/Structural/${comp.name}`)
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
buildDependencyContentsSync(jsContent, componentPath) {
|
|
1809
|
+
const dependencies = this.analyzeDependencies(jsContent, componentPath);
|
|
1810
|
+
const dependencyContents = {};
|
|
1811
|
+
|
|
1812
|
+
for (const dep of dependencies) {
|
|
1813
|
+
const depPath = dep.path;
|
|
1814
|
+
try {
|
|
1815
|
+
const depContent = fs.readFileSync(depPath, 'utf-8');
|
|
1816
|
+
const depName = path
|
|
1817
|
+
.relative(this.srcPath, depPath)
|
|
1818
|
+
.replace(/\\/g, '/');
|
|
1819
|
+
dependencyContents[depName] = {
|
|
1820
|
+
content: depContent,
|
|
1821
|
+
bindings: dep.bindings || []
|
|
1822
|
+
};
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
console.warn(`Warning: Could not read dependency ${depPath}:`, error.message);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
return dependencyContents;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
getConfiguredPublicFolders() {
|
|
1832
|
+
const publicFolders = Array.isArray(this.sliceConfig?.publicFolders)
|
|
1833
|
+
? this.sliceConfig.publicFolders
|
|
1834
|
+
: [];
|
|
1835
|
+
|
|
1836
|
+
return publicFolders
|
|
1837
|
+
.map((folder) => this.normalizePublicFolder(folder))
|
|
1838
|
+
.filter(Boolean);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
normalizePublicFolder(folder) {
|
|
1842
|
+
if (typeof folder !== 'string') return null;
|
|
1843
|
+
let normalized = folder.trim();
|
|
1844
|
+
if (!normalized) return null;
|
|
1845
|
+
|
|
1846
|
+
if (!normalized.startsWith('/')) {
|
|
1847
|
+
normalized = `/${normalized}`;
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
normalized = normalized.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
1851
|
+
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
1852
|
+
normalized = normalized.slice(0, -1);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
return normalized;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
normalizeImportPath(importPath) {
|
|
1859
|
+
if (typeof importPath !== 'string') return '';
|
|
1860
|
+
const cleanPath = importPath.split(/[?#]/)[0];
|
|
1861
|
+
return cleanPath.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
isRelativeImport(importPath) {
|
|
1865
|
+
return importPath.startsWith('./') || importPath.startsWith('../');
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
isAbsoluteImport(importPath) {
|
|
1869
|
+
return importPath.startsWith('/');
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
isImportInPublicFolders(importPath, publicFolders) {
|
|
1873
|
+
const normalizedImport = this.normalizeImportPath(importPath);
|
|
1874
|
+
return publicFolders.some((folder) => normalizedImport === folder || normalizedImport.startsWith(`${folder}/`));
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
classifyImport(importPath, publicFolders) {
|
|
1878
|
+
if (typeof importPath !== 'string' || !importPath) {
|
|
1879
|
+
return { keep: false, warning: 'Warning: Removing bare import: <unknown>' };
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (this.isRelativeImport(importPath)) {
|
|
1883
|
+
return { keep: false, warning: null };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
if (this.isAbsoluteImport(importPath)) {
|
|
1887
|
+
if (this.isImportInPublicFolders(importPath, publicFolders)) {
|
|
1888
|
+
return { keep: true, warning: null };
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
return {
|
|
1892
|
+
keep: false,
|
|
1893
|
+
warning: `Warning: Removing absolute import outside publicFolders: ${importPath}`
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
return {
|
|
1898
|
+
keep: false,
|
|
1899
|
+
warning: `Warning: Removing bare import: ${importPath}`
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
buildImportWarningMessage(baseMessage, sourceContext) {
|
|
1904
|
+
if (!sourceContext) return baseMessage;
|
|
1905
|
+
return `${baseMessage} [${sourceContext}]`;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
extractLocalBindingsFromImportStatement(statement) {
|
|
1909
|
+
const source = String(statement || '').trim();
|
|
1910
|
+
if (!source.startsWith('import ')) return [];
|
|
1911
|
+
if (/^import\s+['"][^'"]+['"]\s*;?$/.test(source)) return [];
|
|
1912
|
+
|
|
1913
|
+
const bindings = [];
|
|
1914
|
+
|
|
1915
|
+
const defaultMatch = source.match(/^import\s+([A-Za-z_$][\w$]*)\s*(,|\s+from\s+)/);
|
|
1916
|
+
if (defaultMatch && defaultMatch[1] !== '*') {
|
|
1917
|
+
bindings.push(defaultMatch[1]);
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const namespaceMatch = source.match(/,?\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]/);
|
|
1921
|
+
if (namespaceMatch) {
|
|
1922
|
+
bindings.push(namespaceMatch[1]);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const namedMatch = source.match(/\{([\s\S]*?)\}\s*from\s*['"]/);
|
|
1926
|
+
if (namedMatch) {
|
|
1927
|
+
const namedSection = namedMatch[1];
|
|
1928
|
+
for (const part of namedSection.split(',')) {
|
|
1929
|
+
const cleanPart = part.trim();
|
|
1930
|
+
if (!cleanPart) continue;
|
|
1931
|
+
const aliasParts = cleanPart.split(/\s+as\s+/i).map((v) => v.trim()).filter(Boolean);
|
|
1932
|
+
const localName = aliasParts.length > 1 ? aliasParts[1] : aliasParts[0];
|
|
1933
|
+
if (/^[A-Za-z_$][\w$]*$/.test(localName)) {
|
|
1934
|
+
bindings.push(localName);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
return Array.from(new Set(bindings));
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
validateHoistedImportCollisions(importStatements, reservedIdentifiers = new Set()) {
|
|
1943
|
+
const reserved = reservedIdentifiers instanceof Set
|
|
1944
|
+
? reservedIdentifiers
|
|
1945
|
+
: new Set(reservedIdentifiers || []);
|
|
1946
|
+
const bindingToStatement = new Map();
|
|
1947
|
+
|
|
1948
|
+
for (const statement of importStatements || []) {
|
|
1949
|
+
const normalizedStatement = String(statement || '').trim();
|
|
1950
|
+
if (!normalizedStatement) continue;
|
|
1951
|
+
const localBindings = this.extractLocalBindingsFromImportStatement(normalizedStatement);
|
|
1952
|
+
|
|
1953
|
+
for (const localBinding of localBindings) {
|
|
1954
|
+
if (reserved.has(localBinding)) {
|
|
1955
|
+
throw new Error(`Hoisted import reserved identifier collision: ${localBinding}`);
|
|
1956
|
+
}
|
|
1957
|
+
const previousStatement = bindingToStatement.get(localBinding);
|
|
1958
|
+
if (previousStatement && previousStatement !== normalizedStatement) {
|
|
1959
|
+
throw new Error(`Hoisted import binding collision: ${localBinding}`);
|
|
1960
|
+
}
|
|
1961
|
+
bindingToStatement.set(localBinding, normalizedStatement);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
parseImportsFromCode(code) {
|
|
1967
|
+
const ast = parse(code, {
|
|
1968
|
+
sourceType: 'module',
|
|
1969
|
+
plugins: ['jsx']
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
const importNodes = [];
|
|
1973
|
+
traverse.default(ast, {
|
|
1974
|
+
ImportDeclaration(pathNode) {
|
|
1975
|
+
importNodes.push(pathNode.node);
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
return importNodes
|
|
1980
|
+
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
|
|
1981
|
+
.sort((a, b) => a.start - b.start);
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
parseImportsWithFallbackScanner(code) {
|
|
1985
|
+
const entries = [];
|
|
1986
|
+
const importRegex = /\bimport\b/g;
|
|
1987
|
+
let match = null;
|
|
1988
|
+
|
|
1989
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
1990
|
+
const start = match.index;
|
|
1991
|
+
const nextChar = code[start + 'import'.length];
|
|
1992
|
+
if (nextChar === '(') {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
let index = start + 'import'.length;
|
|
1997
|
+
let quote = null;
|
|
1998
|
+
let escaped = false;
|
|
1999
|
+
|
|
2000
|
+
while (index < code.length) {
|
|
2001
|
+
const char = code[index];
|
|
2002
|
+
|
|
2003
|
+
if (quote) {
|
|
2004
|
+
if (escaped) {
|
|
2005
|
+
escaped = false;
|
|
2006
|
+
} else if (char === '\\') {
|
|
2007
|
+
escaped = true;
|
|
2008
|
+
} else if (char === quote) {
|
|
2009
|
+
quote = null;
|
|
2010
|
+
}
|
|
2011
|
+
index += 1;
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
if (char === '\'' || char === '"' || char === '`') {
|
|
2016
|
+
quote = char;
|
|
2017
|
+
index += 1;
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
if (char === ';') {
|
|
2022
|
+
index += 1;
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
index += 1;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
const end = index;
|
|
2030
|
+
const statement = code.slice(start, end);
|
|
2031
|
+
const fromMatch = statement.match(/\bfrom\s+['"]([^'"]+)['"]/);
|
|
2032
|
+
const sideEffectMatch = statement.match(/\bimport\s+['"]([^'"]+)['"]/);
|
|
2033
|
+
const importPath = fromMatch?.[1] || sideEffectMatch?.[1] || null;
|
|
2034
|
+
|
|
2035
|
+
if (!importPath) {
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
entries.push({ start, end, statement, importPath });
|
|
2040
|
+
importRegex.lastIndex = end;
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
return entries;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports) {
|
|
2047
|
+
const hoistedImports = [];
|
|
2048
|
+
const importEntries = this.parseImportsWithFallbackScanner(code);
|
|
2049
|
+
if (importEntries.length === 0) {
|
|
2050
|
+
return { code, hoistedImports };
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
let cleanedCode = '';
|
|
2054
|
+
let cursor = 0;
|
|
2055
|
+
for (const entry of importEntries) {
|
|
2056
|
+
const { start, end, statement, importPath } = entry;
|
|
2057
|
+
const classification = this.classifyImport(importPath, publicFolders);
|
|
2058
|
+
cleanedCode += code.slice(cursor, start);
|
|
2059
|
+
if (classification.keep) {
|
|
2060
|
+
if (collectHoistedImports) {
|
|
2061
|
+
hoistedImports.push(statement.trim());
|
|
2062
|
+
} else {
|
|
2063
|
+
cleanedCode += statement;
|
|
2064
|
+
}
|
|
2065
|
+
} else if (classification.warning) {
|
|
2066
|
+
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
2067
|
+
}
|
|
2068
|
+
cursor = end;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
cleanedCode += code.slice(cursor);
|
|
2072
|
+
|
|
2073
|
+
return { code: cleanedCode, hoistedImports };
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
stripImports(code, options = {}) {
|
|
2077
|
+
const { sourceContext = null, collectHoistedImports = false } = options;
|
|
2078
|
+
const publicFolders = this.getConfiguredPublicFolders();
|
|
2079
|
+
const hoistedImports = [];
|
|
2080
|
+
|
|
2081
|
+
try {
|
|
2082
|
+
const importNodes = this.parseImportsFromCode(code);
|
|
2083
|
+
|
|
2084
|
+
if (importNodes.length === 0) {
|
|
2085
|
+
return collectHoistedImports ? { code, hoistedImports } : code;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
let cleaned = '';
|
|
2089
|
+
let cursor = 0;
|
|
2090
|
+
|
|
2091
|
+
for (const node of importNodes) {
|
|
2092
|
+
const importPath = node.source?.value;
|
|
2093
|
+
const classification = this.classifyImport(importPath, publicFolders);
|
|
2094
|
+
const statement = code.slice(node.start, node.end);
|
|
2095
|
+
|
|
2096
|
+
cleaned += code.slice(cursor, node.start);
|
|
2097
|
+
if (classification.keep) {
|
|
2098
|
+
if (collectHoistedImports) {
|
|
2099
|
+
hoistedImports.push(statement.trim());
|
|
2100
|
+
} else {
|
|
2101
|
+
cleaned += statement;
|
|
2102
|
+
}
|
|
2103
|
+
} else if (classification.warning) {
|
|
2104
|
+
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
cursor = node.end;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
cleaned += code.slice(cursor);
|
|
2111
|
+
return collectHoistedImports
|
|
2112
|
+
? { code: cleaned, hoistedImports }
|
|
2113
|
+
: cleaned;
|
|
2114
|
+
} catch (error) {
|
|
2115
|
+
const fallback = this.stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports);
|
|
2116
|
+
return collectHoistedImports ? fallback : fallback.code;
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
async loadComponentsMap() {
|
|
2121
|
+
const componentsConfigPath = path.join(this.componentsPath, 'components.js');
|
|
2122
|
+
if (!await fs.pathExists(componentsConfigPath)) {
|
|
2123
|
+
return {};
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
const content = await fs.readFile(componentsConfigPath, 'utf-8');
|
|
2127
|
+
return this.parseComponentsConfig(content);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
parseComponentsConfig(content) {
|
|
2131
|
+
try {
|
|
2132
|
+
const ast = parse(content, {
|
|
2133
|
+
sourceType: 'module',
|
|
2134
|
+
plugins: ['jsx']
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
let componentsNode = null;
|
|
2138
|
+
|
|
2139
|
+
traverse.default(ast, {
|
|
2140
|
+
VariableDeclarator(path) {
|
|
2141
|
+
if (path.node.id?.type === 'Identifier' && path.node.id.name === 'components') {
|
|
2142
|
+
componentsNode = path.node.init;
|
|
2143
|
+
path.stop();
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
if (!componentsNode || componentsNode.type !== 'ObjectExpression') {
|
|
2149
|
+
throw new Error('components object not found');
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
const config = {};
|
|
2153
|
+
for (const prop of componentsNode.properties) {
|
|
2154
|
+
if (prop.type !== 'ObjectProperty') continue;
|
|
2155
|
+
|
|
2156
|
+
const key = this.extractStringValue(prop.key);
|
|
2157
|
+
const value = this.extractStringValue(prop.value);
|
|
2158
|
+
|
|
2159
|
+
if (!key || !value) {
|
|
2160
|
+
throw new Error('Invalid components entry');
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
config[key] = value;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
return config;
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
console.warn(`Could not parse components.js: ${error.message}`);
|
|
2169
|
+
return {};
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
extractStringValue(node) {
|
|
2174
|
+
if (!node) return null;
|
|
2175
|
+
|
|
2176
|
+
if (node.type === 'StringLiteral') {
|
|
2177
|
+
return node.value;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
if (node.type === 'Identifier') {
|
|
2181
|
+
return node.name;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
2185
|
+
return node.quasis.map((q) => q.value.cooked).join('');
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
/**
|
|
2192
|
+
* Converts a route to filename
|
|
2193
|
+
*/
|
|
2194
|
+
routeToFileName(routePath) {
|
|
2195
|
+
if (routePath === '/') return 'home';
|
|
2196
|
+
return routePath
|
|
2197
|
+
.replace(/^\//, '')
|
|
2198
|
+
.replace(/\//g, '-')
|
|
2199
|
+
.replace(/[^a-zA-Z0-9-]/g, '')
|
|
2200
|
+
.toLowerCase();
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
/**
|
|
2204
|
+
* Saves the configuration to file
|
|
2205
|
+
*/
|
|
2206
|
+
async saveBundleConfig(config) {
|
|
2207
|
+
// Ensure bundles directory exists
|
|
2208
|
+
await fs.ensureDir(this.bundlesPath);
|
|
2209
|
+
|
|
2210
|
+
// Save JSON config
|
|
2211
|
+
const configPath = path.join(this.bundlesPath, 'bundle.config.json');
|
|
2212
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
2213
|
+
|
|
2214
|
+
// Generate JavaScript module for direct import
|
|
2215
|
+
const jsConfigPath = path.join(this.bundlesPath, 'bundle.config.js');
|
|
2216
|
+
const jsConfig = this.generateBundleConfigJS(config);
|
|
2217
|
+
await fs.writeFile(jsConfigPath, jsConfig, 'utf-8');
|
|
2218
|
+
|
|
2219
|
+
console.log(`✓ Configuration saved to ${configPath}`);
|
|
2220
|
+
console.log(`✓ JavaScript config generated: ${jsConfigPath}`);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
/**
|
|
2224
|
+
* Creates a default bundle config file if none exists
|
|
2225
|
+
*/
|
|
2226
|
+
async createDefaultBundleConfig() {
|
|
2227
|
+
const defaultConfigPath = path.join(this.srcPath, 'bundles', 'bundle.config.js');
|
|
2228
|
+
|
|
2229
|
+
// Only create if it doesn't exist
|
|
2230
|
+
if (await fs.pathExists(defaultConfigPath)) {
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
await fs.ensureDir(path.dirname(defaultConfigPath));
|
|
2235
|
+
|
|
2236
|
+
const defaultConfig = `/**
|
|
2237
|
+
* Slice.js Bundle Configuration
|
|
2238
|
+
* Default empty configuration - no bundles available
|
|
2239
|
+
* Run 'slice build' to generate optimized bundles
|
|
2240
|
+
*/
|
|
2241
|
+
|
|
2242
|
+
// No bundles available - using individual component loading
|
|
2243
|
+
export const SLICE_BUNDLE_CONFIG = null;
|
|
2244
|
+
|
|
2245
|
+
// No auto-initialization needed for default config
|
|
2246
|
+
`;
|
|
2247
|
+
|
|
2248
|
+
await fs.writeFile(defaultConfigPath, defaultConfig, 'utf-8');
|
|
2249
|
+
console.log(`✓ Default bundle config created: ${defaultConfigPath}`);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
/**
|
|
2253
|
+
* Generates JavaScript module for direct import
|
|
2254
|
+
*/
|
|
2255
|
+
generateBundleConfigJS(config) {
|
|
2256
|
+
return `/**
|
|
2257
|
+
* Slice.js Bundle Configuration
|
|
2258
|
+
* Generated: ${new Date().toISOString()}
|
|
2259
|
+
* Strategy: ${config.strategy}
|
|
2260
|
+
*/
|
|
2261
|
+
|
|
2262
|
+
// Direct bundle configuration (no fetch required)
|
|
2263
|
+
export const SLICE_BUNDLE_CONFIG = ${JSON.stringify(config, null, 2)};
|
|
2264
|
+
|
|
2265
|
+
// Auto-initialization if slice is available
|
|
2266
|
+
if (typeof window !== 'undefined' && window.slice && window.slice.controller) {
|
|
2267
|
+
window.slice.controller.bundleConfig = SLICE_BUNDLE_CONFIG;
|
|
2268
|
+
|
|
2269
|
+
// Load critical bundle automatically
|
|
2270
|
+
if (SLICE_BUNDLE_CONFIG.bundles.critical && !window.slice.controller.criticalBundleLoaded) {
|
|
2271
|
+
(async () => {
|
|
2272
|
+
const bundlePath = "/bundles/" + SLICE_BUNDLE_CONFIG.bundles.critical.file;
|
|
2273
|
+
const integrity = SLICE_BUNDLE_CONFIG.bundles.critical.integrity;
|
|
2274
|
+
|
|
2275
|
+
if (typeof window.slice.controller.verifyBundleIntegrity === 'function') {
|
|
2276
|
+
const ok = await window.slice.controller.verifyBundleIntegrity(bundlePath, integrity);
|
|
2277
|
+
if (!ok) {
|
|
2278
|
+
console.warn('Failed to load critical bundle: integrity check failed');
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
import('./slice-bundle.critical.js').catch(err =>
|
|
2284
|
+
console.warn('Failed to load critical bundle:', err)
|
|
2285
|
+
);
|
|
2286
|
+
window.slice.controller.criticalBundleLoaded = true;
|
|
2287
|
+
})();
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
`;
|
|
2291
|
+
}
|
|
2292
|
+
}
|