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.
Files changed (39) hide show
  1. package/README.md +34 -15
  2. package/client.js +67 -20
  3. package/commands/createComponent/createComponent.js +6 -2
  4. package/commands/deleteComponent/deleteComponent.js +4 -0
  5. package/commands/doctor/doctor.js +78 -3
  6. package/commands/getComponent/getComponent.js +33 -25
  7. package/commands/init/init.js +106 -28
  8. package/commands/utils/PackageManager.js +148 -0
  9. package/commands/utils/VersionChecker.js +6 -4
  10. package/commands/utils/bundling/BundleGenerator.js +271 -38
  11. package/commands/utils/sliceScripts.js +21 -0
  12. package/commands/utils/updateManager.js +54 -35
  13. package/package.json +15 -1
  14. package/post.js +8 -16
  15. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
  16. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  17. package/.github/pull_request_template.md +0 -22
  18. package/AGENTS.md +0 -247
  19. package/CODE_OF_CONDUCT.md +0 -126
  20. package/ECOSYSTEM.md +0 -9
  21. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +0 -182
  22. package/tests/bundle-generator.test.js +0 -691
  23. package/tests/bundle-v2-register-output.test.js +0 -470
  24. package/tests/client-launcher-contract.test.js +0 -211
  25. package/tests/client-update-flow-contract.test.js +0 -272
  26. package/tests/component-registry-parse.test.js +0 -34
  27. package/tests/dependency-analyzer.test.js +0 -24
  28. package/tests/fixtures/components.js +0 -8
  29. package/tests/fixtures/sliceConfig.json +0 -74
  30. package/tests/getcomponent.test.js +0 -407
  31. package/tests/helpers/setup.js +0 -97
  32. package/tests/init-command-contract.test.js +0 -46
  33. package/tests/local-cli-delegation.test.js +0 -81
  34. package/tests/path-helper.test.js +0 -206
  35. package/tests/postinstall-command.test.js +0 -72
  36. package/tests/types-breakage.test.js +0 -491
  37. package/tests/types-generator-errors.test.js +0 -361
  38. package/tests/types-generator.test.js +0 -344
  39. package/tests/update-manager-notifications.test.js +0 -88
@@ -5,10 +5,62 @@ import ora from 'ora';
5
5
  import Print from '../Print.js';
6
6
  import { getProjectRoot, getApiPath, getSrcPath, getPath } from '../utils/PathHelper.js';
7
7
  import { execSync } from 'child_process';
8
+ import {
9
+ resolvePackageManager,
10
+ getPackageManagerVersion,
11
+ installCommand
12
+ } from '../utils/PackageManager.js';
13
+ import { SLICE_SCRIPTS } from '../utils/sliceScripts.js';
8
14
 
9
15
  // Import ComponentRegistry class from getComponent
10
16
  import { ComponentRegistry } from '../getComponent/getComponent.js';
11
17
 
18
+ // Fetch the latest published version straight from the npm registry. This is
19
+ // informational only (we never pin installs to it): it avoids depending on
20
+ // `npm view` (absent on pnpm-only machines) and plays nice with pnpm's
21
+ // minimumReleaseAge quarantine, which may legitimately resolve an older version.
22
+ async function fetchLatestVersion(packageName) {
23
+ try {
24
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
25
+ headers: { 'Accept': 'application/json' }
26
+ });
27
+ if (!response.ok) return null;
28
+ const data = await response.json();
29
+ return data.version || null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ // Create the project manifest BEFORE any install runs. Without a package.json in
36
+ // the project folder, npm/pnpm walk up the directory tree looking for the nearest
37
+ // manifest and anchor node_modules (and the dependency entry) OUTSIDE the project.
38
+ // Exported for tests (init-project-isolation.test.js).
39
+ export async function ensureProjectManifest(projectRoot, packageManager) {
40
+ const pkgPath = path.join(projectRoot, 'package.json');
41
+ if (await fs.pathExists(pkgPath)) return pkgPath;
42
+
43
+ const pkg = {
44
+ name: path.basename(projectRoot),
45
+ version: '1.0.0',
46
+ description: 'Slice.js project',
47
+ main: 'api/index.js',
48
+ type: 'module',
49
+ engines: { node: '>=20.0.0' },
50
+ scripts: {}
51
+ };
52
+
53
+ // Persist the chosen package manager (corepack convention) so every later
54
+ // command — slice update, slice doctor — detects it deterministically.
55
+ const pmVersion = getPackageManagerVersion(packageManager);
56
+ if (pmVersion) {
57
+ pkg.packageManager = `${packageManager}@${pmVersion}`;
58
+ }
59
+
60
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
61
+ return pkgPath;
62
+ }
63
+
12
64
  // Visual components used by the App Shell + MultiRoute starter project.
