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.
@@ -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
- try {
124
- const response = await makeRequest(url, {
125
- method: 'POST',
126
- headers: {
127
- 'Content-Type': contentType,
128
- 'Content-Length': String(body.length),
129
- },
130
- body,
131
- auth: {
132
- username: credentials.username,
133
- password: credentials.password,
134
- },
135
- });
136
- if (response.statusCode === 200) {
137
- // Write signed zip to output path
138
- const outputDir = path.dirname(outputPath);
139
- if (!fs.existsSync(outputDir)) {
140
- fs.mkdirSync(outputDir, { recursive: true });
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
- if (response.statusCode === 401) {
149
- return {
150
- success: false,
151
- error: 'Unauthorized. Check username and password.',
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.toString()}`,
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.toString()}`,
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.toString()}`,
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
- // Try to find webpack config in standard locations
53
- // Project-specific configs take priority, fall back to @sitevision/sitevision-scripts
54
- const configPaths = [
55
- path.join(this.projectRoot, 'webpack.config.js'),
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
  }
@@ -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
- * Uses the system `zip` command for cross-platform compatibility.
11
- * Falls back to a basic implementation if zip is not available.
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