slicejs-cli 3.5.1 → 3.6.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.
Files changed (75) hide show
  1. package/README.md +81 -26
  2. package/client.js +73 -23
  3. package/commands/buildProduction/buildProduction.js +6 -3
  4. package/commands/doctor/doctor.js +68 -3
  5. package/commands/getComponent/getComponent.js +33 -25
  6. package/commands/init/init.js +176 -49
  7. package/commands/utils/PackageManager.js +148 -0
  8. package/commands/utils/VersionChecker.js +6 -4
  9. package/commands/utils/sliceScripts.js +23 -0
  10. package/commands/utils/updateManager.js +54 -35
  11. package/package.json +12 -1
  12. package/post.js +13 -19
  13. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -29
  14. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -25
  15. package/.github/pull_request_template.md +0 -22
  16. package/.github/workflows/ci.yml +0 -43
  17. package/AGENTS.md +0 -247
  18. package/CODE_OF_CONDUCT.md +0 -126
  19. package/ECOSYSTEM.md +0 -9
  20. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +0 -182
  21. package/playwright.config.js +0 -51
  22. package/tests/build-command-integration.test.js +0 -87
  23. package/tests/build-production-e2e.test.js +0 -140
  24. package/tests/builder-edge-cases.test.js +0 -322
  25. package/tests/bundle-generate-e2e.test.js +0 -115
  26. package/tests/bundle-generator.test.js +0 -691
  27. package/tests/bundle-v2-register-output.test.js +0 -470
  28. package/tests/bundling-dependency-edges.test.js +0 -127
  29. package/tests/bundling-imports-unit.test.js +0 -267
  30. package/tests/client-launcher-contract.test.js +0 -211
  31. package/tests/client-update-flow-contract.test.js +0 -272
  32. package/tests/commands-component-crud.test.js +0 -102
  33. package/tests/commands-doctor.test.js +0 -80
  34. package/tests/commands-version-checker.test.js +0 -37
  35. package/tests/component-registry-parse.test.js +0 -34
  36. package/tests/dependency-analyzer.test.js +0 -24
  37. package/tests/e2e/bundles.spec.js +0 -91
  38. package/tests/e2e/dependency-scenarios.spec.js +0 -56
  39. package/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
  40. package/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
  41. package/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
  42. package/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
  43. package/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
  44. package/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
  45. package/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
  46. package/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
  47. package/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
  48. package/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
  49. package/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
  50. package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
  51. package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
  52. package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
  53. package/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
  54. package/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
  55. package/tests/e2e/fixtures/components/registry.json +0 -12
  56. package/tests/e2e/fixtures/vendor-components.mjs +0 -65
  57. package/tests/e2e/navigation.spec.js +0 -44
  58. package/tests/e2e/render.spec.js +0 -34
  59. package/tests/e2e/serve.mjs +0 -264
  60. package/tests/e2e/shared-deps.spec.js +0 -61
  61. package/tests/e2e/unminified.spec.js +0 -33
  62. package/tests/e2e-serve.test.js +0 -148
  63. package/tests/fixtures/components.js +0 -8
  64. package/tests/fixtures/sliceConfig.json +0 -74
  65. package/tests/getcomponent.test.js +0 -407
  66. package/tests/helpers/setup.js +0 -102
  67. package/tests/init-command-contract.test.js +0 -46
  68. package/tests/local-cli-delegation.test.js +0 -81
  69. package/tests/path-helper.test.js +0 -206
  70. package/tests/perf-budget.test.js +0 -86
  71. package/tests/postinstall-command.test.js +0 -72
  72. package/tests/types-breakage.test.js +0 -491
  73. package/tests/types-generator-errors.test.js +0 -361
  74. package/tests/types-generator.test.js +0 -346
  75. package/tests/update-manager-notifications.test.js +0 -88
@@ -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, globalPrefix };
55
+ return { type: 'global', cliRoot, projectRoot, packageManager: 'npm' };
39
56
  }
