sitevision-cli 1.0.0-beta.23 → 1.0.0-beta.24
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/dist/cli.js +27 -0
- package/dist/commands/deploy.js +2 -3
- package/dist/commands/dev.js +0 -12
- package/dist/shell/Shell.js +14 -6
- package/dist/shell/Tabs.js +3 -3
- package/dist/shell/actions.js +0 -0
- package/dist/utils/i18n.js +5 -3
- package/dist/utils/process-runner.d.ts +13 -4
- package/dist/utils/process-runner.js +63 -37
- package/dist/utils/project-detection.d.ts +5 -0
- package/dist/utils/project-detection.js +14 -0
- package/dist/utils/sitevision-scripts-runner.d.ts +2 -1
- package/dist/utils/sitevision-scripts-runner.js +18 -7
- package/dist/utils/tasks.js +26 -10
- package/package.json +1 -1
- package/readme.md +2 -2
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,9 @@ import { render } from 'ink';
|
|
|
4
4
|
import { Text, Box } from 'ink';
|
|
5
5
|
import meow from 'meow';
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { runningTasks } from './utils/tasks.js';
|
|
9
|
+
import { killAllChildren } from './utils/process-runner.js';
|
|
7
10
|
import { Shell } from './shell/Shell.js';
|
|
8
11
|
import { getCommand } from './commands/index.js';
|
|
9
12
|
import { requireProject, detectProject, migrateLegacyPassword, } from './utils/project-detection.js';
|
|
@@ -105,6 +108,20 @@ function printMasthead(version) {
|
|
|
105
108
|
`${spaces(gap)}${DIM}${right}${RESET}${spaces(padding)}${CYAN}│${RESET}`);
|
|
106
109
|
console.log(`${CYAN}╰${border}╯${RESET}`);
|
|
107
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Stop every running task and kill leftover child processes. In raw mode Ink
|
|
113
|
+
* reads Ctrl+C as a keypress, so no SIGINT reaches anything else. Exits when
|
|
114
|
+
* there was work to stop (or on a signal); otherwise the process ends on its
|
|
115
|
+
* own, so a command's last output is never cut off.
|
|
116
|
+
*/
|
|
117
|
+
function shutdown(code) {
|
|
118
|
+
const busy = runningTasks();
|
|
119
|
+
for (const task of busy)
|
|
120
|
+
task.stop();
|
|
121
|
+
const killed = killAllChildren();
|
|
122
|
+
if (busy.length > 0 || killed > 0 || code !== undefined)
|
|
123
|
+
process.exit(code);
|
|
124
|
+
}
|
|
108
125
|
function fail(message, hint) {
|
|
109
126
|
render(_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { color: "red", children: ["Error: ", message] }), _jsx(Text, { dimColor: true, children: hint })] }));
|
|
110
127
|
process.exit(1);
|
|
@@ -130,6 +147,8 @@ async function playIntro(art) {
|
|
|
130
147
|
// inside the same buffer, when the terminal is wide enough for it.
|
|
131
148
|
async function runShell(apps, workspaceRoot) {
|
|
132
149
|
process.stdout.write('\x1b[?1049h\x1b[H');
|
|
150
|
+
// Also leave the alternate screen when a signal exits past the finally.
|
|
151
|
+
process.once('exit', () => process.stdout.write('\x1b[?1049l'));
|
|
133
152
|
try {
|
|
134
153
|
const art = process.stdin.isTTY && settings.introAnimation && !cli.flags.minimal
|
|
135
154
|
? pickIntroArt(process.stdout.columns ?? 0)
|
|
@@ -146,6 +165,11 @@ async function runShell(apps, workspaceRoot) {
|
|
|
146
165
|
}
|
|
147
166
|
}
|
|
148
167
|
async function main() {
|
|
168
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
169
|
+
process.on(signal, () => {
|
|
170
|
+
shutdown(128 + os.constants.signals[signal]);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
149
173
|
// On the very first run we show a dedicated welcome screen instead of the
|
|
150
174
|
// masthead, so the branding is the moment. Only when stdin is a TTY — the
|
|
151
175
|
// welcome is interactive and would hang in CI / piped input.
|
|
@@ -188,6 +212,7 @@ async function main() {
|
|
|
188
212
|
fail('No Sitevision apps found here.', 'Run svc inside an app directory (manifest.json) or at the root of a repo that contains apps.');
|
|
189
213
|
}
|
|
190
214
|
await runShell(apps, process.cwd());
|
|
215
|
+
shutdown();
|
|
191
216
|
return;
|
|
192
217
|
}
|
|
193
218
|
if (firstRun) {
|
|
@@ -201,6 +226,7 @@ async function main() {
|
|
|
201
226
|
});
|
|
202
227
|
}
|
|
203
228
|
await runShell([project]);
|
|
229
|
+
shutdown();
|
|
204
230
|
return;
|
|
205
231
|
}
|
|
206
232
|
// Check if we're in a Sitevision project
|
|
@@ -255,6 +281,7 @@ async function main() {
|
|
|
255
281
|
flags: cli.flags,
|
|
256
282
|
args,
|
|
257
283
|
});
|
|
284
|
+
shutdown();
|
|
258
285
|
}
|
|
259
286
|
main().catch(error => {
|
|
260
287
|
console.error('Fatal error:', error);
|
package/dist/commands/deploy.js
CHANGED
|
@@ -3,7 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { render, Box, Text, useInput } from 'ink';
|
|
4
4
|
import { StatusIndicator } from '../components/StatusIndicator.js';
|
|
5
5
|
import { deployApp, deployProduction } from '../utils/sitevision-api.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getDeployZipPath, getSignedZipPath, getAppType, } from '../utils/project-detection.js';
|
|
7
7
|
import { zipExists } from '../utils/zip.js';
|
|
8
8
|
import { promptPassword, promptYesNo } from '../utils/password-prompt.js';
|
|
9
9
|
import { setDeployPassword, deleteSessionCookie, deleteOAuth2RefreshToken, } from '../utils/keychain.js';
|
|
@@ -161,8 +161,7 @@ export function DeployScreen({ projectRoot, manifest, devProperties, force, prod
|
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
else {
|
|
164
|
-
|
|
165
|
-
const zipPath = getZipPath(projectRoot, manifest);
|
|
164
|
+
const zipPath = getDeployZipPath(projectRoot, manifest);
|
|
166
165
|
if (!zipExists(zipPath)) {
|
|
167
166
|
setState({
|
|
168
167
|
status: 'error',
|
package/dist/commands/dev.js
CHANGED
|
@@ -21,18 +21,6 @@ export function DevScreen({ project, options }) {
|
|
|
21
21
|
exit();
|
|
22
22
|
}
|
|
23
23
|
});
|
|
24
|
-
React.useEffect(() => {
|
|
25
|
-
const stop = () => {
|
|
26
|
-
live.stop();
|
|
27
|
-
exit();
|
|
28
|
-
};
|
|
29
|
-
process.on('SIGINT', stop);
|
|
30
|
-
process.on('SIGTERM', stop);
|
|
31
|
-
return () => {
|
|
32
|
-
process.off('SIGINT', stop);
|
|
33
|
-
process.off('SIGTERM', stop);
|
|
34
|
-
};
|
|
35
|
-
}, [live, exit]);
|
|
36
24
|
const status = live.status === 'running'
|
|
37
25
|
? live.phase === 'error'
|
|
38
26
|
? 'error'
|
package/dist/shell/Shell.js
CHANGED
|
@@ -251,7 +251,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
251
251
|
useInput((raw, key) => {
|
|
252
252
|
// Terminals speaking the kitty keyboard protocol report shift+s as
|
|
253
253
|
// "s" plus a shift flag; fold that back into the uppercase letter so
|
|
254
|
-
// P
|
|
254
|
+
// P and K behave the same everywhere.
|
|
255
255
|
const input = key.shift && raw.length === 1 && /[a-z]/.test(raw)
|
|
256
256
|
? raw.toUpperCase()
|
|
257
257
|
: raw;
|
|
@@ -270,7 +270,8 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
270
270
|
setOverlay({ kind: 'palette' });
|
|
271
271
|
return;
|
|
272
272
|
}
|
|
273
|
-
|
|
273
|
+
// The config form uses Tab/Shift+Tab to move between fields.
|
|
274
|
+
if (key.tab && !formActive) {
|
|
274
275
|
setFilter('');
|
|
275
276
|
setFocus(f => f === 'nav' && !single ? 'content' : single ? 'content' : 'nav');
|
|
276
277
|
return;
|
|
@@ -346,7 +347,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
346
347
|
setVersionRow(r => Math.max(0, r - 1));
|
|
347
348
|
if (key.downArrow)
|
|
348
349
|
setVersionRow(r => Math.min(Math.max(0, count - 1), r + 1));
|
|
349
|
-
if (input === '
|
|
350
|
+
if (input === 'r')
|
|
350
351
|
void fetchVersions(false);
|
|
351
352
|
else if (input === 'a' && count > 0)
|
|
352
353
|
void activateSelected();
|
|
@@ -426,8 +427,9 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
426
427
|
: tab === 'versions'
|
|
427
428
|
? h([
|
|
428
429
|
['a', 'activate'],
|
|
429
|
-
['
|
|
430
|
+
['r', 'refresh'],
|
|
430
431
|
['p', 'deploy'],
|
|
432
|
+
['P', 'force'],
|
|
431
433
|
['/', 'commands'],
|
|
432
434
|
['q', 'quit'],
|
|
433
435
|
])
|
|
@@ -437,6 +439,7 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
437
439
|
['x', 'wrap'],
|
|
438
440
|
['K', 'stop'],
|
|
439
441
|
['p', 'deploy'],
|
|
442
|
+
['P', 'force'],
|
|
440
443
|
['/', 'commands'],
|
|
441
444
|
['q', 'quit'],
|
|
442
445
|
])
|
|
@@ -468,9 +471,14 @@ export function Shell({ apps: initialApps, workspaceRoot, version, minimal = fal
|
|
|
468
471
|
['b', 'build'],
|
|
469
472
|
['s', 'sign'],
|
|
470
473
|
['p', 'deploy'],
|
|
471
|
-
['
|
|
472
|
-
['
|
|
474
|
+
['P', 'force'],
|
|
475
|
+
['v', 'env'],
|
|
476
|
+
['K', 'stop'],
|
|
477
|
+
['a', 'versions'],
|
|
478
|
+
['e', 'config'],
|
|
473
479
|
['i', 'install'],
|
|
480
|
+
['l', 'login'],
|
|
481
|
+
[',', 'settings'],
|
|
474
482
|
['/', 'commands'],
|
|
475
483
|
['q', 'quit'],
|
|
476
484
|
]);
|
package/dist/shell/Tabs.js
CHANGED
|
@@ -72,8 +72,8 @@ export function Versions({ project, state, selected, }) {
|
|
|
72
72
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsxs(Text, { dimColor: true, children: [t('APP IDENTIFIER').padEnd(30), t('VERSION').padEnd(12), t('ACTIVE'), state.loading && (_jsxs(Text, { color: ACCENT, children: [' '.repeat(3), _jsx(Spinner, { type: "dots" })] }))] }), state.error && _jsx(Text, { color: "red", children: state.error }), list.map((e, i) => (_jsxs(Text, { backgroundColor: i === selected ? ACCENT : undefined, color: i === selected ? 'black' : undefined, wrap: "truncate", children: [e.appIdentifier.padEnd(30).slice(0, 30), e.appVersion.padEnd(12), e.active ? '●' : '○', i === selected ? ` ${e.id}` : ''] }, e.id))), !state.loading && !state.error && list.length === 0 && (_jsx(Text, { dimColor: true, children: t('No versions uploaded to {addon}.', {
|
|
73
73
|
addon: project.devProperties.addonName,
|
|
74
74
|
}) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [list.length === 1
|
|
75
|
-
? t('1 version · a activate selected ·
|
|
76
|
-
: t('{n} versions · a activate selected ·
|
|
75
|
+
? t('1 version · a activate selected · r refresh')
|
|
76
|
+
: t('{n} versions · a activate selected · r refresh', {
|
|
77
77
|
n: list.length,
|
|
78
78
|
}), state.fetchedAt
|
|
79
79
|
? t(' · fetched {time}', { time: time(state.fetchedAt) })
|
|
@@ -86,5 +86,5 @@ export function Log({ task, height, scroll, wrap, }) {
|
|
|
86
86
|
const visible = Math.max(1, height - 2);
|
|
87
87
|
const end = Math.max(0, task.lines.length - scroll);
|
|
88
88
|
const lines = task.lines.slice(Math.max(0, end - visible), end);
|
|
89
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsxs(Text, { children: [STATUS_GLYPH[task.status], " ", task.label, " ", task.appName, ' ', _jsxs(Text, { dimColor: true, children: [task.phase, " \u00B7 ", elapsed(task), scroll > 0 ? ` · ↑${scroll}` : t(' · following')] })] }), lines.map((line, i) => (_jsxs(Text, { wrap: wrap ? 'wrap' : 'truncate', color: LEVEL_COLOR[line.level], children: [_jsxs(Text, { dimColor: true, children: [time(line.time, true), " ", line.tag.padEnd(3)] }), ' ', line.text] }, i)))] }));
|
|
89
|
+
return (_jsxs(Box, { width: "100%", flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsxs(Text, { children: [STATUS_GLYPH[task.status], " ", task.label, " ", task.appName, ' ', _jsxs(Text, { dimColor: true, children: [task.phase, " \u00B7 ", elapsed(task), scroll > 0 ? ` · ↑${scroll}` : t(' · following')] })] }), _jsx(Box, { flexDirection: "column", maxHeight: visible, justifyContent: "flex-end", overflow: "hidden", children: lines.map((line, i) => (_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: wrap ? 'wrap' : 'truncate', color: LEVEL_COLOR[line.level], children: [_jsxs(Text, { dimColor: true, children: [time(line.time, true), " ", line.tag.padEnd(3)] }), ' ', line.text] }) }, i))) }, wrap ? 'wrap' : 'truncate')] }));
|
|
90
90
|
}
|
package/dist/shell/actions.js
CHANGED
|
Binary file
|
package/dist/utils/i18n.js
CHANGED
|
@@ -68,8 +68,8 @@ const sv = {
|
|
|
68
68
|
VERSION: 'VERSION',
|
|
69
69
|
ACTIVE: 'AKTIV',
|
|
70
70
|
'No versions uploaded to {addon}.': 'Inga versioner uppladdade till {addon}.',
|
|
71
|
-
'{n} versions · a activate selected ·
|
|
72
|
-
'1 version · a activate selected ·
|
|
71
|
+
'{n} versions · a activate selected · r refresh': '{n} versioner · a aktivera vald · r uppdatera',
|
|
72
|
+
'1 version · a activate selected · r refresh': '1 version · a aktivera vald · r uppdatera',
|
|
73
73
|
' · fetched {time}': ' · hämtad {time}',
|
|
74
74
|
'No task yet. d dev · w watch · b build · s sign · p deploy': 'Ingen uppgift ännu. d dev · w watch · b build · s sign · p deploy',
|
|
75
75
|
' · following': ' · följer',
|
|
@@ -97,7 +97,7 @@ const sv = {
|
|
|
97
97
|
'e.g. test or prod, overriding domain and auth': 't.ex. test eller prod, med egen domän och auth',
|
|
98
98
|
'Environment name (e.g. test, prod)': 'Miljönamn (t.ex. test, prod)',
|
|
99
99
|
'Deploy the signed {id} to {env} and activate it?': 'Driftsätt signerade {id} till {env} och aktivera?',
|
|
100
|
-
'Dev never deploys to a production environment ({env}). Switch with
|
|
100
|
+
'Dev never deploys to a production environment ({env}). Switch with v.': 'Dev driftsätter aldrig till en produktionsmiljö ({env}). Byt med v.',
|
|
101
101
|
'switched to {env}': 'bytte till {env}',
|
|
102
102
|
'environment {env} added': 'miljön {env} tillagd',
|
|
103
103
|
environment: 'miljö',
|
|
@@ -217,6 +217,8 @@ const sv = {
|
|
|
217
217
|
activate: 'aktivera',
|
|
218
218
|
refresh: 'uppdatera',
|
|
219
219
|
deploy: 'driftsätt',
|
|
220
|
+
force: 'tvinga',
|
|
221
|
+
versions: 'versioner',
|
|
220
222
|
commands: 'kommandon',
|
|
221
223
|
follow: 'följ',
|
|
222
224
|
wrap: 'radbryt',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ChildProcess, type SpawnOptions } from 'child_process';
|
|
1
2
|
import { EventEmitter } from 'events';
|
|
2
3
|
export interface ProcessOutput {
|
|
3
4
|
type: 'stdout' | 'stderr';
|
|
@@ -7,15 +8,23 @@ export interface ProcessResult {
|
|
|
7
8
|
exitCode: number;
|
|
8
9
|
output: ProcessOutput[];
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Spawn a child in its own process group, so stopping it also stops whatever it
|
|
13
|
+
* starts (npm, webpack workers), and track it so the CLI can kill leftovers on
|
|
14
|
+
* exit.
|
|
15
|
+
*/
|
|
16
|
+
export declare function spawnChild(command: string, args: string[], options?: SpawnOptions): ChildProcess;
|
|
17
|
+
/** Stop a child and everything it started. */
|
|
18
|
+
export declare function killChild(child: ChildProcess): void;
|
|
19
|
+
/** Kill every child still running; returns how many were signalled. */
|
|
20
|
+
export declare function killAllChildren(): number;
|
|
10
21
|
export declare class ProcessRunner extends EventEmitter {
|
|
11
22
|
private process;
|
|
12
|
-
private output;
|
|
23
|
+
private readonly output;
|
|
13
24
|
private readonly command;
|
|
14
25
|
private readonly args;
|
|
15
26
|
private readonly cwd?;
|
|
16
|
-
|
|
17
|
-
private readonly customEnv?;
|
|
18
|
-
constructor(command: string, args?: string[], cwd?: string, interactive?: boolean, customEnv?: Record<string, string>);
|
|
27
|
+
constructor(command: string, args?: string[], cwd?: string);
|
|
19
28
|
run(): Promise<ProcessResult>;
|
|
20
29
|
kill(): void;
|
|
21
30
|
getOutput(): ProcessOutput[];
|
|
@@ -1,54 +1,81 @@
|
|
|
1
|
-
import { spawn } from 'child_process';
|
|
1
|
+
import { spawn, spawnSync, } from 'child_process';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
|
+
const children = new Set();
|
|
4
|
+
/**
|
|
5
|
+
* Spawn a child in its own process group, so stopping it also stops whatever it
|
|
6
|
+
* starts (npm, webpack workers), and track it so the CLI can kill leftovers on
|
|
7
|
+
* exit.
|
|
8
|
+
*/
|
|
9
|
+
export function spawnChild(command, args, options = {}) {
|
|
10
|
+
const child = spawn(command, args, {
|
|
11
|
+
...options,
|
|
12
|
+
detached: process.platform !== 'win32',
|
|
13
|
+
});
|
|
14
|
+
children.add(child);
|
|
15
|
+
const forget = () => children.delete(child);
|
|
16
|
+
child.on('close', forget);
|
|
17
|
+
child.on('error', forget);
|
|
18
|
+
return child;
|
|
19
|
+
}
|
|
20
|
+
/** Stop a child and everything it started. */
|
|
21
|
+
export function killChild(child) {
|
|
22
|
+
if (child.pid === undefined ||
|
|
23
|
+
child.exitCode !== null ||
|
|
24
|
+
child.signalCode !== null) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
// No process groups on Windows: taskkill /T takes the whole tree.
|
|
30
|
+
spawnSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], {
|
|
31
|
+
stdio: 'ignore',
|
|
32
|
+
windowsHide: true,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Already gone.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Kill every child still running; returns how many were signalled. */
|
|
44
|
+
export function killAllChildren() {
|
|
45
|
+
const running = [...children];
|
|
46
|
+
for (const child of running)
|
|
47
|
+
killChild(child);
|
|
48
|
+
return running.length;
|
|
49
|
+
}
|
|
3
50
|
export class ProcessRunner extends EventEmitter {
|
|
4
51
|
process = null;
|
|
5
52
|
output = [];
|
|
6
53
|
command;
|
|
7
54
|
args;
|
|
8
55
|
cwd;
|
|
9
|
-
|
|
10
|
-
customEnv;
|
|
11
|
-
constructor(command, args = [], cwd, interactive = false, customEnv) {
|
|
56
|
+
constructor(command, args = [], cwd) {
|
|
12
57
|
super();
|
|
13
58
|
this.command = command;
|
|
14
59
|
this.args = args;
|
|
15
60
|
this.cwd = cwd;
|
|
16
|
-
this.interactive = interactive;
|
|
17
|
-
this.customEnv = customEnv;
|
|
18
61
|
}
|
|
19
62
|
run() {
|
|
20
63
|
return new Promise((resolve, reject) => {
|
|
21
|
-
this.process =
|
|
64
|
+
this.process = spawnChild(this.command, this.args, {
|
|
22
65
|
cwd: this.cwd || process.cwd(),
|
|
23
|
-
env: { ...process.env, ...this.customEnv },
|
|
24
66
|
shell: true,
|
|
25
|
-
stdio:
|
|
67
|
+
stdio: 'pipe',
|
|
26
68
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
this.emit('output', output);
|
|
39
|
-
});
|
|
40
|
-
this.process.stderr?.on('data', data => {
|
|
41
|
-
const output = {
|
|
42
|
-
type: 'stderr',
|
|
43
|
-
data: data.toString(),
|
|
44
|
-
};
|
|
45
|
-
this.output.push(output);
|
|
46
|
-
if (this.output.length > 1000) {
|
|
47
|
-
this.output.shift();
|
|
48
|
-
}
|
|
49
|
-
this.emit('output', output);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
69
|
+
const capture = (type) => (data) => {
|
|
70
|
+
const output = { type, data: data.toString() };
|
|
71
|
+
this.output.push(output);
|
|
72
|
+
if (this.output.length > 1000) {
|
|
73
|
+
this.output.shift();
|
|
74
|
+
}
|
|
75
|
+
this.emit('output', output);
|
|
76
|
+
};
|
|
77
|
+
this.process.stdout?.on('data', capture('stdout'));
|
|
78
|
+
this.process.stderr?.on('data', capture('stderr'));
|
|
52
79
|
this.process.on('error', error => {
|
|
53
80
|
this.emit('error', error);
|
|
54
81
|
reject(error);
|
|
@@ -64,9 +91,8 @@ export class ProcessRunner extends EventEmitter {
|
|
|
64
91
|
});
|
|
65
92
|
}
|
|
66
93
|
kill() {
|
|
67
|
-
if (this.process)
|
|
68
|
-
this.process
|
|
69
|
-
}
|
|
94
|
+
if (this.process)
|
|
95
|
+
killChild(this.process);
|
|
70
96
|
}
|
|
71
97
|
getOutput() {
|
|
72
98
|
return this.output;
|
|
@@ -62,6 +62,11 @@ export declare function getZipPath(projectRoot: string, manifest: SitevisionMani
|
|
|
62
62
|
* Get the full path to the signed zip file in dist/
|
|
63
63
|
*/
|
|
64
64
|
export declare function getSignedZipPath(projectRoot: string, manifest: SitevisionManifest): string;
|
|
65
|
+
/**
|
|
66
|
+
* The zip a non-production deploy uploads: the signed one when it is at least
|
|
67
|
+
* as new as the build, since some sites reject unsigned apps even on dev.
|
|
68
|
+
*/
|
|
69
|
+
export declare function getDeployZipPath(projectRoot: string, manifest: SitevisionManifest): string;
|
|
65
70
|
/**
|
|
66
71
|
* Get API endpoints for the given app type
|
|
67
72
|
*/
|
|
@@ -192,6 +192,20 @@ export function getZipPath(projectRoot, manifest) {
|
|
|
192
192
|
export function getSignedZipPath(projectRoot, manifest) {
|
|
193
193
|
return path.join(projectRoot, 'dist', getSignedZipFilename(manifest));
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* The zip a non-production deploy uploads: the signed one when it is at least
|
|
197
|
+
* as new as the build, since some sites reject unsigned apps even on dev.
|
|
198
|
+
*/
|
|
199
|
+
export function getDeployZipPath(projectRoot, manifest) {
|
|
200
|
+
const zip = getZipPath(projectRoot, manifest);
|
|
201
|
+
const signed = getSignedZipPath(projectRoot, manifest);
|
|
202
|
+
const mtime = (file) => fs.statSync(file, { throwIfNoEntry: false })?.mtimeMs;
|
|
203
|
+
const signedAt = mtime(signed);
|
|
204
|
+
if (signedAt === undefined)
|
|
205
|
+
return zip;
|
|
206
|
+
const builtAt = mtime(zip);
|
|
207
|
+
return builtAt === undefined || signedAt >= builtAt ? signed : zip;
|
|
208
|
+
}
|
|
195
209
|
// =============================================================================
|
|
196
210
|
// API ENDPOINT UTILITIES
|
|
197
211
|
// =============================================================================
|
|
@@ -79,5 +79,6 @@ export interface SitevisionBuildResult {
|
|
|
79
79
|
*
|
|
80
80
|
* @param projectRoot - Project root directory (used as cwd)
|
|
81
81
|
* @param onOutput - Optional callback for streaming output chunks
|
|
82
|
+
* @param signal - Aborting kills the build and everything it started
|
|
82
83
|
*/
|
|
83
|
-
export declare function runSitevisionScriptsBuild(projectRoot: string, onOutput?: (chunk: string) => void): Promise<SitevisionBuildResult>;
|
|
84
|
+
export declare function runSitevisionScriptsBuild(projectRoot: string, onOutput?: (chunk: string) => void, signal?: AbortSignal): Promise<SitevisionBuildResult>;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import fs from 'fs';
|
|
20
|
-
import {
|
|
20
|
+
import { killChild, spawnChild } from './process-runner.js';
|
|
21
21
|
/**
|
|
22
22
|
* Resolve the path to the sitevision-scripts CLI entry inside a project.
|
|
23
23
|
* Returns null if the package is not installed.
|
|
@@ -145,8 +145,9 @@ const MAX_OUTPUT_CHARS = 50_000;
|
|
|
145
145
|
*
|
|
146
146
|
* @param projectRoot - Project root directory (used as cwd)
|
|
147
147
|
* @param onOutput - Optional callback for streaming output chunks
|
|
148
|
+
* @param signal - Aborting kills the build and everything it started
|
|
148
149
|
*/
|
|
149
|
-
export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
150
|
+
export async function runSitevisionScriptsBuild(projectRoot, onOutput, signal) {
|
|
150
151
|
const bin = getSitevisionScriptsBin(projectRoot);
|
|
151
152
|
if (!bin) {
|
|
152
153
|
return {
|
|
@@ -157,10 +158,16 @@ export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
|
157
158
|
}
|
|
158
159
|
return new Promise(resolve => {
|
|
159
160
|
let output = '';
|
|
160
|
-
const child =
|
|
161
|
+
const child = spawnChild(process.execPath, [bin, 'build'], {
|
|
161
162
|
cwd: projectRoot,
|
|
162
163
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
163
164
|
});
|
|
165
|
+
const abort = () => {
|
|
166
|
+
killChild(child);
|
|
167
|
+
};
|
|
168
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
169
|
+
if (signal?.aborted)
|
|
170
|
+
abort();
|
|
164
171
|
const handleData = (data) => {
|
|
165
172
|
const text = data.toString();
|
|
166
173
|
output += text;
|
|
@@ -175,12 +182,16 @@ export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
|
175
182
|
resolve({ success: false, output, error: error.message });
|
|
176
183
|
});
|
|
177
184
|
child.on('close', code => {
|
|
185
|
+
signal?.removeEventListener('abort', abort);
|
|
186
|
+
const stopped = signal?.aborted ?? false;
|
|
178
187
|
resolve({
|
|
179
|
-
success: code === 0,
|
|
188
|
+
success: code === 0 && !stopped,
|
|
180
189
|
output,
|
|
181
|
-
error:
|
|
182
|
-
?
|
|
183
|
-
:
|
|
190
|
+
error: stopped
|
|
191
|
+
? 'Build stopped.'
|
|
192
|
+
: code === 0
|
|
193
|
+
? undefined
|
|
194
|
+
: `sitevision-scripts build exited with code ${code}`,
|
|
184
195
|
});
|
|
185
196
|
});
|
|
186
197
|
});
|
package/dist/utils/tasks.js
CHANGED
|
@@ -5,7 +5,7 @@ import { useSyncExternalStore } from 'react';
|
|
|
5
5
|
import { WebpackRunner, hasLocalWebpackConfig } from './webpack-runner.js';
|
|
6
6
|
import { hasSitevisionScripts, runSitevisionScriptsBuild, getDelegatedZipPath, checkSitevisionScriptsCompatibility, } from './sitevision-scripts-runner.js';
|
|
7
7
|
import { copyStaticToBuild, copySrcToBuild, cleanBuild, createBuildZip, zipExists, } from './zip.js';
|
|
8
|
-
import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, localizedText, } from './project-detection.js';
|
|
8
|
+
import { isBundledApp, getAppType, getFullAppId, getZipPath, getSignedZipPath, getDeployZipPath, localizedText, } from './project-detection.js';
|
|
9
9
|
import { signApp, deployApp, deployProduction, activateApp, } from './sitevision-api.js';
|
|
10
10
|
import { ProcessRunner } from './process-runner.js';
|
|
11
11
|
const MAX_LINES = 2000;
|
|
@@ -78,7 +78,7 @@ const errorText = (error) => error instanceof Error ? error.message : String(err
|
|
|
78
78
|
// One-shot steps shared by the tasks below.
|
|
79
79
|
// ---------------------------------------------------------------------------
|
|
80
80
|
/** Build the app to dist/<appId>.zip. Returns the zip path or throws. */
|
|
81
|
-
async function buildOnce(project, task, log, mode) {
|
|
81
|
+
async function buildOnce(project, task, log, mode, signal) {
|
|
82
82
|
const { root, manifest } = project;
|
|
83
83
|
cleanBuild(root);
|
|
84
84
|
if (isBundledApp(manifest) && !hasLocalWebpackConfig(root)) {
|
|
@@ -90,7 +90,7 @@ async function buildOnce(project, task, log, mode) {
|
|
|
90
90
|
log('bld', warning, 'warn');
|
|
91
91
|
setPhase(task, 'building');
|
|
92
92
|
log('bld', 'building via sitevision-scripts');
|
|
93
|
-
const result = await runSitevisionScriptsBuild(root, chunk => log('bld', chunk));
|
|
93
|
+
const result = await runSitevisionScriptsBuild(root, chunk => log('bld', chunk), signal);
|
|
94
94
|
if (!result.success)
|
|
95
95
|
throw new Error(result.error ?? 'Build failed');
|
|
96
96
|
const zipPath = getDelegatedZipPath(root, manifest.id);
|
|
@@ -148,7 +148,7 @@ async function signOnce(project, task, log, zipPath, credentials) {
|
|
|
148
148
|
async function deployOnce(project, task, log, zipPath, config, options) {
|
|
149
149
|
setPhase(task, 'deploying');
|
|
150
150
|
const appType = getAppType(project.manifest);
|
|
151
|
-
log('dep', `POST multipart → ${options.production ? 'production' : 'dev'} import · ${config.addonName}`);
|
|
151
|
+
log('dep', `POST multipart → ${options.production ? 'production' : 'dev'} import · ${config.addonName} · ${path.basename(zipPath)}`);
|
|
152
152
|
const result = options.production
|
|
153
153
|
? await deployProduction(zipPath, { ...config, activate: options.activate }, appType)
|
|
154
154
|
: await deployApp(zipPath, config, appType, options.force);
|
|
@@ -161,10 +161,14 @@ async function deployOnce(project, task, log, zipPath, config, options) {
|
|
|
161
161
|
// Tasks
|
|
162
162
|
// ---------------------------------------------------------------------------
|
|
163
163
|
export function startBuild(project) {
|
|
164
|
-
const
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const { task, log, finish } = createTask('build', project, 'build', () => {
|
|
166
|
+
controller.abort();
|
|
167
|
+
finish('stopped');
|
|
168
|
+
});
|
|
165
169
|
void (async () => {
|
|
166
170
|
try {
|
|
167
|
-
const zip = await buildOnce(project, task, log, 'production');
|
|
171
|
+
const zip = await buildOnce(project, task, log, 'production', controller.signal);
|
|
168
172
|
log('bld', `created ${zip}`, 'ok');
|
|
169
173
|
finish('success');
|
|
170
174
|
}
|
|
@@ -197,7 +201,7 @@ export function startDeploy(project, config, options) {
|
|
|
197
201
|
try {
|
|
198
202
|
const zipPath = options.production
|
|
199
203
|
? getSignedZipPath(project.root, project.manifest)
|
|
200
|
-
:
|
|
204
|
+
: getDeployZipPath(project.root, project.manifest);
|
|
201
205
|
if (!zipExists(zipPath)) {
|
|
202
206
|
throw new Error(`${options.production ? 'Signed zip' : 'Zip'} not found: ${zipPath}. Run ${options.production ? 'sign' : 'build'} first.`);
|
|
203
207
|
}
|
|
@@ -230,7 +234,10 @@ export function startActivate(project, config, executableId, versionLabel) {
|
|
|
230
234
|
}
|
|
231
235
|
export function startInstall(project) {
|
|
232
236
|
const runner = new ProcessRunner('npm', ['install'], project.root);
|
|
233
|
-
const { task, log, finish } = createTask('install', project, 'npm install', () =>
|
|
237
|
+
const { task, log, finish } = createTask('install', project, 'npm install', () => {
|
|
238
|
+
runner.kill();
|
|
239
|
+
finish('stopped');
|
|
240
|
+
});
|
|
234
241
|
setPhase(task, 'installing');
|
|
235
242
|
runner.on('output', (output) => log('npm', output.data, output.type === 'stderr' ? 'warn' : 'info'));
|
|
236
243
|
runner
|
|
@@ -260,7 +267,9 @@ export function startDev(project, options) {
|
|
|
260
267
|
let debounce;
|
|
261
268
|
let building = false;
|
|
262
269
|
let pending = false;
|
|
270
|
+
const controller = new AbortController();
|
|
263
271
|
const stop = () => {
|
|
272
|
+
controller.abort();
|
|
264
273
|
clearTimeout(debounce);
|
|
265
274
|
for (const watcher of watchers)
|
|
266
275
|
watcher.close();
|
|
@@ -269,6 +278,9 @@ export function startDev(project, options) {
|
|
|
269
278
|
};
|
|
270
279
|
const { task, log, finish } = createTask(options.deploy ? 'dev' : 'watch', project, options.deploy ? 'dev' : 'watch', stop);
|
|
271
280
|
const afterBuild = async (zipPath) => {
|
|
281
|
+
// Stopped mid-build: never sign or deploy what is left.
|
|
282
|
+
if (controller.signal.aborted)
|
|
283
|
+
return;
|
|
272
284
|
let deployZip = zipPath;
|
|
273
285
|
if (options.signingCredentials) {
|
|
274
286
|
deployZip = await signOnce(project, task, log, zipPath, options.signingCredentials);
|
|
@@ -282,6 +294,8 @@ export function startDev(project, options) {
|
|
|
282
294
|
log('svc', 'watching for changes');
|
|
283
295
|
};
|
|
284
296
|
const fail = (error) => {
|
|
297
|
+
if (controller.signal.aborted)
|
|
298
|
+
return;
|
|
285
299
|
log('svc', errorText(error), 'error');
|
|
286
300
|
setPhase(task, 'error');
|
|
287
301
|
};
|
|
@@ -296,12 +310,14 @@ export function startDev(project, options) {
|
|
|
296
310
|
pending = false;
|
|
297
311
|
try {
|
|
298
312
|
// eslint-disable-next-line no-await-in-loop
|
|
299
|
-
|
|
313
|
+
const zip = await buildOnce(project, task, log, 'development', controller.signal);
|
|
314
|
+
// eslint-disable-next-line no-await-in-loop
|
|
315
|
+
await afterBuild(zip);
|
|
300
316
|
}
|
|
301
317
|
catch (error) {
|
|
302
318
|
fail(error);
|
|
303
319
|
}
|
|
304
|
-
} while (pending);
|
|
320
|
+
} while (pending && !controller.signal.aborted);
|
|
305
321
|
}
|
|
306
322
|
finally {
|
|
307
323
|
building = false;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -42,7 +42,7 @@ svc
|
|
|
42
42
|
3. Press `i` to install dependencies if needed, then `d` to start dev: build on
|
|
43
43
|
every change and deploy. Output is in the **Log** tab (`4`).
|
|
44
44
|
|
|
45
|
-
For production: switch environment with `
|
|
45
|
+
For production: switch environment with `v`, press `b` to build, `s` to sign
|
|
46
46
|
and `p` to deploy and activate.
|
|
47
47
|
|
|
48
48
|
## The shell
|
|
@@ -53,7 +53,7 @@ and `p` to deploy and activate.
|
|
|
53
53
|
| `b` / `s` | Build / Sign |
|
|
54
54
|
| `p` / `P` | Deploy / force deploy to the active environment |
|
|
55
55
|
| `a` | Versions: list and activate uploaded versions |
|
|
56
|
-
| `
|
|
56
|
+
| `v` | Switch environment |
|
|
57
57
|
| `e` / `y` / `i` | Config tab / sync `package.json` / `npm install` |
|
|
58
58
|
| `l` | Log in again |
|
|
59
59
|
| `K` | Stop running tasks |
|