slicejs-cli 3.4.0 → 3.5.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 (35) hide show
  1. package/AGENTS.md +247 -0
  2. package/client.js +63 -64
  3. package/commands/Print.js +11 -15
  4. package/commands/Validations.js +12 -23
  5. package/commands/buildProduction/buildProduction.js +23 -26
  6. package/commands/bundle/bundle.js +10 -11
  7. package/commands/createComponent/createComponent.js +14 -16
  8. package/commands/deleteComponent/deleteComponent.js +6 -6
  9. package/commands/doctor/doctor.js +11 -14
  10. package/commands/getComponent/getComponent.js +99 -162
  11. package/commands/init/init.js +77 -26
  12. package/commands/listComponents/listComponents.js +18 -21
  13. package/commands/startServer/startServer.js +21 -24
  14. package/commands/startServer/watchServer.js +7 -7
  15. package/commands/types/types.js +53 -18
  16. package/commands/utils/PathHelper.js +9 -2
  17. package/commands/utils/VersionChecker.js +3 -3
  18. package/commands/utils/bundling/DependencyAnalyzer.js +8 -16
  19. package/commands/utils/loadConfig.js +31 -0
  20. package/commands/utils/updateManager.js +3 -4
  21. package/docs/superpowers/specs/2026-05-10-pwa-generate-design.md +105 -105
  22. package/package.json +14 -2
  23. package/post.js +2 -2
  24. package/tests/bundle-generator.test.js +3 -20
  25. package/tests/component-registry-parse.test.js +34 -0
  26. package/tests/fixtures/components.js +8 -0
  27. package/tests/fixtures/sliceConfig.json +74 -0
  28. package/tests/getcomponent.test.js +407 -0
  29. package/tests/helpers/setup.js +97 -0
  30. package/tests/init-command-contract.test.js +46 -0
  31. package/tests/local-cli-delegation.test.js +7 -5
  32. package/tests/path-helper.test.js +206 -0
  33. package/tests/types-breakage.test.js +491 -0
  34. package/tests/types-generator-errors.test.js +361 -0
  35. package/tests/types-generator.test.js +172 -184