40
- return { type: 'unknown', cliRoot, projectRoot, globalPrefix };
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
- if (info.type === 'global') {
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: `npm install ${pkg}@latest` });
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
- let installCmd = `npm install ${packageName}@latest`;
205
- let uninstallCmd = `npm uninstall ${packageName}`;
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
- installCmd = `npm install -g slicejs-cli@latest`;
212
- uninstallCmd = `npm uninstall -g slicejs-cli`;
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
- // Try uninstall first (ignore failure)
221
- try {
222
- await execAsync(uninstallCmd, options);
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(' Suggestion: npm install -g slicejs-cli@latest');
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
- let confirmUpdate = options.yes === true;
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.5.1",
3
+ "version": "3.6.1",
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": {
package/post.js CHANGED
@@ -2,35 +2,29 @@ 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';
6
+ import { resolvePackageManager, runScriptCommand } from './commands/utils/PackageManager.js';
5
7
 
6
8
  const __filename = fileURLToPath(import.meta.url);
7
9
 
8
- const isGlobal = process.env.npm_config_global === 'true';
10
+ // npm sets npm_config_global; pnpm does not — for pnpm a global install lives
11
+ // under PNPM_HOME, so detect it by where this script is running from.
12
+ const pnpmHome = process.env.PNPM_HOME;
13
+ const isGlobal = process.env.npm_config_global === 'true'
14
+ || (pnpmHome && __filename.startsWith(pnpmHome));
9
15
 
10
16
  if (isGlobal) {
11
17
  console.log('⚠️ Global installation of slicejs-cli detected.');
12
18
  console.log(' We strongly recommend using a local installation to avoid version mismatches.');
13
- console.log(' Uninstall global: npm uninstall -g slicejs-cli');
19
+ console.log(` Uninstall global: ${pnpmHome ? 'pnpm remove -g slicejs-cli' : 'npm uninstall -g slicejs-cli'}`);
14
20
  process.exit(0);
15
21
  }
16
22
 
17
23
  const projectRoot = getProjectRoot(import.meta.url);
18
24
  const pkgPath = getPath(import.meta.url, 'package.json');
25
+ const packageManager = resolvePackageManager(projectRoot).name;
19
26
 
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
- };
27
+ const sliceScripts = SLICE_SCRIPTS;
34
28
 
35
29
  try {
36
30
  let pkg = {};
@@ -55,12 +49,12 @@ try {
55
49
  }
56
50
 
57
51
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), 'utf-8');
58
- console.log(`✅ slicejs-cli installed successfully. Added ${addedCount} npm scripts to package.json.`);
59
- console.log(' Run: npm run slice:dev');
52
+ console.log(`✅ slicejs-cli installed successfully. Added ${addedCount} package scripts to package.json.`);
53
+ console.log(` Run: ${runScriptCommand(packageManager, 'slice:dev')}`);
60
54
  } catch (err) {
61
55
  console.log('✅ slicejs-cli installed successfully.');
62
56
  console.log(' Could not auto-configure scripts:', err.message);
63
- console.log(' Run: npx slice dev');
57
+ console.log(` Configure scripts manually and run: ${runScriptCommand(packageManager, 'slice:dev')}`);
64
58
  }
65
59
 
66
60
  process.exit(0);
