sitevision-cli 1.0.0-beta.9 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -0
- package/LICENSE +21 -0
- package/dist/cli.js +194 -70
- package/dist/commands/TaskScreen.js +32 -0
- package/dist/commands/build.js +6 -185
- package/dist/commands/deploy.js +95 -212
- package/dist/commands/dev.js +87 -416
- package/dist/commands/info.js +1 -1
- package/dist/commands/setup-signing.js +50 -33
- package/dist/commands/sign.js +9 -86
- package/dist/commands/use-finish.js +17 -0
- package/dist/commands/watch.js +5 -31
- package/dist/components/AnimatedLogo.js +8 -2
- package/dist/components/AuthLoginScreen.js +33 -15
- package/dist/components/PasswordInput.js +2 -1
- package/dist/scaffold/inquirer-stub.js +119 -0
- package/dist/scaffold/register.js +15 -0
- package/dist/shell/AddonPicker.js +54 -0
- package/dist/shell/Changelog.js +50 -0
- package/dist/shell/CommandPalette.js +63 -0
- package/dist/shell/ConfigForm.js +703 -0
- package/dist/shell/Frame.js +138 -0
- package/dist/shell/Help.js +96 -0
- package/dist/shell/Popover.js +15 -0
- package/dist/shell/Settings.js +149 -0
- package/dist/shell/Shell.js +953 -0
- package/dist/shell/Tabs.js +96 -0
- package/dist/shell/actions.js +493 -0
- package/dist/utils/config.js +62 -2
- package/dist/utils/create-app.js +231 -0
- package/dist/utils/debug.js +32 -0
- package/dist/utils/environments.js +74 -0
- package/dist/utils/i18n.js +369 -0
- package/dist/utils/jsonc.js +2 -1
- package/dist/utils/keychain.js +54 -6
- package/dist/utils/oauth2-auth.js +146 -43
- package/dist/utils/password-prompt.js +37 -3
- package/dist/utils/process-runner.js +77 -38
- package/dist/utils/project-detection.js +457 -112
- package/dist/utils/session-cookie-auth.js +10 -6
- package/dist/utils/sitevision-api.js +134 -25
- package/dist/utils/sitevision-scripts-runner.js +50 -22
- package/dist/utils/tasks.js +510 -0
- package/dist/utils/version-check.js +3 -3
- package/dist/utils/webpack-runner.js +9 -5
- package/dist/utils/workspace.js +105 -0
- package/dist/utils/zip.js +18 -19
- package/package.json +29 -5
- package/readme.md +117 -141
- package/dist/app.d.ts +0 -6
- package/dist/app.js +0 -307
- package/dist/cli.d.ts +0 -2
- package/dist/commands/build.d.ts +0 -12
- package/dist/commands/deploy.d.ts +0 -16
- package/dist/commands/dev.d.ts +0 -16
- package/dist/commands/index.d.ts +0 -4
- package/dist/commands/info.d.ts +0 -2
- package/dist/commands/setup-signing.d.ts +0 -2
- package/dist/commands/sign.d.ts +0 -14
- package/dist/commands/types.d.ts +0 -18
- package/dist/commands/watch.d.ts +0 -2
- package/dist/components/AnimatedLogo.d.ts +0 -10
- package/dist/components/AuthLoginScreen.d.ts +0 -21
- package/dist/components/DevPropertiesForm.d.ts +0 -10
- package/dist/components/DevPropertiesForm.js +0 -210
- package/dist/components/InfoScreen.d.ts +0 -7
- package/dist/components/InfoScreen.js +0 -14
- package/dist/components/KeychainPasswordChoice.d.ts +0 -8
- package/dist/components/KeychainPasswordChoice.js +0 -30
- package/dist/components/MainMenu.d.ts +0 -7
- package/dist/components/MainMenu.js +0 -83
- package/dist/components/PasswordInput.d.ts +0 -10
- package/dist/components/ProcessOutput.d.ts +0 -6
- package/dist/components/ProcessOutput.js +0 -30
- package/dist/components/SetupFlow.d.ts +0 -8
- package/dist/components/SetupFlow.js +0 -233
- package/dist/components/SigningPropertiesForm.d.ts +0 -7
- package/dist/components/SigningPropertiesForm.js +0 -38
- package/dist/components/StatusIndicator.d.ts +0 -8
- package/dist/components/TextInput.d.ts +0 -10
- package/dist/components/TextInput.js +0 -29
- package/dist/components/WelcomeScreen.d.ts +0 -14
- package/dist/types/index.d.ts +0 -284
- package/dist/utils/branding.d.ts +0 -28
- package/dist/utils/config.d.ts +0 -17
- package/dist/utils/jsonc.d.ts +0 -19
- package/dist/utils/keychain.d.ts +0 -15
- package/dist/utils/oauth2-auth.d.ts +0 -27
- package/dist/utils/password-prompt.d.ts +0 -14
- package/dist/utils/process-runner.d.ts +0 -22
- package/dist/utils/project-detection.d.ts +0 -151
- package/dist/utils/session-cookie-auth.d.ts +0 -34
- package/dist/utils/signing-password.d.ts +0 -12
- package/dist/utils/signing-step.d.ts +0 -15
- package/dist/utils/signing-step.js +0 -16
- package/dist/utils/sitevision-api.d.ts +0 -112
- package/dist/utils/sitevision-scripts-runner.d.ts +0 -83
- package/dist/utils/version-check.d.ts +0 -9
- package/dist/utils/webpack-runner.d.ts +0 -76
- package/dist/utils/zip.d.ts +0 -71
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { setSessionCookie } from './keychain.js';
|
|
2
|
+
import { debug } from './debug.js';
|
|
2
3
|
function bareDomain(domain) {
|
|
3
4
|
return domain.replace(/^\./, '');
|
|
4
5
|
}
|
|
@@ -15,14 +16,15 @@ function domainRelated(a, b) {
|
|
|
15
16
|
* browser — so it's unit-testable.
|
|
16
17
|
*/
|
|
17
18
|
export function selectSessionCookie(all, siteDomain) {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// Only a session on the deploy site counts: a Java IdP sets a JSESSIONID of
|
|
20
|
+
// its own, and that one must never be sent to the site.
|
|
21
|
+
const chosen = all.find(c => c.name === 'JSESSIONID' && domainRelated(c.domain, siteDomain));
|
|
22
|
+
if (!chosen) {
|
|
20
23
|
const domains = [...new Set(all.map(c => bareDomain(c.domain)))];
|
|
21
24
|
return {
|
|
22
|
-
error: `No JSESSIONID among ${all.length} cookies. Domains seen: ${domains.join(', ') || 'none'}. If these are only your IdP, open a Sitevision page in the browser, then press Enter again.`,
|
|
25
|
+
error: `No JSESSIONID for ${siteDomain} among ${all.length} cookies. Domains seen: ${domains.join(', ') || 'none'}. If these are only your IdP, open a Sitevision page in the browser, then press Enter again.`,
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
|
-
const chosen = sessions.find(c => domainRelated(c.domain, siteDomain)) ?? sessions[0];
|
|
26
28
|
const cookies = all.filter(c => domainRelated(c.domain, chosen.domain));
|
|
27
29
|
return {
|
|
28
30
|
cookie: cookies.map(c => `${c.name}=${c.value}`).join('; '),
|
|
@@ -56,7 +58,7 @@ async function readAllCookies(browser, page) {
|
|
|
56
58
|
*/
|
|
57
59
|
export async function beginCookieLogin(dev) {
|
|
58
60
|
const { domain, username } = dev;
|
|
59
|
-
if (!domain
|
|
61
|
+
if (!domain)
|
|
60
62
|
return null;
|
|
61
63
|
let puppeteer;
|
|
62
64
|
try {
|
|
@@ -70,12 +72,14 @@ export async function beginCookieLogin(dev) {
|
|
|
70
72
|
browser = await puppeteer.launch({ headless: false, channel: 'chrome' });
|
|
71
73
|
const page = await browser.newPage();
|
|
72
74
|
const loginUrl = dev.sessionLoginUrl || `https://${domain}/`;
|
|
75
|
+
debug('auth', `cookie login ${loginUrl}`);
|
|
73
76
|
await page.goto(loginUrl, { waitUntil: 'domcontentloaded' }).catch(() => {
|
|
74
77
|
// A SAML redirect may abort the initial navigation — that's fine.
|
|
75
78
|
});
|
|
76
79
|
const capture = async () => {
|
|
77
80
|
const all = await readAllCookies(browser, page);
|
|
78
81
|
const result = selectSessionCookie(all, domain);
|
|
82
|
+
debug('auth', `cookie capture ${domain}: ${result.cookie ? 'session found' : 'no session'} (${all.length} cookies)`);
|
|
79
83
|
if (result.cookie) {
|
|
80
84
|
setSessionCookie(domain, username, result.cookie);
|
|
81
85
|
}
|
|
@@ -86,7 +90,7 @@ export async function beginCookieLogin(dev) {
|
|
|
86
90
|
// Best-effort close.
|
|
87
91
|
});
|
|
88
92
|
};
|
|
89
|
-
return { capture, close };
|
|
93
|
+
return { capture, close, loginUrl };
|
|
90
94
|
}
|
|
91
95
|
catch {
|
|
92
96
|
if (browser) {
|
|
@@ -12,7 +12,8 @@ import fs from 'fs';
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import https from 'https';
|
|
14
14
|
import http from 'http';
|
|
15
|
-
import { buildImportEndpointUrl, buildAddonEndpointUrl, } from './project-detection.js';
|
|
15
|
+
import { buildImportEndpointUrl, buildAddonEndpointUrl, buildApiBaseUrl, } from './project-detection.js';
|
|
16
|
+
import { debug } from './debug.js';
|
|
16
17
|
// =============================================================================
|
|
17
18
|
// CONSTANTS
|
|
18
19
|
// =============================================================================
|
|
@@ -89,6 +90,9 @@ function createMultipartFormData(filePath, fieldName, boundary) {
|
|
|
89
90
|
export function makeRequest(url, options) {
|
|
90
91
|
return new Promise((resolve, reject) => {
|
|
91
92
|
const parsedUrl = new URL(url);
|
|
93
|
+
const startedAt = Date.now();
|
|
94
|
+
const describe = `${options.method} ${parsedUrl.host}${parsedUrl.pathname}`;
|
|
95
|
+
debug('http', describe);
|
|
92
96
|
const isHttps = parsedUrl.protocol === 'https:';
|
|
93
97
|
const transport = isHttps ? https : http;
|
|
94
98
|
const headers = {
|
|
@@ -121,18 +125,28 @@ export function makeRequest(url, options) {
|
|
|
121
125
|
chunks.push(chunk);
|
|
122
126
|
});
|
|
123
127
|
res.on('end', () => {
|
|
128
|
+
debug('http', `${describe} -> ${res.statusCode} ${res.headers['content-type'] ?? ''} ${Date.now() - startedAt}ms`);
|
|
124
129
|
resolve({
|
|
125
130
|
statusCode: res.statusCode || 0,
|
|
126
131
|
body: Buffer.concat(chunks),
|
|
127
132
|
headers: res.headers,
|
|
128
133
|
});
|
|
129
134
|
});
|
|
135
|
+
// A connection cut mid-body never emits 'end'.
|
|
136
|
+
res.on('error', reject);
|
|
137
|
+
res.on('close', () => {
|
|
138
|
+
if (!res.complete)
|
|
139
|
+
reject(new Error('Response was cut off'));
|
|
140
|
+
});
|
|
130
141
|
});
|
|
131
142
|
// Abort hung connections instead of blocking the CLI indefinitely.
|
|
132
143
|
req.setTimeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, () => {
|
|
133
144
|
req.destroy(new Error(`Request timed out after ${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`));
|
|
134
145
|
});
|
|
135
|
-
req.on('error',
|
|
146
|
+
req.on('error', error => {
|
|
147
|
+
debug('http', `${describe} failed: ${error.message}`);
|
|
148
|
+
reject(error);
|
|
149
|
+
});
|
|
136
150
|
if (options.body) {
|
|
137
151
|
req.write(options.body);
|
|
138
152
|
}
|
|
@@ -336,8 +350,9 @@ export async function deployApp(zipPath, config, appType, force = false) {
|
|
|
336
350
|
if (force) {
|
|
337
351
|
url += '?force=true';
|
|
338
352
|
}
|
|
339
|
-
// The import endpoints
|
|
340
|
-
// (per the webAppImport/restAppImport docs
|
|
353
|
+
// The import endpoints read the archive from a multipart part named "data"
|
|
354
|
+
// (per the webAppImport/restAppImport docs, and confirmed against a live
|
|
355
|
+
// server). Signing uses "file" against a different endpoint.
|
|
341
356
|
const boundary = generateBoundary();
|
|
342
357
|
const { body, contentType } = createMultipartFormData(zipPath, 'data', boundary);
|
|
343
358
|
const { auth, kind } = configAuth(config);
|
|
@@ -390,6 +405,14 @@ export async function deployApp(zipPath, config, appType, force = false) {
|
|
|
390
405
|
error: 'Conflict. Addon already exists. Use --force to overwrite.',
|
|
391
406
|
};
|
|
392
407
|
}
|
|
408
|
+
if (response.statusCode === 400 &&
|
|
409
|
+
response.body.toString().includes('could not resolve context node')) {
|
|
410
|
+
return {
|
|
411
|
+
success: false,
|
|
412
|
+
error: `Deployment failed with status 400: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
413
|
+
contextNodeMissing: true,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
393
416
|
return {
|
|
394
417
|
success: false,
|
|
395
418
|
error: `Deployment failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
@@ -415,23 +438,24 @@ export async function deployProduction(signedZipPath, config, appType) {
|
|
|
415
438
|
if (!deployResult.success) {
|
|
416
439
|
return deployResult;
|
|
417
440
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (!activationResult.success) {
|
|
422
|
-
return {
|
|
423
|
-
success: true,
|
|
424
|
-
executableId: deployResult.executableId,
|
|
425
|
-
message: `Deployed successfully but activation failed: ${activationResult.error}`,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
441
|
+
if (!config.activate)
|
|
442
|
+
return deployResult;
|
|
443
|
+
if (!deployResult.executableId) {
|
|
428
444
|
return {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
message: 'Deployed
|
|
445
|
+
...deployResult,
|
|
446
|
+
activated: false,
|
|
447
|
+
message: 'Deployed, but not activated: the server did not return an executable id.',
|
|
432
448
|
};
|
|
433
449
|
}
|
|
434
|
-
|
|
450
|
+
const activationResult = await activateApp(deployResult.executableId, config, appType);
|
|
451
|
+
return {
|
|
452
|
+
success: true,
|
|
453
|
+
executableId: deployResult.executableId,
|
|
454
|
+
activated: activationResult.success,
|
|
455
|
+
message: activationResult.success
|
|
456
|
+
? 'Deployed and activated successfully'
|
|
457
|
+
: `Deployed successfully but activation failed: ${activationResult.error}`,
|
|
458
|
+
};
|
|
435
459
|
}
|
|
436
460
|
// =============================================================================
|
|
437
461
|
// ADDON MANAGEMENT API
|
|
@@ -444,10 +468,11 @@ export async function deployProduction(signedZipPath, config, appType) {
|
|
|
444
468
|
*/
|
|
445
469
|
export async function createAddon(config, appType) {
|
|
446
470
|
const url = buildAddonEndpointUrl(config.domain, config.siteName, appType, config.useHTTP);
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
471
|
+
const payload = { name: config.addonName };
|
|
472
|
+
// Only the custommodule endpoints take (and require) a category.
|
|
473
|
+
if (appType === 'web' || appType === 'widget')
|
|
474
|
+
payload['category'] = 'Other';
|
|
475
|
+
const body = JSON.stringify(payload);
|
|
451
476
|
const { auth, kind } = configAuth(config);
|
|
452
477
|
try {
|
|
453
478
|
const response = await makeRequest(url, {
|
|
@@ -508,9 +533,8 @@ export async function createAddon(config, appType) {
|
|
|
508
533
|
export async function activateApp(executableId, config, _appType) {
|
|
509
534
|
const protocol = config.useHTTP ? 'http' : 'https';
|
|
510
535
|
const url = `${protocol}://${config.domain}/rest-api/1/0/${encodeURIComponent(config.siteName)}/Addon%20Repository/${encodeURIComponent(config.addonName)}/activateCustomModuleExecutable`;
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
});
|
|
536
|
+
// Property name per the ActivateCustomModuleExecutable PUT docs.
|
|
537
|
+
const body = JSON.stringify({ customModuleExecutableId: executableId });
|
|
514
538
|
const { auth, kind } = configAuth(config);
|
|
515
539
|
try {
|
|
516
540
|
const response = await makeRequest(url, {
|
|
@@ -544,6 +568,91 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
544
568
|
};
|
|
545
569
|
}
|
|
546
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* List the executables (uploaded versions) of the configured addon.
|
|
573
|
+
*/
|
|
574
|
+
export async function listExecutables(config) {
|
|
575
|
+
const protocol = config.useHTTP ? 'http' : 'https';
|
|
576
|
+
const url = `${protocol}://${config.domain}/rest-api/1/0/${encodeURIComponent(config.siteName)}/Addon%20Repository/${encodeURIComponent(config.addonName)}/activateCustomModuleExecutable`;
|
|
577
|
+
const { auth, kind } = configAuth(config);
|
|
578
|
+
try {
|
|
579
|
+
const response = await makeRequest(url, { method: 'GET', auth });
|
|
580
|
+
if (response.statusCode === 401) {
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: unauthorizedMessage(kind),
|
|
584
|
+
authExpired: true,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (response.statusCode !== 200) {
|
|
588
|
+
return {
|
|
589
|
+
success: false,
|
|
590
|
+
error: `Listing versions failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const data = JSON.parse(response.body.toString());
|
|
594
|
+
return { success: true, executables: data.executables ?? [] };
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
return {
|
|
598
|
+
success: false,
|
|
599
|
+
error: `Listing versions failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
const ADDON_TYPES = {
|
|
604
|
+
'sv:customModule': 'web',
|
|
605
|
+
'sv:marketplaceCustomModule': 'web',
|
|
606
|
+
'sv:widgetCustomModule': 'widget',
|
|
607
|
+
'sv:marketplaceWidgetCustomModule': 'widget',
|
|
608
|
+
'sv:headlessCustomModule': 'rest',
|
|
609
|
+
'sv:marketplaceHeadlessCustomModule': 'rest',
|
|
610
|
+
'sv:mcpServerCustomModule': 'mcp',
|
|
611
|
+
};
|
|
612
|
+
/**
|
|
613
|
+
* List the addons (custom modules) in the site's Addon Repository.
|
|
614
|
+
*/
|
|
615
|
+
export async function listAddons(config) {
|
|
616
|
+
const url = `${buildApiBaseUrl(config.domain, config.siteName, config.useHTTP)}/Addon%20Repository/nodes`;
|
|
617
|
+
const { auth, kind } = configAuth(config);
|
|
618
|
+
try {
|
|
619
|
+
const response = await makeRequest(url, { method: 'GET', auth });
|
|
620
|
+
if (response.statusCode === 401) {
|
|
621
|
+
return { success: false, error: unauthorizedMessage(kind) };
|
|
622
|
+
}
|
|
623
|
+
if (response.statusCode !== 200) {
|
|
624
|
+
return {
|
|
625
|
+
success: false,
|
|
626
|
+
error: `Listing addons failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
const nodes = JSON.parse(response.body.toString());
|
|
630
|
+
return {
|
|
631
|
+
success: true,
|
|
632
|
+
addons: nodes
|
|
633
|
+
.filter(node => Object.hasOwn(ADDON_TYPES, node.type))
|
|
634
|
+
.map(node => ({ ...node, appType: ADDON_TYPES[node.type] })),
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
return {
|
|
639
|
+
success: false,
|
|
640
|
+
error: `Listing addons failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Whether an addon exists, judged from a listAddons result. A failed or empty
|
|
646
|
+
* listing is "unknown": a dead session can look exactly like an empty site.
|
|
647
|
+
*/
|
|
648
|
+
export function classifyAddon(listed, name) {
|
|
649
|
+
if (!listed.success || !listed.addons?.length)
|
|
650
|
+
return 'unknown';
|
|
651
|
+
const wanted = name.toLowerCase();
|
|
652
|
+
return listed.addons.some(addon => addon.name.toLowerCase() === wanted)
|
|
653
|
+
? 'present'
|
|
654
|
+
: 'missing';
|
|
655
|
+
}
|
|
547
656
|
// =============================================================================
|
|
548
657
|
// HELPER EXPORTS
|
|
549
658
|
// =============================================================================
|
|
@@ -17,14 +17,33 @@
|
|
|
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
|
+
import { getAppIdConfig, getFullAppId } from './project-detection.js';
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
|
+
* Directory of an installed package, looked up the way Node does: the project's
|
|
24
|
+
* node_modules, then each parent's, since workspaces hoist to the repo root.
|
|
25
|
+
*/
|
|
26
|
+
export function findInstalledPackage(projectRoot, name) {
|
|
27
|
+
let dir = path.resolve(projectRoot);
|
|
28
|
+
for (;;) {
|
|
29
|
+
const candidate = path.join(dir, 'node_modules', name);
|
|
30
|
+
if (fs.existsSync(candidate))
|
|
31
|
+
return candidate;
|
|
32
|
+
const parent = path.dirname(dir);
|
|
33
|
+
if (parent === dir)
|
|
34
|
+
return null;
|
|
35
|
+
dir = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const PACKAGE = '@sitevision/sitevision-scripts';
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the path to the sitevision-scripts CLI entry for a project.
|
|
23
41
|
* Returns null if the package is not installed.
|
|
24
42
|
*/
|
|
25
43
|
export function getSitevisionScriptsBin(projectRoot) {
|
|
26
|
-
const
|
|
27
|
-
|
|
44
|
+
const dir = findInstalledPackage(projectRoot, PACKAGE);
|
|
45
|
+
const bin = dir && path.join(dir, 'bin', 'sitevision-scripts.js');
|
|
46
|
+
return bin && fs.existsSync(bin) ? bin : null;
|
|
28
47
|
}
|
|
29
48
|
/**
|
|
30
49
|
* Whether the sitevision-scripts package is available in the project.
|
|
@@ -35,18 +54,12 @@ export function hasSitevisionScripts(projectRoot) {
|
|
|
35
54
|
/**
|
|
36
55
|
* Path of the zip that `sitevision-scripts build` writes.
|
|
37
56
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* builds the package is the one writing the file, so its convention is the source
|
|
42
|
-
* of truth — using `getZipPath` here would look for the wrong filename whenever a
|
|
43
|
-
* prefix/suffix is configured.
|
|
57
|
+
* sitevision-scripts names it from `APP_ID_PREFIX`/`APP_ID_SUFFIX`; the build is
|
|
58
|
+
* started with those set from `getAppIdConfig`, so this is the same file that
|
|
59
|
+
* sign and deploy look for.
|
|
44
60
|
*/
|
|
45
61
|
export function getDelegatedZipPath(projectRoot, manifestId) {
|
|
46
|
-
|
|
47
|
-
const suffix = process.env['APP_ID_SUFFIX'] ?? '';
|
|
48
|
-
const appId = `${prefix}${manifestId}${suffix}`;
|
|
49
|
-
return path.join(projectRoot, 'dist', `${appId}.zip`);
|
|
62
|
+
return path.join(projectRoot, 'dist', `${getFullAppId(manifestId)}.zip`);
|
|
50
63
|
}
|
|
51
64
|
// =============================================================================
|
|
52
65
|
// VERSION COMPATIBILITY
|
|
@@ -68,9 +81,11 @@ export const SUPPORTED_SITEVISION_SCRIPTS_RANGE = `>=${SUPPORTED_SITEVISION_SCRI
|
|
|
68
81
|
* is not installed / unreadable.
|
|
69
82
|
*/
|
|
70
83
|
export function getSitevisionScriptsVersion(projectRoot) {
|
|
71
|
-
const
|
|
84
|
+
const dir = findInstalledPackage(projectRoot, PACKAGE);
|
|
85
|
+
if (!dir)
|
|
86
|
+
return null;
|
|
72
87
|
try {
|
|
73
|
-
const parsed = JSON.parse(fs.readFileSync(
|
|
88
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
74
89
|
return parsed.version ?? null;
|
|
75
90
|
}
|
|
76
91
|
catch {
|
|
@@ -145,8 +160,9 @@ const MAX_OUTPUT_CHARS = 50_000;
|
|
|
145
160
|
*
|
|
146
161
|
* @param projectRoot - Project root directory (used as cwd)
|
|
147
162
|
* @param onOutput - Optional callback for streaming output chunks
|
|
163
|
+
* @param signal - Aborting kills the build and everything it started
|
|
148
164
|
*/
|
|
149
|
-
export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
165
|
+
export async function runSitevisionScriptsBuild(projectRoot, onOutput, signal) {
|
|
150
166
|
const bin = getSitevisionScriptsBin(projectRoot);
|
|
151
167
|
if (!bin) {
|
|
152
168
|
return {
|
|
@@ -157,10 +173,18 @@ export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
|
157
173
|
}
|
|
158
174
|
return new Promise(resolve => {
|
|
159
175
|
let output = '';
|
|
160
|
-
const
|
|
176
|
+
const { prefix, suffix } = getAppIdConfig();
|
|
177
|
+
const child = spawnChild(process.execPath, [bin, 'build'], {
|
|
161
178
|
cwd: projectRoot,
|
|
179
|
+
env: { ...process.env, APP_ID_PREFIX: prefix, APP_ID_SUFFIX: suffix },
|
|
162
180
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
163
181
|
});
|
|
182
|
+
const abort = () => {
|
|
183
|
+
killChild(child);
|
|
184
|
+
};
|
|
185
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
186
|
+
if (signal?.aborted)
|
|
187
|
+
abort();
|
|
164
188
|
const handleData = (data) => {
|
|
165
189
|
const text = data.toString();
|
|
166
190
|
output += text;
|
|
@@ -175,12 +199,16 @@ export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
|
175
199
|
resolve({ success: false, output, error: error.message });
|
|
176
200
|
});
|
|
177
201
|
child.on('close', code => {
|
|
202
|
+
signal?.removeEventListener('abort', abort);
|
|
203
|
+
const stopped = signal?.aborted ?? false;
|
|
178
204
|
resolve({
|
|
179
|
-
success: code === 0,
|
|
205
|
+
success: code === 0 && !stopped,
|
|
180
206
|
output,
|
|
181
|
-
error:
|
|
182
|
-
?
|
|
183
|
-
:
|
|
207
|
+
error: stopped
|
|
208
|
+
? 'Build stopped.'
|
|
209
|
+
: code === 0
|
|
210
|
+
? undefined
|
|
211
|
+
: `sitevision-scripts build exited with code ${code}`,
|
|
184
212
|
});
|
|
185
213
|
});
|
|
186
214
|
});
|