slicejs-cli 3.4.1 → 3.5.1
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/workflows/ci.yml +43 -0
- package/commands/createComponent/createComponent.js +6 -2
- package/commands/deleteComponent/deleteComponent.js +4 -0
- package/commands/doctor/doctor.js +9 -0
- package/commands/init/init.js +53 -6
- package/commands/utils/bundling/BundleGenerator.js +271 -38
- package/package.json +5 -2
- package/playwright.config.js +51 -0
- package/tests/build-command-integration.test.js +87 -0
- package/tests/build-production-e2e.test.js +140 -0
- package/tests/builder-edge-cases.test.js +322 -0
- package/tests/bundle-generate-e2e.test.js +115 -0
- package/tests/bundling-dependency-edges.test.js +127 -0
- package/tests/bundling-imports-unit.test.js +267 -0
- package/tests/commands-component-crud.test.js +102 -0
- package/tests/commands-doctor.test.js +80 -0
- package/tests/commands-version-checker.test.js +37 -0
- package/tests/component-registry-parse.test.js +1 -1
- package/tests/e2e/bundles.spec.js +91 -0
- package/tests/e2e/dependency-scenarios.spec.js +56 -0
- package/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +136 -0
- package/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +149 -0
- package/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +45 -0
- package/tests/e2e/fixtures/components/Visual/Button/Button.css +106 -0
- package/tests/e2e/fixtures/components/Visual/Button/Button.html +5 -0
- package/tests/e2e/fixtures/components/Visual/Button/Button.js +158 -0
- package/tests/e2e/fixtures/components/Visual/Link/Link.js +33 -0
- package/tests/e2e/fixtures/components/Visual/Loading/Loading.css +56 -0
- package/tests/e2e/fixtures/components/Visual/Loading/Loading.html +83 -0
- package/tests/e2e/fixtures/components/Visual/Loading/Loading.js +164 -0
- package/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +167 -0
- package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +116 -0
- package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +44 -0
- package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +180 -0
- package/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +20 -0
- package/tests/e2e/fixtures/components/Visual/Route/Route.js +181 -0
- package/tests/e2e/fixtures/components/registry.json +12 -0
- package/tests/e2e/fixtures/vendor-components.mjs +65 -0
- package/tests/e2e/navigation.spec.js +44 -0
- package/tests/e2e/render.spec.js +34 -0
- package/tests/e2e/serve.mjs +264 -0
- package/tests/e2e/shared-deps.spec.js +61 -0
- package/tests/e2e/unminified.spec.js +33 -0
- package/tests/e2e-serve.test.js +148 -0
- package/tests/helpers/setup.js +6 -1
- package/tests/perf-budget.test.js +86 -0
- package/tests/types-generator.test.js +2 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master, main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
timeout-minutes: 20
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- uses: pnpm/action-setup@v4
|
|
16
|
+
with:
|
|
17
|
+
version: 11
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: 22
|
|
22
|
+
cache: pnpm
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: pnpm install --frozen-lockfile
|
|
26
|
+
|
|
27
|
+
- name: Install Playwright browser
|
|
28
|
+
run: pnpm exec playwright install --with-deps chromium
|
|
29
|
+
|
|
30
|
+
# Serialized to avoid resource contention between the build-heavy
|
|
31
|
+
# integration tests under parallel runners.
|
|
32
|
+
- name: Unit & integration tests
|
|
33
|
+
run: node --test --test-concurrency=1
|
|
34
|
+
|
|
35
|
+
- name: Browser E2E tests
|
|
36
|
+
run: pnpm exec playwright test
|
|
37
|
+
|
|
38
|
+
- uses: actions/upload-artifact@v4
|
|
39
|
+
if: ${{ !cancelled() }}
|
|
40
|
+
with:
|
|
41
|
+
name: playwright-report
|
|
42
|
+
path: playwright-report/
|
|
43
|
+
retention-days: 7
|
|
@@ -23,6 +23,10 @@ function createComponent(componentName, category) {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// Components follow a PascalCase convention: normalize the initial to
|
|
27
|
+
// uppercase so the folder name, registry entry and existence checks agree.
|
|
28
|
+
componentName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
|
|
29
|
+
|
|
26
30
|
// Validation: Component already exists
|
|
27
31
|
if(Validations.componentExists(componentName)){
|
|
28
32
|
Print.error(`Component '${componentName}' already exists in your project`);
|
|
@@ -42,8 +46,8 @@ function createComponent(componentName, category) {
|
|
|
42
46
|
}
|
|
43
47
|
category = flagCategory.category;
|
|
44
48
|
|
|
45
|
-
// Create class name and file name
|
|
46
|
-
const className = componentName
|
|
49
|
+
// Create class name and file name (componentName is already PascalCase).
|
|
50
|
+
const className = componentName;
|
|
47
51
|
const fileName = `${className}.js`;
|
|
48
52
|
let template;
|
|
49
53
|
|
|
@@ -21,6 +21,10 @@ function deleteComponent(componentName, category) {
|
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// Components follow a PascalCase convention: normalize the initial so the
|
|
25
|
+
// lookup matches the folder name created by `slice component create`.
|
|
26
|
+
componentName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
|
|
27
|
+
|
|
24
28
|
// Validation: Valid category
|
|
25
29
|
let flagCategory = Validations.isValidCategory(category);
|
|
26
30
|
|
|
@@ -259,6 +259,15 @@ async function checkComponents() {
|
|
|
259
259
|
/**
|
|
260
260
|
* Main diagnostic command
|
|
261
261
|
*/
|
|
262
|
+
export {
|
|
263
|
+
checkNodeVersion,
|
|
264
|
+
checkDirectoryStructure,
|
|
265
|
+
checkConfig,
|
|
266
|
+
checkPort,
|
|
267
|
+
checkDependencies,
|
|
268
|
+
checkComponents
|
|
269
|
+
};
|
|
270
|
+
|
|
262
271
|
export default async function runDiagnostics() {
|
|
263
272
|
Print.newLine();
|
|
264
273
|
Print.title('🔍 Running Slice.js Diagnostics...');
|
package/commands/init/init.js
CHANGED
|
@@ -21,6 +21,15 @@ const STARTER_VISUAL_COMPONENTS = [
|
|
|
21
21
|
'Route'
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
+
// Service components are now also pulled from the registry on init (instead of
|
|
25
|
+
// being vendored in the framework package), so Visual and Service share a single
|
|
26
|
+
// source of truth. Newcomers add more on demand with `slice get <Name>`.
|
|
27
|
+
const STARTER_SERVICE_COMPONENTS = [
|
|
28
|
+
'FetchManager',
|
|
29
|
+
'IndexedDbManager',
|
|
30
|
+
'LocalStorageManager'
|
|
31
|
+
];
|
|
32
|
+
|
|
24
33
|
export default async function initializeProject(projectType) {
|
|
25
34
|
try {
|
|
26
35
|
const projectRoot = getProjectRoot(import.meta.url);
|
|
@@ -101,7 +110,7 @@ export default async function initializeProject(projectType) {
|
|
|
101
110
|
|
|
102
111
|
if (stat.isDirectory()) {
|
|
103
112
|
if (item === 'Components') {
|
|
104
|
-
// Create Components structure but without copying Visual
|
|
113
|
+
// Create Components structure but without copying Visual or Service
|
|
105
114
|
await fs.ensureDir(destItemPath);
|
|
106
115
|
|
|
107
116
|
const componentItems = await fs.readdir(srcItemPath);
|
|
@@ -109,11 +118,11 @@ export default async function initializeProject(projectType) {
|
|
|
109
118
|
const componentItemPath = path.join(srcItemPath, componentItem);
|
|
110
119
|
const destComponentItemPath = path.join(destItemPath, componentItem);
|
|
111
120
|
|
|
112
|
-
if (componentItem !== 'Visual') {
|
|
113
|
-
// Copy
|
|
121
|
+
if (componentItem !== 'Visual' && componentItem !== 'Service') {
|
|
122
|
+
// Copy AppComponents and other template types from the framework
|
|
114
123
|
await fs.copy(componentItemPath, destComponentItemPath, { recursive: true });
|
|
115
124
|
} else {
|
|
116
|
-
//
|
|
125
|
+
// Visual and Service are installed from the registry below
|
|
117
126
|
await fs.ensureDir(destComponentItemPath);
|
|
118
127
|
}
|
|
119
128
|
}
|
|
@@ -176,6 +185,42 @@ export default async function initializeProject(projectType) {
|
|
|
176
185
|
Print.info('You can add them later using "slice get <component-name>"');
|
|
177
186
|
}
|
|
178
187
|
|
|
188
|
+
// 3b. DOWNLOAD STARTER SERVICE COMPONENTS FROM OFFICIAL REPOSITORY
|
|
189
|
+
const serviceSpinner = ora('Installing starter Service components...').start();
|
|
190
|
+
try {
|
|
191
|
+
const registry = new ComponentRegistry();
|
|
192
|
+
await registry.loadRegistry();
|
|
193
|
+
|
|
194
|
+
if (STARTER_SERVICE_COMPONENTS.length > 0) {
|
|
195
|
+
Print.info(`Installing ${STARTER_SERVICE_COMPONENTS.length} starter Service components: ${STARTER_SERVICE_COMPONENTS.join(', ')}`);
|
|
196
|
+
serviceSpinner.text = `Installing ${STARTER_SERVICE_COMPONENTS.length} starter Service components...`;
|
|
197
|
+
|
|
198
|
+
const results = await registry.installMultipleComponents(
|
|
199
|
+
STARTER_SERVICE_COMPONENTS,
|
|
200
|
+
'Service',
|
|
201
|
+
true // force = true for initial installation
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const successful = results.filter(r => r.success).length;
|
|
205
|
+
const failed = results.filter(r => !r.success).length;
|
|
206
|
+
|
|
207
|
+
if (successful > 0 && failed === 0) {
|
|
208
|
+
serviceSpinner.succeed(`All ${successful} Service components installed successfully`);
|
|
209
|
+
} else if (successful > 0) {
|
|
210
|
+
serviceSpinner.warn(`${successful} Service components installed, ${failed} failed`);
|
|
211
|
+
Print.info('You can install failed components later using "slice get <component-name>"');
|
|
212
|
+
} else {
|
|
213
|
+
serviceSpinner.fail('Failed to install Service components');
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
serviceSpinner.succeed('No starter Service components to install');
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
serviceSpinner.fail('Could not download Service components from official repository');
|
|
220
|
+
Print.error(`Repository error: ${error.message}`);
|
|
221
|
+
Print.info('You can add them later using "slice get <component-name>"');
|
|
222
|
+
}
|
|
223
|
+
|
|
179
224
|
// 4. CONFIGURE SCRIPTS IN PROJECT package.json
|
|
180
225
|
const pkgSpinner = ora('Configuring npm scripts...').start();
|
|
181
226
|
try {
|
|
@@ -264,6 +309,8 @@ export default async function initializeProject(projectType) {
|
|
|
264
309
|
}
|
|
265
310
|
}
|
|
266
311
|
|
|
267
|
-
// NOTE: `slice init`
|
|
312
|
+
// NOTE: `slice init` installs only STARTER_VISUAL_COMPONENTS and
|
|
313
|
+
// STARTER_SERVICE_COMPONENTS (see top of file); both Visual and Service are pulled
|
|
314
|
+
// from the registry rather than vendored in the framework package.
|
|
268
315
|
// To install every registry component instead, iterate
|
|
269
|
-
// `Object.keys(registry.getAvailableComponents('Visual'))
|
|
316
|
+
// `Object.keys(registry.getAvailableComponents('Visual'))` (and likewise 'Service').
|
|
@@ -1249,7 +1249,11 @@ export default class BundleGenerator {
|
|
|
1249
1249
|
const omittedDependencies = options.omittedDependencies instanceof Set
|
|
1250
1250
|
? options.omittedDependencies
|
|
1251
1251
|
: new Set(options.omittedDependencies || []);
|
|
1252
|
-
|
|
1252
|
+
// Emit in topological order so a module is registered before any module
|
|
1253
|
+
// that depends on it (its transitive imports resolve at IIFE-eval time).
|
|
1254
|
+
const filteredModules = this.sortDependencyModulesTopologically(
|
|
1255
|
+
modules.filter((module) => !omittedDependencies.has(module.name))
|
|
1256
|
+
);
|
|
1253
1257
|
|
|
1254
1258
|
const lines = [
|
|
1255
1259
|
'const SLICE_BUNDLE_DEPENDENCIES = {};',
|
|
@@ -1260,32 +1264,88 @@ export default class BundleGenerator {
|
|
|
1260
1264
|
}
|
|
1261
1265
|
filteredModules.forEach((module, index) => {
|
|
1262
1266
|
const exportVar = `__sliceDepExports${index}`;
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1267
|
+
// Evaluate each dependency inside its own IIFE so its private,
|
|
1268
|
+
// non-exported top-level bindings stay local and cannot collide with
|
|
1269
|
+
// another dependency's (or the bundle's) identifiers. Only the exports
|
|
1270
|
+
// object escapes the closure.
|
|
1271
|
+
const transformedContent = this.transformDependencyContent(module.content, '__sliceExports', module.name);
|
|
1272
|
+
// Bind this module's own (transitive) imports inside its IIFE — they were
|
|
1273
|
+
// registered by earlier modules in the topological order.
|
|
1274
|
+
const importBindings = this.buildDependencyBindings(
|
|
1275
|
+
Object.fromEntries(
|
|
1276
|
+
(module.moduleImports || [])
|
|
1277
|
+
.filter((mi) => mi.bindings && mi.bindings.length)
|
|
1278
|
+
.map((mi) => [mi.depName, { bindings: mi.bindings }])
|
|
1279
|
+
),
|
|
1280
|
+
{ preferShared: !!options.includeSharedResolver }
|
|
1281
|
+
);
|
|
1282
|
+
const body = transformedContent.trim();
|
|
1283
|
+
lines.push(`const ${exportVar} = (() => {`);
|
|
1284
|
+
lines.push('const __sliceExports = {};');
|
|
1285
|
+
if (importBindings) lines.push(importBindings);
|
|
1286
|
+
if (body) lines.push(body);
|
|
1287
|
+
lines.push('return __sliceExports;');
|
|
1288
|
+
lines.push('})();');
|
|
1266
1289
|
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1267
1290
|
});
|
|
1268
1291
|
|
|
1269
1292
|
return lines.join('\n');
|
|
1270
1293
|
}
|
|
1271
1294
|
|
|
1295
|
+
sortDependencyModulesTopologically(modules = []) {
|
|
1296
|
+
const byName = new Map(modules.map((module) => [module.name, module]));
|
|
1297
|
+
const visited = new Set();
|
|
1298
|
+
const ordered = [];
|
|
1299
|
+
const visit = (module, stack) => {
|
|
1300
|
+
if (visited.has(module.name) || stack.has(module.name)) return;
|
|
1301
|
+
stack.add(module.name);
|
|
1302
|
+
for (const imp of module.moduleImports || []) {
|
|
1303
|
+
const dependency = byName.get(imp.depName);
|
|
1304
|
+
if (dependency) visit(dependency, stack);
|
|
1305
|
+
}
|
|
1306
|
+
stack.delete(module.name);
|
|
1307
|
+
visited.add(module.name);
|
|
1308
|
+
ordered.push(module);
|
|
1309
|
+
};
|
|
1310
|
+
for (const module of modules) visit(module, new Set());
|
|
1311
|
+
return ordered;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1272
1314
|
async buildDependencyContents(jsContent, componentPath) {
|
|
1273
|
-
const dependencies = this.analyzeDependencies(jsContent, componentPath);
|
|
1274
1315
|
const dependencyContents = {};
|
|
1316
|
+
const visited = new Set();
|
|
1275
1317
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1318
|
+
// Recursively resolve the relative-import graph rooted at `content`, so a
|
|
1319
|
+
// dependency module's OWN (transitive) imports are inlined too. Returns the
|
|
1320
|
+
// consumer's direct imports as [{ depName, bindings }].
|
|
1321
|
+
const resolveModule = async (content, basePath) => {
|
|
1322
|
+
const consumerImports = [];
|
|
1323
|
+
|
|
1324
|
+
for (const dep of this.analyzeDependencies(content, basePath)) {
|
|
1325
|
+
const depName = path.relative(this.srcPath, dep.path).replace(/\\/g, '/');
|
|
1326
|
+
consumerImports.push({ depName, bindings: dep.bindings || [] });
|
|
1327
|
+
|
|
1328
|
+
if (visited.has(depName)) continue;
|
|
1329
|
+
visited.add(depName);
|
|
1330
|
+
|
|
1331
|
+
try {
|
|
1332
|
+
const depContent = await fs.readFile(dep.path, 'utf-8');
|
|
1333
|
+
// Resolve this module's own transitive imports first.
|
|
1334
|
+
const moduleImports = await resolveModule(depContent, path.dirname(dep.path));
|
|
1335
|
+
dependencyContents[depName] = { content: depContent, bindings: [], moduleImports };
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
console.warn(`Warning: Could not read dependency ${dep.path}:`, error.message);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
return consumerImports;
|
|
1342
|
+
};
|
|
1343
|
+
|
|
1344
|
+
const directImports = await resolveModule(jsContent, componentPath);
|
|
1345
|
+
// The component's direct imports drive its class-factory bindings.
|
|
1346
|
+
for (const { depName, bindings } of directImports) {
|
|
1347
|
+
if (dependencyContents[depName]) {
|
|
1348
|
+
dependencyContents[depName].bindings = bindings;
|
|
1289
1349
|
}
|
|
1290
1350
|
}
|
|
1291
1351
|
|
|
@@ -1428,10 +1488,17 @@ if (window.slice && window.slice.controller) {
|
|
|
1428
1488
|
|
|
1429
1489
|
dependencyModules.forEach((module, index) => {
|
|
1430
1490
|
const exportVar = `__sliceDepExports${index}`;
|
|
1431
|
-
|
|
1491
|
+
// Each dependency lives in its own IIFE scope (see
|
|
1492
|
+
// buildV2DependencyModuleBlockFromModules) so private helpers cannot
|
|
1493
|
+
// collide across modules.
|
|
1494
|
+
const content = this.transformDependencyContent(module.content, '__sliceExports', module.name);
|
|
1495
|
+
const body = content.trim();
|
|
1432
1496
|
lines.push(`// Dependency: ${module.name}`);
|
|
1433
|
-
lines.push(`const ${exportVar} = {
|
|
1434
|
-
lines.push(
|
|
1497
|
+
lines.push(`const ${exportVar} = (() => {`);
|
|
1498
|
+
lines.push('const __sliceExports = {};');
|
|
1499
|
+
if (body) lines.push(body);
|
|
1500
|
+
lines.push('return __sliceExports;');
|
|
1501
|
+
lines.push('})();');
|
|
1435
1502
|
lines.push(`SLICE_BUNDLE_DEPENDENCIES[${JSON.stringify(module.name)}] = ${exportVar};`);
|
|
1436
1503
|
});
|
|
1437
1504
|
|
|
@@ -1458,7 +1525,8 @@ if (window.slice && window.slice.controller) {
|
|
|
1458
1525
|
if (modules.has(moduleName)) continue;
|
|
1459
1526
|
const content = typeof entry === 'string' ? entry : entry?.content;
|
|
1460
1527
|
if (!content) continue;
|
|
1461
|
-
|
|
1528
|
+
const moduleImports = (entry && typeof entry === 'object' ? entry.moduleImports : null) || [];
|
|
1529
|
+
modules.set(moduleName, { name: moduleName, content, moduleImports });
|
|
1462
1530
|
}
|
|
1463
1531
|
}
|
|
1464
1532
|
return Array.from(modules.values());
|
|
@@ -1469,6 +1537,158 @@ if (window.slice && window.slice.controller) {
|
|
|
1469
1537
|
}
|
|
1470
1538
|
|
|
1471
1539
|
transformDependencyContent(content, exportVar, moduleName) {
|
|
1540
|
+
let ast;
|
|
1541
|
+
try {
|
|
1542
|
+
ast = parse(content, { sourceType: 'module', plugins: ['jsx'] });
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
// Unparseable content (e.g. TS syntax): fall back to the regex transform
|
|
1545
|
+
// so we never lose a dependency entirely.
|
|
1546
|
+
return this.transformDependencyContentRegexFallback(content, exportVar, moduleName);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const fallbackKey = this.getDependencyDefaultFallbackKey(moduleName);
|
|
1550
|
+
const statements = ast.program.body
|
|
1551
|
+
.filter((node) => typeof node.start === 'number' && typeof node.end === 'number')
|
|
1552
|
+
.sort((a, b) => a.start - b.start);
|
|
1553
|
+
|
|
1554
|
+
let cursor = 0;
|
|
1555
|
+
let output = '';
|
|
1556
|
+
for (const node of statements) {
|
|
1557
|
+
output += content.slice(cursor, node.start);
|
|
1558
|
+
output += this.transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey);
|
|
1559
|
+
cursor = node.end;
|
|
1560
|
+
}
|
|
1561
|
+
output += content.slice(cursor);
|
|
1562
|
+
return output;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
describeExportTarget(exportVar, name) {
|
|
1566
|
+
return /^[A-Za-z_$][\w$]*$/.test(name)
|
|
1567
|
+
? `${exportVar}.${name}`
|
|
1568
|
+
: `${exportVar}[${JSON.stringify(name)}]`;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
collectPatternIdentifiers(node, acc = []) {
|
|
1572
|
+
if (!node) return acc;
|
|
1573
|
+
switch (node.type) {
|
|
1574
|
+
case 'Identifier':
|
|
1575
|
+
acc.push(node.name);
|
|
1576
|
+
break;
|
|
1577
|
+
case 'ObjectPattern':
|
|
1578
|
+
for (const prop of node.properties) {
|
|
1579
|
+
if (prop.type === 'RestElement') {
|
|
1580
|
+
this.collectPatternIdentifiers(prop.argument, acc);
|
|
1581
|
+
} else {
|
|
1582
|
+
this.collectPatternIdentifiers(prop.value, acc);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
break;
|
|
1586
|
+
case 'ArrayPattern':
|
|
1587
|
+
for (const element of node.elements) {
|
|
1588
|
+
if (element) this.collectPatternIdentifiers(element, acc);
|
|
1589
|
+
}
|
|
1590
|
+
break;
|
|
1591
|
+
case 'AssignmentPattern':
|
|
1592
|
+
this.collectPatternIdentifiers(node.left, acc);
|
|
1593
|
+
break;
|
|
1594
|
+
case 'RestElement':
|
|
1595
|
+
this.collectPatternIdentifiers(node.argument, acc);
|
|
1596
|
+
break;
|
|
1597
|
+
default:
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
return acc;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
transformExportedDeclaration(decl, content, exportVar) {
|
|
1604
|
+
const sourceOf = (n) => content.slice(n.start, n.end);
|
|
1605
|
+
|
|
1606
|
+
if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') {
|
|
1607
|
+
const name = decl.id?.name;
|
|
1608
|
+
if (!name) return sourceOf(decl);
|
|
1609
|
+
// Keep the declaration so other code in the module can still reference the
|
|
1610
|
+
// name (intra-module references), then mirror it onto the exports object.
|
|
1611
|
+
// Each dependency is IIFE-scoped, so this local binding can't collide.
|
|
1612
|
+
return `${sourceOf(decl)}\n${this.describeExportTarget(exportVar, name)} = ${name};`;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
if (decl.type === 'VariableDeclaration') {
|
|
1616
|
+
// Keep the declaration verbatim — preserving intra-module references and
|
|
1617
|
+
// initializer evaluation — then export every bound name.
|
|
1618
|
+
const names = [];
|
|
1619
|
+
for (const declarator of decl.declarations) {
|
|
1620
|
+
names.push(...this.collectPatternIdentifiers(declarator.id, []));
|
|
1621
|
+
}
|
|
1622
|
+
const assigns = names
|
|
1623
|
+
.map((n) => `${this.describeExportTarget(exportVar, n)} = ${n};`)
|
|
1624
|
+
.join('\n');
|
|
1625
|
+
return `${sourceOf(decl)}\n${assigns}`;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return sourceOf(decl);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
transformDependencyStatement(node, content, exportVar, moduleName, fallbackKey) {
|
|
1632
|
+
const sourceOf = (n) => content.slice(n.start, n.end);
|
|
1633
|
+
|
|
1634
|
+
if (node.type === 'ImportDeclaration') {
|
|
1635
|
+
// Transitive imports of a bundled dependency cannot be resolved at
|
|
1636
|
+
// runtime; strip them so they never leak into the emitted bundle.
|
|
1637
|
+
console.warn(this.buildImportWarningMessage(
|
|
1638
|
+
`Warning: Stripping unsupported import inside bundled dependency: ${node.source?.value}`,
|
|
1639
|
+
moduleName
|
|
1640
|
+
));
|
|
1641
|
+
return '';
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
if (node.type === 'ExportAllDeclaration') {
|
|
1645
|
+
console.warn(this.buildImportWarningMessage(
|
|
1646
|
+
`Warning: Dropping unsupported 'export *' inside bundled dependency: ${node.source?.value}`,
|
|
1647
|
+
moduleName
|
|
1648
|
+
));
|
|
1649
|
+
return '';
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
if (node.type === 'ExportDefaultDeclaration') {
|
|
1653
|
+
const declSource = sourceOf(node.declaration);
|
|
1654
|
+
const lines = [`${exportVar}.default = (${declSource});`];
|
|
1655
|
+
if (fallbackKey && fallbackKey !== 'default') {
|
|
1656
|
+
// Preserve the historical `<basename>Data` key so existing default
|
|
1657
|
+
// bindings (which pass it as the preferred key) keep resolving.
|
|
1658
|
+
lines.push(`${this.describeExportTarget(exportVar, fallbackKey)} = ${exportVar}.default;`);
|
|
1659
|
+
}
|
|
1660
|
+
return lines.join('\n');
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
if (node.type === 'ExportNamedDeclaration') {
|
|
1664
|
+
if (node.source) {
|
|
1665
|
+
console.warn(this.buildImportWarningMessage(
|
|
1666
|
+
`Warning: Dropping unsupported re-export inside bundled dependency: ${node.source.value}`,
|
|
1667
|
+
moduleName
|
|
1668
|
+
));
|
|
1669
|
+
return '';
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
if (node.declaration) {
|
|
1673
|
+
return this.transformExportedDeclaration(node.declaration, content, exportVar);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// `export { local as exported, ... }` — key the exports object by the
|
|
1677
|
+
// PUBLIC (exported) name, mapped to the local binding's value.
|
|
1678
|
+
return node.specifiers
|
|
1679
|
+
.map((spec) => {
|
|
1680
|
+
const localName = spec.local.name;
|
|
1681
|
+
const exportedName = spec.exported.name ?? spec.exported.value;
|
|
1682
|
+
return `${this.describeExportTarget(exportVar, exportedName)} = ${localName};`;
|
|
1683
|
+
})
|
|
1684
|
+
.join('\n');
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// Any other top-level statement is kept verbatim.
|
|
1688
|
+
return sourceOf(node);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
transformDependencyContentRegexFallback(content, exportVar, moduleName) {
|
|
1472
1692
|
const dataName = this.getDependencyDefaultFallbackKey(moduleName);
|
|
1473
1693
|
const exportPrefix = dataName ? `${exportVar}.${dataName} = ` : `${exportVar}.default = `;
|
|
1474
1694
|
|
|
@@ -1594,11 +1814,18 @@ if (window.slice && window.slice.controller) {
|
|
|
1594
1814
|
}
|
|
1595
1815
|
|
|
1596
1816
|
toSafeIdentifier(name) {
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1817
|
+
// Injective encoding: every character outside [A-Za-z0-9] (including '_')
|
|
1818
|
+
// is escaped to `_<hex>_`. This guarantees that two distinct component
|
|
1819
|
+
// names can never collapse to the same identifier (e.g. "my-btn" and
|
|
1820
|
+
// "my_btn" used to both yield "SliceComponent_my_btn", emitting duplicate
|
|
1821
|
+
// `const` declarations and producing invalid bundle JS). The leading
|
|
1822
|
+
// `SliceComponent_` prefix keeps the result a valid identifier even when
|
|
1823
|
+
// the name starts with a digit.
|
|
1824
|
+
const encoded = String(name).replace(
|
|
1825
|
+
/[^a-zA-Z0-9]/g,
|
|
1826
|
+
(char) => `_${char.charCodeAt(0).toString(16)}_`
|
|
1827
|
+
);
|
|
1828
|
+
return `SliceComponent_${encoded}`;
|
|
1602
1829
|
}
|
|
1603
1830
|
|
|
1604
1831
|
/**
|
|
@@ -1633,17 +1860,23 @@ if (window.slice && window.slice.controller) {
|
|
|
1633
1860
|
integrity: null,
|
|
1634
1861
|
components: []
|
|
1635
1862
|
},
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1863
|
+
// Only advertise the vendor-shared bundle when it was actually emitted
|
|
1864
|
+
// (i.e. there were shared dependencies). Otherwise the config would
|
|
1865
|
+
// reference a file that does not exist on disk -> 404 for any runtime
|
|
1866
|
+
// that resolves it.
|
|
1867
|
+
vendorShared: this.vendorShared.bundle
|
|
1868
|
+
? {
|
|
1869
|
+
bundleKey: 'vendor-shared',
|
|
1870
|
+
type: 'vendor-shared',
|
|
1871
|
+
file: this.vendorShared.file,
|
|
1872
|
+
size: this.vendorShared.bundle?.size || 0,
|
|
1873
|
+
hash: this.vendorShared.bundle?.hash || null,
|
|
1874
|
+
integrity: this.vendorShared.bundle?.integrity || null,
|
|
1875
|
+
dependencies: Array.from(this.vendorShared.sharedDependencySet).sort((a, b) => a.localeCompare(b)),
|
|
1876
|
+
dependencyCount: this.vendorShared.sharedDependencySet.size,
|
|
1877
|
+
routes: Array.from(this.vendorShared.bundleKeysUsingSharedDependencies).sort((a, b) => a.localeCompare(b))
|
|
1878
|
+
}
|
|
1879
|
+
: null,
|
|
1647
1880
|
critical: {
|
|
1648
1881
|
file: this.bundles.critical.file,
|
|
1649
1882
|
size: this.bundles.critical.size,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slicejs-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"description": "Command client for developing web applications with Slice.js framework",
|
|
5
5
|
"main": "client.js",
|
|
6
6
|
"bin": {
|
|
@@ -54,5 +54,8 @@
|
|
|
54
54
|
"ora": "^8.2.0",
|
|
55
55
|
"slicejs-web-framework": "latest",
|
|
56
56
|
"terser": "^5.43.1"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@playwright/test": "^1.60.0"
|
|
57
60
|
}
|
|
58
|
-
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const PORT = process.env.E2E_PORT ? Number(process.env.E2E_PORT) : 3210;
|
|
4
|
+
const UNMIN_PORT = PORT + 4;
|
|
5
|
+
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
6
|
+
const UNMIN_URL = `http://127.0.0.1:${UNMIN_PORT}`;
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
testDir: './tests/e2e',
|
|
10
|
+
testMatch: '**/*.spec.js',
|
|
11
|
+
fullyParallel: false,
|
|
12
|
+
workers: 1,
|
|
13
|
+
timeout: 30_000,
|
|
14
|
+
expect: { timeout: 10_000 },
|
|
15
|
+
reporter: process.env.CI ? [['list'], ['html', { open: 'never' }]] : [['list']],
|
|
16
|
+
use: {
|
|
17
|
+
trace: 'retain-on-failure',
|
|
18
|
+
},
|
|
19
|
+
projects: [
|
|
20
|
+
{
|
|
21
|
+
name: 'chromium',
|
|
22
|
+
testIgnore: '**/unminified.spec.js',
|
|
23
|
+
use: { ...devices['Desktop Chrome'], baseURL: BASE_URL },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'chromium-unminified',
|
|
27
|
+
testMatch: '**/unminified.spec.js',
|
|
28
|
+
use: { ...devices['Desktop Chrome'], baseURL: UNMIN_URL },
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
webServer: [
|
|
32
|
+
{
|
|
33
|
+
command: 'node tests/e2e/serve.mjs',
|
|
34
|
+
url: `${BASE_URL}/slice-env.json`,
|
|
35
|
+
env: { E2E_PORT: String(PORT) },
|
|
36
|
+
timeout: 120_000,
|
|
37
|
+
reuseExistingServer: !process.env.CI,
|
|
38
|
+
stdout: 'pipe',
|
|
39
|
+
stderr: 'pipe',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
command: 'node tests/e2e/serve.mjs',
|
|
43
|
+
url: `${UNMIN_URL}/slice-env.json`,
|
|
44
|
+
env: { E2E_PORT: String(UNMIN_PORT), E2E_MINIFY: 'false' },
|
|
45
|
+
timeout: 120_000,
|
|
46
|
+
reuseExistingServer: !process.env.CI,
|
|
47
|
+
stdout: 'pipe',
|
|
48
|
+
stderr: 'pipe',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|