sitevision-cli 0.4.0-beta.2 → 0.6.0-beta.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/dist/cli.js +24 -4
- package/dist/commands/build.js +52 -4
- package/dist/commands/dev.js +139 -22
- package/dist/components/AnimatedLogo.d.ts +9 -0
- package/dist/components/AnimatedLogo.js +79 -0
- package/dist/components/InfoScreen.js +3 -1
- package/dist/utils/branding.d.ts +9 -0
- package/dist/utils/branding.js +18 -0
- package/dist/utils/process-runner.d.ts +0 -8
- package/dist/utils/process-runner.js +0 -15
- package/dist/utils/sitevision-api.d.ts +32 -0
- package/dist/utils/sitevision-api.js +111 -42
- package/dist/utils/sitevision-scripts-runner.d.ts +83 -0
- package/dist/utils/sitevision-scripts-runner.js +187 -0
- package/dist/utils/webpack-runner.d.ts +9 -0
- package/dist/utils/webpack-runner.js +30 -15
- package/dist/utils/zip.d.ts +10 -3
- package/dist/utils/zip.js +171 -59
- package/package.json +1 -1
|
@@ -18,6 +18,12 @@ import { buildImportEndpointUrl, buildAddonEndpointUrl, } from './project-detect
|
|
|
18
18
|
// =============================================================================
|
|
19
19
|
const SIGNING_API_HOST = 'developer.sitevision.se';
|
|
20
20
|
const SIGNING_API_PATH = '/rest-api/appsigner/signapp';
|
|
21
|
+
/** Default per-request timeout. Generous because signing uploads a full zip. */
|
|
22
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
23
|
+
/** Max attempts for transient failures (network errors, timeouts, 5xx). */
|
|
24
|
+
const SIGN_MAX_ATTEMPTS = 3;
|
|
25
|
+
/** Base backoff between retries; grows exponentially per attempt. */
|
|
26
|
+
const RETRY_BASE_DELAY_MS = 1000;
|
|
21
27
|
// =============================================================================
|
|
22
28
|
// UTILITY FUNCTIONS
|
|
23
29
|
// =============================================================================
|
|
@@ -56,7 +62,7 @@ function createMultipartFormData(filePath, fieldName, boundary) {
|
|
|
56
62
|
/**
|
|
57
63
|
* Make an HTTP/HTTPS request
|
|
58
64
|
*/
|
|
59
|
-
function makeRequest(url, options) {
|
|
65
|
+
export function makeRequest(url, options) {
|
|
60
66
|
return new Promise((resolve, reject) => {
|
|
61
67
|
const parsedUrl = new URL(url);
|
|
62
68
|
const isHttps = parsedUrl.protocol === 'https:';
|
|
@@ -87,6 +93,10 @@ function makeRequest(url, options) {
|
|
|
87
93
|
});
|
|
88
94
|
});
|
|
89
95
|
});
|
|
96
|
+
// Abort hung connections instead of blocking the CLI indefinitely.
|
|
97
|
+
req.setTimeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, () => {
|
|
98
|
+
req.destroy(new Error(`Request timed out after ${options.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`));
|
|
99
|
+
});
|
|
90
100
|
req.on('error', reject);
|
|
91
101
|
if (options.body) {
|
|
92
102
|
req.write(options.body);
|
|
@@ -94,6 +104,47 @@ function makeRequest(url, options) {
|
|
|
94
104
|
req.end();
|
|
95
105
|
});
|
|
96
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Whether an HTTP status is worth retrying (transient server-side failures).
|
|
109
|
+
*/
|
|
110
|
+
export function isRetryableStatus(statusCode) {
|
|
111
|
+
return statusCode === 408 || statusCode === 429 || statusCode >= 500;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Sleep helper for backoff between retries.
|
|
115
|
+
*/
|
|
116
|
+
async function delay(ms) {
|
|
117
|
+
return new Promise(resolve => {
|
|
118
|
+
setTimeout(resolve, ms);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Summarize a non-success response body for error messages.
|
|
123
|
+
* Avoids dumping raw bytes (e.g. an HTML error page or a binary blob) by
|
|
124
|
+
* trimming text bodies and labelling binary ones by their content type.
|
|
125
|
+
*/
|
|
126
|
+
export function summarizeErrorBody(body, headers) {
|
|
127
|
+
const contentType = headers['content-type'] ?? 'unknown';
|
|
128
|
+
const isText = contentType.includes('text') ||
|
|
129
|
+
contentType.includes('json') ||
|
|
130
|
+
contentType.includes('xml');
|
|
131
|
+
if (!isText) {
|
|
132
|
+
return `(${contentType}, ${body.length} bytes)`;
|
|
133
|
+
}
|
|
134
|
+
const text = body.toString('utf8').replaceAll(/\s+/g, ' ').trim();
|
|
135
|
+
const max = 300;
|
|
136
|
+
const summary = text.length > max ? text.slice(0, max) + '…' : text;
|
|
137
|
+
return summary.length > 0 ? summary : `(${contentType}, empty body)`;
|
|
138
|
+
}
|
|
139
|
+
/** ZIP local-file-header magic bytes: "PK\x03\x04". */
|
|
140
|
+
const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
141
|
+
/**
|
|
142
|
+
* Check that a buffer begins with the ZIP magic bytes. Used to fail fast when
|
|
143
|
+
* the signing endpoint returns an error page with HTTP 200.
|
|
144
|
+
*/
|
|
145
|
+
export function looksLikeZip(body) {
|
|
146
|
+
return body.length >= 4 && body.subarray(0, 4).equals(ZIP_MAGIC);
|
|
147
|
+
}
|
|
97
148
|
// =============================================================================
|
|
98
149
|
// SIGNING API
|
|
99
150
|
// =============================================================================
|
|
@@ -120,48 +171,66 @@ export async function signApp(zipPath, credentials, outputPath) {
|
|
|
120
171
|
// Create multipart form data
|
|
121
172
|
const boundary = generateBoundary();
|
|
122
173
|
const { body, contentType } = createMultipartFormData(zipPath, 'file', boundary);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
174
|
+
// Signing is idempotent (same input → same signed output), so transient
|
|
175
|
+
// failures (network errors, timeouts, 5xx) are safe to retry with backoff.
|
|
176
|
+
let lastError = 'Signing failed';
|
|
177
|
+
for (let attempt = 1; attempt <= SIGN_MAX_ATTEMPTS; attempt++) {
|
|
178
|
+
try {
|
|
179
|
+
const response = await makeRequest(url, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: {
|
|
182
|
+
'Content-Type': contentType,
|
|
183
|
+
'Content-Length': String(body.length),
|
|
184
|
+
},
|
|
185
|
+
body,
|
|
186
|
+
auth: {
|
|
187
|
+
username: credentials.username,
|
|
188
|
+
password: credentials.password,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
if (response.statusCode === 200) {
|
|
192
|
+
// Guard against an error page returned with a 200 status.
|
|
193
|
+
if (!looksLikeZip(response.body)) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: `Signing returned a non-zip response: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Write signed zip to output path
|
|
200
|
+
const outputDir = path.dirname(outputPath);
|
|
201
|
+
if (!fs.existsSync(outputDir)) {
|
|
202
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
fs.writeFileSync(outputPath, response.body);
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
signedFilePath: outputPath,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (response.statusCode === 401) {
|
|
211
|
+
// Auth failures will not resolve on retry.
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
error: 'Unauthorized. Check username and password.',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
lastError = `Signing failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`;
|
|
218
|
+
if (!isRetryableStatus(response.statusCode)) {
|
|
219
|
+
return { success: false, error: lastError };
|
|
141
220
|
}
|
|
142
|
-
fs.writeFileSync(outputPath, response.body);
|
|
143
|
-
return {
|
|
144
|
-
success: true,
|
|
145
|
-
signedFilePath: outputPath,
|
|
146
|
-
};
|
|
147
221
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
222
|
+
catch (error) {
|
|
223
|
+
lastError = `Signing request failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
224
|
+
}
|
|
225
|
+
// Back off before the next attempt (skip after the final attempt).
|
|
226
|
+
if (attempt < SIGN_MAX_ATTEMPTS) {
|
|
227
|
+
await delay(RETRY_BASE_DELAY_MS * 2 ** (attempt - 1));
|
|
153
228
|
}
|
|
154
|
-
return {
|
|
155
|
-
success: false,
|
|
156
|
-
error: `Signing failed with status ${response.statusCode}: ${response.body.toString()}`,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
return {
|
|
161
|
-
success: false,
|
|
162
|
-
error: `Signing request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
163
|
-
};
|
|
164
229
|
}
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
error: `${lastError} (after ${SIGN_MAX_ATTEMPTS} attempts)`,
|
|
233
|
+
};
|
|
165
234
|
}
|
|
166
235
|
// =============================================================================
|
|
167
236
|
// DEPLOYMENT API
|
|
@@ -233,7 +302,7 @@ export async function deployApp(zipPath, config, appType, force = false) {
|
|
|
233
302
|
}
|
|
234
303
|
return {
|
|
235
304
|
success: false,
|
|
236
|
-
error: `Deployment failed with status ${response.statusCode}: ${response.body.
|
|
305
|
+
error: `Deployment failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
237
306
|
};
|
|
238
307
|
}
|
|
239
308
|
catch (error) {
|
|
@@ -330,7 +399,7 @@ export async function createAddon(config, appType) {
|
|
|
330
399
|
}
|
|
331
400
|
return {
|
|
332
401
|
success: false,
|
|
333
|
-
error: `Create addon failed with status ${response.statusCode}: ${response.body.
|
|
402
|
+
error: `Create addon failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
334
403
|
};
|
|
335
404
|
}
|
|
336
405
|
catch (error) {
|
|
@@ -377,7 +446,7 @@ export async function activateApp(executableId, config, _appType) {
|
|
|
377
446
|
}
|
|
378
447
|
return {
|
|
379
448
|
success: false,
|
|
380
|
-
error: `Activation failed with status ${response.statusCode}: ${response.body.
|
|
449
|
+
error: `Activation failed with status ${response.statusCode}: ${summarizeErrorBody(response.body, response.headers)}`,
|
|
381
450
|
};
|
|
382
451
|
}
|
|
383
452
|
catch (error) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitevision Scripts Runner
|
|
3
|
+
*
|
|
4
|
+
* Delegates the compile/build step to the official sitevision-scripts npm
|
|
5
|
+
* package when a project has no local webpack config of its own.
|
|
6
|
+
*
|
|
7
|
+
* Sitevision WebApp builds are tightly coupled to the platform runtime (a
|
|
8
|
+
* dual server/client multi-compiler, AMD externals for React and the sitevision
|
|
9
|
+
* api packages, an embedded ES5 server engine, and a precise addon zip layout).
|
|
10
|
+
* Rather than
|
|
11
|
+
* reproduce that contract — which lives in proprietary babel presets and an
|
|
12
|
+
* undocumented internal config — we shell out to the package's public CLI, which
|
|
13
|
+
* is the canonical, maintained source of that build pipeline.
|
|
14
|
+
*
|
|
15
|
+
* `sitevision-scripts build` runs build + zip + cleanup and writes the archive to
|
|
16
|
+
* `dist/<appId>.zip` — the exact path the CLI's own sign/deploy steps already use.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the path to the sitevision-scripts CLI entry inside a project.
|
|
20
|
+
* Returns null if the package is not installed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getSitevisionScriptsBin(projectRoot: string): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Whether the sitevision-scripts package is available in the project.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hasSitevisionScripts(projectRoot: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Path of the zip that `sitevision-scripts build` writes.
|
|
29
|
+
*
|
|
30
|
+
* IMPORTANT: this mirrors sitevision-scripts' own app-id convention
|
|
31
|
+
* (`APP_ID_PREFIX`/`APP_ID_SUFFIX` env vars + `dist/<appId>.zip`), which differs
|
|
32
|
+
* from the CLI's own `getZipPath` env vars (`SITEVISION_APP_ID_*`). For delegated
|
|
33
|
+
* builds the package is the one writing the file, so its convention is the source
|
|
34
|
+
* of truth — using `getZipPath` here would look for the wrong filename whenever a
|
|
35
|
+
* prefix/suffix is configured.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getDelegatedZipPath(projectRoot: string, manifestId: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Range of the sitevision-scripts package the CLI's build delegation has been
|
|
40
|
+
* validated against. The delegation depends on the package's CLI commands, its
|
|
41
|
+
* `dist/<appId>.zip` output, and the app-id convention — all stable within a
|
|
42
|
+
* major. A new major may change that contract, so we warn rather than assume.
|
|
43
|
+
*
|
|
44
|
+
* Bump these (and re-validate) when adopting a new sitevision-scripts major.
|
|
45
|
+
*/
|
|
46
|
+
export declare const SUPPORTED_SITEVISION_SCRIPTS_MIN = "8.0.0";
|
|
47
|
+
/** Human-readable supported range, e.g. ">=8.0.0 <9.0.0". */
|
|
48
|
+
export declare const SUPPORTED_SITEVISION_SCRIPTS_RANGE = ">=8.0.0 <9.0.0";
|
|
49
|
+
export type SitevisionScriptsCompatStatus = 'ok' | 'too-old' | 'too-new' | 'not-installed' | 'unknown';
|
|
50
|
+
export interface SitevisionScriptsCompat {
|
|
51
|
+
installed: string | null;
|
|
52
|
+
supportedRange: string;
|
|
53
|
+
status: SitevisionScriptsCompatStatus;
|
|
54
|
+
/** Populated for 'too-old'/'too-new' — a ready-to-display warning. */
|
|
55
|
+
warning?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read the installed sitevision-scripts version from the project, or null if it
|
|
59
|
+
* is not installed / unreadable.
|
|
60
|
+
*/
|
|
61
|
+
export declare function getSitevisionScriptsVersion(projectRoot: string): string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Check the project's installed sitevision-scripts against the supported range.
|
|
64
|
+
* Use the `warning` field to surface a message when the version has drifted.
|
|
65
|
+
*/
|
|
66
|
+
export declare function checkSitevisionScriptsCompatibility(projectRoot: string): SitevisionScriptsCompat;
|
|
67
|
+
export interface SitevisionBuildResult {
|
|
68
|
+
success: boolean;
|
|
69
|
+
/** Combined stdout/stderr (tail-trimmed) for error reporting. */
|
|
70
|
+
output: string;
|
|
71
|
+
error?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Run `sitevision-scripts build` (build + zip + cleanup) as a subprocess.
|
|
75
|
+
*
|
|
76
|
+
* The package's own webpack pipeline produces the deployable `dist/<appId>.zip`.
|
|
77
|
+
* Invoked via the current Node binary so it works cross-platform without relying
|
|
78
|
+
* on the `node_modules/.bin` shims or shell PATH resolution.
|
|
79
|
+
*
|
|
80
|
+
* @param projectRoot - Project root directory (used as cwd)
|
|
81
|
+
* @param onOutput - Optional callback for streaming output chunks
|
|
82
|
+
*/
|
|
83
|
+
export declare function runSitevisionScriptsBuild(projectRoot: string, onOutput?: (chunk: string) => void): Promise<SitevisionBuildResult>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitevision Scripts Runner
|
|
3
|
+
*
|
|
4
|
+
* Delegates the compile/build step to the official sitevision-scripts npm
|
|
5
|
+
* package when a project has no local webpack config of its own.
|
|
6
|
+
*
|
|
7
|
+
* Sitevision WebApp builds are tightly coupled to the platform runtime (a
|
|
8
|
+
* dual server/client multi-compiler, AMD externals for React and the sitevision
|
|
9
|
+
* api packages, an embedded ES5 server engine, and a precise addon zip layout).
|
|
10
|
+
* Rather than
|
|
11
|
+
* reproduce that contract — which lives in proprietary babel presets and an
|
|
12
|
+
* undocumented internal config — we shell out to the package's public CLI, which
|
|
13
|
+
* is the canonical, maintained source of that build pipeline.
|
|
14
|
+
*
|
|
15
|
+
* `sitevision-scripts build` runs build + zip + cleanup and writes the archive to
|
|
16
|
+
* `dist/<appId>.zip` — the exact path the CLI's own sign/deploy steps already use.
|
|
17
|
+
*/
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import { spawn } from 'child_process';
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the path to the sitevision-scripts CLI entry inside a project.
|
|
23
|
+
* Returns null if the package is not installed.
|
|
24
|
+
*/
|
|
25
|
+
export function getSitevisionScriptsBin(projectRoot) {
|
|
26
|
+
const bin = path.join(projectRoot, 'node_modules', '@sitevision', 'sitevision-scripts', 'bin', 'sitevision-scripts.js');
|
|
27
|
+
return fs.existsSync(bin) ? bin : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether the sitevision-scripts package is available in the project.
|
|
31
|
+
*/
|
|
32
|
+
export function hasSitevisionScripts(projectRoot) {
|
|
33
|
+
return getSitevisionScriptsBin(projectRoot) !== null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Path of the zip that `sitevision-scripts build` writes.
|
|
37
|
+
*
|
|
38
|
+
* IMPORTANT: this mirrors sitevision-scripts' own app-id convention
|
|
39
|
+
* (`APP_ID_PREFIX`/`APP_ID_SUFFIX` env vars + `dist/<appId>.zip`), which differs
|
|
40
|
+
* from the CLI's own `getZipPath` env vars (`SITEVISION_APP_ID_*`). For delegated
|
|
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.
|
|
44
|
+
*/
|
|
45
|
+
export function getDelegatedZipPath(projectRoot, manifestId) {
|
|
46
|
+
const prefix = process.env['APP_ID_PREFIX'] ?? '';
|
|
47
|
+
const suffix = process.env['APP_ID_SUFFIX'] ?? '';
|
|
48
|
+
const appId = `${prefix}${manifestId}${suffix}`;
|
|
49
|
+
return path.join(projectRoot, 'dist', `${appId}.zip`);
|
|
50
|
+
}
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// VERSION COMPATIBILITY
|
|
53
|
+
// =============================================================================
|
|
54
|
+
/**
|
|
55
|
+
* Range of the sitevision-scripts package the CLI's build delegation has been
|
|
56
|
+
* validated against. The delegation depends on the package's CLI commands, its
|
|
57
|
+
* `dist/<appId>.zip` output, and the app-id convention — all stable within a
|
|
58
|
+
* major. A new major may change that contract, so we warn rather than assume.
|
|
59
|
+
*
|
|
60
|
+
* Bump these (and re-validate) when adopting a new sitevision-scripts major.
|
|
61
|
+
*/
|
|
62
|
+
export const SUPPORTED_SITEVISION_SCRIPTS_MIN = '8.0.0';
|
|
63
|
+
const SUPPORTED_SITEVISION_SCRIPTS_MAX_EXCLUSIVE_MAJOR = 9;
|
|
64
|
+
/** Human-readable supported range, e.g. ">=8.0.0 <9.0.0". */
|
|
65
|
+
export const SUPPORTED_SITEVISION_SCRIPTS_RANGE = `>=${SUPPORTED_SITEVISION_SCRIPTS_MIN} <${SUPPORTED_SITEVISION_SCRIPTS_MAX_EXCLUSIVE_MAJOR}.0.0`;
|
|
66
|
+
/**
|
|
67
|
+
* Read the installed sitevision-scripts version from the project, or null if it
|
|
68
|
+
* is not installed / unreadable.
|
|
69
|
+
*/
|
|
70
|
+
export function getSitevisionScriptsVersion(projectRoot) {
|
|
71
|
+
const packageJsonPath = path.join(projectRoot, 'node_modules', '@sitevision', 'sitevision-scripts', 'package.json');
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
74
|
+
return parsed.version ?? null;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parse a semver string into [major, minor, patch], ignoring any prerelease
|
|
82
|
+
* suffix. Returns null if it does not look like a version.
|
|
83
|
+
*/
|
|
84
|
+
function parseVersion(version) {
|
|
85
|
+
const match = /^(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/.exec(version);
|
|
86
|
+
if (!match?.groups) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return [
|
|
90
|
+
Number(match.groups['major']),
|
|
91
|
+
Number(match.groups['minor']),
|
|
92
|
+
Number(match.groups['patch']),
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Compare two parsed versions: negative if a < b, 0 if equal, positive if a > b.
|
|
97
|
+
*/
|
|
98
|
+
function compareVersions(a, b) {
|
|
99
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check the project's installed sitevision-scripts against the supported range.
|
|
103
|
+
* Use the `warning` field to surface a message when the version has drifted.
|
|
104
|
+
*/
|
|
105
|
+
export function checkSitevisionScriptsCompatibility(projectRoot) {
|
|
106
|
+
const installed = getSitevisionScriptsVersion(projectRoot);
|
|
107
|
+
const supportedRange = SUPPORTED_SITEVISION_SCRIPTS_RANGE;
|
|
108
|
+
if (!installed) {
|
|
109
|
+
return {
|
|
110
|
+
installed: null,
|
|
111
|
+
supportedRange,
|
|
112
|
+
status: hasSitevisionScripts(projectRoot) ? 'unknown' : 'not-installed',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const parsed = parseVersion(installed);
|
|
116
|
+
if (!parsed) {
|
|
117
|
+
return { installed, supportedRange, status: 'unknown' };
|
|
118
|
+
}
|
|
119
|
+
if (compareVersions(parsed, parseVersion(SUPPORTED_SITEVISION_SCRIPTS_MIN)) < 0) {
|
|
120
|
+
return {
|
|
121
|
+
installed,
|
|
122
|
+
supportedRange,
|
|
123
|
+
status: 'too-old',
|
|
124
|
+
warning: `@sitevision/sitevision-scripts ${installed} is older than the supported range (${supportedRange}). Update it in your project: npm install @sitevision/sitevision-scripts@latest`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (parsed[0] >= SUPPORTED_SITEVISION_SCRIPTS_MAX_EXCLUSIVE_MAJOR) {
|
|
128
|
+
return {
|
|
129
|
+
installed,
|
|
130
|
+
supportedRange,
|
|
131
|
+
status: 'too-new',
|
|
132
|
+
warning: `@sitevision/sitevision-scripts ${installed} is newer than the range this CLI was validated against (${supportedRange}). The build may still work; update sitevision-cli if you hit problems.`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { installed, supportedRange, status: 'ok' };
|
|
136
|
+
}
|
|
137
|
+
/** Keep at most this many trailing characters of build output in memory. */
|
|
138
|
+
const MAX_OUTPUT_CHARS = 50_000;
|
|
139
|
+
/**
|
|
140
|
+
* Run `sitevision-scripts build` (build + zip + cleanup) as a subprocess.
|
|
141
|
+
*
|
|
142
|
+
* The package's own webpack pipeline produces the deployable `dist/<appId>.zip`.
|
|
143
|
+
* Invoked via the current Node binary so it works cross-platform without relying
|
|
144
|
+
* on the `node_modules/.bin` shims or shell PATH resolution.
|
|
145
|
+
*
|
|
146
|
+
* @param projectRoot - Project root directory (used as cwd)
|
|
147
|
+
* @param onOutput - Optional callback for streaming output chunks
|
|
148
|
+
*/
|
|
149
|
+
export async function runSitevisionScriptsBuild(projectRoot, onOutput) {
|
|
150
|
+
const bin = getSitevisionScriptsBin(projectRoot);
|
|
151
|
+
if (!bin) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
output: '',
|
|
155
|
+
error: '@sitevision/sitevision-scripts not found in project. Run npm install.',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return new Promise(resolve => {
|
|
159
|
+
let output = '';
|
|
160
|
+
const child = spawn(process.execPath, [bin, 'build'], {
|
|
161
|
+
cwd: projectRoot,
|
|
162
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
163
|
+
});
|
|
164
|
+
const handleData = (data) => {
|
|
165
|
+
const text = data.toString();
|
|
166
|
+
output += text;
|
|
167
|
+
if (output.length > MAX_OUTPUT_CHARS) {
|
|
168
|
+
output = output.slice(-MAX_OUTPUT_CHARS);
|
|
169
|
+
}
|
|
170
|
+
onOutput?.(text);
|
|
171
|
+
};
|
|
172
|
+
child.stdout?.on('data', handleData);
|
|
173
|
+
child.stderr?.on('data', handleData);
|
|
174
|
+
child.on('error', error => {
|
|
175
|
+
resolve({ success: false, output, error: error.message });
|
|
176
|
+
});
|
|
177
|
+
child.on('close', code => {
|
|
178
|
+
resolve({
|
|
179
|
+
success: code === 0,
|
|
180
|
+
output,
|
|
181
|
+
error: code === 0
|
|
182
|
+
? undefined
|
|
183
|
+
: `sitevision-scripts build exited with code ${code}`,
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* Dynamically loads webpack from the target project's node_modules.
|
|
6
6
|
*/
|
|
7
7
|
import type { BuildOptions, BuildResult } from '../types/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Find the project's own webpack config, or null if it has none.
|
|
10
|
+
*/
|
|
11
|
+
export declare function findLocalWebpackConfig(projectRoot: string): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Whether the project ships its own webpack config (in-house build path),
|
|
14
|
+
* as opposed to relying on the sitevision-scripts package.
|
|
15
|
+
*/
|
|
16
|
+
export declare function hasLocalWebpackConfig(projectRoot: string): boolean;
|
|
8
17
|
export declare class WebpackRunner {
|
|
9
18
|
private webpack;
|
|
10
19
|
private config;
|
|
@@ -9,6 +9,32 @@ import fs from 'fs';
|
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
10
|
import { copyChunksToResources } from './zip.js';
|
|
11
11
|
// =============================================================================
|
|
12
|
+
// LOCAL CONFIG DETECTION
|
|
13
|
+
// =============================================================================
|
|
14
|
+
/**
|
|
15
|
+
* Standard locations for a project-local webpack config, highest priority first.
|
|
16
|
+
*/
|
|
17
|
+
function localWebpackConfigPaths(projectRoot) {
|
|
18
|
+
return [
|
|
19
|
+
path.join(projectRoot, 'webpack.config.js'),
|
|
20
|
+
path.join(projectRoot, 'webpack.config.mjs'),
|
|
21
|
+
path.join(projectRoot, 'config', 'webpack', 'webpack.config.js'),
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Find the project's own webpack config, or null if it has none.
|
|
26
|
+
*/
|
|
27
|
+
export function findLocalWebpackConfig(projectRoot) {
|
|
28
|
+
return (localWebpackConfigPaths(projectRoot).find(p => fs.existsSync(p)) ?? null);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether the project ships its own webpack config (in-house build path),
|
|
32
|
+
* as opposed to relying on the sitevision-scripts package.
|
|
33
|
+
*/
|
|
34
|
+
export function hasLocalWebpackConfig(projectRoot) {
|
|
35
|
+
return findLocalWebpackConfig(projectRoot) !== null;
|
|
36
|
+
}
|
|
37
|
+
// =============================================================================
|
|
12
38
|
// WEBPACK RUNNER CLASS
|
|
13
39
|
// =============================================================================
|
|
14
40
|
export class WebpackRunner {
|
|
@@ -49,21 +75,10 @@ export class WebpackRunner {
|
|
|
49
75
|
* Load webpack configuration from the project
|
|
50
76
|
*/
|
|
51
77
|
async loadConfig() {
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
path.join(this.projectRoot, 'webpack.config.mjs'),
|
|
57
|
-
path.join(this.projectRoot, 'config', 'webpack', 'webpack.config.js'),
|
|
58
|
-
path.join(this.projectRoot, 'node_modules', '@sitevision', 'sitevision-scripts', 'config', 'webpack', 'webpack.config.js'),
|
|
59
|
-
];
|
|
60
|
-
let configPath = null;
|
|
61
|
-
for (const p of configPaths) {
|
|
62
|
-
if (fs.existsSync(p)) {
|
|
63
|
-
configPath = p;
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
78
|
+
// Only project-local webpack configs are consumed in-process. Projects
|
|
79
|
+
// without one are built by delegating to @sitevision/sitevision-scripts
|
|
80
|
+
// (see sitevision-scripts-runner), so there is no config fallback here.
|
|
81
|
+
const configPath = findLocalWebpackConfig(this.projectRoot);
|
|
67
82
|
if (!configPath) {
|
|
68
83
|
throw new Error('webpack.config.js not found. Make sure your project has a webpack configuration.');
|
|
69
84
|
}
|
package/dist/utils/zip.d.ts
CHANGED
|
@@ -5,10 +5,17 @@
|
|
|
5
5
|
* Also handles webpack chunk organization.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* Create a zip archive of a directory
|
|
8
|
+
* Create a zip archive of a directory.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* In-house, dependency-free implementation: walks the directory, deflates each
|
|
11
|
+
* file with Node's built-in zlib, and assembles a standard ZIP container (local
|
|
12
|
+
* file headers + central directory + end-of-central-directory record). This
|
|
13
|
+
* removes the previous reliance on the external `zip`/`tar`/PowerShell binaries
|
|
14
|
+
* and behaves identically across macOS, Linux, and Windows.
|
|
15
|
+
*
|
|
16
|
+
* Mirrors `zip -r <out> .` run from inside `sourceDir`: archive paths are
|
|
17
|
+
* relative to `sourceDir`, use forward slashes, and directory entries are
|
|
18
|
+
* emitted so empty directories are preserved.
|
|
12
19
|
*
|
|
13
20
|
* @param sourceDir - Directory to zip
|
|
14
21
|
* @param outputPath - Path for the output zip file
|