@@ -0,0 +1,97 @@
1
+ import fs from 'fs-extra'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const FW_DIR = path.resolve(__dirname, '../../../slice.js')
8
+ const FIXTURES_DIR = path.resolve(__dirname, '../fixtures')
9
+
10
+ let counter = 0
11
+
12
+ export async function createTestProject(options = {}) {
13
+ const {
14
+ visualComponents = [],
15
+ frameworkDir = FW_DIR,
16
+ } = options
17
+
18
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), `slice-test-${process.pid}-${counter++}-`))
19
+
20
+ const fwExists = await fs.pathExists(frameworkDir)
21
+ const srcDir = path.join(dir, 'src')
22
+
23
+ if (fwExists) {
24
+ const fwSrc = path.join(frameworkDir, 'src')
25
+ const fwApi = path.join(frameworkDir, 'api')
26
+
27
+ if (await fs.pathExists(fwSrc)) {
28
+ await fs.copy(fwSrc, srcDir)
29
+ }
30
+ if (await fs.pathExists(fwApi)) {
31
+ await fs.copy(fwApi, path.join(dir, 'api'))
32
+ }
33
+ }
34
+
35
+ if (!(await fs.pathExists(srcDir))) {
36
+ await createMinimalScaffold(dir)
37
+ }
38
+
39
+ await fs.ensureDir(path.join(srcDir, 'Components', 'Visual'))
40
+
41
+ if (visualComponents.length > 0) {
42
+ const registryLines = visualComponents.map(n => ` "${n}": "Visual"`).join(',\n')
43
+ await fs.writeFile(
44
+ path.join(srcDir, 'Components', 'components.js'),
45
+ `const components = {\n${registryLines}\n}; export default components;\n`
46
+ )
47
+ for (const name of visualComponents) {
48
+ const compDir = path.join(srcDir, 'Components', 'Visual', name)
49
+ await fs.ensureDir(compDir)
50
+ await fs.writeFile(path.join(compDir, `${name}.js`), `export default class ${name} {}`)
51
+ }
52
+ }
53
+
54
+ return dir
55
+ }
56
+
57
+ export async function cleanupTestProject(dir) {
58
+ await fs.remove(dir)
59
+ }
60
+
61
+ export async function withTestProject(fn, options = {}) {
62
+ const dir = await createTestProject(options)
63
+ const origInitCwd = process.env.INIT_CWD
64
+ try {
65
+ process.env.INIT_CWD = dir
66
+ return await fn(dir)
67
+ } finally {
68
+ if (origInitCwd === undefined) {
69
+ delete process.env.INIT_CWD
70
+ } else {
71
+ process.env.INIT_CWD = origInitCwd
72
+ }
73
+ await cleanupTestProject(dir)
74
+ }
75
+ }
76
+
77
+ async function createMinimalScaffold(dir) {
78
+ const srcDir = path.join(dir, 'src')
79
+ await fs.ensureDir(path.join(srcDir, 'App'))
80
+ await fs.ensureDir(path.join(srcDir, 'Components', 'AppComponents', 'AppShell'))
81
+ await fs.ensureDir(path.join(srcDir, 'Components', 'Service', 'FetchManager'))
82
+ await fs.ensureDir(path.join(srcDir, 'Components', 'Visual'))
83
+ await fs.ensureDir(path.join(srcDir, 'Styles'))
84
+ await fs.ensureDir(path.join(srcDir, 'Themes'))
85
+ await fs.ensureDir(path.join(dir, 'api', 'middleware'))
86
+ await fs.ensureDir(path.join(dir, 'api', 'utils'))
87
+
88
+ const fixtureConfig = path.join(FIXTURES_DIR, 'sliceConfig.json')
89
+ if (await fs.pathExists(fixtureConfig)) {
90
+ await fs.copy(fixtureConfig, path.join(srcDir, 'sliceConfig.json'))
91
+ }
92
+
93
+ const fixtureComponents = path.join(FIXTURES_DIR, 'components.js')
94
+ if (await fs.pathExists(fixtureComponents)) {
95
+ await fs.copy(fixtureComponents, path.join(srcDir, 'Components', 'components.js'))
96
+ }
97
+ }
@@ -0,0 +1,46 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const clientPath = path.join(__dirname, '..', 'client.js');
9
+ const source = fs.readFileSync(clientPath, 'utf-8');
10
+
11
+ test('init command is registered in client.js', () => {
12
+ const hasInit = source.includes('.command("init")');
13
+ assert.ok(hasInit, 'client.js must register an "init" command');
14
+ });
15
+
16
+ test('init command has a description', () => {
17
+ const match = source.match(/\.command\(["']init["']\)\s*\.description\(["']([^"']+)["']\)/);
18
+ assert.ok(match, 'init command must have a .description() call');
19
+ assert.ok(match[1].length > 0, 'init command description must not be empty');
20
+ });
21
+
22
+ test('init command has -y / --yes option', () => {
23
+ const hasShort = source.includes('"-y, --yes');
24
+ assert.ok(hasShort, 'init command must have a -y/--yes option for non-interactive use');
25
+ });
26
+
27
+ test('init command action calls initializeProject', () => {
28
+ const hasCall = source.includes('initializeProject()');
29
+ assert.ok(hasCall, 'init command action must call initializeProject');
30
+ });
31
+
32
+ test('init command normalizes project name (lowercase, hyphens)', () => {
33
+ const hasFilter = source.includes('.toLowerCase()');
34
+ assert.ok(hasFilter, 'init command must normalize project name to lowercase');
35
+ assert.ok(source.includes('.replace(/\\s+/g'), 'init command must replace spaces with hyphens');
36
+ });
37
+
38
+ test('init command validates project name', () => {
39
+ assert.ok(source.includes("'Project name cannot be empty'"), 'init command must validate non-empty name');
40
+ assert.ok(source.includes("'Use a simple name, not a path'"), 'init command must reject path separators');
41
+ });
42
+
43
+ test('init command creates project directory', () => {
44
+ assert.ok(source.includes('fs.mkdirSync(projectDir'), 'init command must create the project directory');
45
+ assert.ok(source.includes('process.chdir(projectDir)'), 'init command must chdir into project');
46
+ });
@@ -43,20 +43,22 @@ test('findNearestLocalCliEntry returns null when no candidate resolver hits', ()
43
43
  });
44
44
 
45
45
  test('findNearestLocalCliEntry returns first match while traversing upward', () => {
46
- const cwd = '/repo/apps/web/src';
46
+ const cwd = path.resolve('/repo/apps/web/src');
47
+ const matchDir = path.resolve('/repo/apps/web');
48
+ const matchResult = path.resolve('/repo/apps/web/node_modules/slicejs-cli/client.js');
47
49
  const calls = [];
48
50
  const resolver = (dir) => {
49
51
  calls.push(dir);
50
- if (dir === '/repo/apps/web') {
51
- return '/repo/apps/web/node_modules/slicejs-cli/client.js';
52
+ if (dir === matchDir) {
53
+ return matchResult;
52
54
  }
53
55
  return null;
54
56
  };
55
57
 
56
58
  const result = findNearestLocalCliEntry(cwd, resolver);
57
59
 
58
- assert.equal(result, '/repo/apps/web/node_modules/slicejs-cli/client.js');
59
- assert.deepEqual(calls, ['/repo/apps/web/src', '/repo/apps/web']);
60
+ assert.equal(result, matchResult);
61
+ assert.deepEqual(calls, [cwd, matchDir]);
60
62
  });
61
63
 
62
64
  test('findNearestLocalCliEntry returns null when resolver is not a function', () => {
@@ -0,0 +1,206 @@
1
+ import { test, describe, before, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import { createTestProject, cleanupTestProject } from './helpers/setup.js';
6
+
7
+ /** @type {string} */
8
+ let tmpRoot;
9
+ const origInitCwd = process.env.INIT_CWD;
10
+
11
+ before(async () => {
12
+ tmpRoot = await createTestProject();
13
+ process.env.INIT_CWD = tmpRoot;
14
+ });
15
+
16
+ after(async () => {
17
+ if (origInitCwd === undefined) {
18
+ delete process.env.INIT_CWD;
19
+ } else {
20
+ process.env.INIT_CWD = origInitCwd;
21
+ }
22
+ await cleanupTestProject(tmpRoot);
23
+ });
24
+
25
+ describe('getProjectRoot', () => {
26
+ test('resolves project root via INIT_CWD when pointing to a real project', async () => {
27
+ const { getProjectRoot } = await import('../commands/utils/PathHelper.js');
28
+ const root = getProjectRoot(import.meta.url);
29
+ assert.ok(root, 'should return a path');
30
+ assert.ok(fs.existsSync(root), `resolved path should exist: ${root}`);
31
+ assert.equal(path.resolve(root), path.resolve(tmpRoot));
32
+ });
33
+
34
+ test('resolves project root via cwd when INIT_CWD is not set', async () => {
35
+ const prev = process.env.INIT_CWD;
36
+ delete process.env.INIT_CWD;
37
+ try {
38
+ const { getProjectRoot } = await import('../commands/utils/PathHelper.js');
39
+ const root = getProjectRoot(import.meta.url);
40
+ assert.ok(root, 'should return a path');
41
+ assert.ok(fs.existsSync(root), `resolved path should exist: ${root}`);
42
+ } finally {
43
+ process.env.INIT_CWD = prev;
44
+ }
45
+ });
46
+ });
47
+
48
+ describe('getSrcPath', () => {
49
+ test('returns src path relative to project root', async () => {
50
+ const { getSrcPath } = await import('../commands/utils/PathHelper.js');
51
+ const srcPath = getSrcPath(import.meta.url);
52
+ assert.ok(srcPath.endsWith('src'), `should end with 'src': ${srcPath}`);
53
+ assert.ok(fs.existsSync(srcPath), `src path should exist: ${srcPath}`);
54
+ });
55
+
56
+ test('accepts subpath segments', async () => {
57
+ const { getSrcPath } = await import('../commands/utils/PathHelper.js');
58
+ const result = getSrcPath(import.meta.url, 'Components', 'components.js');
59
+ assert.ok(result.endsWith('src/Components/components.js') || result.endsWith('src\\Components\\components.js'));
60
+ assert.ok(fs.existsSync(result), `file should exist: ${result}`);
61
+ });
62
+ });
63
+
64
+ describe('getApiPath', () => {
65
+ test('returns api path relative to project root', async () => {
66
+ const { getApiPath } = await import('../commands/utils/PathHelper.js');
67
+ const apiPath = getApiPath(import.meta.url);
68
+ assert.ok(apiPath.endsWith('api'), `should end with 'api': ${apiPath}`);
69
+ assert.ok(fs.existsSync(apiPath), `api path should exist: ${apiPath}`);
70
+ });
71
+
72
+ test('appends subpath to api', async () => {
73
+ const { getApiPath } = await import('../commands/utils/PathHelper.js');
74
+ const result = getApiPath(import.meta.url, 'index.js');
75
+ assert.ok(result.endsWith('api/index.js') || result.endsWith('api\\index.js'));
76
+ });
77
+ });
78
+
79
+ describe('getDistPath', () => {
80
+ test('returns dist path relative to project root', async () => {
81
+ const { getDistPath } = await import('../commands/utils/PathHelper.js');
82
+ const distPath = getDistPath(import.meta.url);
83
+ assert.ok(distPath.endsWith('dist'), `should end with 'dist': ${distPath}`);
84
+ });
85
+ });
86
+
87
+ describe('getConfigPath', () => {
88
+ test('returns sliceConfig.json path', async () => {
89
+ const { getConfigPath } = await import('../commands/utils/PathHelper.js');
90
+ const configPath = getConfigPath(import.meta.url);
91
+ assert.ok(configPath.endsWith('sliceConfig.json'), `should end with 'sliceConfig.json': ${configPath}`);
92
+ assert.ok(fs.existsSync(configPath), `config file should exist: ${configPath}`);
93
+ });
94
+ });
95
+
96
+ describe('getComponentsJsPath', () => {
97
+ test('returns components.js path', async () => {
98
+ const { getComponentsJsPath } = await import('../commands/utils/PathHelper.js');
99
+ const compPath = getComponentsJsPath(import.meta.url);
100
+ assert.ok(compPath.endsWith('components.js'), `should end with 'components.js': ${compPath}`);
101
+ assert.ok(fs.existsSync(compPath), `components.js should exist: ${compPath}`);
102
+ });
103
+ });
104
+
105
+ describe('getPath', () => {
106
+ test('joins arbitrary segments to project root', async () => {
107
+ const { getPath } = await import('../commands/utils/PathHelper.js');
108
+ const result = getPath(import.meta.url, 'src', 'routes.js');
109
+ assert.ok(result.endsWith('routes.js'), `should end with 'routes.js': ${result}`);
110
+ assert.ok(fs.existsSync(result), `routes.js should exist: ${result}`);
111
+ });
112
+
113
+ test('sanitizes leading slashes from segments', async () => {
114
+ const { getPath } = await import('../commands/utils/PathHelper.js');
115
+ const result = getPath(import.meta.url, '/src/', '/routes.js');
116
+ assert.ok(result.endsWith('routes.js'), `should handle sanitized segments: ${result}`);
117
+ });
118
+ });
119
+
120
+ describe('resolveProjectRoot (fallback via candidates)', () => {
121
+ test('uses candidates() when INIT_CWD is not a valid path', async () => {
122
+ const orig = process.env.INIT_CWD;
123
+ process.env.INIT_CWD = 'C:\\nonexistent\\path';
124
+
125
+ try {
126
+ const { getProjectRoot } = await import('../commands/utils/PathHelper.js');
127
+ const root = getProjectRoot(import.meta.url);
128
+ assert.ok(root, 'should return a path');
129
+ assert.ok(fs.existsSync(root), `fallback path should exist: ${root}`);
130
+ } finally {
131
+ process.env.INIT_CWD = orig;
132
+ }
133
+ });
134
+ });
135
+
136
+ describe('joinRoot', () => {
137
+ test('joins segments relative to explicit root path', async () => {
138
+ const { joinRoot } = await import('../commands/utils/PathHelper.js');
139
+ const root = path.resolve('/test/project');
140
+ const result = joinRoot(root, 'src', 'components');
141
+ assert.equal(result, path.join(root, 'src', 'components'));
142
+ });
143
+
144
+ test('sanitizes leading slashes from segments', async () => {
145
+ const { joinRoot } = await import('../commands/utils/PathHelper.js');
146
+ const root = path.resolve('/test/project');
147
+ const result = joinRoot(root, '/src', '/components');
148
+ assert.equal(result, path.join(root, 'src', 'components'));
149
+ });
150
+
151
+ test('handles single segment', async () => {
152
+ const { joinRoot } = await import('../commands/utils/PathHelper.js');
153
+ const root = path.resolve('/test/project');
154
+ const result = joinRoot(root, 'package.json');
155
+ assert.equal(result, path.join(root, 'package.json'));
156
+ });
157
+
158
+ test('handles empty segments', async () => {
159
+ const { joinRoot } = await import('../commands/utils/PathHelper.js');
160
+ const root = path.resolve('/test/project');
161
+ const result = joinRoot(root);
162
+ assert.equal(result, root);
163
+ });
164
+ });
165
+
166
+ describe('getConfigPath with explicit root', () => {
167
+ test('returns sliceConfig.json under explicit root', async () => {
168
+ const { getConfigPath } = await import('../commands/utils/PathHelper.js');
169
+ const explicitRoot = path.resolve('/custom/project');
170
+ const result = getConfigPath(import.meta.url, explicitRoot);
171
+ assert.equal(result, path.join(explicitRoot, 'src', 'sliceConfig.json'));
172
+ });
173
+
174
+ test('returns sliceConfig.json without root (auto-resolve)', async () => {
175
+ const { getConfigPath } = await import('../commands/utils/PathHelper.js');
176
+ const result = getConfigPath(import.meta.url);
177
+ assert.ok(result.endsWith('sliceConfig.json'));
178
+ assert.ok(fs.existsSync(result));
179
+ });
180
+ });
181
+
182
+ describe('getComponentsJsPath with explicit root', () => {
183
+ test('returns components.js under explicit root', async () => {
184
+ const { getComponentsJsPath } = await import('../commands/utils/PathHelper.js');
185
+ const explicitRoot = path.resolve('/custom/project');
186
+ const result = getComponentsJsPath(import.meta.url, explicitRoot);
187
+ assert.equal(result, path.join(explicitRoot, 'src', 'Components', 'components.js'));
188
+ });
189
+
190
+ test('returns components.js without root (auto-resolve)', async () => {
191
+ const { getComponentsJsPath } = await import('../commands/utils/PathHelper.js');
192
+ const result = getComponentsJsPath(import.meta.url);
193
+ assert.ok(result.endsWith('components.js'));
194
+ assert.ok(fs.existsSync(result));
195
+ });
196
+ });
197
+
198
+ describe('getSrcPath integration', () => {
199
+ test('getSrcPath matches joinRoot with src prefix', async () => {
200
+ const { getSrcPath, getProjectRoot, joinRoot } = await import('../commands/utils/PathHelper.js');
201
+ const root = getProjectRoot(import.meta.url);
202
+ const viaHelper = getSrcPath(import.meta.url, 'Components', 'components.js');
203
+ const viaJoinRoot = joinRoot(root, 'src', 'Components', 'components.js');
204
+ assert.equal(viaHelper, viaJoinRoot);
205
+ });
206
+ });