@magentrix-corp/magentrix-cli 1.3.16 → 1.3.17
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/LICENSE +25 -25
- package/README.md +1166 -1166
- package/actions/autopublish.old.js +293 -293
- package/actions/config.js +182 -182
- package/actions/create.js +466 -466
- package/actions/help.js +164 -164
- package/actions/iris/buildStage.js +874 -874
- package/actions/iris/delete.js +256 -256
- package/actions/iris/dev.js +391 -391
- package/actions/iris/index.js +6 -6
- package/actions/iris/link.js +375 -375
- package/actions/iris/recover.js +268 -268
- package/actions/main.js +80 -80
- package/actions/publish.js +1420 -1420
- package/actions/pull.js +684 -684
- package/actions/setup.js +148 -148
- package/actions/status.js +17 -17
- package/actions/update.js +248 -248
- package/bin/magentrix.js +393 -393
- package/package.json +55 -55
- package/utils/assetPaths.js +158 -158
- package/utils/autopublishLock.js +77 -77
- package/utils/cacher.js +206 -206
- package/utils/cli/checkInstanceUrl.js +76 -74
- package/utils/cli/helpers/compare.js +282 -282
- package/utils/cli/helpers/ensureApiKey.js +63 -63
- package/utils/cli/helpers/ensureCredentials.js +68 -68
- package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
- package/utils/cli/writeRecords.js +262 -262
- package/utils/compare.js +135 -135
- package/utils/compress.js +17 -17
- package/utils/config.js +527 -527
- package/utils/debug.js +144 -144
- package/utils/diagnostics/testPublishLogic.js +96 -96
- package/utils/diff.js +49 -49
- package/utils/downloadAssets.js +291 -291
- package/utils/filetag.js +115 -115
- package/utils/hash.js +14 -14
- package/utils/iris/backup.js +411 -411
- package/utils/iris/builder.js +541 -541
- package/utils/iris/config-reader.js +664 -664
- package/utils/iris/deleteHelper.js +150 -150
- package/utils/iris/errors.js +537 -537
- package/utils/iris/linker.js +601 -601
- package/utils/iris/lock.js +360 -360
- package/utils/iris/validation.js +360 -360
- package/utils/iris/validator.js +281 -281
- package/utils/iris/zipper.js +248 -248
- package/utils/logger.js +291 -291
- package/utils/magentrix/api/assets.js +220 -220
- package/utils/magentrix/api/auth.js +107 -107
- package/utils/magentrix/api/createEntity.js +61 -61
- package/utils/magentrix/api/deleteEntity.js +55 -55
- package/utils/magentrix/api/iris.js +251 -251
- package/utils/magentrix/api/meqlQuery.js +36 -36
- package/utils/magentrix/api/retrieveEntity.js +86 -86
- package/utils/magentrix/api/updateEntity.js +66 -66
- package/utils/magentrix/fetch.js +168 -168
- package/utils/merge.js +22 -22
- package/utils/permissionError.js +70 -70
- package/utils/preferences.js +40 -40
- package/utils/progress.js +469 -469
- package/utils/spinner.js +43 -43
- package/utils/template.js +52 -52
- package/utils/updateFileBase.js +121 -121
- package/utils/workspaces.js +108 -108
- package/vars/config.js +11 -11
- package/vars/global.js +50 -50
package/utils/iris/builder.js
CHANGED
|
@@ -1,541 +1,541 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, cpSync, rmSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
-
import { join, resolve, basename } from 'node:path';
|
|
4
|
-
import { validateIrisBuild } from './validator.js';
|
|
5
|
-
import { updateLastBuild } from './linker.js';
|
|
6
|
-
import { EXPORT_ROOT, IRIS_APPS_DIR } from '../../vars/global.js';
|
|
7
|
-
import { detectErrorType, ErrorTypes, formatPermissionError, formatDiskFullError, formatFileLockError } from './errors.js';
|
|
8
|
-
import { acquireLock, releaseLock, LockTypes, createProjectContext } from './lock.js';
|
|
9
|
-
|
|
10
|
-
// Files to exclude when staging Iris apps (not needed on server)
|
|
11
|
-
const EXCLUDED_FILES = new Set([
|
|
12
|
-
'index.html',
|
|
13
|
-
'favicon.ico',
|
|
14
|
-
'.DS_Store',
|
|
15
|
-
'Thumbs.db',
|
|
16
|
-
'.gitignore',
|
|
17
|
-
'.gitkeep'
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Maximum dist folder size before warning (500MB)
|
|
22
|
-
*/
|
|
23
|
-
const MAX_DIST_SIZE_WARNING = 500 * 1024 * 1024;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Build a Vue project using npm run build.
|
|
27
|
-
*
|
|
28
|
-
* @param {string} projectPath - Path to the Vue project
|
|
29
|
-
* @param {Object} options - Build options
|
|
30
|
-
* @param {boolean} options.silent - Suppress build output
|
|
31
|
-
* @returns {Promise<{
|
|
32
|
-
* success: boolean,
|
|
33
|
-
* output: string,
|
|
34
|
-
* error: string | null,
|
|
35
|
-
* distPath: string | null,
|
|
36
|
-
* warnings: string[]
|
|
37
|
-
* }>}
|
|
38
|
-
*/
|
|
39
|
-
export async function buildVueProject(projectPath, options = {}) {
|
|
40
|
-
const { silent = false } = options;
|
|
41
|
-
const resolvedPath = resolve(projectPath);
|
|
42
|
-
const warnings = [];
|
|
43
|
-
|
|
44
|
-
// Check for package.json
|
|
45
|
-
if (!existsSync(join(resolvedPath, 'package.json'))) {
|
|
46
|
-
return {
|
|
47
|
-
success: false,
|
|
48
|
-
output: '',
|
|
49
|
-
error: `No package.json found in ${resolvedPath}`,
|
|
50
|
-
distPath: null,
|
|
51
|
-
warnings
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Check for node_modules
|
|
56
|
-
if (!existsSync(join(resolvedPath, 'node_modules'))) {
|
|
57
|
-
return {
|
|
58
|
-
success: false,
|
|
59
|
-
output: '',
|
|
60
|
-
error: `No node_modules found.\n\nRun the following command first:\n cd "${resolvedPath}"\n npm install`,
|
|
61
|
-
distPath: null,
|
|
62
|
-
warnings
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Acquire build lock to prevent concurrent builds
|
|
67
|
-
// Use os.tmpdir() as basePath to avoid permission issues in project directories
|
|
68
|
-
const lockContext = createProjectContext(resolvedPath);
|
|
69
|
-
const lockResult = acquireLock(LockTypes.BUILD, {
|
|
70
|
-
context: lockContext,
|
|
71
|
-
operation: `building ${basename(resolvedPath)}`,
|
|
72
|
-
basePath: join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks')
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// If lock acquisition fails due to permissions, warn but continue
|
|
76
|
-
// (locking is nice-to-have, not critical for builds)
|
|
77
|
-
let lockAcquired = lockResult.acquired;
|
|
78
|
-
if (!lockResult.acquired) {
|
|
79
|
-
// Check if it's a permission error vs actual concurrent operation
|
|
80
|
-
if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
|
|
81
|
-
warnings.push('Could not create build lock (permission issue). Proceeding without lock.');
|
|
82
|
-
lockAcquired = false;
|
|
83
|
-
} else {
|
|
84
|
-
// Actual concurrent operation - fail
|
|
85
|
-
return {
|
|
86
|
-
success: false,
|
|
87
|
-
output: '',
|
|
88
|
-
error: lockResult.error,
|
|
89
|
-
distPath: null,
|
|
90
|
-
warnings
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
return await new Promise((resolvePromise) => {
|
|
97
|
-
const output = [];
|
|
98
|
-
const errorOutput = [];
|
|
99
|
-
|
|
100
|
-
// Determine npm command based on platform
|
|
101
|
-
const isWindows = process.platform === 'win32';
|
|
102
|
-
const npmCmd = isWindows ? 'npm.cmd' : 'npm';
|
|
103
|
-
|
|
104
|
-
const child = spawn(npmCmd, ['run', 'build'], {
|
|
105
|
-
cwd: resolvedPath,
|
|
106
|
-
shell: isWindows, // Windows requires shell: true for .cmd files
|
|
107
|
-
env: { ...process.env, FORCE_COLOR: '1' }
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
child.stdout.on('data', (data) => {
|
|
111
|
-
const text = data.toString();
|
|
112
|
-
output.push(text);
|
|
113
|
-
if (!silent) {
|
|
114
|
-
process.stdout.write(text);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
child.stderr.on('data', (data) => {
|
|
119
|
-
const text = data.toString();
|
|
120
|
-
errorOutput.push(text);
|
|
121
|
-
if (!silent) {
|
|
122
|
-
process.stderr.write(text);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
child.on('close', (code) => {
|
|
127
|
-
const fullOutput = output.join('');
|
|
128
|
-
const fullError = errorOutput.join('');
|
|
129
|
-
|
|
130
|
-
if (code !== 0) {
|
|
131
|
-
resolvePromise({
|
|
132
|
-
success: false,
|
|
133
|
-
output: fullOutput,
|
|
134
|
-
error: fullError || `Build process exited with code ${code}`,
|
|
135
|
-
distPath: null,
|
|
136
|
-
warnings
|
|
137
|
-
});
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Find dist directory (Vue typically outputs to 'dist')
|
|
142
|
-
const possibleDistPaths = ['dist', 'build', 'output'];
|
|
143
|
-
let distPath = null;
|
|
144
|
-
|
|
145
|
-
for (const distDir of possibleDistPaths) {
|
|
146
|
-
const fullPath = join(resolvedPath, distDir);
|
|
147
|
-
if (existsSync(fullPath)) {
|
|
148
|
-
distPath = fullPath;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!distPath) {
|
|
154
|
-
resolvePromise({
|
|
155
|
-
success: false,
|
|
156
|
-
output: fullOutput,
|
|
157
|
-
error: 'Build completed but no dist/build directory found.\n\n' +
|
|
158
|
-
'Expected one of: dist/, build/, or output/',
|
|
159
|
-
distPath: null,
|
|
160
|
-
warnings
|
|
161
|
-
});
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Check dist folder size
|
|
166
|
-
const distSize = getDirectorySize(distPath);
|
|
167
|
-
if (distSize > MAX_DIST_SIZE_WARNING) {
|
|
168
|
-
const sizeMB = Math.round(distSize / (1024 * 1024));
|
|
169
|
-
warnings.push(`Large build output (${sizeMB}MB). This may take longer to stage and publish.`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
resolvePromise({
|
|
173
|
-
success: true,
|
|
174
|
-
output: fullOutput,
|
|
175
|
-
error: null,
|
|
176
|
-
distPath,
|
|
177
|
-
warnings
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
child.on('error', (err) => {
|
|
182
|
-
resolvePromise({
|
|
183
|
-
success: false,
|
|
184
|
-
output: output.join(''),
|
|
185
|
-
error: `Failed to start build process: ${err.message}`,
|
|
186
|
-
distPath: null,
|
|
187
|
-
warnings
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
} finally {
|
|
192
|
-
// Release the lock only if we acquired it
|
|
193
|
-
if (lockAcquired) {
|
|
194
|
-
releaseLock(LockTypes.BUILD, {
|
|
195
|
-
context: lockContext,
|
|
196
|
-
basePath: join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks')
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Calculate total size of a directory recursively.
|
|
204
|
-
*
|
|
205
|
-
* @param {string} dirPath - Directory path
|
|
206
|
-
* @returns {number} - Total size in bytes
|
|
207
|
-
*/
|
|
208
|
-
function getDirectorySize(dirPath) {
|
|
209
|
-
let totalSize = 0;
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const items = readdirSync(dirPath, { withFileTypes: true });
|
|
213
|
-
for (const item of items) {
|
|
214
|
-
const itemPath = join(dirPath, item.name);
|
|
215
|
-
if (item.isDirectory()) {
|
|
216
|
-
totalSize += getDirectorySize(itemPath);
|
|
217
|
-
} else {
|
|
218
|
-
try {
|
|
219
|
-
const stats = statSync(itemPath);
|
|
220
|
-
totalSize += stats.size;
|
|
221
|
-
} catch {
|
|
222
|
-
// Skip files we can't stat
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
} catch {
|
|
227
|
-
// Return what we have so far
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return totalSize;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Stage build output to a Magentrix workspace's iris-apps directory.
|
|
235
|
-
*
|
|
236
|
-
* @param {string} distPath - Path to the build output (dist directory)
|
|
237
|
-
* @param {string} slug - The app slug (folder name)
|
|
238
|
-
* @param {string} workspacePath - Path to the Magentrix workspace (defaults to CWD)
|
|
239
|
-
* @returns {{
|
|
240
|
-
* success: boolean,
|
|
241
|
-
* stagedPath: string | null,
|
|
242
|
-
* error: string | null,
|
|
243
|
-
* fileCount: number,
|
|
244
|
-
* warnings: string[]
|
|
245
|
-
* }}
|
|
246
|
-
*/
|
|
247
|
-
export function stageToWorkspace(distPath, slug, workspacePath = process.cwd()) {
|
|
248
|
-
const resolvedDistPath = resolve(distPath);
|
|
249
|
-
const irisAppsDir = join(workspacePath, EXPORT_ROOT, IRIS_APPS_DIR);
|
|
250
|
-
const targetDir = join(irisAppsDir, slug);
|
|
251
|
-
const warnings = [];
|
|
252
|
-
|
|
253
|
-
// Validate dist path exists
|
|
254
|
-
if (!existsSync(resolvedDistPath)) {
|
|
255
|
-
return {
|
|
256
|
-
success: false,
|
|
257
|
-
stagedPath: null,
|
|
258
|
-
error: `Dist path does not exist: ${resolvedDistPath}\n\n` +
|
|
259
|
-
`The source folder may have been deleted or moved.`,
|
|
260
|
-
fileCount: 0,
|
|
261
|
-
warnings
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Validate build output
|
|
266
|
-
const validation = validateIrisBuild(resolvedDistPath);
|
|
267
|
-
if (!validation.valid) {
|
|
268
|
-
return {
|
|
269
|
-
success: false,
|
|
270
|
-
stagedPath: null,
|
|
271
|
-
error: `Invalid build output:\n${validation.errors.join('\n')}`,
|
|
272
|
-
fileCount: 0,
|
|
273
|
-
warnings
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Acquire staging lock (use user home for consistent lock location)
|
|
278
|
-
const lockBasePath = join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks');
|
|
279
|
-
const lockResult = acquireLock(LockTypes.STAGE, {
|
|
280
|
-
context: slug,
|
|
281
|
-
operation: `staging ${slug}`,
|
|
282
|
-
basePath: lockBasePath
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// If lock acquisition fails due to permissions, warn but continue
|
|
286
|
-
let lockAcquired = lockResult.acquired;
|
|
287
|
-
if (!lockResult.acquired) {
|
|
288
|
-
if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
|
|
289
|
-
warnings.push('Could not create staging lock (permission issue). Proceeding without lock.');
|
|
290
|
-
lockAcquired = false;
|
|
291
|
-
} else {
|
|
292
|
-
return {
|
|
293
|
-
success: false,
|
|
294
|
-
stagedPath: null,
|
|
295
|
-
error: lockResult.error,
|
|
296
|
-
fileCount: 0,
|
|
297
|
-
warnings
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
// Ensure iris-apps directory exists
|
|
304
|
-
try {
|
|
305
|
-
if (!existsSync(irisAppsDir)) {
|
|
306
|
-
mkdirSync(irisAppsDir, { recursive: true });
|
|
307
|
-
}
|
|
308
|
-
} catch (err) {
|
|
309
|
-
const errorType = detectErrorType(err);
|
|
310
|
-
if (errorType === ErrorTypes.PERMISSION) {
|
|
311
|
-
return {
|
|
312
|
-
success: false,
|
|
313
|
-
stagedPath: null,
|
|
314
|
-
error: formatPermissionError({
|
|
315
|
-
operation: 'create directory',
|
|
316
|
-
path: irisAppsDir
|
|
317
|
-
}),
|
|
318
|
-
fileCount: 0,
|
|
319
|
-
warnings
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
success: false,
|
|
324
|
-
stagedPath: null,
|
|
325
|
-
error: `Failed to create iris-apps directory: ${err.message}`,
|
|
326
|
-
fileCount: 0,
|
|
327
|
-
warnings
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Remove existing app directory if it exists
|
|
332
|
-
if (existsSync(targetDir)) {
|
|
333
|
-
try {
|
|
334
|
-
rmSync(targetDir, { recursive: true, force: true });
|
|
335
|
-
} catch (err) {
|
|
336
|
-
const errorType = detectErrorType(err);
|
|
337
|
-
if (errorType === ErrorTypes.PERMISSION) {
|
|
338
|
-
return {
|
|
339
|
-
success: false,
|
|
340
|
-
stagedPath: null,
|
|
341
|
-
error: formatPermissionError({
|
|
342
|
-
operation: 'remove existing directory',
|
|
343
|
-
path: targetDir
|
|
344
|
-
}),
|
|
345
|
-
fileCount: 0,
|
|
346
|
-
warnings
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
if (errorType === ErrorTypes.FILE_LOCKED) {
|
|
350
|
-
return {
|
|
351
|
-
success: false,
|
|
352
|
-
stagedPath: null,
|
|
353
|
-
error: formatFileLockError({ path: targetDir }),
|
|
354
|
-
fileCount: 0,
|
|
355
|
-
warnings
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
return {
|
|
359
|
-
success: false,
|
|
360
|
-
stagedPath: null,
|
|
361
|
-
error: `Failed to remove existing app directory: ${err.message}`,
|
|
362
|
-
fileCount: 0,
|
|
363
|
-
warnings
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Copy dist contents to target, excluding unnecessary files
|
|
369
|
-
try {
|
|
370
|
-
cpSync(resolvedDistPath, targetDir, {
|
|
371
|
-
recursive: true,
|
|
372
|
-
filter: (src) => {
|
|
373
|
-
// Use basename for reliable cross-platform filename extraction
|
|
374
|
-
const filename = basename(src);
|
|
375
|
-
// Only exclude specific files, not directories
|
|
376
|
-
if (EXCLUDED_FILES.has(filename)) {
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
return true;
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
} catch (err) {
|
|
383
|
-
const errorType = detectErrorType(err);
|
|
384
|
-
if (errorType === ErrorTypes.PERMISSION) {
|
|
385
|
-
return {
|
|
386
|
-
success: false,
|
|
387
|
-
stagedPath: null,
|
|
388
|
-
error: formatPermissionError({
|
|
389
|
-
operation: 'copy files',
|
|
390
|
-
path: targetDir
|
|
391
|
-
}),
|
|
392
|
-
fileCount: 0,
|
|
393
|
-
warnings
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
if (errorType === ErrorTypes.DISK_FULL) {
|
|
397
|
-
return {
|
|
398
|
-
success: false,
|
|
399
|
-
stagedPath: null,
|
|
400
|
-
error: formatDiskFullError({
|
|
401
|
-
operation: 'copy build files',
|
|
402
|
-
path: targetDir
|
|
403
|
-
}),
|
|
404
|
-
fileCount: 0,
|
|
405
|
-
warnings
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
return {
|
|
409
|
-
success: false,
|
|
410
|
-
stagedPath: null,
|
|
411
|
-
error: `Failed to copy build output: ${err.message}`,
|
|
412
|
-
fileCount: 0,
|
|
413
|
-
warnings
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Count files copied
|
|
418
|
-
const fileCount = countFiles(targetDir);
|
|
419
|
-
|
|
420
|
-
// Update last build timestamp for linked project (ignore errors)
|
|
421
|
-
const updateResult = updateLastBuild(slug);
|
|
422
|
-
if (!updateResult.success && updateResult.error) {
|
|
423
|
-
warnings.push(`Could not update last build timestamp: ${updateResult.error}`);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return {
|
|
427
|
-
success: true,
|
|
428
|
-
stagedPath: targetDir,
|
|
429
|
-
error: null,
|
|
430
|
-
fileCount,
|
|
431
|
-
warnings
|
|
432
|
-
};
|
|
433
|
-
} finally {
|
|
434
|
-
// Release the lock only if we acquired it
|
|
435
|
-
if (lockAcquired) {
|
|
436
|
-
releaseLock(LockTypes.STAGE, { context: slug, basePath: lockBasePath });
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Count files recursively in a directory.
|
|
443
|
-
*
|
|
444
|
-
* @param {string} dir - Directory to count files in
|
|
445
|
-
* @returns {number} - Number of files
|
|
446
|
-
*/
|
|
447
|
-
function countFiles(dir) {
|
|
448
|
-
if (!existsSync(dir)) return 0;
|
|
449
|
-
|
|
450
|
-
let count = 0;
|
|
451
|
-
const items = readdirSync(dir, { withFileTypes: true });
|
|
452
|
-
|
|
453
|
-
for (const item of items) {
|
|
454
|
-
if (item.isDirectory()) {
|
|
455
|
-
count += countFiles(join(dir, item.name));
|
|
456
|
-
} else {
|
|
457
|
-
count++;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return count;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Find an existing dist directory in a Vue project.
|
|
466
|
-
*
|
|
467
|
-
* @param {string} projectPath - Path to the Vue project
|
|
468
|
-
* @returns {string | null} - Path to dist directory or null if not found
|
|
469
|
-
*/
|
|
470
|
-
export function findDistDirectory(projectPath) {
|
|
471
|
-
const possibleDistPaths = ['dist', 'build', 'output'];
|
|
472
|
-
|
|
473
|
-
for (const distDir of possibleDistPaths) {
|
|
474
|
-
const fullPath = join(projectPath, distDir);
|
|
475
|
-
if (existsSync(fullPath)) {
|
|
476
|
-
return fullPath;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return null;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* Format build error for display.
|
|
485
|
-
*
|
|
486
|
-
* @param {string} projectPath - Path to the Vue project
|
|
487
|
-
* @param {string} errorOutput - Error output from build
|
|
488
|
-
* @returns {string} - Formatted error message
|
|
489
|
-
*/
|
|
490
|
-
export function formatBuildError(projectPath, errorOutput) {
|
|
491
|
-
return `Build Failed
|
|
492
|
-
────────────────────────────────────────────────────
|
|
493
|
-
|
|
494
|
-
The Vue.js build process failed. This is a project issue, not a CLI issue.
|
|
495
|
-
|
|
496
|
-
Error output:
|
|
497
|
-
${errorOutput.split('\n').map(line => ` ${line}`).join('\n')}
|
|
498
|
-
|
|
499
|
-
To fix this:
|
|
500
|
-
1. Run 'npm run build' manually in your Vue project:
|
|
501
|
-
cd ${projectPath}
|
|
502
|
-
npm run build
|
|
503
|
-
|
|
504
|
-
2. Fix any compilation errors shown above
|
|
505
|
-
|
|
506
|
-
3. Run 'magentrix vue-run-build' again
|
|
507
|
-
|
|
508
|
-
Alternatively, use --skip-build to stage an existing dist/ folder.`;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Format validation error for display.
|
|
513
|
-
*
|
|
514
|
-
* @param {string} distPath - Path to the dist directory
|
|
515
|
-
* @param {string[]} errors - Validation errors
|
|
516
|
-
* @returns {string} - Formatted error message
|
|
517
|
-
*/
|
|
518
|
-
export function formatValidationError(distPath, errors) {
|
|
519
|
-
return `Invalid Build Output
|
|
520
|
-
────────────────────────────────────────────────────
|
|
521
|
-
|
|
522
|
-
The build output at ${distPath} is missing required files.
|
|
523
|
-
|
|
524
|
-
Missing:
|
|
525
|
-
${errors.map(e => ` ✗ ${e}`).join('\n')}
|
|
526
|
-
|
|
527
|
-
Required files for an Iris app:
|
|
528
|
-
- remoteEntry.js (Module Federation entry point)
|
|
529
|
-
- assets/hostInit*.js (Host initialization script)
|
|
530
|
-
- assets/main*.js (Main entry point)
|
|
531
|
-
- assets/index*.js (Index entry)
|
|
532
|
-
|
|
533
|
-
This usually means:
|
|
534
|
-
1. The project is not configured as a Module Federation remote
|
|
535
|
-
2. The build did not complete successfully
|
|
536
|
-
3. The dist directory contains an old or incorrect build
|
|
537
|
-
|
|
538
|
-
Try rebuilding the project:
|
|
539
|
-
cd ${distPath.replace('/dist', '')}
|
|
540
|
-
npm run build`;
|
|
541
|
-
}
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, cpSync, rmSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, resolve, basename } from 'node:path';
|
|
4
|
+
import { validateIrisBuild } from './validator.js';
|
|
5
|
+
import { updateLastBuild } from './linker.js';
|
|
6
|
+
import { EXPORT_ROOT, IRIS_APPS_DIR } from '../../vars/global.js';
|
|
7
|
+
import { detectErrorType, ErrorTypes, formatPermissionError, formatDiskFullError, formatFileLockError } from './errors.js';
|
|
8
|
+
import { acquireLock, releaseLock, LockTypes, createProjectContext } from './lock.js';
|
|
9
|
+
|
|
10
|
+
// Files to exclude when staging Iris apps (not needed on server)
|
|
11
|
+
const EXCLUDED_FILES = new Set([
|
|
12
|
+
'index.html',
|
|
13
|
+
'favicon.ico',
|
|
14
|
+
'.DS_Store',
|
|
15
|
+
'Thumbs.db',
|
|
16
|
+
'.gitignore',
|
|
17
|
+
'.gitkeep'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maximum dist folder size before warning (500MB)
|
|
22
|
+
*/
|
|
23
|
+
const MAX_DIST_SIZE_WARNING = 500 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build a Vue project using npm run build.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} projectPath - Path to the Vue project
|
|
29
|
+
* @param {Object} options - Build options
|
|
30
|
+
* @param {boolean} options.silent - Suppress build output
|
|
31
|
+
* @returns {Promise<{
|
|
32
|
+
* success: boolean,
|
|
33
|
+
* output: string,
|
|
34
|
+
* error: string | null,
|
|
35
|
+
* distPath: string | null,
|
|
36
|
+
* warnings: string[]
|
|
37
|
+
* }>}
|
|
38
|
+
*/
|
|
39
|
+
export async function buildVueProject(projectPath, options = {}) {
|
|
40
|
+
const { silent = false } = options;
|
|
41
|
+
const resolvedPath = resolve(projectPath);
|
|
42
|
+
const warnings = [];
|
|
43
|
+
|
|
44
|
+
// Check for package.json
|
|
45
|
+
if (!existsSync(join(resolvedPath, 'package.json'))) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
output: '',
|
|
49
|
+
error: `No package.json found in ${resolvedPath}`,
|
|
50
|
+
distPath: null,
|
|
51
|
+
warnings
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for node_modules
|
|
56
|
+
if (!existsSync(join(resolvedPath, 'node_modules'))) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
output: '',
|
|
60
|
+
error: `No node_modules found.\n\nRun the following command first:\n cd "${resolvedPath}"\n npm install`,
|
|
61
|
+
distPath: null,
|
|
62
|
+
warnings
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Acquire build lock to prevent concurrent builds
|
|
67
|
+
// Use os.tmpdir() as basePath to avoid permission issues in project directories
|
|
68
|
+
const lockContext = createProjectContext(resolvedPath);
|
|
69
|
+
const lockResult = acquireLock(LockTypes.BUILD, {
|
|
70
|
+
context: lockContext,
|
|
71
|
+
operation: `building ${basename(resolvedPath)}`,
|
|
72
|
+
basePath: join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks')
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// If lock acquisition fails due to permissions, warn but continue
|
|
76
|
+
// (locking is nice-to-have, not critical for builds)
|
|
77
|
+
let lockAcquired = lockResult.acquired;
|
|
78
|
+
if (!lockResult.acquired) {
|
|
79
|
+
// Check if it's a permission error vs actual concurrent operation
|
|
80
|
+
if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
|
|
81
|
+
warnings.push('Could not create build lock (permission issue). Proceeding without lock.');
|
|
82
|
+
lockAcquired = false;
|
|
83
|
+
} else {
|
|
84
|
+
// Actual concurrent operation - fail
|
|
85
|
+
return {
|
|
86
|
+
success: false,
|
|
87
|
+
output: '',
|
|
88
|
+
error: lockResult.error,
|
|
89
|
+
distPath: null,
|
|
90
|
+
warnings
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
return await new Promise((resolvePromise) => {
|
|
97
|
+
const output = [];
|
|
98
|
+
const errorOutput = [];
|
|
99
|
+
|
|
100
|
+
// Determine npm command based on platform
|
|
101
|
+
const isWindows = process.platform === 'win32';
|
|
102
|
+
const npmCmd = isWindows ? 'npm.cmd' : 'npm';
|
|
103
|
+
|
|
104
|
+
const child = spawn(npmCmd, ['run', 'build'], {
|
|
105
|
+
cwd: resolvedPath,
|
|
106
|
+
shell: isWindows, // Windows requires shell: true for .cmd files
|
|
107
|
+
env: { ...process.env, FORCE_COLOR: '1' }
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
child.stdout.on('data', (data) => {
|
|
111
|
+
const text = data.toString();
|
|
112
|
+
output.push(text);
|
|
113
|
+
if (!silent) {
|
|
114
|
+
process.stdout.write(text);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
child.stderr.on('data', (data) => {
|
|
119
|
+
const text = data.toString();
|
|
120
|
+
errorOutput.push(text);
|
|
121
|
+
if (!silent) {
|
|
122
|
+
process.stderr.write(text);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
child.on('close', (code) => {
|
|
127
|
+
const fullOutput = output.join('');
|
|
128
|
+
const fullError = errorOutput.join('');
|
|
129
|
+
|
|
130
|
+
if (code !== 0) {
|
|
131
|
+
resolvePromise({
|
|
132
|
+
success: false,
|
|
133
|
+
output: fullOutput,
|
|
134
|
+
error: fullError || `Build process exited with code ${code}`,
|
|
135
|
+
distPath: null,
|
|
136
|
+
warnings
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Find dist directory (Vue typically outputs to 'dist')
|
|
142
|
+
const possibleDistPaths = ['dist', 'build', 'output'];
|
|
143
|
+
let distPath = null;
|
|
144
|
+
|
|
145
|
+
for (const distDir of possibleDistPaths) {
|
|
146
|
+
const fullPath = join(resolvedPath, distDir);
|
|
147
|
+
if (existsSync(fullPath)) {
|
|
148
|
+
distPath = fullPath;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!distPath) {
|
|
154
|
+
resolvePromise({
|
|
155
|
+
success: false,
|
|
156
|
+
output: fullOutput,
|
|
157
|
+
error: 'Build completed but no dist/build directory found.\n\n' +
|
|
158
|
+
'Expected one of: dist/, build/, or output/',
|
|
159
|
+
distPath: null,
|
|
160
|
+
warnings
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check dist folder size
|
|
166
|
+
const distSize = getDirectorySize(distPath);
|
|
167
|
+
if (distSize > MAX_DIST_SIZE_WARNING) {
|
|
168
|
+
const sizeMB = Math.round(distSize / (1024 * 1024));
|
|
169
|
+
warnings.push(`Large build output (${sizeMB}MB). This may take longer to stage and publish.`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
resolvePromise({
|
|
173
|
+
success: true,
|
|
174
|
+
output: fullOutput,
|
|
175
|
+
error: null,
|
|
176
|
+
distPath,
|
|
177
|
+
warnings
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
child.on('error', (err) => {
|
|
182
|
+
resolvePromise({
|
|
183
|
+
success: false,
|
|
184
|
+
output: output.join(''),
|
|
185
|
+
error: `Failed to start build process: ${err.message}`,
|
|
186
|
+
distPath: null,
|
|
187
|
+
warnings
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
} finally {
|
|
192
|
+
// Release the lock only if we acquired it
|
|
193
|
+
if (lockAcquired) {
|
|
194
|
+
releaseLock(LockTypes.BUILD, {
|
|
195
|
+
context: lockContext,
|
|
196
|
+
basePath: join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks')
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Calculate total size of a directory recursively.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} dirPath - Directory path
|
|
206
|
+
* @returns {number} - Total size in bytes
|
|
207
|
+
*/
|
|
208
|
+
function getDirectorySize(dirPath) {
|
|
209
|
+
let totalSize = 0;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const items = readdirSync(dirPath, { withFileTypes: true });
|
|
213
|
+
for (const item of items) {
|
|
214
|
+
const itemPath = join(dirPath, item.name);
|
|
215
|
+
if (item.isDirectory()) {
|
|
216
|
+
totalSize += getDirectorySize(itemPath);
|
|
217
|
+
} else {
|
|
218
|
+
try {
|
|
219
|
+
const stats = statSync(itemPath);
|
|
220
|
+
totalSize += stats.size;
|
|
221
|
+
} catch {
|
|
222
|
+
// Skip files we can't stat
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// Return what we have so far
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return totalSize;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Stage build output to a Magentrix workspace's iris-apps directory.
|
|
235
|
+
*
|
|
236
|
+
* @param {string} distPath - Path to the build output (dist directory)
|
|
237
|
+
* @param {string} slug - The app slug (folder name)
|
|
238
|
+
* @param {string} workspacePath - Path to the Magentrix workspace (defaults to CWD)
|
|
239
|
+
* @returns {{
|
|
240
|
+
* success: boolean,
|
|
241
|
+
* stagedPath: string | null,
|
|
242
|
+
* error: string | null,
|
|
243
|
+
* fileCount: number,
|
|
244
|
+
* warnings: string[]
|
|
245
|
+
* }}
|
|
246
|
+
*/
|
|
247
|
+
export function stageToWorkspace(distPath, slug, workspacePath = process.cwd()) {
|
|
248
|
+
const resolvedDistPath = resolve(distPath);
|
|
249
|
+
const irisAppsDir = join(workspacePath, EXPORT_ROOT, IRIS_APPS_DIR);
|
|
250
|
+
const targetDir = join(irisAppsDir, slug);
|
|
251
|
+
const warnings = [];
|
|
252
|
+
|
|
253
|
+
// Validate dist path exists
|
|
254
|
+
if (!existsSync(resolvedDistPath)) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
stagedPath: null,
|
|
258
|
+
error: `Dist path does not exist: ${resolvedDistPath}\n\n` +
|
|
259
|
+
`The source folder may have been deleted or moved.`,
|
|
260
|
+
fileCount: 0,
|
|
261
|
+
warnings
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Validate build output
|
|
266
|
+
const validation = validateIrisBuild(resolvedDistPath);
|
|
267
|
+
if (!validation.valid) {
|
|
268
|
+
return {
|
|
269
|
+
success: false,
|
|
270
|
+
stagedPath: null,
|
|
271
|
+
error: `Invalid build output:\n${validation.errors.join('\n')}`,
|
|
272
|
+
fileCount: 0,
|
|
273
|
+
warnings
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Acquire staging lock (use user home for consistent lock location)
|
|
278
|
+
const lockBasePath = join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.magentrix-locks');
|
|
279
|
+
const lockResult = acquireLock(LockTypes.STAGE, {
|
|
280
|
+
context: slug,
|
|
281
|
+
operation: `staging ${slug}`,
|
|
282
|
+
basePath: lockBasePath
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// If lock acquisition fails due to permissions, warn but continue
|
|
286
|
+
let lockAcquired = lockResult.acquired;
|
|
287
|
+
if (!lockResult.acquired) {
|
|
288
|
+
if (lockResult.error?.includes('permission') || lockResult.error?.includes('EACCES')) {
|
|
289
|
+
warnings.push('Could not create staging lock (permission issue). Proceeding without lock.');
|
|
290
|
+
lockAcquired = false;
|
|
291
|
+
} else {
|
|
292
|
+
return {
|
|
293
|
+
success: false,
|
|
294
|
+
stagedPath: null,
|
|
295
|
+
error: lockResult.error,
|
|
296
|
+
fileCount: 0,
|
|
297
|
+
warnings
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Ensure iris-apps directory exists
|
|
304
|
+
try {
|
|
305
|
+
if (!existsSync(irisAppsDir)) {
|
|
306
|
+
mkdirSync(irisAppsDir, { recursive: true });
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
const errorType = detectErrorType(err);
|
|
310
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
311
|
+
return {
|
|
312
|
+
success: false,
|
|
313
|
+
stagedPath: null,
|
|
314
|
+
error: formatPermissionError({
|
|
315
|
+
operation: 'create directory',
|
|
316
|
+
path: irisAppsDir
|
|
317
|
+
}),
|
|
318
|
+
fileCount: 0,
|
|
319
|
+
warnings
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
stagedPath: null,
|
|
325
|
+
error: `Failed to create iris-apps directory: ${err.message}`,
|
|
326
|
+
fileCount: 0,
|
|
327
|
+
warnings
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Remove existing app directory if it exists
|
|
332
|
+
if (existsSync(targetDir)) {
|
|
333
|
+
try {
|
|
334
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
335
|
+
} catch (err) {
|
|
336
|
+
const errorType = detectErrorType(err);
|
|
337
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
338
|
+
return {
|
|
339
|
+
success: false,
|
|
340
|
+
stagedPath: null,
|
|
341
|
+
error: formatPermissionError({
|
|
342
|
+
operation: 'remove existing directory',
|
|
343
|
+
path: targetDir
|
|
344
|
+
}),
|
|
345
|
+
fileCount: 0,
|
|
346
|
+
warnings
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (errorType === ErrorTypes.FILE_LOCKED) {
|
|
350
|
+
return {
|
|
351
|
+
success: false,
|
|
352
|
+
stagedPath: null,
|
|
353
|
+
error: formatFileLockError({ path: targetDir }),
|
|
354
|
+
fileCount: 0,
|
|
355
|
+
warnings
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
success: false,
|
|
360
|
+
stagedPath: null,
|
|
361
|
+
error: `Failed to remove existing app directory: ${err.message}`,
|
|
362
|
+
fileCount: 0,
|
|
363
|
+
warnings
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Copy dist contents to target, excluding unnecessary files
|
|
369
|
+
try {
|
|
370
|
+
cpSync(resolvedDistPath, targetDir, {
|
|
371
|
+
recursive: true,
|
|
372
|
+
filter: (src) => {
|
|
373
|
+
// Use basename for reliable cross-platform filename extraction
|
|
374
|
+
const filename = basename(src);
|
|
375
|
+
// Only exclude specific files, not directories
|
|
376
|
+
if (EXCLUDED_FILES.has(filename)) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
} catch (err) {
|
|
383
|
+
const errorType = detectErrorType(err);
|
|
384
|
+
if (errorType === ErrorTypes.PERMISSION) {
|
|
385
|
+
return {
|
|
386
|
+
success: false,
|
|
387
|
+
stagedPath: null,
|
|
388
|
+
error: formatPermissionError({
|
|
389
|
+
operation: 'copy files',
|
|
390
|
+
path: targetDir
|
|
391
|
+
}),
|
|
392
|
+
fileCount: 0,
|
|
393
|
+
warnings
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (errorType === ErrorTypes.DISK_FULL) {
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
stagedPath: null,
|
|
400
|
+
error: formatDiskFullError({
|
|
401
|
+
operation: 'copy build files',
|
|
402
|
+
path: targetDir
|
|
403
|
+
}),
|
|
404
|
+
fileCount: 0,
|
|
405
|
+
warnings
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
success: false,
|
|
410
|
+
stagedPath: null,
|
|
411
|
+
error: `Failed to copy build output: ${err.message}`,
|
|
412
|
+
fileCount: 0,
|
|
413
|
+
warnings
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Count files copied
|
|
418
|
+
const fileCount = countFiles(targetDir);
|
|
419
|
+
|
|
420
|
+
// Update last build timestamp for linked project (ignore errors)
|
|
421
|
+
const updateResult = updateLastBuild(slug);
|
|
422
|
+
if (!updateResult.success && updateResult.error) {
|
|
423
|
+
warnings.push(`Could not update last build timestamp: ${updateResult.error}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
success: true,
|
|
428
|
+
stagedPath: targetDir,
|
|
429
|
+
error: null,
|
|
430
|
+
fileCount,
|
|
431
|
+
warnings
|
|
432
|
+
};
|
|
433
|
+
} finally {
|
|
434
|
+
// Release the lock only if we acquired it
|
|
435
|
+
if (lockAcquired) {
|
|
436
|
+
releaseLock(LockTypes.STAGE, { context: slug, basePath: lockBasePath });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Count files recursively in a directory.
|
|
443
|
+
*
|
|
444
|
+
* @param {string} dir - Directory to count files in
|
|
445
|
+
* @returns {number} - Number of files
|
|
446
|
+
*/
|
|
447
|
+
function countFiles(dir) {
|
|
448
|
+
if (!existsSync(dir)) return 0;
|
|
449
|
+
|
|
450
|
+
let count = 0;
|
|
451
|
+
const items = readdirSync(dir, { withFileTypes: true });
|
|
452
|
+
|
|
453
|
+
for (const item of items) {
|
|
454
|
+
if (item.isDirectory()) {
|
|
455
|
+
count += countFiles(join(dir, item.name));
|
|
456
|
+
} else {
|
|
457
|
+
count++;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return count;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Find an existing dist directory in a Vue project.
|
|
466
|
+
*
|
|
467
|
+
* @param {string} projectPath - Path to the Vue project
|
|
468
|
+
* @returns {string | null} - Path to dist directory or null if not found
|
|
469
|
+
*/
|
|
470
|
+
export function findDistDirectory(projectPath) {
|
|
471
|
+
const possibleDistPaths = ['dist', 'build', 'output'];
|
|
472
|
+
|
|
473
|
+
for (const distDir of possibleDistPaths) {
|
|
474
|
+
const fullPath = join(projectPath, distDir);
|
|
475
|
+
if (existsSync(fullPath)) {
|
|
476
|
+
return fullPath;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Format build error for display.
|
|
485
|
+
*
|
|
486
|
+
* @param {string} projectPath - Path to the Vue project
|
|
487
|
+
* @param {string} errorOutput - Error output from build
|
|
488
|
+
* @returns {string} - Formatted error message
|
|
489
|
+
*/
|
|
490
|
+
export function formatBuildError(projectPath, errorOutput) {
|
|
491
|
+
return `Build Failed
|
|
492
|
+
────────────────────────────────────────────────────
|
|
493
|
+
|
|
494
|
+
The Vue.js build process failed. This is a project issue, not a CLI issue.
|
|
495
|
+
|
|
496
|
+
Error output:
|
|
497
|
+
${errorOutput.split('\n').map(line => ` ${line}`).join('\n')}
|
|
498
|
+
|
|
499
|
+
To fix this:
|
|
500
|
+
1. Run 'npm run build' manually in your Vue project:
|
|
501
|
+
cd ${projectPath}
|
|
502
|
+
npm run build
|
|
503
|
+
|
|
504
|
+
2. Fix any compilation errors shown above
|
|
505
|
+
|
|
506
|
+
3. Run 'magentrix vue-run-build' again
|
|
507
|
+
|
|
508
|
+
Alternatively, use --skip-build to stage an existing dist/ folder.`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Format validation error for display.
|
|
513
|
+
*
|
|
514
|
+
* @param {string} distPath - Path to the dist directory
|
|
515
|
+
* @param {string[]} errors - Validation errors
|
|
516
|
+
* @returns {string} - Formatted error message
|
|
517
|
+
*/
|
|
518
|
+
export function formatValidationError(distPath, errors) {
|
|
519
|
+
return `Invalid Build Output
|
|
520
|
+
────────────────────────────────────────────────────
|
|
521
|
+
|
|
522
|
+
The build output at ${distPath} is missing required files.
|
|
523
|
+
|
|
524
|
+
Missing:
|
|
525
|
+
${errors.map(e => ` ✗ ${e}`).join('\n')}
|
|
526
|
+
|
|
527
|
+
Required files for an Iris app:
|
|
528
|
+
- remoteEntry.js (Module Federation entry point)
|
|
529
|
+
- assets/hostInit*.js (Host initialization script)
|
|
530
|
+
- assets/main*.js (Main entry point)
|
|
531
|
+
- assets/index*.js (Index entry)
|
|
532
|
+
|
|
533
|
+
This usually means:
|
|
534
|
+
1. The project is not configured as a Module Federation remote
|
|
535
|
+
2. The build did not complete successfully
|
|
536
|
+
3. The dist directory contains an old or incorrect build
|
|
537
|
+
|
|
538
|
+
Try rebuilding the project:
|
|
539
|
+
cd ${distPath.replace('/dist', '')}
|
|
540
|
+
npm run build`;
|
|
541
|
+
}
|