slicejs-cli 3.5.0 → 3.6.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/README.md +34 -15
- package/client.js +67 -20
- package/commands/createComponent/createComponent.js +6 -2
- package/commands/deleteComponent/deleteComponent.js +4 -0
- package/commands/doctor/doctor.js +78 -3
- package/commands/getComponent/getComponent.js +33 -25
- package/commands/init/init.js +106 -28
- package/commands/utils/PackageManager.js +148 -0
- package/commands/utils/VersionChecker.js +6 -4
- package/commands/utils/bundling/BundleGenerator.js +271 -38
- package/commands/utils/sliceScripts.js +21 -0
- package/commands/utils/updateManager.js +54 -35
- package/package.json +15 -1
- package/post.js +8 -16
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
- package/.github/pull_request_template.md +0 -22
- package/AGENTS.md +0 -247
- package/CODE_OF_CONDUCT.md +0 -126
- package/ECOSYSTEM.md +0 -9
- package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +0 -182
- package/tests/bundle-generator.test.js +0 -691
- package/tests/bundle-v2-register-output.test.js +0 -470
- package/tests/client-launcher-contract.test.js +0 -211
- package/tests/client-update-flow-contract.test.js +0 -272
- package/tests/component-registry-parse.test.js +0 -34
- package/tests/dependency-analyzer.test.js +0 -24
- package/tests/fixtures/components.js +0 -8
- package/tests/fixtures/sliceConfig.json +0 -74
- package/tests/getcomponent.test.js +0 -407
- package/tests/helpers/setup.js +0 -97
- package/tests/init-command-contract.test.js +0 -46
- package/tests/local-cli-delegation.test.js +0 -81
- package/tests/path-helper.test.js +0 -206
- package/tests/postinstall-command.test.js +0 -72
- package/tests/types-breakage.test.js +0 -491
- package/tests/types-generator-errors.test.js +0 -361
- package/tests/types-generator.test.js +0 -344
- package/tests/update-manager-notifications.test.js +0 -88
|
@@ -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,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// commands/utils/sliceScripts.js
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for the slice:* package scripts configured by the CLI.
|
|
4
|
+
// Used by post.js (the postinstall hook), the `slice postinstall` command in
|
|
5
|
+
// client.js, and `slice init` — so the three can never drift apart (they did:
|
|
6
|
+
// client.js was missing slice:types, and none of them had slice:build).
|
|
7
|
+
export const SLICE_SCRIPTS = {
|
|
8
|
+
'slice:init': 'slice init',
|
|
9
|
+
'slice:dev': 'slice dev',
|
|
10
|
+
'slice:build': 'slice build',
|
|
11
|
+
'slice:start': 'slice start',
|
|
12
|
+
'slice:create': 'slice component create',
|
|
13
|
+
'slice:list': 'slice component list',
|
|
14
|
+
'slice:delete': 'slice component delete',
|
|
15
|
+
'slice:get': 'slice get',
|
|
16
|
+
'slice:browse': 'slice browse',
|
|
17
|
+
'slice:sync': 'slice sync',
|
|
18
|
+
'slice:version': 'slice version',
|
|
19
|
+
'slice:update': 'slice update',
|
|
20
|
+
'slice:types': 'slice types generate',
|
|
21
|
+
};
|
|
@@ -7,6 +7,7 @@ import ora from "ora";
|
|
|
7
7
|
import Print from "../Print.js";
|
|
8
8
|
import versionChecker from "./VersionChecker.js";
|
|
9
9
|
import { getProjectRoot, getApiPath, getPath } from "../utils/PathHelper.js";
|
|
10
|
+
import { resolvePackageManager, installCommand } from "../utils/PackageManager.js";
|
|
10
11
|
import path from "path";
|
|
11
12
|
import { fileURLToPath } from "url";
|
|
12
13
|
import fs from "fs-extra";
|
|
@@ -18,28 +19,45 @@ export class UpdateManager {
|
|
|
18
19
|
this.packagesToUpdate = [];
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
getPackageManager() {
|
|
23
|
+
if (!this._packageManager) {
|
|
24
|
+
this._packageManager = resolvePackageManager(getProjectRoot(import.meta.url)).name;
|
|
25
|
+
}
|
|
26
|
+
return this._packageManager;
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
async detectCliInstall() {
|
|
22
30
|
try {
|
|
23
31
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
24
32
|
const cliRoot = path.join(moduleDir, '../../');
|
|
25
33
|
const projectRoot = getProjectRoot(import.meta.url);
|
|
34
|
+
const packageManager = this.getPackageManager();
|
|
35
|
+
const localNodeModules = path.join(projectRoot, 'node_modules');
|
|
36
|
+
|
|
37
|
+
if (cliRoot.startsWith(localNodeModules)) {
|
|
38
|
+
return { type: 'local', cliRoot, projectRoot, packageManager };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Global pnpm installs live under PNPM_HOME — `npm config get prefix`
|
|
42
|
+
// knows nothing about them (and npm may not even exist on the machine).
|
|
43
|
+
const pnpmHome = process.env.PNPM_HOME;
|
|
44
|
+
if (pnpmHome && cliRoot.startsWith(pnpmHome)) {
|
|
45
|
+
return { type: 'global', cliRoot, projectRoot, packageManager: 'pnpm' };
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
let globalPrefix = '';
|
|
27
49
|
try {
|
|
28
50
|
const { stdout } = await execAsync('npm config get prefix');
|
|
29
51
|
globalPrefix = stdout.toString().trim();
|
|
30
52
|
} catch {}
|
|
31
|
-
const localNodeModules = path.join(projectRoot, 'node_modules');
|
|
32
53
|
const globalNodeModules = globalPrefix ? path.join(globalPrefix, 'node_modules') : '';
|
|
33
|
-
|
|
34
|
-
if (cliRoot.startsWith(localNodeModules)) {
|
|
35
|
-
return { type: 'local', cliRoot, projectRoot, globalPrefix };
|
|
36
|
-
}
|
|
37
54
|
if (globalNodeModules && cliRoot.startsWith(globalNodeModules)) {
|
|
38
|
-
return { type: 'global', cliRoot, projectRoot,
|
|
55
|
+
return { type: 'global', cliRoot, projectRoot, packageManager: 'npm' };
|
|
39
56
|
}
|
|
40
|
-
|
|
57
|
+
|
|
58
|
+
return { type: 'unknown', cliRoot, projectRoot, packageManager };
|
|
41
59
|
} catch (error) {
|
|
42
|
-
return { type: 'unknown' };
|
|
60
|
+
return { type: 'unknown', packageManager: this.getPackageManager() };
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
63
|
|
|
@@ -180,17 +198,12 @@ export class UpdateManager {
|
|
|
180
198
|
async buildUpdatePlan(packages) {
|
|
181
199
|
const plan = [];
|
|
182
200
|
const info = await this.detectCliInstall();
|
|
201
|
+
const pm = info.packageManager || this.getPackageManager();
|
|
183
202
|
for (const pkg of packages) {
|
|
184
|
-
if (pkg === 'slicejs-cli') {
|
|
185
|
-
|
|
186
|
-
plan.push({ package: pkg, target: 'global', command: 'npm install -g slicejs-cli@latest' });
|
|
187
|
-
} else {
|
|
188
|
-
plan.push({ package: pkg, target: 'project', command: 'npm install slicejs-cli@latest' });
|
|
189
|
-
}
|
|
190
|
-
} else if (pkg === 'slicejs-web-framework') {
|
|
191
|
-
plan.push({ package: pkg, target: 'project', command: 'npm install slicejs-web-framework@latest' });
|
|
203
|
+
if (pkg === 'slicejs-cli' && info.type === 'global') {
|
|
204
|
+
plan.push({ package: pkg, target: 'global', command: installCommand(pm, 'slicejs-cli@latest', { global: true }) });
|
|
192
205
|
} else {
|
|
193
|
-
plan.push({ package: pkg, target: 'project', command:
|
|
206
|
+
plan.push({ package: pkg, target: 'project', command: installCommand(pm, `${pkg}@latest`) });
|
|
194
207
|
}
|
|
195
208
|
}
|
|
196
209
|
return plan;
|
|
@@ -201,15 +214,15 @@ export class UpdateManager {
|
|
|
201
214
|
*/
|
|
202
215
|
async updatePackage(packageName) {
|
|
203
216
|
try {
|
|
204
|
-
|
|
205
|
-
let
|
|
217
|
+
const pm = this.getPackageManager();
|
|
218
|
+
let installCmd = installCommand(pm, `${packageName}@latest`);
|
|
206
219
|
let options = {};
|
|
207
220
|
|
|
208
221
|
if (packageName === 'slicejs-cli') {
|
|
209
222
|
const info = await this.detectCliInstall();
|
|
210
223
|
if (info.type === 'global') {
|
|
211
|
-
|
|
212
|
-
|
|
224
|
+
const globalPm = info.packageManager || pm;
|
|
225
|
+
installCmd = installCommand(globalPm, 'slicejs-cli@latest', { global: true });
|
|
213
226
|
} else {
|
|
214
227
|
options.cwd = info.projectRoot || getProjectRoot(import.meta.url);
|
|
215
228
|
}
|
|
@@ -217,11 +230,9 @@ export class UpdateManager {
|
|
|
217
230
|
options.cwd = getProjectRoot(import.meta.url);
|
|
218
231
|
}
|
|
219
232
|
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
} catch {}
|
|
224
|
-
|
|
233
|
+
// Install directly — npm/pnpm upgrade in place. (We used to uninstall
|
|
234
|
+
// first, which left the project without the package whenever the
|
|
235
|
+
// subsequent install failed, e.g. offline or under a release-age policy.)
|
|
225
236
|
const { stdout, stderr } = await execAsync(installCmd, options);
|
|
226
237
|
|
|
227
238
|
return {
|
|
@@ -284,13 +295,12 @@ export class UpdateManager {
|
|
|
284
295
|
return false;
|
|
285
296
|
}
|
|
286
297
|
|
|
287
|
-
if (updateInfo.allCurrent) {
|
|
288
|
-
Print.success('✅ All components are up to date!');
|
|
289
|
-
return true;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (!updateInfo.hasUpdates) {
|
|
298
|
+
if (updateInfo.allCurrent || !updateInfo.hasUpdates) {
|
|
293
299
|
Print.success('✅ All components are up to date!');
|
|
300
|
+
// --update-api works even when no package update runs.
|
|
301
|
+
if (options.updateApi) {
|
|
302
|
+
await this.updateApiIndexIfNeeded(options);
|
|
303
|
+
}
|
|
294
304
|
return true;
|
|
295
305
|
}
|
|
296
306
|
|
|
@@ -341,7 +351,7 @@ export class UpdateManager {
|
|
|
341
351
|
}
|
|
342
352
|
} else {
|
|
343
353
|
Print.warning('Global CLI detected. It is recommended to update slicejs-cli globally to keep aligned with the framework.');
|
|
344
|
-
console.log(
|
|
354
|
+
console.log(` Suggestion: ${installCommand(cliInfo.packageManager || this.getPackageManager(), 'slicejs-cli@latest', { global: true })}`);
|
|
345
355
|
console.log('');
|
|
346
356
|
}
|
|
347
357
|
}
|
|
@@ -386,7 +396,7 @@ export class UpdateManager {
|
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
const frameworkUpdated = results.find(r => r.packageName === 'slicejs-web-framework' && r.success);
|
|
389
|
-
if (frameworkUpdated) {
|
|
399
|
+
if (frameworkUpdated || options.updateApi) {
|
|
390
400
|
await this.updateApiIndexIfNeeded(options);
|
|
391
401
|
}
|
|
392
402
|
|
|
@@ -417,7 +427,16 @@ export class UpdateManager {
|
|
|
417
427
|
|
|
418
428
|
Print.warning('⚠️ Detected changes in framework api/index.js.');
|
|
419
429
|
|
|
420
|
-
|
|
430
|
+
// Overwriting api/index.js is opt-in: `--update-api` is the only way
|
|
431
|
+
// to auto-confirm. `-y/--yes` deliberately does NOT imply it — the
|
|
432
|
+
// project file may carry local changes, so a blanket "yes to updates"
|
|
433
|
+
// must not silently replace it.
|
|
434
|
+
let confirmUpdate = options.updateApi === true;
|
|
435
|
+
if (!confirmUpdate && options.yes === true) {
|
|
436
|
+
Print.info('Skipping api/index.js update (not updated by default).');
|
|
437
|
+
Print.info('Run "slice update --update-api" to update it (a .bak backup is created).');
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
421
440
|
if (!confirmUpdate) {
|
|
422
441
|
const answers = await inquirer.prompt([
|
|
423
442
|
{
|
package/package.json
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slicejs-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Command client for developing web applications with Slice.js framework",
|
|
5
5
|
"main": "client.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"slice": "./client.js"
|
|
8
8
|
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20.0.0"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"client.js",
|
|
14
|
+
"post.js",
|
|
15
|
+
"commands",
|
|
16
|
+
"assets"
|
|
17
|
+
],
|
|
9
18
|
"repository": {
|
|
10
19
|
"type": "git",
|
|
11
20
|
"url": "https://github.com/vkneider/slicejs-cli.git"
|
|
@@ -14,6 +23,7 @@
|
|
|
14
23
|
"test": "node --test",
|
|
15
24
|
"postinstall": "node post.js",
|
|
16
25
|
"slice:dev": "slice dev",
|
|
26
|
+
"slice:build": "slice build",
|
|
17
27
|
"slice:start": "slice start",
|
|
18
28
|
"slice:create": "slice component create",
|
|
19
29
|
"slice:list": "slice component list",
|
|
@@ -38,6 +48,7 @@
|
|
|
38
48
|
],
|
|
39
49
|
"author": "vkneider",
|
|
40
50
|
"type": "module",
|
|
51
|
+
"packageManager": "pnpm@11.1.1",
|
|
41
52
|
"preferGlobal": false,
|
|
42
53
|
"license": "ISC",
|
|
43
54
|
"dependencies": {
|
|
@@ -54,5 +65,8 @@
|
|
|
54
65
|
"ora": "^8.2.0",
|
|
55
66
|
"slicejs-web-framework": "latest",
|
|
56
67
|
"terser": "^5.43.1"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@playwright/test": "^1.60.0"
|
|
57
71
|
}
|
|
58
72
|
}
|
package/post.js
CHANGED
|
@@ -2,35 +2,27 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { getProjectRoot, getPath } from './commands/utils/PathHelper.js';
|
|
5
|
+
import { SLICE_SCRIPTS } from './commands/utils/sliceScripts.js';
|
|
5
6
|
|
|
6
7
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
// npm sets npm_config_global; pnpm does not — for pnpm a global install lives
|
|
10
|
+
// under PNPM_HOME, so detect it by where this script is running from.
|
|
11
|
+
const pnpmHome = process.env.PNPM_HOME;
|
|
12
|
+
const isGlobal = process.env.npm_config_global === 'true'
|
|
13
|
+
|| (pnpmHome && __filename.startsWith(pnpmHome));
|
|
9
14
|
|
|
10
15
|
if (isGlobal) {
|
|
11
16
|
console.log('⚠️ Global installation of slicejs-cli detected.');
|
|
12
17
|
console.log(' We strongly recommend using a local installation to avoid version mismatches.');
|
|
13
|
-
console.log(
|
|
18
|
+
console.log(` Uninstall global: ${pnpmHome ? 'pnpm remove -g slicejs-cli' : 'npm uninstall -g slicejs-cli'}`);
|
|
14
19
|
process.exit(0);
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
const projectRoot = getProjectRoot(import.meta.url);
|
|
18
23
|
const pkgPath = getPath(import.meta.url, 'package.json');
|
|
19
24
|
|
|
20
|
-
const sliceScripts =
|
|
21
|
-
'slice:dev': 'slice dev',
|
|
22
|
-
'slice:start': 'slice start',
|
|
23
|
-
'slice:create': 'slice component create',
|
|
24
|
-
'slice:list': 'slice component list',
|
|
25
|
-
'slice:delete': 'slice component delete',
|
|
26
|
-
'slice:init': 'slice init',
|
|
27
|
-
'slice:get': 'slice get',
|
|
28
|
-
'slice:browse': 'slice browse',
|
|
29
|
-
'slice:sync': 'slice sync',
|
|
30
|
-
'slice:version': 'slice version',
|
|
31
|
-
'slice:update': 'slice update',
|
|
32
|
-
'slice:types': 'slice types generate',
|
|
33
|
-
};
|
|
25
|
+
const sliceScripts = SLICE_SCRIPTS;
|
|
34
26
|
|
|
35
27
|
try {
|
|
36
28
|
let pkg = {};
|