slicejs-cli 3.0.0 → 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.
- package/README.md +33 -5
- package/client.js +35 -5
- package/commands/utils/LocalCliDelegation.js +53 -0
- package/commands/utils/bundling/BundleGenerator.js +362 -21
- package/commands/utils/updateManager.js +19 -3
- package/package.json +1 -1
- package/tests/bundle-generator.test.js +182 -0
- package/tests/bundle-v2-register-output.test.js +110 -0
- package/tests/client-launcher-contract.test.js +211 -0
- package/tests/client-update-flow-contract.test.js +272 -0
- package/tests/local-cli-delegation.test.js +79 -0
- package/tests/update-manager-notifications.test.js +88 -0
package/README.md
CHANGED
|
@@ -45,6 +45,29 @@ npm run dev
|
|
|
45
45
|
npm run slice -- get Button
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
4. Use `slice` directly when the launcher command is available on your system
|
|
49
|
+
(commonly after a global install that puts `slice` in your PATH).
|
|
50
|
+
The launcher delegates to your nearest project-local `node_modules/slicejs-cli`
|
|
51
|
+
so project-pinned behavior is used from the project root and subdirectories.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
slice dev
|
|
55
|
+
slice build
|
|
56
|
+
slice version
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
If `slice` is not available in your shell, use:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx slicejs-cli dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
You can disable launcher delegation for a command when needed:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
SLICE_NO_LOCAL_DELEGATION=1 slice version
|
|
69
|
+
```
|
|
70
|
+
|
|
48
71
|
### Global (Not Recommended)
|
|
49
72
|
|
|
50
73
|
Global installations can lead to version mismatches and "works on my machine" issues.
|
|
@@ -55,7 +78,10 @@ npm install -g slicejs-cli
|
|
|
55
78
|
|
|
56
79
|
## Usage
|
|
57
80
|
|
|
58
|
-
After installation,
|
|
81
|
+
After installation, prefer your project-local CLI. When the `slice` launcher command is
|
|
82
|
+
available, it automatically delegates to the nearest local `slicejs-cli` install.
|
|
83
|
+
|
|
84
|
+
Use the `slice` command directly:
|
|
59
85
|
|
|
60
86
|
```bash
|
|
61
87
|
slice [command] [options]
|
|
@@ -67,6 +93,8 @@ Or with npx (without global install):
|
|
|
67
93
|
npx slicejs-cli [command]
|
|
68
94
|
```
|
|
69
95
|
|
|
96
|
+
Use `npx slicejs-cli [command]` as a fallback when the `slice` launcher command is unavailable.
|
|
97
|
+
|
|
70
98
|
## Essential Commands
|
|
71
99
|
|
|
72
100
|
### Initialize a project
|
|
@@ -154,7 +182,7 @@ slice sync
|
|
|
154
182
|
```bash
|
|
155
183
|
# Version info
|
|
156
184
|
slice version
|
|
157
|
-
slice
|
|
185
|
+
slice v
|
|
158
186
|
|
|
159
187
|
# Updates (CLI and Framework)
|
|
160
188
|
slice update # Check and prompt to update
|
|
@@ -325,16 +353,16 @@ slice init
|
|
|
325
353
|
### Command not found
|
|
326
354
|
|
|
327
355
|
```bash
|
|
328
|
-
#
|
|
356
|
+
# If the launcher command is unavailable, run the local CLI via npx
|
|
329
357
|
npx slicejs-cli dev
|
|
330
358
|
|
|
331
|
-
#
|
|
359
|
+
# Optional: install globally to expose the slice launcher command
|
|
332
360
|
npm install -g slicejs-cli
|
|
333
361
|
```
|
|
334
362
|
|
|
335
363
|
## Links
|
|
336
364
|
|
|
337
|
-
- 📘 Documentation: https://slice-js-docs.vercel.app/
|
|
365
|
+
- 📘 CLI Documentation: https://slice-js-docs.vercel.app/Documentation/CLI
|
|
338
366
|
- 🐙 GitHub: https://github.com/VKneider/slice-cli
|
|
339
367
|
- 📦 npm: https://www.npmjs.com/package/slicejs-cli
|
|
340
368
|
|
package/client.js
CHANGED
|
@@ -14,12 +14,18 @@ import fs from "fs";
|
|
|
14
14
|
import path from "path";
|
|
15
15
|
import { fileURLToPath } from "url";
|
|
16
16
|
import { getConfigPath, getProjectRoot } from "./commands/utils/PathHelper.js";
|
|
17
|
-
import { exec } from "child_process";
|
|
17
|
+
import { exec, spawnSync } from "node:child_process";
|
|
18
18
|
import { promisify } from "util";
|
|
19
19
|
import validations from "./commands/Validations.js";
|
|
20
20
|
import Print from "./commands/Print.js";
|
|
21
21
|
import build from './commands/build/build.js';
|
|
22
22
|
import { cleanBundles, bundleInfo } from './commands/bundle/bundle.js';
|
|
23
|
+
import {
|
|
24
|
+
isLocalDelegationDisabled,
|
|
25
|
+
findNearestLocalCliEntry,
|
|
26
|
+
resolveLocalCliCandidate,
|
|
27
|
+
shouldDelegateToLocalCli
|
|
28
|
+
} from './commands/utils/LocalCliDelegation.js';
|
|
23
29
|
|
|
24
30
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
31
|
|
|
@@ -84,10 +90,7 @@ async function runWithVersionCheck(commandFunction, ...args) {
|
|
|
84
90
|
} catch {}
|
|
85
91
|
})();
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
if (updateInfo && updateInfo.hasUpdates) {
|
|
89
|
-
await updateManager.checkAndPromptUpdates({});
|
|
90
|
-
}
|
|
93
|
+
updateManager.notifyAvailableUpdates().catch(() => {});
|
|
91
94
|
|
|
92
95
|
const result = await commandFunction(...args);
|
|
93
96
|
|
|
@@ -102,6 +105,33 @@ async function runWithVersionCheck(commandFunction, ...args) {
|
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
|
|
108
|
+
function maybeDelegateToLocalCli() {
|
|
109
|
+
if (isLocalDelegationDisabled(process.env)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const currentEntryPath = fileURLToPath(import.meta.url);
|
|
114
|
+
const localEntryPath = findNearestLocalCliEntry(process.cwd(), resolveLocalCliCandidate);
|
|
115
|
+
|
|
116
|
+
if (!shouldDelegateToLocalCli(currentEntryPath, localEntryPath)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const child = spawnSync(
|
|
121
|
+
process.execPath,
|
|
122
|
+
[localEntryPath, ...process.argv.slice(2)],
|
|
123
|
+
{
|
|
124
|
+
stdio: 'inherit',
|
|
125
|
+
cwd: process.cwd(),
|
|
126
|
+
env: process.env
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
process.exit(child.status ?? 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
maybeDelegateToLocalCli();
|
|
134
|
+
|
|
105
135
|
const sliceClient = program;
|
|
106
136
|
|
|
107
137
|
try {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function getParentDirectory(dir) {
|
|
5
|
+
const parent = path.dirname(dir);
|
|
6
|
+
return parent === dir ? null : parent;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isLocalDelegationDisabled(env = process.env) {
|
|
10
|
+
return env.SLICE_NO_LOCAL_DELEGATION === '1';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function findNearestLocalCliEntry(startDirectory, resolveCandidate) {
|
|
14
|
+
if (!startDirectory || typeof resolveCandidate !== 'function') {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let current = path.resolve(startDirectory);
|
|
19
|
+
while (current) {
|
|
20
|
+
const candidate = resolveCandidate(current);
|
|
21
|
+
if (candidate) {
|
|
22
|
+
return candidate;
|
|
23
|
+
}
|
|
24
|
+
current = getParentDirectory(current);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveLocalCliCandidate(directory) {
|
|
31
|
+
const candidate = path.join(directory, 'node_modules', 'slicejs-cli', 'client.js');
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const stats = fs.statSync(candidate);
|
|
35
|
+
return stats.isFile() ? candidate : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function shouldDelegateToLocalCli(currentEntryPath, localEntryPath) {
|
|
42
|
+
if (!localEntryPath) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const currentReal = fs.realpathSync(currentEntryPath);
|
|
48
|
+
const localReal = fs.realpathSync(localEntryPath);
|
|
49
|
+
return currentReal !== localReal;
|
|
50
|
+
} catch {
|
|
51
|
+
return path.resolve(currentEntryPath) !== path.resolve(localEntryPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -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() {
|