13
65
  // We install only these on init; newcomers add more on demand with `slice get <Name>`.
14
66
  const STARTER_VISUAL_COMPONENTS = [
@@ -30,32 +82,52 @@ const STARTER_SERVICE_COMPONENTS = [
30
82
  'LocalStorageManager'
31
83
  ];
32
84
 
33
- export default async function initializeProject(projectType) {
85
+ export default async function initializeProject(options = {}) {
34
86
  try {
35
87
  const projectRoot = getProjectRoot(import.meta.url);
36
88
  const destinationApi = getApiPath(import.meta.url);
37
89
  const destinationSrc = getSrcPath(import.meta.url);
38
90
 
91
+ // Resolve the package manager chosen in `slice init` (or detect it when
92
+ // initializeProject is invoked directly, e.g. inside an existing folder).
93
+ const packageManager = options.packageManager
94
+ || resolvePackageManager(projectRoot).name;
95
+
96
+ // 0. CREATE PROJECT MANIFEST FIRST — must exist before any install so the
97
+ // package manager anchors node_modules inside the project folder.
98
+ await ensureProjectManifest(projectRoot, packageManager);
99
+
39
100
  const fwSpinner = ora('Ensuring latest Slice framework...').start();
40
101
  let latestVersion = null;
102
+ let installedVersion = null;
41
103
  let sliceBaseDir;
42
104
  try {
43
- const latest = execSync('npm view slicejs-web-framework version', { cwd: projectRoot }).toString().trim();
44
- latestVersion = latest;
105
+ latestVersion = await fetchLatestVersion('slicejs-web-framework');
45
106
  const installedPkgPath = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework', 'package.json');
46
107
  let installed = null;
47
108
  if (await fs.pathExists(installedPkgPath)) {
48
109
  const pkg = await fs.readJson(installedPkgPath);
49
110
  installed = pkg.version;
50
111
  }
51
- if (installed !== latest) {
52
- execSync(`npm install slicejs-web-framework@${latest} --save`, { cwd: projectRoot, stdio: 'inherit' });
112
+ if (!installed || (latestVersion && installed !== latestVersion)) {
113
+ // Install WITHOUT pinning an exact version: the package manager
114
+ // resolves it under its own policies (e.g. pnpm minimumReleaseAge
115
+ // quarantines versions younger than the configured age — pinning
116
+ // the registry's freshest version would make resolution fail).
117
+ execSync(installCommand(packageManager, 'slicejs-web-framework'), { cwd: projectRoot, stdio: 'inherit' });
118
+ }
119
+ if (await fs.pathExists(installedPkgPath)) {
120
+ const pkg = await fs.readJson(installedPkgPath);
121
+ installedVersion = pkg.version;
53
122
  }
54
123
  sliceBaseDir = getPath(import.meta.url, 'node_modules', 'slicejs-web-framework');
55
- fwSpinner.succeed(`slicejs-web-framework@${latest} ready`);
124
+ fwSpinner.succeed(`slicejs-web-framework@${installedVersion || 'unknown'} ready`);
125
+ if (latestVersion && installedVersion && installedVersion !== latestVersion) {
126
+ Print.info(`Latest published is ${latestVersion}; your package manager resolved ${installedVersion} (release-age policy or cached registry).`);
127
+ }
56
128
  } catch (err) {
57
129
  // Fallback uses __dirname-style path because it looks for a local development copy,
58
- // not a project-relative path — npm install failed, so we fall back to monorepo sibling.
130
+ // not a project-relative path — the install failed, so we fall back to monorepo sibling.
59
131
  const fallback = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../slicejs-web-framework');
60
132
  if (await fs.pathExists(fallback)) {
61
133
  sliceBaseDir = fallback;
@@ -67,6 +139,21 @@ export default async function initializeProject(projectType) {
67
139
  }
68
140
  }
69
141
 
142
+ // 0b. INSTALL THE CLI LOCALLY (devDependency) so the generated scripts
143
+ // (`npm run dev` → `slice dev`) resolve via local delegation to a version
144
+ // pinned per project, as the docs recommend.
145
+ const cliSpinner = ora('Installing slicejs-cli as devDependency...').start();
146
+ try {
147
+ const cliPkgPath = getPath(import.meta.url, 'node_modules', 'slicejs-cli', 'package.json');
148
+ if (!(await fs.pathExists(cliPkgPath))) {
149
+ execSync(installCommand(packageManager, 'slicejs-cli', { dev: true }), { cwd: projectRoot, stdio: 'inherit' });
150
+ }
151
+ cliSpinner.succeed('slicejs-cli installed locally');
152
+ } catch (err) {
153
+ cliSpinner.warn('Could not install slicejs-cli locally — scripts will use the global CLI');
154
+ Print.info(`You can add it later with: ${installCommand(packageManager, 'slicejs-cli', { dev: true })}`);
155
+ }
156
+
70
157
  // These derive from sliceBaseDir (which comes from npm install or fallback),
71
158
  // so they're already dynamic — no PathHelper needed.
72
159
  const apiDir = path.join(sliceBaseDir, 'api');
@@ -245,6 +332,7 @@ export default async function initializeProject(projectType) {
245
332
 
246
333
  // Comandos principales
247
334
  pkg.scripts['dev'] = 'slice dev';
335
+ pkg.scripts['build'] = 'slice build';
248
336
  pkg.scripts['start'] = 'slice start';
249
337
 
250
338
  // Component management
@@ -257,39 +345,28 @@ export default async function initializeProject(projectType) {
257
345
  pkg.scripts['browse'] = 'slice browse';
258
346
  pkg.scripts['sync'] = 'slice sync';
259
347
 
260
- // Utilidades
261
- pkg.scripts['slice:version'] = 'slice version';
262
- pkg.scripts['slice:update'] = 'slice update';
263
- pkg.scripts['slice:types'] = 'slice types generate';
264
-
265
- // Legacy (compatibility)
266
- pkg.scripts['slice:init'] = 'slice init';
267
- pkg.scripts['slice:start'] = 'slice start';
268
- pkg.scripts['slice:dev'] = 'slice dev';
269
- pkg.scripts['slice:create'] = 'slice component create';
270
- pkg.scripts['slice:list'] = 'slice component list';
271
- pkg.scripts['slice:delete'] = 'slice component delete';
272
- pkg.scripts['slice:get'] = 'slice get';
273
- pkg.scripts['slice:browse'] = 'slice browse';
274
- pkg.scripts['slice:sync'] = 'slice sync';
348
+ // slice:* namespaced set — shared with post.js and `slice postinstall`
349
+ // (commands/utils/sliceScripts.js) so the three never drift apart.
350
+ Object.assign(pkg.scripts, SLICE_SCRIPTS);
275
351
  pkg.scripts['run'] = 'slice dev';
276
352
 
277
353
  // Module configuration
278
354
  pkg.type = 'module';
279
355
  pkg.engines = pkg.engines || { node: '>=20.0.0' };
280
356
 
281
- // Ensure framework dependency is present
357
+ // Ensure framework dependency is present (the install above normally
358
+ // already wrote it; this is a fallback for the monorepo-sibling path).
282
359
  if (!pkg.dependencies['slicejs-web-framework']) {
283
- pkg.dependencies['slicejs-web-framework'] = latestVersion ? latestVersion : 'latest';
360
+ pkg.dependencies['slicejs-web-framework'] = installedVersion ? `^${installedVersion}` : 'latest';
284
361
  }
285
362
 
286
363
  await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2), 'utf8');
287
- pkgSpinner.succeed('npm scripts configured successfully');
364
+ pkgSpinner.succeed('Package scripts configured successfully');
288
365
 
289
366
  Print.title('New recommended commands:');
290
- console.log(' npm run dev - Start development server');
291
- console.log(' npm run get - Install components');
292
- console.log(' npm run browse - Browse components');
367
+ console.log(` ${packageManager} run dev - Start development server`);
368
+ console.log(` ${packageManager} run get - Install components`);
369
+ console.log(` ${packageManager} run browse - Browse components`);
293
370
  } catch (error) {
294
371
  pkgSpinner.fail('Failed to configure npm scripts');
295
372
  Print.error(error.message);
@@ -300,6 +377,7 @@ export default async function initializeProject(projectType) {
300
377
  Print.newLine();
301
378
  Print.title('Next steps:');
302
379
  console.log(` cd ${projectName}`);
380
+ console.log(` ${packageManager} run dev - Start development server`);
303
381
  console.log(' slice browse - View available components');
304
382
  console.log(' slice get Button - Install specific components');
305
383
  console.log(' slice sync - Update all components to latest versions');
@@ -0,0 +1,148 @@
1
+ // commands/utils/PackageManager.js
2
+ //
3
+ // Package manager detection and command building (npm / pnpm / yarn).
4
+ // Resolution priority for an existing project:
5
+ // 1. "packageManager" field in the project package.json (corepack convention)
6
+ // 2. Lockfile present at the project root
7
+ // 3. npm_config_user_agent (set when the CLI runs via `npx` / `pnpm dlx` / a PM script)
8
+ // 4. The only PM binary available on PATH (if exactly one)
9
+ // When everything is ambiguous, detectPackageManager() returns null so callers
10
+ // can prompt the user (interactive init) or fall back to npm (non-interactive).
11
+
12
+ import fs from 'fs-extra'
13
+ import path from 'path'
14
+ import { spawnSync } from 'node:child_process'
15
+
16
+ export const SUPPORTED_PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn']
17
+
18
+ const LOCKFILES = {
19
+ 'pnpm-lock.yaml': 'pnpm',
20
+ 'package-lock.json': 'npm',
21
+ 'yarn.lock': 'yarn'
22
+ }
23
+
24
+ export function parseUserAgent(userAgent = process.env.npm_config_user_agent) {
25
+ if (!userAgent) return null
26
+ const match = userAgent.match(/^(npm|pnpm|yarn)\/(\S+)/)
27
+ if (!match) return null
28
+ return { name: match[1], version: match[2], source: 'user-agent' }
29
+ }
30
+
31
+ export function fromPackageManagerField(projectRoot) {
32
+ try {
33
+ const pkgPath = path.join(projectRoot, 'package.json')
34
+ if (!fs.pathExistsSync(pkgPath)) return null
35
+ const pkg = fs.readJsonSync(pkgPath)
36
+ if (typeof pkg.packageManager !== 'string') return null
37
+ const match = pkg.packageManager.match(/^(npm|pnpm|yarn)@(\S+)/)
38
+ if (!match) return null
39
+ return { name: match[1], version: match[2], source: 'package-manager-field' }
40
+ } catch {
41
+ return null
42
+ }
43
+ }
44
+
45
+ export function fromLockfile(projectRoot) {
46
+ for (const [lockfile, name] of Object.entries(LOCKFILES)) {
47
+ if (fs.pathExistsSync(path.join(projectRoot, lockfile))) {
48
+ return { name, version: null, source: `lockfile (${lockfile})` }
49
+ }
50
+ }
51
+ return null
52
+ }
53
+
54
+ function runPmBinary(name, args) {
55
+ // On Windows the PM entry points are .cmd shims, which require shell resolution.
56
+ const isWindows = process.platform === 'win32'
57
+ return spawnSync(name, args, {
58
+ stdio: ['ignore', 'pipe', 'ignore'],
59
+ shell: isWindows,
60
+ encoding: 'utf-8'
61
+ })
62
+ }
63
+
64
+ export function isPackageManagerAvailable(name) {
65
+ try {
66
+ const result = runPmBinary(name, ['--version'])
67
+ return result.status === 0
68
+ } catch {
69
+ return false
70
+ }
71
+ }
72
+
73
+ export function getPackageManagerVersion(name) {
74
+ try {
75
+ const result = runPmBinary(name, ['--version'])
76
+ if (result.status !== 0) return null
77
+ return (result.stdout || '').toString().trim() || null
78
+ } catch {
79
+ return null
80
+ }
81
+ }
82
+
83
+ export function getAvailablePackageManagers() {
84
+ return SUPPORTED_PACKAGE_MANAGERS.filter(isPackageManagerAvailable)
85
+ }
86
+
87
+ /**
88
+ * Detect the package manager for a project.
89
+ * Returns { name, version, source } or null when genuinely ambiguous.
90
+ */
91
+ export function detectPackageManager(projectRoot, { userAgent = process.env.npm_config_user_agent } = {}) {
92
+ if (projectRoot) {
93
+ const fromField = fromPackageManagerField(projectRoot)
94
+ if (fromField) return fromField
95
+
96
+ const fromLock = fromLockfile(projectRoot)
97
+ if (fromLock) return fromLock
98
+ }
99
+
100
+ const fromUa = parseUserAgent(userAgent)
101
+ if (fromUa) return fromUa
102
+
103
+ const available = getAvailablePackageManagers()
104
+ if (available.length === 1) {
105
+ return { name: available[0], version: null, source: 'only available binary' }
106
+ }
107
+
108
+ return null
109
+ }
110
+
111
+ /**
112
+ * Non-interactive resolution: detection first, then npm if available,
113
+ * then whatever binary exists. Never returns null so update/doctor flows
114
+ * always have a usable PM name (commands fail later with a clear error
115
+ * if no PM is actually installed).
116
+ */
117
+ export function resolvePackageManager(projectRoot, options = {}) {
118
+ const detected = detectPackageManager(projectRoot, options)
119
+ if (detected) return detected
120
+
121
+ const available = getAvailablePackageManagers()
122
+ if (available.includes('npm')) return { name: 'npm', version: null, source: 'fallback' }
123
+ if (available.length > 0) return { name: available[0], version: null, source: 'fallback' }
124
+ return { name: 'npm', version: null, source: 'fallback (none detected)' }
125
+ }
126
+
127
+ export function installCommand(pmName, packages, { dev = false, global: isGlobal = false } = {}) {
128
+ const pkgs = Array.isArray(packages) ? packages.join(' ') : packages
129
+ const flags = [isGlobal ? '-g' : '', dev ? '-D' : ''].filter(Boolean).join(' ')
130
+ const flagSuffix = flags ? ` ${flags}` : ''
131
+ if (pmName === 'pnpm' || pmName === 'yarn') {
132
+ return `${pmName} add${flagSuffix} ${pkgs}`
133
+ }
134
+ return `npm install${flagSuffix} ${pkgs}`
135
+ }
136
+
137
+ export function uninstallCommand(pmName, packages, { global: isGlobal = false } = {}) {
138
+ const pkgs = Array.isArray(packages) ? packages.join(' ') : packages
139
+ const flagSuffix = isGlobal ? ' -g' : ''
140
+ if (pmName === 'pnpm' || pmName === 'yarn') {
141
+ return `${pmName} remove${flagSuffix} ${pkgs}`
142
+ }
143
+ return `npm uninstall${flagSuffix} ${pkgs}`
144
+ }
145
+
146
+ export function runScriptCommand(pmName, script) {
147
+ return `${pmName} run ${script}`
148
+ }
@@ -5,6 +5,7 @@ import path from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import Print from "../Print.js";
7
7
  import { getProjectRoot, getPath } from "../utils/PathHelper.js";
8
+ import { resolvePackageManager } from "../utils/PackageManager.js";
8
9
 
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
 
@@ -110,17 +111,18 @@ class VersionChecker {
110
111
  const frameworkStatus = this.compareVersions(current.framework, latest.framework);
111
112
 
112
113
  if (!silent && (cliStatus === 'outdated' || frameworkStatus === 'outdated')) {
114
+ const pm = resolvePackageManager(getProjectRoot(import.meta.url)).name;
113
115
  console.log(''); // Line break
114
116
  Print.warning('📦 Available Updates:');
115
-
117
+
116
118
  if (cliStatus === 'outdated') {
117
119
  console.log(` 🔧 CLI: ${current.cli} → ${latest.cli}`);
118
- console.log(` npm update slicejs-cli`);
120
+ console.log(` ${pm} update slicejs-cli`);
119
121
  }
120
-
122
+
121
123
  if (frameworkStatus === 'outdated') {
122
124
  console.log(` ⚡ Framework: ${current.framework} → ${latest.framework}`);
123
- console.log(` npm update slicejs-web-framework`);
125
+ console.log(` ${pm} update slicejs-web-framework`);
124
126
  }
125
127
 
126
128
  console.log(' 📚 Changelog: https://github.com/VKneider/slice.js/releases');