slicejs-cli 3.0.2 → 3.0.3
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.
|
@@ -953,11 +953,14 @@ export default class BundleGenerator {
|
|
|
953
953
|
cssContent = await fs.readFile(cssPath, 'utf-8');
|
|
954
954
|
}
|
|
955
955
|
|
|
956
|
+
const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
|
|
957
|
+
|
|
956
958
|
bundleComponents.push({
|
|
957
959
|
name: comp.name,
|
|
958
960
|
category: comp.category,
|
|
959
961
|
categoryType: comp.categoryType,
|
|
960
|
-
js:
|
|
962
|
+
js: cleanedJavaScript.code,
|
|
963
|
+
hoistedImports: cleanedJavaScript.hoistedImports,
|
|
961
964
|
html: htmlContent,
|
|
962
965
|
css: cssContent,
|
|
963
966
|
externalDependencies: await this.buildDependencyContents(jsContent, comp.path),
|
|
@@ -988,7 +991,22 @@ export default class BundleGenerator {
|
|
|
988
991
|
? 'framework'
|
|
989
992
|
: this.routeToFileName(routePath || fileName.replace('slice-bundle.', '').replace('.js', ''));
|
|
990
993
|
|
|
994
|
+
const dependencyModules = this.collectDependencyModulesFromComponents(uniqueComponents);
|
|
991
995
|
const dependencyModuleBlock = this.buildV2DependencyModuleBlock(uniqueComponents);
|
|
996
|
+
const rawHoistedImports = uniqueComponents
|
|
997
|
+
.flatMap((component) => component.hoistedImports || [])
|
|
998
|
+
.map((statement) => String(statement).trim())
|
|
999
|
+
.filter(Boolean);
|
|
1000
|
+
const reservedIdentifiers = new Set([
|
|
1001
|
+
'SLICE_BUNDLE_META',
|
|
1002
|
+
'SLICE_BUNDLE_DEPENDENCIES',
|
|
1003
|
+
...uniqueComponents.map((component) => this.classFactoryName(component.name)),
|
|
1004
|
+
...uniqueComponents.map((component) => `__templateElement_${this.toSafeIdentifier(component.name)}`),
|
|
1005
|
+
...this.getDependencyExportVariableNames(dependencyModules)
|
|
1006
|
+
]);
|
|
1007
|
+
this.validateHoistedImportCollisions(rawHoistedImports, reservedIdentifiers);
|
|
1008
|
+
const hoistedImports = Array.from(new Set(rawHoistedImports));
|
|
1009
|
+
const hoistedImportBlock = hoistedImports.join('\n');
|
|
992
1010
|
|
|
993
1011
|
const classFactoryDefinitions = uniqueComponents
|
|
994
1012
|
.map((component) => {
|
|
@@ -1052,23 +1070,14 @@ export default class BundleGenerator {
|
|
|
1052
1070
|
componentCount: uniqueComponents.length
|
|
1053
1071
|
};
|
|
1054
1072
|
|
|
1055
|
-
return
|
|
1073
|
+
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`;
|
|
1056
1074
|
}
|
|
1057
1075
|
|
|
1058
1076
|
buildV2DependencyModuleBlock(components) {
|
|
1059
|
-
const modules =
|
|
1060
|
-
for (const component of components || []) {
|
|
1061
|
-
const externalDependencies = component.externalDependencies || {};
|
|
1062
|
-
for (const [moduleName, entry] of Object.entries(externalDependencies)) {
|
|
1063
|
-
if (modules.has(moduleName)) continue;
|
|
1064
|
-
const content = typeof entry === 'string' ? entry : entry?.content;
|
|
1065
|
-
if (!content) continue;
|
|
1066
|
-
modules.set(moduleName, { name: moduleName, content });
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1077
|
+
const modules = this.collectDependencyModulesFromComponents(components);
|
|
1069
1078
|
|
|
1070
1079
|
const lines = ['const SLICE_BUNDLE_DEPENDENCIES = {};'];
|
|
1071
|
-
|
|
1080
|
+
modules.forEach((module, index) => {
|
|
1072
1081
|
const exportVar = `__sliceDepExports${index}`;
|
|
1073
1082
|
const transformedContent = this.transformDependencyContent(module.content, exportVar, module.name);
|
|
1074
1083
|
lines.push(`const ${exportVar} = {};`);
|
|
@@ -1105,12 +1114,17 @@ export default class BundleGenerator {
|
|
|
1105
1114
|
/**
|
|
1106
1115
|
* Cleans JavaScript code by removing imports/exports and ensuring class is available globally
|
|
1107
1116
|
*/
|
|
1108
|
-
cleanJavaScript(code, componentName) {
|
|
1117
|
+
cleanJavaScript(code, componentName, sourceContext = componentName) {
|
|
1109
1118
|
// Remove export default
|
|
1110
1119
|
code = code.replace(/export\s+default\s+/g, '');
|
|
1111
1120
|
|
|
1112
|
-
// Remove imports (
|
|
1113
|
-
|
|
1121
|
+
// Remove only unsupported imports (relative always removed, allowed absolute kept)
|
|
1122
|
+
const stripped = this.stripImports(code, {
|
|
1123
|
+
sourceContext,
|
|
1124
|
+
collectHoistedImports: true
|
|
1125
|
+
});
|
|
1126
|
+
const hoistedImports = stripped.hoistedImports || [];
|
|
1127
|
+
code = stripped.code;
|
|
1114
1128
|
|
|
1115
1129
|
// Guard customElements.define to avoid duplicate registrations
|
|
1116
1130
|
code = code.replace(
|
|
@@ -1144,7 +1158,10 @@ export default class BundleGenerator {
|
|
|
1144
1158
|
// Add return statement for bundle evaluation compatibility
|
|
1145
1159
|
code += `\nreturn ${componentName};`;
|
|
1146
1160
|
|
|
1147
|
-
return
|
|
1161
|
+
return {
|
|
1162
|
+
code,
|
|
1163
|
+
hoistedImports
|
|
1164
|
+
};
|
|
1148
1165
|
}
|
|
1149
1166
|
|
|
1150
1167
|
/**
|
|
@@ -1173,10 +1190,28 @@ export default class BundleGenerator {
|
|
|
1173
1190
|
.update(JSON.stringify(integrityPayload))
|
|
1174
1191
|
.digest('hex')}`;
|
|
1175
1192
|
|
|
1193
|
+
const dependencyModules = this.collectDependencyModules(componentsData);
|
|
1194
|
+
const frameworkComponentKeys = Object.keys(componentsData || {});
|
|
1195
|
+
const frameworkClassIdentifiers = frameworkComponentKeys.map((key) => this.toSafeIdentifier(key));
|
|
1196
|
+
const frameworkReservedIdentifiers = new Set([
|
|
1197
|
+
'SLICE_BUNDLE',
|
|
1198
|
+
'SLICE_BUNDLE_COMPONENTS',
|
|
1199
|
+
'SLICE_BUNDLE_DEPENDENCIES',
|
|
1200
|
+
'SLICE_FRAMEWORK_CLASSES',
|
|
1201
|
+
...frameworkClassIdentifiers,
|
|
1202
|
+
...this.getDependencyExportVariableNames(dependencyModules)
|
|
1203
|
+
]);
|
|
1204
|
+
const rawHoistedImports = Object.values(componentsData || {})
|
|
1205
|
+
.flatMap((component) => component?.hoistedImports || [])
|
|
1206
|
+
.map((statement) => String(statement).trim())
|
|
1207
|
+
.filter(Boolean);
|
|
1208
|
+
this.validateHoistedImportCollisions(rawHoistedImports, frameworkReservedIdentifiers);
|
|
1209
|
+
const hoistedImportBlock = Array.from(new Set(rawHoistedImports)).join('\n');
|
|
1210
|
+
|
|
1176
1211
|
const dependencyBlock = this.buildDependencyModuleBlock(componentsData);
|
|
1177
1212
|
const componentBlock = this.buildComponentBundleBlock(componentsData);
|
|
1178
1213
|
|
|
1179
|
-
return
|
|
1214
|
+
return `${hoistedImportBlock}${hoistedImportBlock ? '\n\n' : ''}/**
|
|
1180
1215
|
* Slice.js Bundle
|
|
1181
1216
|
* Type: ${metadata.type}
|
|
1182
1217
|
* Generated: ${metadata.generated}
|
|
@@ -1231,6 +1266,24 @@ if (window.slice && window.slice.controller) {
|
|
|
1231
1266
|
return Array.from(modules.values());
|
|
1232
1267
|
}
|
|
1233
1268
|
|
|
1269
|
+
collectDependencyModulesFromComponents(components = []) {
|
|
1270
|
+
const modules = new Map();
|
|
1271
|
+
for (const component of components || []) {
|
|
1272
|
+
const externalDependencies = component.externalDependencies || {};
|
|
1273
|
+
for (const [moduleName, entry] of Object.entries(externalDependencies)) {
|
|
1274
|
+
if (modules.has(moduleName)) continue;
|
|
1275
|
+
const content = typeof entry === 'string' ? entry : entry?.content;
|
|
1276
|
+
if (!content) continue;
|
|
1277
|
+
modules.set(moduleName, { name: moduleName, content });
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return Array.from(modules.values());
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
getDependencyExportVariableNames(dependencyModules = []) {
|
|
1284
|
+
return (dependencyModules || []).map((_, index) => `__sliceDepExports${index}`);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1234
1287
|
transformDependencyContent(content, exportVar, moduleName) {
|
|
1235
1288
|
const baseName = moduleName.split('/').pop().replace(/\.[^.]+$/, '');
|
|
1236
1289
|
const dataName = baseName ? `${baseName}Data` : null;
|
|
@@ -1450,12 +1503,15 @@ if (window.slice && window.slice.controller) {
|
|
|
1450
1503
|
const jsPath = path.join(comp.path, `${fileBaseName}.js`);
|
|
1451
1504
|
const jsContent = fs.readFileSync(jsPath, 'utf-8');
|
|
1452
1505
|
const dependencyContents = this.buildDependencyContentsSync(jsContent, comp.path);
|
|
1506
|
+
const cleanedJavaScript = this.cleanJavaScript(jsContent, comp.name, jsPath);
|
|
1507
|
+
|
|
1453
1508
|
componentsData[componentKey] = {
|
|
1454
1509
|
name: comp.name,
|
|
1455
1510
|
category: comp.category,
|
|
1456
1511
|
categoryType: comp.categoryType,
|
|
1457
1512
|
isFramework: true,
|
|
1458
|
-
js:
|
|
1513
|
+
js: cleanedJavaScript.code,
|
|
1514
|
+
hoistedImports: cleanedJavaScript.hoistedImports,
|
|
1459
1515
|
externalDependencies: dependencyContents,
|
|
1460
1516
|
componentDependencies: Array.from(comp.dependencies),
|
|
1461
1517
|
html: fs.existsSync(path.join(comp.path, `${fileBaseName}.html`))
|
|
@@ -1511,8 +1567,293 @@ if (window.slice && window.slice.controller) {
|
|
|
1511
1567
|
return dependencyContents;
|
|
1512
1568
|
}
|
|
1513
1569
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1570
|
+
getConfiguredPublicFolders() {
|
|
1571
|
+
const publicFolders = Array.isArray(this.sliceConfig?.publicFolders)
|
|
1572
|
+
? this.sliceConfig.publicFolders
|
|
1573
|
+
: [];
|
|
1574
|
+
|
|
1575
|
+
return publicFolders
|
|
1576
|
+
.map((folder) => this.normalizePublicFolder(folder))
|
|
1577
|
+
.filter(Boolean);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
normalizePublicFolder(folder) {
|
|
1581
|
+
if (typeof folder !== 'string') return null;
|
|
1582
|
+
let normalized = folder.trim();
|
|
1583
|
+
if (!normalized) return null;
|
|
1584
|
+
|
|
1585
|
+
if (!normalized.startsWith('/')) {
|
|
1586
|
+
normalized = `/${normalized}`;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
normalized = normalized.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
1590
|
+
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
1591
|
+
normalized = normalized.slice(0, -1);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
return normalized;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
normalizeImportPath(importPath) {
|
|
1598
|
+
if (typeof importPath !== 'string') return '';
|
|
1599
|
+
const cleanPath = importPath.split(/[?#]/)[0];
|
|
1600
|
+
return cleanPath.replace(/\\+/g, '/').replace(/\/+/g, '/');
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
isRelativeImport(importPath) {
|
|
1604
|
+
return importPath.startsWith('./') || importPath.startsWith('../');
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
isAbsoluteImport(importPath) {
|
|
1608
|
+
return importPath.startsWith('/');
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
isImportInPublicFolders(importPath, publicFolders) {
|
|
1612
|
+
const normalizedImport = this.normalizeImportPath(importPath);
|
|
1613
|
+
return publicFolders.some((folder) => normalizedImport === folder || normalizedImport.startsWith(`${folder}/`));
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
classifyImport(importPath, publicFolders) {
|
|
1617
|
+
if (typeof importPath !== 'string' || !importPath) {
|
|
1618
|
+
return { keep: false, warning: 'Warning: Removing bare import: <unknown>' };
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (this.isRelativeImport(importPath)) {
|
|
1622
|
+
return { keep: false, warning: null };
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (this.isAbsoluteImport(importPath)) {
|
|
1626
|
+
if (this.isImportInPublicFolders(importPath, publicFolders)) {
|
|
1627
|
+
return { keep: true, warning: null };
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
return {
|
|
1631
|
+
keep: false,
|
|
1632
|
+
warning: `Warning: Removing absolute import outside publicFolders: ${importPath}`
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
return {
|
|
1637
|
+
keep: false,
|
|
1638
|
+
warning: `Warning: Removing bare import: ${importPath}`
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
buildImportWarningMessage(baseMessage, sourceContext) {
|
|
1643
|
+
if (!sourceContext) return baseMessage;
|
|
1644
|
+
return `${baseMessage} [${sourceContext}]`;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
extractLocalBindingsFromImportStatement(statement) {
|
|
1648
|
+
const source = String(statement || '').trim();
|
|
1649
|
+
if (!source.startsWith('import ')) return [];
|
|
1650
|
+
if (/^import\s+['"][^'"]+['"]\s*;?$/.test(source)) return [];
|
|
1651
|
+
|
|
1652
|
+
const bindings = [];
|
|
1653
|
+
|
|
1654
|
+
const defaultMatch = source.match(/^import\s+([A-Za-z_$][\w$]*)\s*(,|\s+from\s+)/);
|
|
1655
|
+
if (defaultMatch && defaultMatch[1] !== '*') {
|
|
1656
|
+
bindings.push(defaultMatch[1]);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const namespaceMatch = source.match(/,?\s*\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"]/);
|
|
1660
|
+
if (namespaceMatch) {
|
|
1661
|
+
bindings.push(namespaceMatch[1]);
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const namedMatch = source.match(/\{([\s\S]*?)\}\s*from\s*['"]/);
|
|
1665
|
+
if (namedMatch) {
|
|
1666
|
+
const namedSection = namedMatch[1];
|
|
1667
|
+
for (const part of namedSection.split(',')) {
|
|
1668
|
+
const cleanPart = part.trim();
|
|
1669
|
+
if (!cleanPart) continue;
|
|
1670
|
+
const aliasParts = cleanPart.split(/\s+as\s+/i).map((v) => v.trim()).filter(Boolean);
|
|
1671
|
+
const localName = aliasParts.length > 1 ? aliasParts[1] : aliasParts[0];
|
|
1672
|
+
if (/^[A-Za-z_$][\w$]*$/.test(localName)) {
|
|
1673
|
+
bindings.push(localName);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
return Array.from(new Set(bindings));
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
validateHoistedImportCollisions(importStatements, reservedIdentifiers = new Set()) {
|
|
1682
|
+
const reserved = reservedIdentifiers instanceof Set
|
|
1683
|
+
? reservedIdentifiers
|
|
1684
|
+
: new Set(reservedIdentifiers || []);
|
|
1685
|
+
const bindingToStatement = new Map();
|
|
1686
|
+
|
|
1687
|
+
for (const statement of importStatements || []) {
|
|
1688
|
+
const normalizedStatement = String(statement || '').trim();
|
|
1689
|
+
if (!normalizedStatement) continue;
|
|
1690
|
+
const localBindings = this.extractLocalBindingsFromImportStatement(normalizedStatement);
|
|
1691
|
+
|
|
1692
|
+
for (const localBinding of localBindings) {
|
|
1693
|
+
if (reserved.has(localBinding)) {
|
|
1694
|
+
throw new Error(`Hoisted import reserved identifier collision: ${localBinding}`);
|
|
1695
|
+
}
|
|
1696
|
+
const previousStatement = bindingToStatement.get(localBinding);
|
|
1697
|
+
if (previousStatement && previousStatement !== normalizedStatement) {
|
|
1698
|
+
throw new Error(`Hoisted import binding collision: ${localBinding}`);
|
|
1699
|
+
}
|
|
1700
|
+
bindingToStatement.set(localBinding, normalizedStatement);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
parseImportsFromCode(code) {
|
|
1706
|
+
const ast = parse(code, {
|
|
1707
|
+
sourceType: 'module',
|
|
1708
|
+
plugins: ['jsx']
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1711
|
+
const importNodes = [];
|
|
1712
|
+
traverse.default(ast, {
|
|
1713
|
+
ImportDeclaration(pathNode) {
|
|
1714
|
+
importNodes.push(pathNode.node);
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
return importNodes
|
|
1719
|
+
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
|
|
1720
|
+
.sort((a, b) => a.start - b.start);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
parseImportsWithFallbackScanner(code) {
|
|
1724
|
+
const entries = [];
|
|
1725
|
+
const importRegex = /\bimport\b/g;
|
|
1726
|
+
let match = null;
|
|
1727
|
+
|
|
1728
|
+
while ((match = importRegex.exec(code)) !== null) {
|
|
1729
|
+
const start = match.index;
|
|
1730
|
+
const nextChar = code[start + 'import'.length];
|
|
1731
|
+
if (nextChar === '(') {
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
let index = start + 'import'.length;
|
|
1736
|
+
let quote = null;
|
|
1737
|
+
let escaped = false;
|
|
1738
|
+
|
|
1739
|
+
while (index < code.length) {
|
|
1740
|
+
const char = code[index];
|
|
1741
|
+
|
|
1742
|
+
if (quote) {
|
|
1743
|
+
if (escaped) {
|
|
1744
|
+
escaped = false;
|
|
1745
|
+
} else if (char === '\\') {
|
|
1746
|
+
escaped = true;
|
|
1747
|
+
} else if (char === quote) {
|
|
1748
|
+
quote = null;
|
|
1749
|
+
}
|
|
1750
|
+
index += 1;
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
if (char === '\'' || char === '"' || char === '`') {
|
|
1755
|
+
quote = char;
|
|
1756
|
+
index += 1;
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
if (char === ';') {
|
|
1761
|
+
index += 1;
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
index += 1;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
const end = index;
|
|
1769
|
+
const statement = code.slice(start, end);
|
|
1770
|
+
const fromMatch = statement.match(/\bfrom\s+['"]([^'"]+)['"]/);
|
|
1771
|
+
const sideEffectMatch = statement.match(/\bimport\s+['"]([^'"]+)['"]/);
|
|
1772
|
+
const importPath = fromMatch?.[1] || sideEffectMatch?.[1] || null;
|
|
1773
|
+
|
|
1774
|
+
if (!importPath) {
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
entries.push({ start, end, statement, importPath });
|
|
1779
|
+
importRegex.lastIndex = end;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
return entries;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports) {
|
|
1786
|
+
const hoistedImports = [];
|
|
1787
|
+
const importEntries = this.parseImportsWithFallbackScanner(code);
|
|
1788
|
+
if (importEntries.length === 0) {
|
|
1789
|
+
return { code, hoistedImports };
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
let cleanedCode = '';
|
|
1793
|
+
let cursor = 0;
|
|
1794
|
+
for (const entry of importEntries) {
|
|
1795
|
+
const { start, end, statement, importPath } = entry;
|
|
1796
|
+
const classification = this.classifyImport(importPath, publicFolders);
|
|
1797
|
+
cleanedCode += code.slice(cursor, start);
|
|
1798
|
+
if (classification.keep) {
|
|
1799
|
+
if (collectHoistedImports) {
|
|
1800
|
+
hoistedImports.push(statement.trim());
|
|
1801
|
+
} else {
|
|
1802
|
+
cleanedCode += statement;
|
|
1803
|
+
}
|
|
1804
|
+
} else if (classification.warning) {
|
|
1805
|
+
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
1806
|
+
}
|
|
1807
|
+
cursor = end;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
cleanedCode += code.slice(cursor);
|
|
1811
|
+
|
|
1812
|
+
return { code: cleanedCode, hoistedImports };
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
stripImports(code, options = {}) {
|
|
1816
|
+
const { sourceContext = null, collectHoistedImports = false } = options;
|
|
1817
|
+
const publicFolders = this.getConfiguredPublicFolders();
|
|
1818
|
+
const hoistedImports = [];
|
|
1819
|
+
|
|
1820
|
+
try {
|
|
1821
|
+
const importNodes = this.parseImportsFromCode(code);
|
|
1822
|
+
|
|
1823
|
+
if (importNodes.length === 0) {
|
|
1824
|
+
return collectHoistedImports ? { code, hoistedImports } : code;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
let cleaned = '';
|
|
1828
|
+
let cursor = 0;
|
|
1829
|
+
|
|
1830
|
+
for (const node of importNodes) {
|
|
1831
|
+
const importPath = node.source?.value;
|
|
1832
|
+
const classification = this.classifyImport(importPath, publicFolders);
|
|
1833
|
+
const statement = code.slice(node.start, node.end);
|
|
1834
|
+
|
|
1835
|
+
cleaned += code.slice(cursor, node.start);
|
|
1836
|
+
if (classification.keep) {
|
|
1837
|
+
if (collectHoistedImports) {
|
|
1838
|
+
hoistedImports.push(statement.trim());
|
|
1839
|
+
} else {
|
|
1840
|
+
cleaned += statement;
|
|
1841
|
+
}
|
|
1842
|
+
} else if (classification.warning) {
|
|
1843
|
+
console.warn(this.buildImportWarningMessage(classification.warning, sourceContext));
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
cursor = node.end;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
cleaned += code.slice(cursor);
|
|
1850
|
+
return collectHoistedImports
|
|
1851
|
+
? { code: cleaned, hoistedImports }
|
|
1852
|
+
: cleaned;
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
const fallback = this.stripImportsWithFallbackRegex(code, publicFolders, sourceContext, collectHoistedImports);
|
|
1855
|
+
return collectHoistedImports ? fallback : fallback.code;
|
|
1856
|
+
}
|
|
1516
1857
|
}
|
|
1517
1858
|
|
|
1518
1859
|
async loadComponentsMap() {
|
package/package.json
CHANGED
|
@@ -204,3 +204,185 @@ test('rebalance merge preserves and merges route path metadata deterministically
|
|
|
204
204
|
assert.equal(Object.keys(bundles).length, 2);
|
|
205
205
|
assert.deepEqual(bundles.beta.paths, ['/beta', '/beta-alt', '/gamma']);
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
test('stripImports preserves absolute imports from configured publicFolders', () => {
|
|
209
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
210
|
+
components: [],
|
|
211
|
+
routes: [],
|
|
212
|
+
metrics: {},
|
|
213
|
+
sliceConfig: {
|
|
214
|
+
publicFolders: ['/public', '/assets']
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const source = "import logo from '/public/logo.js';\nimport hero from '/assets/hero.js';\nclass Demo {}\n";
|
|
219
|
+
const cleaned = generator.stripImports(source);
|
|
220
|
+
|
|
221
|
+
assert.match(cleaned, /import\s+logo\s+from\s+'\/public\/logo\.js';/);
|
|
222
|
+
assert.match(cleaned, /import\s+hero\s+from\s+'\/assets\/hero\.js';/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('stripImports removes relative imports', () => {
|
|
226
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
227
|
+
components: [],
|
|
228
|
+
routes: [],
|
|
229
|
+
metrics: {},
|
|
230
|
+
sliceConfig: {
|
|
231
|
+
publicFolders: ['/public']
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const source = "import localDep from './local.js';\nimport parentDep from '../parent.js';\nclass Demo {}\n";
|
|
236
|
+
const cleaned = generator.stripImports(source);
|
|
237
|
+
|
|
238
|
+
assert.doesNotMatch(cleaned, /\.\/local\.js/);
|
|
239
|
+
assert.doesNotMatch(cleaned, /\.\.\/parent\.js/);
|
|
240
|
+
assert.match(cleaned, /class Demo \{\}/);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('stripImports warns on bare imports', () => {
|
|
244
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
245
|
+
components: [],
|
|
246
|
+
routes: [],
|
|
247
|
+
metrics: {},
|
|
248
|
+
sliceConfig: {
|
|
249
|
+
publicFolders: ['/public']
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const warnings = [];
|
|
254
|
+
const originalWarn = console.warn;
|
|
255
|
+
console.warn = (...args) => warnings.push(args.join(' '));
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const source = "import { html } from 'lit';\nclass Demo {}\n";
|
|
259
|
+
const cleaned = generator.stripImports(source);
|
|
260
|
+
|
|
261
|
+
assert.doesNotMatch(cleaned, /from\s+'lit'/);
|
|
262
|
+
assert.ok(warnings.some((msg) => msg.includes('bare import') && msg.includes('lit')));
|
|
263
|
+
} finally {
|
|
264
|
+
console.warn = originalWarn;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('stripImports warns on absolute imports outside publicFolders', () => {
|
|
269
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
270
|
+
components: [],
|
|
271
|
+
routes: [],
|
|
272
|
+
metrics: {},
|
|
273
|
+
sliceConfig: {
|
|
274
|
+
publicFolders: ['/public']
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const warnings = [];
|
|
279
|
+
const originalWarn = console.warn;
|
|
280
|
+
console.warn = (...args) => warnings.push(args.join(' '));
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const source = "import secret from '/private/secret.js';\nclass Demo {}\n";
|
|
284
|
+
const cleaned = generator.stripImports(source);
|
|
285
|
+
|
|
286
|
+
assert.doesNotMatch(cleaned, /\/private\/secret\.js/);
|
|
287
|
+
assert.ok(warnings.some((msg) => msg.includes('outside publicFolders') && msg.includes('/private/secret.js')));
|
|
288
|
+
} finally {
|
|
289
|
+
console.warn = originalWarn;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('stripImports supports side-effect and multiline imports in fallback mode', () => {
|
|
294
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
295
|
+
components: [],
|
|
296
|
+
routes: [],
|
|
297
|
+
metrics: {},
|
|
298
|
+
sliceConfig: {
|
|
299
|
+
publicFolders: ['/public']
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const warnings = [];
|
|
304
|
+
const originalWarn = console.warn;
|
|
305
|
+
console.warn = (...args) => warnings.push(args.join(' '));
|
|
306
|
+
|
|
307
|
+
const originalParse = generator.parseImportsFromCode;
|
|
308
|
+
generator.parseImportsFromCode = () => {
|
|
309
|
+
throw new Error('forced parser failure');
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const source = [
|
|
314
|
+
"import '/public/effects.js';",
|
|
315
|
+
"import '/private/effects.js';",
|
|
316
|
+
"import {",
|
|
317
|
+
' html,',
|
|
318
|
+
' css',
|
|
319
|
+
"} from 'lit';",
|
|
320
|
+
'class Demo {}'
|
|
321
|
+
].join('\n');
|
|
322
|
+
const cleaned = generator.stripImports(source, { sourceContext: 'DemoComponent' });
|
|
323
|
+
|
|
324
|
+
assert.match(cleaned, /import '\/public\/effects\.js';/);
|
|
325
|
+
assert.doesNotMatch(cleaned, /\/private\/effects\.js/);
|
|
326
|
+
assert.doesNotMatch(cleaned, /from 'lit'/);
|
|
327
|
+
assert.ok(warnings.some((msg) => msg.includes('outside publicFolders') && msg.includes('/private/effects.js') && msg.includes('[DemoComponent]')));
|
|
328
|
+
assert.ok(warnings.some((msg) => msg.includes('bare import') && msg.includes('lit') && msg.includes('[DemoComponent]')));
|
|
329
|
+
} finally {
|
|
330
|
+
generator.parseImportsFromCode = originalParse;
|
|
331
|
+
console.warn = originalWarn;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('cleanJavaScript hoists allowed absolute imports and removes them from component code', () => {
|
|
336
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
337
|
+
components: [],
|
|
338
|
+
routes: [],
|
|
339
|
+
metrics: {},
|
|
340
|
+
sliceConfig: {
|
|
341
|
+
publicFolders: ['/public']
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const source = [
|
|
346
|
+
"import hero from '/public/hero.js';",
|
|
347
|
+
'class Demo extends HTMLElement {}',
|
|
348
|
+
'customElements.define("x-demo", Demo);'
|
|
349
|
+
].join('\n');
|
|
350
|
+
|
|
351
|
+
const result = generator.cleanJavaScript(source, 'Demo', 'DemoPath.js');
|
|
352
|
+
|
|
353
|
+
assert.doesNotMatch(result.code, /import hero from '\/public\/hero\.js';/);
|
|
354
|
+
assert.ok(result.hoistedImports.includes("import hero from '/public/hero.js';"));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('formatBundleFile emits hoisted imports for framework-compatible output', () => {
|
|
358
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
359
|
+
components: [],
|
|
360
|
+
routes: [],
|
|
361
|
+
metrics: {}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const source = generator.formatBundleFile({
|
|
365
|
+
'Framework/Structural/Bootstrap': {
|
|
366
|
+
name: 'Bootstrap',
|
|
367
|
+
category: 'Framework',
|
|
368
|
+
categoryType: 'Structural',
|
|
369
|
+
componentDependencies: [],
|
|
370
|
+
externalDependencies: {},
|
|
371
|
+
hoistedImports: ["import boot from '/public/bootstrap.js';"],
|
|
372
|
+
js: 'class Bootstrap extends HTMLElement {}\nreturn Bootstrap;',
|
|
373
|
+
html: '',
|
|
374
|
+
css: '',
|
|
375
|
+
size: 10,
|
|
376
|
+
isFramework: true
|
|
377
|
+
}
|
|
378
|
+
}, {
|
|
379
|
+
type: 'framework',
|
|
380
|
+
generated: new Date().toISOString(),
|
|
381
|
+
strategy: 'hybrid',
|
|
382
|
+
componentCount: 1,
|
|
383
|
+
totalSize: 10
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
assert.match(source, /import boot from '\/public\/bootstrap\.js';/);
|
|
387
|
+
assert.match(source, /const SLICE_BUNDLE_DEPENDENCIES = \{\};/);
|
|
388
|
+
});
|
|
@@ -139,3 +139,113 @@ test('bundle output inlines dependency modules and binds imported symbols in cla
|
|
|
139
139
|
assert.match(source, /SLICE_BUNDLE_DEPENDENCIES\["App\/documentationRoutes\.js"\] = __sliceDepExports0;/);
|
|
140
140
|
assert.match(source, /const documentationRoutes = SLICE_BUNDLE_DEPENDENCIES\["App\/documentationRoutes\.js"\]\.documentationRoutes;/);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
test('bundle output hoists allowed absolute imports to module top-level', () => {
|
|
144
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
145
|
+
components: [],
|
|
146
|
+
routes: [],
|
|
147
|
+
metrics: {
|
|
148
|
+
totalComponents: 0,
|
|
149
|
+
totalRoutes: 0,
|
|
150
|
+
sharedPercentage: 0,
|
|
151
|
+
totalSize: 0
|
|
152
|
+
}
|
|
153
|
+
}, { output: 'src' });
|
|
154
|
+
|
|
155
|
+
const source = generator.generateBundleFileContent(
|
|
156
|
+
'slice-bundle.test.js',
|
|
157
|
+
'route',
|
|
158
|
+
[{
|
|
159
|
+
name: 'HeroCard',
|
|
160
|
+
category: 'Visual',
|
|
161
|
+
categoryType: 'Visual',
|
|
162
|
+
dependencies: new Set(),
|
|
163
|
+
size: 100,
|
|
164
|
+
js: 'class HeroCard extends HTMLElement {}\nwindow.HeroCard = HeroCard;\nreturn HeroCard;',
|
|
165
|
+
html: '',
|
|
166
|
+
css: '',
|
|
167
|
+
hoistedImports: ["import hero from '/public/hero.js';"]
|
|
168
|
+
}],
|
|
169
|
+
'/test'
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
assert.match(source, /import hero from '\/public\/hero\.js';/);
|
|
173
|
+
assert.doesNotMatch(source, /SLICE_CLASS_FACTORY_SliceComponent_HeroCard = \(\) => \{[\s\S]*import hero from '\/public\/hero\.js';/);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('generateBundleFileContent throws on hoisted import local binding collisions', () => {
|
|
177
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
178
|
+
components: [],
|
|
179
|
+
routes: [],
|
|
180
|
+
metrics: {
|
|
181
|
+
totalComponents: 0,
|
|
182
|
+
totalRoutes: 0,
|
|
183
|
+
sharedPercentage: 0,
|
|
184
|
+
totalSize: 0
|
|
185
|
+
}
|
|
186
|
+
}, { output: 'src' });
|
|
187
|
+
|
|
188
|
+
assert.throws(() => {
|
|
189
|
+
generator.generateBundleFileContent(
|
|
190
|
+
'slice-bundle.test.js',
|
|
191
|
+
'route',
|
|
192
|
+
[
|
|
193
|
+
{
|
|
194
|
+
name: 'CompA',
|
|
195
|
+
category: 'Visual',
|
|
196
|
+
categoryType: 'Visual',
|
|
197
|
+
dependencies: new Set(),
|
|
198
|
+
size: 100,
|
|
199
|
+
js: 'class CompA extends HTMLElement {}\nwindow.CompA = CompA;\nreturn CompA;',
|
|
200
|
+
html: '',
|
|
201
|
+
css: '',
|
|
202
|
+
hoistedImports: ["import foo from '/public/a.js';"]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'CompB',
|
|
206
|
+
category: 'Visual',
|
|
207
|
+
categoryType: 'Visual',
|
|
208
|
+
dependencies: new Set(),
|
|
209
|
+
size: 100,
|
|
210
|
+
js: 'class CompB extends HTMLElement {}\nwindow.CompB = CompB;\nreturn CompB;',
|
|
211
|
+
html: '',
|
|
212
|
+
css: '',
|
|
213
|
+
hoistedImports: ["import foo from '/public/b.js';"]
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
'/test'
|
|
217
|
+
);
|
|
218
|
+
}, /Hoisted import binding collision: foo/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('generateBundleFileContent throws on reserved identifier collision', () => {
|
|
222
|
+
const generator = new BundleGenerator(import.meta.url, {
|
|
223
|
+
components: [],
|
|
224
|
+
routes: [],
|
|
225
|
+
metrics: {
|
|
226
|
+
totalComponents: 0,
|
|
227
|
+
totalRoutes: 0,
|
|
228
|
+
sharedPercentage: 0,
|
|
229
|
+
totalSize: 0
|
|
230
|
+
}
|
|
231
|
+
}, { output: 'src' });
|
|
232
|
+
|
|
233
|
+
assert.throws(() => {
|
|
234
|
+
generator.generateBundleFileContent(
|
|
235
|
+
'slice-bundle.test.js',
|
|
236
|
+
'route',
|
|
237
|
+
[{
|
|
238
|
+
name: 'CompMeta',
|
|
239
|
+
category: 'Visual',
|
|
240
|
+
categoryType: 'Visual',
|
|
241
|
+
dependencies: new Set(),
|
|
242
|
+
size: 100,
|
|
243
|
+
js: 'class CompMeta extends HTMLElement {}\nwindow.CompMeta = CompMeta;\nreturn CompMeta;',
|
|
244
|
+
html: '',
|
|
245
|
+
css: '',
|
|
246
|
+
hoistedImports: ["import SLICE_BUNDLE_META from '/public/meta.js';"]
|
|
247
|
+
}],
|
|
248
|
+
'/test'
|
|
249
|
+
);
|
|
250
|
+
}, /reserved identifier collision: SLICE_BUNDLE_META/);
|
|
251
|
+
});
|