slicejs-cli 3.5.1 → 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/doctor/doctor.js +69 -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/sliceScripts.js +21 -0
- package/commands/utils/updateManager.js +54 -35
- package/package.json +12 -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/.github/workflows/ci.yml +0 -43
- 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/playwright.config.js +0 -51
- package/tests/build-command-integration.test.js +0 -87
- package/tests/build-production-e2e.test.js +0 -140
- package/tests/builder-edge-cases.test.js +0 -322
- package/tests/bundle-generate-e2e.test.js +0 -115
- package/tests/bundle-generator.test.js +0 -691
- package/tests/bundle-v2-register-output.test.js +0 -470
- package/tests/bundling-dependency-edges.test.js +0 -127
- package/tests/bundling-imports-unit.test.js +0 -267
- package/tests/client-launcher-contract.test.js +0 -211
- package/tests/client-update-flow-contract.test.js +0 -272
- package/tests/commands-component-crud.test.js +0 -102
- package/tests/commands-doctor.test.js +0 -80
- package/tests/commands-version-checker.test.js +0 -37
- package/tests/component-registry-parse.test.js +0 -34
- package/tests/dependency-analyzer.test.js +0 -24
- package/tests/e2e/bundles.spec.js +0 -91
- package/tests/e2e/dependency-scenarios.spec.js +0 -56
- package/tests/e2e/fixtures/components/Service/FetchManager/FetchManager.js +0 -136
- package/tests/e2e/fixtures/components/Service/IndexedDbManager/IndexedDbManager.js +0 -149
- package/tests/e2e/fixtures/components/Service/LocalStorageManager/LocalStorageManager.js +0 -45
- package/tests/e2e/fixtures/components/Visual/Button/Button.css +0 -106
- package/tests/e2e/fixtures/components/Visual/Button/Button.html +0 -5
- package/tests/e2e/fixtures/components/Visual/Button/Button.js +0 -158
- package/tests/e2e/fixtures/components/Visual/Link/Link.js +0 -33
- package/tests/e2e/fixtures/components/Visual/Loading/Loading.css +0 -56
- package/tests/e2e/fixtures/components/Visual/Loading/Loading.html +0 -83
- package/tests/e2e/fixtures/components/Visual/Loading/Loading.js +0 -164
- package/tests/e2e/fixtures/components/Visual/MultiRoute/MultiRoute.js +0 -167
- package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.css +0 -116
- package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.html +0 -44
- package/tests/e2e/fixtures/components/Visual/Navbar/Navbar.js +0 -180
- package/tests/e2e/fixtures/components/Visual/NotFound/NotFound.js +0 -20
- package/tests/e2e/fixtures/components/Visual/Route/Route.js +0 -181
- package/tests/e2e/fixtures/components/registry.json +0 -12
- package/tests/e2e/fixtures/vendor-components.mjs +0 -65
- package/tests/e2e/navigation.spec.js +0 -44
- package/tests/e2e/render.spec.js +0 -34
- package/tests/e2e/serve.mjs +0 -264
- package/tests/e2e/shared-deps.spec.js +0 -61
- package/tests/e2e/unminified.spec.js +0 -33
- package/tests/e2e-serve.test.js +0 -148
- 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 -102
- 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/perf-budget.test.js +0 -86
- 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 -346
- package/tests/update-manager-notifications.test.js +0 -88
|
@@ -1,272 +0,0 @@
|
|
|
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
|
-
import { parse } from '@babel/parser';
|
|
7
|
-
|
|
8
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
const clientPath = path.join(__dirname, '..', 'client.js');
|
|
10
|
-
const clientSource = fs.readFileSync(clientPath, 'utf-8');
|
|
11
|
-
const ast = parse(clientSource, {
|
|
12
|
-
sourceType: 'module',
|
|
13
|
-
errorRecovery: true,
|
|
14
|
-
loc: true,
|
|
15
|
-
ranges: true
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
function lineOf(node) {
|
|
19
|
-
return node && node.loc && node.loc.start ? node.loc.start.line : null;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function walk(node, visit) {
|
|
23
|
-
if (!node || typeof node !== 'object') {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
visit(node);
|
|
28
|
-
|
|
29
|
-
for (const value of Object.values(node)) {
|
|
30
|
-
if (Array.isArray(value)) {
|
|
31
|
-
for (const item of value) {
|
|
32
|
-
walk(item, visit);
|
|
33
|
-
}
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
walk(value, visit);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isMethodCall(node, methodName) {
|
|
42
|
-
return (
|
|
43
|
-
node &&
|
|
44
|
-
node.type === 'CallExpression' &&
|
|
45
|
-
node.callee &&
|
|
46
|
-
node.callee.type === 'MemberExpression' &&
|
|
47
|
-
node.callee.property &&
|
|
48
|
-
node.callee.property.type === 'Identifier' &&
|
|
49
|
-
node.callee.property.name === methodName
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function isCommandUpdateExpression(node) {
|
|
54
|
-
if (!node || node.type !== 'CallExpression') {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
node.callee &&
|
|
60
|
-
node.callee.type === 'MemberExpression' &&
|
|
61
|
-
node.callee.property &&
|
|
62
|
-
node.callee.property.type === 'Identifier' &&
|
|
63
|
-
node.callee.property.name === 'command' &&
|
|
64
|
-
node.arguments[0] &&
|
|
65
|
-
node.arguments[0].type === 'StringLiteral' &&
|
|
66
|
-
node.arguments[0].value === 'update'
|
|
67
|
-
) {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (node.callee && node.callee.type === 'MemberExpression') {
|
|
72
|
-
return isCommandUpdateExpression(node.callee.object);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function getRunWithVersionCheckNode() {
|
|
79
|
-
let foundNode = null;
|
|
80
|
-
|
|
81
|
-
walk(ast, (node) => {
|
|
82
|
-
if (
|
|
83
|
-
node.type === 'FunctionDeclaration' &&
|
|
84
|
-
node.id &&
|
|
85
|
-
node.id.type === 'Identifier' &&
|
|
86
|
-
node.id.name === 'runWithVersionCheck'
|
|
87
|
-
) {
|
|
88
|
-
foundNode = node;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (node.type !== 'VariableDeclarator') {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!node.id || node.id.type !== 'Identifier' || node.id.name !== 'runWithVersionCheck') {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
node.init &&
|
|
102
|
-
(node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression')
|
|
103
|
-
) {
|
|
104
|
-
foundNode = node.init;
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
return foundNode;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function getFunctionBodyNode(fnNode) {
|
|
112
|
-
if (!fnNode) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (fnNode.type === 'FunctionDeclaration' || fnNode.type === 'FunctionExpression' || fnNode.type === 'ArrowFunctionExpression') {
|
|
117
|
-
return fnNode.body;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function getFunctionLikeByName(name) {
|
|
124
|
-
let foundNode = null;
|
|
125
|
-
|
|
126
|
-
walk(ast, (node) => {
|
|
127
|
-
if (
|
|
128
|
-
node.type === 'FunctionDeclaration' &&
|
|
129
|
-
node.id &&
|
|
130
|
-
node.id.type === 'Identifier' &&
|
|
131
|
-
node.id.name === name
|
|
132
|
-
) {
|
|
133
|
-
foundNode = node;
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (node.type !== 'VariableDeclarator') {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (!node.id || node.id.type !== 'Identifier' || node.id.name !== name) {
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
node.init &&
|
|
147
|
-
(node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression')
|
|
148
|
-
) {
|
|
149
|
-
foundNode = node.init;
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return foundNode;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
test('runWithVersionCheck uses non-blocking update notifications', () => {
|
|
157
|
-
const runWithVersionCheckNode = getRunWithVersionCheckNode();
|
|
158
|
-
const runWithVersionCheckBody = getFunctionBodyNode(runWithVersionCheckNode);
|
|
159
|
-
|
|
160
|
-
assert.ok(
|
|
161
|
-
runWithVersionCheckNode,
|
|
162
|
-
'runWithVersionCheck function should exist as a declaration or function-valued variable'
|
|
163
|
-
);
|
|
164
|
-
assert.ok(runWithVersionCheckBody, 'runWithVersionCheck should have a traversable function body');
|
|
165
|
-
|
|
166
|
-
let hasNotifyCall = false;
|
|
167
|
-
let hasAwaitedNotifyCall = false;
|
|
168
|
-
let notifyCallLine = null;
|
|
169
|
-
let hasPromptCall = false;
|
|
170
|
-
let promptCallLine = null;
|
|
171
|
-
|
|
172
|
-
walk(runWithVersionCheckBody, (node) => {
|
|
173
|
-
if (isMethodCall(node, 'notifyAvailableUpdates')) {
|
|
174
|
-
hasNotifyCall = true;
|
|
175
|
-
notifyCallLine = notifyCallLine ?? lineOf(node);
|
|
176
|
-
}
|
|
177
|
-
if (
|
|
178
|
-
node.type === 'AwaitExpression' &&
|
|
179
|
-
isMethodCall(node.argument, 'notifyAvailableUpdates')
|
|
180
|
-
) {
|
|
181
|
-
hasAwaitedNotifyCall = true;
|
|
182
|
-
}
|
|
183
|
-
if (isMethodCall(node, 'checkAndPromptUpdates')) {
|
|
184
|
-
hasPromptCall = true;
|
|
185
|
-
promptCallLine = promptCallLine ?? lineOf(node);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
assert.equal(
|
|
190
|
-
hasNotifyCall,
|
|
191
|
-
true,
|
|
192
|
-
`runWithVersionCheck must call updateManager.notifyAvailableUpdates() (found at line ${notifyCallLine ?? 'unknown'})`
|
|
193
|
-
);
|
|
194
|
-
assert.equal(
|
|
195
|
-
hasAwaitedNotifyCall,
|
|
196
|
-
false,
|
|
197
|
-
`runWithVersionCheck should fire-and-forget notifyAvailableUpdates() without await (notify call line ${notifyCallLine ?? 'unknown'})`
|
|
198
|
-
);
|
|
199
|
-
assert.equal(
|
|
200
|
-
hasPromptCall,
|
|
201
|
-
false,
|
|
202
|
-
`runWithVersionCheck must not call updateManager.checkAndPromptUpdates() (found at line ${promptCallLine ?? 'unknown'})`
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test('update command remains explicitly interactive', () => {
|
|
207
|
-
let foundAwaitedInteractiveUpdateAction = false;
|
|
208
|
-
let updateActionHandlerLine = null;
|
|
209
|
-
let relatedPromptCallLine = null;
|
|
210
|
-
let handlerReferenceName = null;
|
|
211
|
-
|
|
212
|
-
walk(ast, (node) => {
|
|
213
|
-
if (
|
|
214
|
-
node.type === 'CallExpression' &&
|
|
215
|
-
node.callee &&
|
|
216
|
-
node.callee.type === 'MemberExpression' &&
|
|
217
|
-
node.callee.property &&
|
|
218
|
-
node.callee.property.type === 'Identifier' &&
|
|
219
|
-
node.callee.property.name === 'action' &&
|
|
220
|
-
isCommandUpdateExpression(node.callee.object)
|
|
221
|
-
) {
|
|
222
|
-
const actionHandler = node.arguments[0];
|
|
223
|
-
if (!actionHandler) {
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
let resolvedHandler = null;
|
|
228
|
-
|
|
229
|
-
if (actionHandler.type === 'Identifier') {
|
|
230
|
-
handlerReferenceName = actionHandler.name;
|
|
231
|
-
resolvedHandler = getFunctionLikeByName(actionHandler.name);
|
|
232
|
-
} else if (
|
|
233
|
-
actionHandler.type === 'ArrowFunctionExpression' ||
|
|
234
|
-
actionHandler.type === 'FunctionExpression'
|
|
235
|
-
) {
|
|
236
|
-
resolvedHandler = actionHandler;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (!resolvedHandler || resolvedHandler.async !== true) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const resolvedBody = getFunctionBodyNode(resolvedHandler);
|
|
244
|
-
if (!resolvedBody) {
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
updateActionHandlerLine = updateActionHandlerLine ?? lineOf(resolvedHandler);
|
|
249
|
-
|
|
250
|
-
walk(resolvedBody, (actionNode) => {
|
|
251
|
-
if (isMethodCall(actionNode, 'checkAndPromptUpdates')) {
|
|
252
|
-
relatedPromptCallLine = relatedPromptCallLine ?? lineOf(actionNode);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
actionNode.type !== 'AwaitExpression' ||
|
|
257
|
-
!isMethodCall(actionNode.argument, 'checkAndPromptUpdates')
|
|
258
|
-
) {
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
foundAwaitedInteractiveUpdateAction = true;
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
assert.equal(
|
|
268
|
-
foundAwaitedInteractiveUpdateAction,
|
|
269
|
-
true,
|
|
270
|
-
`update command action must await checkAndPromptUpdates(...) (action line ${updateActionHandlerLine ?? 'unknown'}, related call line ${relatedPromptCallLine ?? 'unknown'}${handlerReferenceName ? `, handler reference ${handlerReferenceName}` : ''})`
|
|
271
|
-
);
|
|
272
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { test, describe } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { withTestProject } from './helpers/setup.js';
|
|
6
|
-
import createComponent from '../commands/createComponent/createComponent.js';
|
|
7
|
-
import deleteComponent from '../commands/deleteComponent/deleteComponent.js';
|
|
8
|
-
import listComponentsReal from '../commands/listComponents/listComponents.js';
|
|
9
|
-
|
|
10
|
-
const visualDir = (root, name) =>
|
|
11
|
-
path.join(root, 'src', 'Components', 'Visual', name);
|
|
12
|
-
|
|
13
|
-
describe('component create', () => {
|
|
14
|
-
test('creates a Visual component with .js/.css/.html', async () => {
|
|
15
|
-
await withTestProject(async (root) => {
|
|
16
|
-
const ok = createComponent('Card', 'Visual');
|
|
17
|
-
assert.equal(ok, true);
|
|
18
|
-
const dir = visualDir(root, 'Card');
|
|
19
|
-
assert.ok(await fs.pathExists(path.join(dir, 'Card.js')));
|
|
20
|
-
assert.ok(await fs.pathExists(path.join(dir, 'Card.css')));
|
|
21
|
-
assert.ok(await fs.pathExists(path.join(dir, 'Card.html')));
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('creates a Service component with only a .js file', async () => {
|
|
26
|
-
await withTestProject(async (root) => {
|
|
27
|
-
const ok = createComponent('Api', 'Service');
|
|
28
|
-
assert.equal(ok, true);
|
|
29
|
-
const dir = path.join(root, 'src', 'Components', 'Service', 'Api');
|
|
30
|
-
assert.ok(await fs.pathExists(path.join(dir, 'Api.js')));
|
|
31
|
-
assert.equal(await fs.pathExists(path.join(dir, 'Api.css')), false);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('rejects an invalid component name', async () => {
|
|
36
|
-
await withTestProject(() => {
|
|
37
|
-
assert.equal(createComponent('1Bad', 'Visual'), false);
|
|
38
|
-
assert.equal(createComponent('bad-name', 'Visual'), false);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('rejects an unknown category', async () => {
|
|
43
|
-
await withTestProject(() => {
|
|
44
|
-
assert.equal(createComponent('Card', 'Nope'), false);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('rejects when the component files already exist', async () => {
|
|
49
|
-
await withTestProject(() => {
|
|
50
|
-
assert.equal(createComponent('Card', 'Visual'), true);
|
|
51
|
-
// Second creation hits the on-disk existence guard.
|
|
52
|
-
assert.equal(createComponent('Card', 'Visual'), false);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('component delete', () => {
|
|
58
|
-
test('deletes an existing component directory', async () => {
|
|
59
|
-
await withTestProject(async (root) => {
|
|
60
|
-
createComponent('Card', 'Visual');
|
|
61
|
-
assert.ok(await fs.pathExists(visualDir(root, 'Card')));
|
|
62
|
-
|
|
63
|
-
const ok = deleteComponent('Card', 'Visual');
|
|
64
|
-
assert.equal(ok, true);
|
|
65
|
-
assert.equal(await fs.pathExists(visualDir(root, 'Card')), false);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('returns false when the component does not exist', async () => {
|
|
70
|
-
await withTestProject(() => {
|
|
71
|
-
assert.equal(deleteComponent('DoesNotExist', 'Visual'), false);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Components are PascalCase by convention: both create and delete normalize
|
|
76
|
-
// the initial to uppercase, so a lower-case name round-trips correctly.
|
|
77
|
-
test('create/delete round-trips a lower-case initial (PascalCase normalization)', async () => {
|
|
78
|
-
await withTestProject(async (root) => {
|
|
79
|
-
assert.equal(createComponent('card', 'Visual'), true);
|
|
80
|
-
assert.ok(await fs.pathExists(visualDir(root, 'Card')), 'folder is created in PascalCase');
|
|
81
|
-
assert.equal(deleteComponent('card', 'Visual'), true);
|
|
82
|
-
assert.equal(await fs.pathExists(visualDir(root, 'Card')), false);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('component list (registry regeneration)', () => {
|
|
88
|
-
test('regenerates components.js from the files on disk', async () => {
|
|
89
|
-
await withTestProject(async (root) => {
|
|
90
|
-
createComponent('Card', 'Visual');
|
|
91
|
-
listComponentsReal();
|
|
92
|
-
|
|
93
|
-
const registryPath = path.join(root, 'src', 'Components', 'components.js');
|
|
94
|
-
const content = await fs.readFile(registryPath, 'utf8');
|
|
95
|
-
const json = JSON.parse(content.match(/const components = ({[\s\S]*?});/)[1]);
|
|
96
|
-
|
|
97
|
-
assert.equal(json.Card, 'Visual');
|
|
98
|
-
// Starter AppComponents must still be present.
|
|
99
|
-
assert.equal(json.AppShell, 'AppComponents');
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { test, describe } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import fs from 'fs-extra';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { withTestProject } from './helpers/setup.js';
|
|
6
|
-
import {
|
|
7
|
-
checkNodeVersion,
|
|
8
|
-
checkDirectoryStructure,
|
|
9
|
-
checkConfig,
|
|
10
|
-
checkComponents,
|
|
11
|
-
} from '../commands/doctor/doctor.js';
|
|
12
|
-
|
|
13
|
-
describe('doctor checks', () => {
|
|
14
|
-
test('checkNodeVersion passes on the supported runtime', async () => {
|
|
15
|
-
const r = await checkNodeVersion();
|
|
16
|
-
assert.equal(r.pass, true);
|
|
17
|
-
assert.match(r.message, /Node\.js version/);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('checkDirectoryStructure passes on the starter project', async () => {
|
|
21
|
-
await withTestProject(async () => {
|
|
22
|
-
const r = await checkDirectoryStructure();
|
|
23
|
-
assert.equal(r.pass, true);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('checkDirectoryStructure fails when src/ is missing', async () => {
|
|
28
|
-
await withTestProject(async (root) => {
|
|
29
|
-
await fs.remove(path.join(root, 'src'));
|
|
30
|
-
const r = await checkDirectoryStructure();
|
|
31
|
-
assert.equal(r.pass, false);
|
|
32
|
-
assert.match(r.message, /Missing directories/);
|
|
33
|
-
assert.match(r.message, /src\//);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('checkConfig passes for a valid sliceConfig.json', async () => {
|
|
38
|
-
await withTestProject(async () => {
|
|
39
|
-
const r = await checkConfig();
|
|
40
|
-
assert.equal(r.pass, true);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('checkConfig fails on invalid JSON', async () => {
|
|
45
|
-
await withTestProject(async (root) => {
|
|
46
|
-
await fs.writeFile(path.join(root, 'src', 'sliceConfig.json'), '{ not valid json ');
|
|
47
|
-
const r = await checkConfig();
|
|
48
|
-
assert.equal(r.pass, false);
|
|
49
|
-
assert.match(r.message, /invalid JSON/);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('checkConfig fails when paths.components is missing', async () => {
|
|
54
|
-
await withTestProject(async (root) => {
|
|
55
|
-
await fs.writeJson(path.join(root, 'src', 'sliceConfig.json'), { server: { port: 3000 } });
|
|
56
|
-
const r = await checkConfig();
|
|
57
|
-
assert.equal(r.pass, false);
|
|
58
|
-
assert.match(r.message, /missing paths\.components/);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('checkComponents reports OK when every component folder has its .js', async () => {
|
|
63
|
-
await withTestProject(async () => {
|
|
64
|
-
const r = await checkComponents();
|
|
65
|
-
assert.equal(r.pass, true);
|
|
66
|
-
assert.match(r.message, /components checked/);
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('checkComponents warns when a component folder lacks its .js file', async () => {
|
|
71
|
-
await withTestProject(async (root) => {
|
|
72
|
-
// AppShell ships with AppShell.js; remove it to simulate a broken component.
|
|
73
|
-
const broken = path.join(root, 'src', 'Components', 'AppComponents', 'Broken');
|
|
74
|
-
await fs.ensureDir(broken); // directory with no Broken.js
|
|
75
|
-
const r = await checkComponents();
|
|
76
|
-
assert.equal(r.warn, true);
|
|
77
|
-
assert.match(r.message, /missing files/);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { test, describe } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import versionChecker from '../commands/utils/VersionChecker.js';
|
|
4
|
-
|
|
5
|
-
describe('VersionChecker.compareVersions', () => {
|
|
6
|
-
test('detects an outdated version', () => {
|
|
7
|
-
assert.equal(versionChecker.compareVersions('1.0.0', '1.0.1'), 'outdated');
|
|
8
|
-
assert.equal(versionChecker.compareVersions('1.2.0', '2.0.0'), 'outdated');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test('detects a newer (local) version', () => {
|
|
12
|
-
assert.equal(versionChecker.compareVersions('2.0.0', '1.9.9'), 'newer');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('detects an up-to-date version', () => {
|
|
16
|
-
assert.equal(versionChecker.compareVersions('3.5.0', '3.5.0'), 'current');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('handles version strings of differing length', () => {
|
|
20
|
-
assert.equal(versionChecker.compareVersions('1.0', '1.0.0'), 'current');
|
|
21
|
-
assert.equal(versionChecker.compareVersions('1.0.0', '1.0.0.1'), 'outdated');
|
|
22
|
-
assert.equal(versionChecker.compareVersions('1.0.1', '1.0'), 'newer');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test('returns null when a version is missing', () => {
|
|
26
|
-
assert.equal(versionChecker.compareVersions(null, '1.0.0'), null);
|
|
27
|
-
assert.equal(versionChecker.compareVersions('1.0.0', undefined), null);
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe('VersionChecker.getCurrentVersions', () => {
|
|
32
|
-
test('reads the CLI version from the package.json (no network)', async () => {
|
|
33
|
-
const current = await versionChecker.getCurrentVersions();
|
|
34
|
-
assert.ok(current, 'should resolve current versions');
|
|
35
|
-
assert.match(current.cli, /^\d+\.\d+\.\d+/);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,34 +0,0 @@
|
|
|
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 { createTestProject, cleanupTestProject, withTestProject } from './helpers/setup.js';
|
|
6
|
-
|
|
7
|
-
test('generateTypesFile parses JSON from components.js (no eval)', async () => {
|
|
8
|
-
const tmpRoot = await createTestProject({ visualComponents: ['Button'] });
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
const srcDir = path.join(tmpRoot, 'src');
|
|
12
|
-
const { generateTypesFile } = await import('../commands/types/types.js');
|
|
13
|
-
const result = await generateTypesFile({
|
|
14
|
-
projectRoot: tmpRoot,
|
|
15
|
-
outputPath: path.join(srcDir, 'slice-build.generated.d.ts')
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
assert.ok(result.componentsProcessed > 0);
|
|
19
|
-
assert.equal(fs.existsSync(result.outputPath), true);
|
|
20
|
-
|
|
21
|
-
const declaration = fs.readFileSync(result.outputPath, 'utf8');
|
|
22
|
-
assert.match(declaration, /export interface ButtonProps/);
|
|
23
|
-
} finally {
|
|
24
|
-
await cleanupTestProject(tmpRoot);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('Validations componentExists with JSON.parse (no eval)', async () => {
|
|
29
|
-
await withTestProject(async (tmpDir) => {
|
|
30
|
-
const validations = (await import('../commands/Validations.js')).default;
|
|
31
|
-
assert.equal(validations.componentExists('Button'), true);
|
|
32
|
-
assert.equal(validations.componentExists('NonExistent'), false);
|
|
33
|
-
}, { visualComponents: ['Button'] });
|
|
34
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import DependencyAnalyzer from '../commands/utils/bundling/DependencyAnalyzer.js';
|
|
4
|
-
|
|
5
|
-
test('parseComponentsConfig returns components map', () => {
|
|
6
|
-
// Arrange
|
|
7
|
-
const analyzer = new DependencyAnalyzer(import.meta.url);
|
|
8
|
-
const content = `const components = {"Button": "Visual", "FetchManager": "Service"};\nexport default components;`;
|
|
9
|
-
|
|
10
|
-
// Act
|
|
11
|
-
const result = analyzer.parseComponentsConfig(content);
|
|
12
|
-
|
|
13
|
-
// Assert
|
|
14
|
-
assert.deepEqual(result, { Button: 'Visual', FetchManager: 'Service' });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('parseComponentsConfig throws for invalid config', () => {
|
|
18
|
-
// Arrange
|
|
19
|
-
const analyzer = new DependencyAnalyzer(import.meta.url);
|
|
20
|
-
const content = `const notComponents = {"Button": "Visual"};`;
|
|
21
|
-
|
|
22
|
-
// Act / Assert
|
|
23
|
-
assert.throws(() => analyzer.parseComponentsConfig(content), /components object not found/);
|
|
24
|
-
});
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { test, expect } from '@playwright/test';
|
|
2
|
-
import { parse } from '@babel/parser';
|
|
3
|
-
|
|
4
|
-
// Recursively collect every emitted bundle file referenced by the config.
|
|
5
|
-
// Only real bundle artifacts (slice-bundle.*.js) — not dependency identifiers
|
|
6
|
-
// that happen to end in `.js` (e.g. vendorShared.dependencies).
|
|
7
|
-
function collectBundleFiles(node, acc = new Set()) {
|
|
8
|
-
if (typeof node === 'string') {
|
|
9
|
-
if (node.startsWith('slice-bundle.') && node.endsWith('.js')) acc.add(node);
|
|
10
|
-
return acc;
|
|
11
|
-
}
|
|
12
|
-
if (Array.isArray(node)) {
|
|
13
|
-
for (const n of node) collectBundleFiles(n, acc);
|
|
14
|
-
return acc;
|
|
15
|
-
}
|
|
16
|
-
if (node && typeof node === 'object') {
|
|
17
|
-
for (const v of Object.values(node)) collectBundleFiles(v, acc);
|
|
18
|
-
return acc;
|
|
19
|
-
}
|
|
20
|
-
return acc;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function getConfig(request) {
|
|
24
|
-
const res = await request.get('/bundles/bundle.config.json');
|
|
25
|
-
expect(res.status()).toBe(200);
|
|
26
|
-
return res.json();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test.describe('bundle quality & content (served production artifacts)', () => {
|
|
30
|
-
test('bundle.config.json is well-formed and maps every route', async ({ request }) => {
|
|
31
|
-
const cfg = await getConfig(request);
|
|
32
|
-
expect(cfg.production).toBe(true);
|
|
33
|
-
expect(cfg.format).toBe('v2');
|
|
34
|
-
expect(cfg.minified).toBe(true);
|
|
35
|
-
expect(cfg.obfuscated).toBe(true);
|
|
36
|
-
expect(cfg.bundles.framework, 'framework bundle present').toBeTruthy();
|
|
37
|
-
expect(cfg.bundles.critical, 'critical bundle present').toBeTruthy();
|
|
38
|
-
|
|
39
|
-
for (const route of ['/', '/about', '/404']) {
|
|
40
|
-
expect(Array.isArray(cfg.routeBundles[route]), `route ${route} is mapped`).toBe(true);
|
|
41
|
-
expect(cfg.routeBundles[route].length).toBeGreaterThan(0);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('every referenced bundle is served as valid, contract-compliant JS', async ({ request }) => {
|
|
46
|
-
const cfg = await getConfig(request);
|
|
47
|
-
const files = [...collectBundleFiles(cfg.bundles)];
|
|
48
|
-
expect(files.length).toBeGreaterThan(0);
|
|
49
|
-
|
|
50
|
-
for (const file of files) {
|
|
51
|
-
const res = await request.get(`/bundles/${file}`);
|
|
52
|
-
expect(res.status(), `${file} is served`).toBe(200);
|
|
53
|
-
expect(res.headers()['content-type'] || '').toMatch(/javascript/);
|
|
54
|
-
|
|
55
|
-
const code = await res.text();
|
|
56
|
-
expect(() => parse(code, { sourceType: 'module', plugins: ['jsx'] }), `${file} is valid JS`).not.toThrow();
|
|
57
|
-
// v2 runtime contract.
|
|
58
|
-
expect(code, `${file} exports SLICE_BUNDLE_META`).toContain('SLICE_BUNDLE_META');
|
|
59
|
-
expect(code, `${file} exports registerAll`).toContain('registerAll');
|
|
60
|
-
// No unresolved relative import may leak into a production bundle.
|
|
61
|
-
expect(/\bfrom\s+['"]\.\.?\//.test(code), `${file} leaks a relative import`).toBe(false);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('the framework bundle carries the structural runtime', async ({ request }) => {
|
|
66
|
-
const code = await (await request.get('/bundles/slice-bundle.framework.js')).text();
|
|
67
|
-
expect(code).toContain('Controller');
|
|
68
|
-
expect(code).toContain('registerAll');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('all starter component classes are registered across the bundles', async ({ request }) => {
|
|
72
|
-
const cfg = await getConfig(request);
|
|
73
|
-
const files = [...collectBundleFiles(cfg.bundles)];
|
|
74
|
-
let combined = '';
|
|
75
|
-
for (const file of files) {
|
|
76
|
-
combined += await (await request.get(`/bundles/${file}`)).text();
|
|
77
|
-
}
|
|
78
|
-
// Component names are emitted as string literals in the register* calls and
|
|
79
|
-
// survive minification (mangle.properties is off, strings are untouched).
|
|
80
|
-
for (const comp of ['AppShell', 'Navbar', 'MultiRoute', 'HomeSection', 'Button', 'AboutSection', 'NotFound']) {
|
|
81
|
-
expect(combined.includes(`"${comp}"`), `${comp} is registered in a bundle`).toBe(true);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('bundles are genuinely minified', async ({ request }) => {
|
|
86
|
-
const code = await (await request.get('/bundles/slice-bundle.framework.js')).text();
|
|
87
|
-
const longestLine = Math.max(...code.split('\n').map((l) => l.length));
|
|
88
|
-
expect(longestLine, 'minified output has long lines').toBeGreaterThan(300);
|
|
89
|
-
expect(code, 'comments are stripped').not.toContain('/**');
|
|
90
|
-
});
|
|
91
|
-
});
|