@@ -1,29 +0,0 @@
1
- ---
2
- name: Bug Report
3
- about: Report a bug in a component or the parser
4
- title: ''
5
- labels: bug
6
- assignees: ''
7
- ---
8
-
9
- ## Description
10
- A clear description of the bug.
11
-
12
- ## Reproduction
13
- Steps to reproduce:
14
- 1. Go to '...'
15
- 2. Click on '...'
16
- 3. Error: '...'
17
-
18
- ## Expected behavior
19
- What should have happened.
20
-
21
- ## Environment
22
- - Browser/Node version:
23
- - OS:
24
- - slice.js_visual_library version:
25
-
26
- ## Screenshots / Console output
27
- If applicable.
28
-
29
- ## Additional context
@@ -1,25 +0,0 @@
1
- ---
2
- name: Feature Request
3
- about: Suggest a new component or improvement
4
- title: ''
5
- labels: enhancement
6
- assignees: ''
7
- ---
8
-
9
- ## Problem
10
- What problem does this solve? (e.g. "I need a component that...")
11
-
12
- ## Proposed solution
13
- How should it work? Include props, behavior, and visual details if applicable.
14
-
15
- ## Alternatives considered
16
- What other approaches have you considered?
17
-
18
- ## Example usage
19
- ```javascript
20
- const component = await slice.build('ComponentName', {
21
- // props here
22
- });
23
- ```
24
-
25
- ## Additional context
@@ -1,22 +0,0 @@
1
- ## Summary
2
- -
3
-
4
- ## Docs Scope
5
- - [ ] Visual library docs pages (`src/markdown/` and generated outputs)
6
- - [ ] Parser logic (`parser/`)
7
- - [ ] Routes/index sync (`documentationRoutes.generated.js`, `docsIndex.js`, `components.js`)
8
-
9
- ## Verification
10
- - [ ] `npm run docs:lint-md`
11
- - [ ] `npm run docs:generate`
12
- - [ ] `node --test parser/tests/index.test.js`
13
-
14
- ## Screenshots / UI Notes
15
- -
16
-
17
- ## Breaking Changes
18
- - [ ] None
19
- - [ ] Yes (describe below)
20
-
21
- ## Additional Context
22
- -
@@ -1,43 +0,0 @@
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
package/AGENTS.md DELETED
@@ -1,247 +0,0 @@
1
- # Slice.js CLI — Agent Context
2
-
3
- ## Project Structure
4
-
5
- ```
6
- slicejs-cli/
7
- ├── client.js # CLI entry point (commander)
8
- ├── commands/
9
- │ ├── init/init.js # slice init
10
- │ ├── startServer/startServer.js # slice dev / slice start
11
- │ ├── build/build.js # slice build
12
- │ ├── getComponent/getComponent.js # slice get / browse / sync
13
- │ ├── createComponent/ # slice component create
14
- │ ├── listComponents/ # slice component list
15
- │ ├── deleteComponent/ # slice component delete
16
- │ ├── doctor/doctor.js # slice doctor
17
- │ ├── types/types.js # slice types generate
18
- │ ├── bundle/bundle.js # bundling logic
19
- │ ├── utils/
20
- │ │ ├── PathHelper.js # Path resolution (critical)
21
- │ │ ├── bundling/BundleGenerator.js
22
- │ │ ├── updateManager.js
23
- │ │ ├── VersionChecker.js
24
- │ │ └── LocalCliDelegation.js
25
- │ └── Print.js # Wrapper for console.log/error
26
- ├── tests/
27
- │ ├── helpers/setup.js # Shared test helper (createTestProject, withTestProject)
28
- │ ├── fixtures/ # Minimal fixture files for tests
29
- │ ├── bundle-generator.test.js
30
- │ ├── bundle-v2-register-output.test.js
31
- │ ├── client-launcher-contract.test.js
32
- │ ├── client-update-flow-contract.test.js
33
- │ ├── component-registry-parse.test.js
34
- │ ├── dependency-analyzer.test.js
35
- │ ├── init-command-contract.test.js
36
- │ ├── local-cli-delegation.test.js
37
- │ ├── path-helper.test.js
38
- │ ├── postinstall-command.test.js
39
- │ ├── types-generator.test.js
40
- │ └── update-manager-notifications.test.js
41
- ├── package.json # type: "module" — ES modules only
42
- └── AGENTS.md # This file
43
- ```
44
-
45
- ## Testing System
46
-
47
- ### Runner
48
- - Uses Node.js built-in test runner: `node --test`
49
- - Run: `npm test`
50
- - Watch mode: `node --test --watch`
51
-
52
- ### Shared Test Helper (`tests/helpers/setup.js`)
53
- Three exported functions:
54
-
55
- ```js
56
- import { createTestProject, cleanupTestProject, withTestProject } from './helpers/setup.js';
57
- ```
58
-
59
- **`createTestProject(options)`** — Creates a temp directory with full Slice.js project scaffold.
60
- - Copies real framework files from `../slice.js/` (sibling directory in monorepo)
61
- - Falls back to `tests/fixtures/` minimal scaffold if framework not available
62
- - Options:
63
- - `visualComponents: ['Button']` — creates stub component files + rewrites `components.js` to include only those
64
- - `frameworkDir` — custom framework source path
65
- - Returns the temp directory path
66
- - Temp dir path: `{os.tmpdir()}/slice-test-{PID}-{N}-{random}/`
67
-
68
- **`cleanupTestProject(dir)`** — Removes the temp directory recursively.
69
-
70
- **`withTestProject(fn, options)`** — Convenience wrapper that:
71
- 1. Calls `createTestProject(options)`
72
- 2. Saves and sets `process.env.INIT_CWD = dir`
73
- 3. Runs `fn(dir)`
74
- 4. Restores `process.env.INIT_CWD` to original value
75
- 5. Calls `cleanupTestProject(dir)` in `finally`
76
-
77
- ### Patterns
78
-
79
- **For tests that need INIT_CWD pointing to the project:**
80
- ```js
81
- test('my test', async () => {
82
- await withTestProject(async (tmpDir) => {
83
- // process.env.INIT_CWD is already set to tmpDir
84
- const result = someFunction(import.meta.url);
85
- assert.ok(result);
86
- });
87
- });
88
- ```
89
-
90
- **For tests that pass projectRoot explicitly:**
91
- ```js
92
- test('my test', async () => {
93
- const tmpRoot = await createTestProject({ visualComponents: ['Button'] });
94
- try {
95
- const result = await someFunction({ projectRoot: tmpRoot });
96
- assert.equal(result, 1);
97
- } finally {
98
- await cleanupTestProject(tmpRoot);
99
- }
100
- });
101
- ```
102
-
103
- **For tests with shared project setup across a describe block:**
104
- ```js
105
- let tmpRoot;
106
- before(async () => {
107
- tmpRoot = await createTestProject();
108
- process.env.INIT_CWD = tmpRoot;
109
- });
110
- after(async () => {
111
- delete process.env.INIT_CWD;
112
- await cleanupTestProject(tmpRoot);
113
- });
114
- ```
115
-
116
- ### Test types
117
-
118
- 1. **Contract tests** (`*-contract.test.js`) — Static analysis of `client.js` source code via `@babel/parser` + AST or regex. Verify command registration, option flags, and function calls. No runtime execution.
119
- 2. **Unit tests** — Test individual functions/modules in isolation. Use temp dirs for filesystem-dependent code.
120
- 3. **Snapshot/Integration tests** — Verify output files, generated declarations, bundle configs.
121
-
122
- ### Rules
123
- - No external mocking libraries (sinon, jest, etc.). Use monkey-patching + try/finally restore.
124
- - All temp dirs MUST be cleaned up in `finally` blocks.
125
- - `process.env.INIT_CWD` must be saved before modification and restored in `finally`.
126
- - Dynamic `import()` is used where module caching matters, but PathHelper reads env vars at call time so cached modules work correctly.
127
-
128
- ## PathHelper Rules (`commands/utils/PathHelper.js`)
129
-
130
- ### Project Root Resolution
131
- `getProjectRoot(moduleUrl)` resolves in this order:
132
- 1. `process.env.INIT_CWD` — set by npm or by `withTestProject` during tests
133
- 2. `process.cwd()` — current working directory
134
- 3. `candidates(moduleUrl)` — heuristic: walk up `../../` and `../../../../` from module location, check for `src/` or `api/`
135
-
136
- ### Functions
137
-
138
- | Function | Returns | Notes |
139
- |---|---|---|
140
- | `getProjectRoot(moduleUrl)` | Resolved project root path | |
141
- | `getSrcPath(moduleUrl, ...seg)` | `<root>/src/[...seg]` | |
142
- | `getApiPath(moduleUrl, ...seg)` | `<root>/api/[...seg]` | |
143
- | `getDistPath(moduleUrl, ...seg)` | `<root>/dist/[...seg]` | |
144
- | `getPath(moduleUrl, ...seg)` | `<root>/[...seg]` | General purpose |
145
- | `getConfigPath(moduleUrl, root?)` | `src/sliceConfig.json` | Optional explicit root param |
146
- | `getComponentsJsPath(moduleUrl, root?)` | `src/Components/components.js` | Optional explicit root param |
147
- | `joinRoot(root, ...seg)` | `<root>/[...seg]` | No moduleUrl needed, pure path join |
148
-
149
- ### Critical Rules
150
-
151
- 1. **`import.meta.url` must be passed** as first argument to all PathHelper functions (except `joinRoot`).
152
- 2. **Explicit root parameter** (`getConfigPath`, `getComponentsJsPath`) is used by `types/types.js` when generating types for a non-cwd project. This keeps functions testable without global state.
153
- 3. **`INIT_CWD` is the primary mechanism** for project root resolution. It's set by npm lifecycle scripts and by `withTestProject`.
154
- 4. **`candidates()` fallback** only works when the CLI is installed inside a project that has `src/` or `api/`. This is intentionally limited.
155
-
156
- ## Code Quality Standards
157
-
158
- ### ES Modules Only
159
- - `"type": "module"` in `package.json`
160
- - Use `import`/`export` everywhere
161
- - NO `require()`, NO `__dirname` at module scope (use `path.dirname(fileURLToPath(import.meta.url))` inline where needed)
162
-
163
- ### No eval()
164
- - `eval()` has been fully replaced with `JSON.parse()` for reading `components.js` files
165
- - Components are written via `JSON.stringify()`, so content is always valid JSON
166
- - Use `JSON.parse()` or the AST-based `ComponentRegistry` for component registry parsing
167
-
168
- ### Error Messages
169
- - Bare error messages (just the error message without context) must NOT be used
170
- - Always wrap errors with context: `Print.error('Context:', error.message)`
171
- - Use `Print.error()` / `Print.success()` / `Print.info()` / `Print.warning()` instead of raw `console.log`/`console.error`
172
- - EXCEPTION: Formatted help/command listing output can use `console.log` directly (avoids `ℹ️ Info:` prefix pollution)
173
-
174
- ### Empty Catch Blocks
175
- - Silent catches are acceptable ONLY for:
176
- - Non-critical operations (update checks, optional config reads)
177
- - Graceful degradation paths
178
- - All silent catches MUST have a comment explaining why: `catch { /* intentional: non-critical */ }`
179
-
180
- ### Port Resolution (startServer)
181
- Priority order:
182
- 1. `--port` CLI flag (if provided by user)
183
- 2. `config.server.port` from `sliceConfig.json`
184
- 3. Hardcoded `3000` fallback
185
-
186
- Commander `.option()` defaults must NOT override config values. Pass `undefined` when flag is not provided:
187
- ```js
188
- port: options.port ? parseInt(options.port) : undefined
189
- ```
190
-
191
- ### Dependency Injection for Testability
192
- - Functions that need a project root accept it as a parameter (`projectRoot`, `root`)
193
- - PathHelper functions that accept an explicit root param enable testing without INIT_CWD gymnastics
194
- - Avoid reading `process.env.INIT_CWD` or `process.cwd()` directly inside business logic; use PathHelper
195
-
196
- ## CLI Architecture (client.js)
197
-
198
- ### Command Registration Pattern
199
- ```js
200
- sliceClient
201
- .command("mycommand")
202
- .description("...")
203
- .option("-x, --flag <value>", "...")
204
- .action(async (options) => {
205
- // 1. Handle --yes / non-interactive flags before prompts
206
- // 2. Prompt for missing required values
207
- // 3. Delegate to command implementation
208
- await runWithVersionCheck(async () => {
209
- await myCommandImplementation(options);
210
- });
211
- });
212
- ```
213
-
214
- ### `runWithVersionCheck(commandFunction)`
215
- - Wraps every command action
216
- - Responsibilities:
217
- 1. Fire-and-forget update notification (`notifyAvailableUpdates().catch(() => {})`)
218
- 2. Execute the command
219
- 3. Background version check (`checkForUpdates(false)` after 100ms delay)
220
- - Does NOT block or prompt the user (pre-flight checks were removed)
221
- - Errors are caught and logged via `Print.error()`
222
-
223
- ### Init Command (`slice init`)
224
- - Default project name: `my-slice-app`
225
- - `-y`/`--yes [name]` flag skips interactive prompts
226
- - Creates project directory, `chdir`s into it, sets `INIT_CWD`
227
- - Calls `initializeProject()` from `commands/init/init.js`
228
- - Name normalization: trim → lowercase → spaces to hyphens → strip non-alphanumeric → collapse hyphens → trim hyphens
229
-
230
- ### Local CLI Delegation
231
- - `maybeDelegateToLocalCli()` runs at module level before command parsing
232
- - If a local `node_modules/slicejs-cli/` exists, spawns it instead of running the global CLI
233
- - Controlled by `SLICE_NO_LOCAL_DELEGATION` env var
234
-
235
- ## Visual Component Registry
236
- - Components downloaded from GitHub: `https://raw.githubusercontent.com/VKneider/slice.js_visual_library/master/src/Components/{category}/{Name}/{file}`
237
- - Registry URL: same base + `src/Components/components.js`
238
- - Starter visual components on init: Button, Link, Loading, MultiRoute, Navbar, NotFound, Route
239
- - Components are registered by writing to `src/Components/components.js`
240
-
241
- ### File Download Rules (in `getAvailableComponents`)
242
- - **Routing/navigation components** (`Route`, `MultiRoute`, `Link`): only `.js` file
243
- - **Other Visual components** (Button, Loading, Navbar, etc.): `.js`, `.html`, `.css`
244
- - **Service components** (FetchManager, etc.): only `.js` file
245
- - File list is determined by hardcoded rules, NOT by checking the remote server
246
- - If `.js` download fails → component install fails (fatal)
247
- - If `.html`/`.css` fails → component install succeeds with warning