slicejs-cli 2.9.5 → 3.0.2
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 +33 -5
- package/client.js +35 -5
- package/commands/utils/LocalCliDelegation.js +53 -0
- package/commands/utils/bundling/BundleGenerator.js +395 -36
- package/commands/utils/updateManager.js +19 -3
- package/package.json +1 -1
- package/tests/bundle-generator.test.js +168 -0
- package/tests/bundle-v2-register-output.test.js +141 -0
- package/tests/client-launcher-contract.test.js +211 -0
- package/tests/client-update-flow-contract.test.js +272 -0
- package/tests/local-cli-delegation.test.js +79 -0
- package/tests/update-manager-notifications.test.js +88 -0
|
@@ -0,0 +1,272 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
findNearestLocalCliEntry,
|
|
8
|
+
resolveLocalCliCandidate,
|
|
9
|
+
shouldDelegateToLocalCli,
|
|
10
|
+
isLocalDelegationDisabled
|
|
11
|
+
} from '../commands/utils/LocalCliDelegation.js';
|
|
12
|
+
|
|
13
|
+
test('isLocalDelegationDisabled returns true when env flag is set', () => {
|
|
14
|
+
const env = { SLICE_NO_LOCAL_DELEGATION: '1' };
|
|
15
|
+
assert.equal(isLocalDelegationDisabled(env), true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('isLocalDelegationDisabled returns false when env flag is missing', () => {
|
|
19
|
+
const env = {};
|
|
20
|
+
assert.equal(isLocalDelegationDisabled(env), false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('shouldDelegateToLocalCli is false when candidate is null', () => {
|
|
24
|
+
assert.equal(shouldDelegateToLocalCli('/tmp/current/client.js', null), false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('shouldDelegateToLocalCli is false when candidate realpath equals current realpath', () => {
|
|
28
|
+
const same = '/tmp/current/client.js';
|
|
29
|
+
assert.equal(shouldDelegateToLocalCli(same, same), false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('shouldDelegateToLocalCli is true when candidate differs from current', () => {
|
|
33
|
+
const current = '/tmp/global/client.js';
|
|
34
|
+
const local = '/tmp/project/node_modules/slicejs-cli/client.js';
|
|
35
|
+
assert.equal(shouldDelegateToLocalCli(current, local), true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('findNearestLocalCliEntry returns null when no candidate resolver hits', () => {
|
|
39
|
+
const cwd = path.join('/tmp', 'slice-nonexistent-project');
|
|
40
|
+
const resolver = () => null;
|
|
41
|
+
const result = findNearestLocalCliEntry(cwd, resolver);
|
|
42
|
+
assert.equal(result, null);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('findNearestLocalCliEntry returns first match while traversing upward', () => {
|
|
46
|
+
const cwd = '/repo/apps/web/src';
|
|
47
|
+
const calls = [];
|
|
48
|
+
const resolver = (dir) => {
|
|
49
|
+
calls.push(dir);
|
|
50
|
+
if (dir === '/repo/apps/web') {
|
|
51
|
+
return '/repo/apps/web/node_modules/slicejs-cli/client.js';
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = findNearestLocalCliEntry(cwd, resolver);
|
|
57
|
+
|
|
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
|
+
});
|
|
61
|
+
|
|
62
|
+
test('findNearestLocalCliEntry returns null when resolver is not a function', () => {
|
|
63
|
+
const result = findNearestLocalCliEntry('/repo/apps/web/src', null);
|
|
64
|
+
assert.equal(result, null);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('resolveLocalCliCandidate returns null when candidate path is a directory', () => {
|
|
68
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'slice-cli-delegation-'));
|
|
69
|
+
const candidateDirectory = path.join(tempRoot, 'node_modules', 'slicejs-cli', 'client.js');
|
|
70
|
+
|
|
71
|
+
fs.mkdirSync(candidateDirectory, { recursive: true });
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const result = resolveLocalCliCandidate(tempRoot);
|
|
75
|
+
assert.equal(result, null);
|
|
76
|
+
} finally {
|
|
77
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import Print from '../commands/Print.js';
|
|
4
|
+
import { UpdateManager } from '../commands/utils/updateManager.js';
|
|
5
|
+
|
|
6
|
+
test('notifyAvailableUpdates shows advisory output and never invokes interactive update flow', async () => {
|
|
7
|
+
const manager = new UpdateManager();
|
|
8
|
+
const calls = {
|
|
9
|
+
display: 0,
|
|
10
|
+
prompted: 0,
|
|
11
|
+
info: [],
|
|
12
|
+
error: []
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
manager.checkForUpdates = async () => ({
|
|
16
|
+
hasUpdates: true,
|
|
17
|
+
updates: [
|
|
18
|
+
{
|
|
19
|
+
name: 'slicejs-web-framework',
|
|
20
|
+
displayName: 'Slice.js Framework',
|
|
21
|
+
current: '2.4.3',
|
|
22
|
+
latest: '2.5.0',
|
|
23
|
+
type: 'framework'
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
allCurrent: false
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
manager.displayUpdates = () => {
|
|
30
|
+
calls.display += 1;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
manager.checkAndPromptUpdates = async () => {
|
|
34
|
+
calls.prompted += 1;
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const originalInfo = Print.info;
|
|
39
|
+
const originalError = Print.error;
|
|
40
|
+
|
|
41
|
+
Print.info = (message) => calls.info.push(message);
|
|
42
|
+
Print.error = (message) => calls.error.push(message);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await manager.notifyAvailableUpdates();
|
|
46
|
+
|
|
47
|
+
assert.equal(result, true);
|
|
48
|
+
assert.equal(calls.display, 1);
|
|
49
|
+
assert.equal(calls.prompted, 0);
|
|
50
|
+
assert.equal(calls.error.length, 0);
|
|
51
|
+
assert.equal(calls.info.length, 1);
|
|
52
|
+
assert.match(calls.info[0], /slice update/);
|
|
53
|
+
} finally {
|
|
54
|
+
Print.info = originalInfo;
|
|
55
|
+
Print.error = originalError;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('notifyAvailableUpdates returns false and prints nothing when no updates are available', async () => {
|
|
60
|
+
const manager = new UpdateManager();
|
|
61
|
+
const calls = {
|
|
62
|
+
display: 0,
|
|
63
|
+
info: []
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
manager.checkForUpdates = async () => ({
|
|
67
|
+
hasUpdates: false,
|
|
68
|
+
updates: [],
|
|
69
|
+
allCurrent: true
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
manager.displayUpdates = () => {
|
|
73
|
+
calls.display += 1;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const originalInfo = Print.info;
|
|
77
|
+
Print.info = (message) => calls.info.push(message);
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result = await manager.notifyAvailableUpdates();
|
|
81
|
+
|
|
82
|
+
assert.equal(result, false);
|
|
83
|
+
assert.equal(calls.display, 0);
|
|
84
|
+
assert.equal(calls.info.length, 0);
|
|
85
|
+
} finally {
|
|
86
|
+
Print.info = originalInfo;
|
|
87
|
+
}
|
|
88
|
+
});
|