@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/validation.js
CHANGED
|
@@ -1,360 +1,360 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Validation utilities for Iris Vue integration.
|
|
3
|
-
* Provides input validation, format checking, and user-friendly error messages.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Valid slug pattern: lowercase alphanumeric, can contain hyphens and underscores
|
|
8
|
-
* Must start with alphanumeric, 1-200 characters total
|
|
9
|
-
*/
|
|
10
|
-
const SLUG_PATTERN = /^[a-z0-9][a-z0-9-_]{0,199}$/;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Characters not allowed in slugs (for error messages)
|
|
14
|
-
*/
|
|
15
|
-
const INVALID_SLUG_CHARS = /[^a-z0-9-_]/g;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Maximum lengths for various fields
|
|
19
|
-
*/
|
|
20
|
-
export const MAX_LENGTHS = {
|
|
21
|
-
slug: 200,
|
|
22
|
-
appName: 255,
|
|
23
|
-
appDescription: 1000,
|
|
24
|
-
siteUrl: 500
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Validate a slug format.
|
|
29
|
-
*
|
|
30
|
-
* @param {string} slug - The slug to validate
|
|
31
|
-
* @returns {{
|
|
32
|
-
* valid: boolean,
|
|
33
|
-
* error: string | null,
|
|
34
|
-
* suggestion: string | null
|
|
35
|
-
* }}
|
|
36
|
-
*/
|
|
37
|
-
export function validateSlug(slug) {
|
|
38
|
-
const result = {
|
|
39
|
-
valid: true,
|
|
40
|
-
error: null,
|
|
41
|
-
suggestion: null
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// Check for empty/null/undefined
|
|
45
|
-
if (!slug || typeof slug !== 'string') {
|
|
46
|
-
result.valid = false;
|
|
47
|
-
result.error = 'Slug is required and must be a string';
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Trim whitespace
|
|
52
|
-
const trimmedSlug = slug.trim();
|
|
53
|
-
|
|
54
|
-
// Check for empty after trim
|
|
55
|
-
if (trimmedSlug.length === 0) {
|
|
56
|
-
result.valid = false;
|
|
57
|
-
result.error = 'Slug cannot be empty or contain only whitespace';
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Check for path traversal attempts (security)
|
|
62
|
-
if (trimmedSlug.includes('..') || trimmedSlug.includes('/') || trimmedSlug.includes('\\')) {
|
|
63
|
-
result.valid = false;
|
|
64
|
-
result.error = 'Slug cannot contain path separators (.., /, \\) for security reasons';
|
|
65
|
-
return result;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check length
|
|
69
|
-
if (trimmedSlug.length > MAX_LENGTHS.slug) {
|
|
70
|
-
result.valid = false;
|
|
71
|
-
result.error = `Slug must be ${MAX_LENGTHS.slug} characters or less (currently ${trimmedSlug.length})`;
|
|
72
|
-
result.suggestion = trimmedSlug.substring(0, MAX_LENGTHS.slug);
|
|
73
|
-
return result;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Check for uppercase (common mistake)
|
|
77
|
-
if (trimmedSlug !== trimmedSlug.toLowerCase()) {
|
|
78
|
-
result.valid = false;
|
|
79
|
-
result.error = 'Slug must be lowercase';
|
|
80
|
-
result.suggestion = trimmedSlug.toLowerCase();
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Check first character
|
|
85
|
-
if (!/^[a-z0-9]/.test(trimmedSlug)) {
|
|
86
|
-
result.valid = false;
|
|
87
|
-
result.error = 'Slug must start with a letter or number (not a hyphen or underscore)';
|
|
88
|
-
// Suggest removing leading special chars
|
|
89
|
-
const cleaned = trimmedSlug.replace(/^[-_]+/, '');
|
|
90
|
-
if (cleaned.length > 0) {
|
|
91
|
-
result.suggestion = cleaned;
|
|
92
|
-
}
|
|
93
|
-
return result;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Check pattern
|
|
97
|
-
if (!SLUG_PATTERN.test(trimmedSlug)) {
|
|
98
|
-
const invalidChars = trimmedSlug.match(INVALID_SLUG_CHARS);
|
|
99
|
-
if (invalidChars) {
|
|
100
|
-
const uniqueChars = [...new Set(invalidChars)].join(', ');
|
|
101
|
-
result.valid = false;
|
|
102
|
-
result.error = `Slug contains invalid characters: ${uniqueChars}`;
|
|
103
|
-
result.suggestion = trimmedSlug.replace(INVALID_SLUG_CHARS, '-').replace(/--+/g, '-');
|
|
104
|
-
} else {
|
|
105
|
-
result.valid = false;
|
|
106
|
-
result.error = 'Slug format is invalid. Use only lowercase letters, numbers, hyphens, and underscores.';
|
|
107
|
-
}
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Validate a URL format.
|
|
116
|
-
*
|
|
117
|
-
* @param {string} url - The URL to validate
|
|
118
|
-
* @param {Object} options - Validation options
|
|
119
|
-
* @param {boolean} options.requireHttps - Require HTTPS protocol (default: false)
|
|
120
|
-
* @param {boolean} options.allowTrailingSlash - Allow trailing slash (default: true)
|
|
121
|
-
* @returns {{
|
|
122
|
-
* valid: boolean,
|
|
123
|
-
* error: string | null,
|
|
124
|
-
* suggestion: string | null,
|
|
125
|
-
* normalized: string | null
|
|
126
|
-
* }}
|
|
127
|
-
*/
|
|
128
|
-
export function validateUrl(url, options = {}) {
|
|
129
|
-
const { requireHttps = false, allowTrailingSlash = true } = options;
|
|
130
|
-
|
|
131
|
-
const result = {
|
|
132
|
-
valid: true,
|
|
133
|
-
error: null,
|
|
134
|
-
suggestion: null,
|
|
135
|
-
normalized: null
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
// Check for empty/null/undefined
|
|
139
|
-
if (!url || typeof url !== 'string') {
|
|
140
|
-
result.valid = false;
|
|
141
|
-
result.error = 'URL is required and must be a string';
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const trimmedUrl = url.trim();
|
|
146
|
-
|
|
147
|
-
// Check for empty after trim
|
|
148
|
-
if (trimmedUrl.length === 0) {
|
|
149
|
-
result.valid = false;
|
|
150
|
-
result.error = 'URL cannot be empty';
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Check length
|
|
155
|
-
if (trimmedUrl.length > MAX_LENGTHS.siteUrl) {
|
|
156
|
-
result.valid = false;
|
|
157
|
-
result.error = `URL must be ${MAX_LENGTHS.siteUrl} characters or less`;
|
|
158
|
-
return result;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Check for protocol
|
|
162
|
-
if (!trimmedUrl.startsWith('http://') && !trimmedUrl.startsWith('https://')) {
|
|
163
|
-
result.valid = false;
|
|
164
|
-
result.error = 'URL must start with http:// or https://';
|
|
165
|
-
// Suggest adding https://
|
|
166
|
-
result.suggestion = `https://${trimmedUrl}`;
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Check for HTTPS requirement
|
|
171
|
-
if (requireHttps && trimmedUrl.startsWith('http://')) {
|
|
172
|
-
result.valid = false;
|
|
173
|
-
result.error = 'URL must use HTTPS for security';
|
|
174
|
-
result.suggestion = trimmedUrl.replace('http://', 'https://');
|
|
175
|
-
return result;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Try to parse as URL
|
|
179
|
-
try {
|
|
180
|
-
const parsed = new URL(trimmedUrl);
|
|
181
|
-
|
|
182
|
-
// Normalize the URL (remove trailing slash if not allowed)
|
|
183
|
-
let normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
184
|
-
if (!allowTrailingSlash && normalized.endsWith('/') && normalized.length > parsed.protocol.length + 3) {
|
|
185
|
-
normalized = normalized.slice(0, -1);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Add back query string and hash if present
|
|
189
|
-
if (parsed.search) {
|
|
190
|
-
normalized += parsed.search;
|
|
191
|
-
}
|
|
192
|
-
if (parsed.hash) {
|
|
193
|
-
normalized += parsed.hash;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
result.normalized = normalized;
|
|
197
|
-
} catch (err) {
|
|
198
|
-
result.valid = false;
|
|
199
|
-
result.error = `Invalid URL format: ${err.message}`;
|
|
200
|
-
return result;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return result;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Validate app name format.
|
|
208
|
-
*
|
|
209
|
-
* @param {string} appName - The app name to validate
|
|
210
|
-
* @returns {{
|
|
211
|
-
* valid: boolean,
|
|
212
|
-
* error: string | null
|
|
213
|
-
* }}
|
|
214
|
-
*/
|
|
215
|
-
export function validateAppName(appName) {
|
|
216
|
-
const result = {
|
|
217
|
-
valid: true,
|
|
218
|
-
error: null
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
// Check for empty/null/undefined
|
|
222
|
-
if (!appName || typeof appName !== 'string') {
|
|
223
|
-
result.valid = false;
|
|
224
|
-
result.error = 'App name is required and must be a string';
|
|
225
|
-
return result;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const trimmed = appName.trim();
|
|
229
|
-
|
|
230
|
-
// Check for empty after trim
|
|
231
|
-
if (trimmed.length === 0) {
|
|
232
|
-
result.valid = false;
|
|
233
|
-
result.error = 'App name cannot be empty or contain only whitespace';
|
|
234
|
-
return result;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Check length
|
|
238
|
-
if (trimmed.length > MAX_LENGTHS.appName) {
|
|
239
|
-
result.valid = false;
|
|
240
|
-
result.error = `App name must be ${MAX_LENGTHS.appName} characters or less (currently ${trimmed.length})`;
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return result;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Check if a string looks like it contains a JavaScript expression.
|
|
249
|
-
* Used to detect template literals or expressions in config values.
|
|
250
|
-
*
|
|
251
|
-
* @param {string} value - The value to check
|
|
252
|
-
* @returns {{
|
|
253
|
-
* isExpression: boolean,
|
|
254
|
-
* type: 'template_literal' | 'variable' | 'function_call' | null
|
|
255
|
-
* }}
|
|
256
|
-
*/
|
|
257
|
-
export function detectExpression(value) {
|
|
258
|
-
const result = {
|
|
259
|
-
isExpression: false,
|
|
260
|
-
type: null
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
if (!value || typeof value !== 'string') {
|
|
264
|
-
return result;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Check for template literal syntax
|
|
268
|
-
if (value.includes('${') && value.includes('}')) {
|
|
269
|
-
result.isExpression = true;
|
|
270
|
-
result.type = 'template_literal';
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Check for variable reference patterns
|
|
275
|
-
if (/^(process\.env\.|env\.|import\.meta\.)/i.test(value)) {
|
|
276
|
-
result.isExpression = true;
|
|
277
|
-
result.type = 'variable';
|
|
278
|
-
return result;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Check for function call patterns
|
|
282
|
-
if (/\([^)]*\)$/.test(value)) {
|
|
283
|
-
result.isExpression = true;
|
|
284
|
-
result.type = 'function_call';
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return result;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Sanitize a string to be safe for use in file paths.
|
|
293
|
-
*
|
|
294
|
-
* @param {string} input - The string to sanitize
|
|
295
|
-
* @returns {string} - Sanitized string
|
|
296
|
-
*/
|
|
297
|
-
export function sanitizeForPath(input) {
|
|
298
|
-
if (!input || typeof input !== 'string') {
|
|
299
|
-
return '';
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return input
|
|
303
|
-
.toLowerCase()
|
|
304
|
-
.replace(/[^a-z0-9-_]/g, '-') // Replace invalid chars with hyphen
|
|
305
|
-
.replace(/--+/g, '-') // Replace multiple hyphens with single
|
|
306
|
-
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
|
307
|
-
.substring(0, MAX_LENGTHS.slug);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Validate that a value is not empty after various processing.
|
|
312
|
-
*
|
|
313
|
-
* @param {*} value - The value to check
|
|
314
|
-
* @param {string} fieldName - Name of the field for error messages
|
|
315
|
-
* @returns {{valid: boolean, error: string | null}}
|
|
316
|
-
*/
|
|
317
|
-
export function validateNotEmpty(value, fieldName) {
|
|
318
|
-
const result = {
|
|
319
|
-
valid: true,
|
|
320
|
-
error: null
|
|
321
|
-
};
|
|
322
|
-
|
|
323
|
-
if (value === null || value === undefined) {
|
|
324
|
-
result.valid = false;
|
|
325
|
-
result.error = `${fieldName} is required`;
|
|
326
|
-
return result;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (typeof value === 'string' && value.trim().length === 0) {
|
|
330
|
-
result.valid = false;
|
|
331
|
-
result.error = `${fieldName} cannot be empty`;
|
|
332
|
-
return result;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (Array.isArray(value) && value.length === 0) {
|
|
336
|
-
result.valid = false;
|
|
337
|
-
result.error = `${fieldName} cannot be empty`;
|
|
338
|
-
return result;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return result;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Format validation errors for display.
|
|
346
|
-
*
|
|
347
|
-
* @param {string} fieldName - Name of the field
|
|
348
|
-
* @param {string} error - Error message
|
|
349
|
-
* @param {string | null} suggestion - Suggested fix
|
|
350
|
-
* @returns {string} - Formatted error message
|
|
351
|
-
*/
|
|
352
|
-
export function formatValidationError(fieldName, error, suggestion = null) {
|
|
353
|
-
let message = `Invalid ${fieldName}: ${error}`;
|
|
354
|
-
|
|
355
|
-
if (suggestion) {
|
|
356
|
-
message += `\n Suggestion: ${suggestion}`;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return message;
|
|
360
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for Iris Vue integration.
|
|
3
|
+
* Provides input validation, format checking, and user-friendly error messages.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Valid slug pattern: lowercase alphanumeric, can contain hyphens and underscores
|
|
8
|
+
* Must start with alphanumeric, 1-200 characters total
|
|
9
|
+
*/
|
|
10
|
+
const SLUG_PATTERN = /^[a-z0-9][a-z0-9-_]{0,199}$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Characters not allowed in slugs (for error messages)
|
|
14
|
+
*/
|
|
15
|
+
const INVALID_SLUG_CHARS = /[^a-z0-9-_]/g;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Maximum lengths for various fields
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_LENGTHS = {
|
|
21
|
+
slug: 200,
|
|
22
|
+
appName: 255,
|
|
23
|
+
appDescription: 1000,
|
|
24
|
+
siteUrl: 500
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validate a slug format.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} slug - The slug to validate
|
|
31
|
+
* @returns {{
|
|
32
|
+
* valid: boolean,
|
|
33
|
+
* error: string | null,
|
|
34
|
+
* suggestion: string | null
|
|
35
|
+
* }}
|
|
36
|
+
*/
|
|
37
|
+
export function validateSlug(slug) {
|
|
38
|
+
const result = {
|
|
39
|
+
valid: true,
|
|
40
|
+
error: null,
|
|
41
|
+
suggestion: null
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Check for empty/null/undefined
|
|
45
|
+
if (!slug || typeof slug !== 'string') {
|
|
46
|
+
result.valid = false;
|
|
47
|
+
result.error = 'Slug is required and must be a string';
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Trim whitespace
|
|
52
|
+
const trimmedSlug = slug.trim();
|
|
53
|
+
|
|
54
|
+
// Check for empty after trim
|
|
55
|
+
if (trimmedSlug.length === 0) {
|
|
56
|
+
result.valid = false;
|
|
57
|
+
result.error = 'Slug cannot be empty or contain only whitespace';
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for path traversal attempts (security)
|
|
62
|
+
if (trimmedSlug.includes('..') || trimmedSlug.includes('/') || trimmedSlug.includes('\\')) {
|
|
63
|
+
result.valid = false;
|
|
64
|
+
result.error = 'Slug cannot contain path separators (.., /, \\) for security reasons';
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check length
|
|
69
|
+
if (trimmedSlug.length > MAX_LENGTHS.slug) {
|
|
70
|
+
result.valid = false;
|
|
71
|
+
result.error = `Slug must be ${MAX_LENGTHS.slug} characters or less (currently ${trimmedSlug.length})`;
|
|
72
|
+
result.suggestion = trimmedSlug.substring(0, MAX_LENGTHS.slug);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for uppercase (common mistake)
|
|
77
|
+
if (trimmedSlug !== trimmedSlug.toLowerCase()) {
|
|
78
|
+
result.valid = false;
|
|
79
|
+
result.error = 'Slug must be lowercase';
|
|
80
|
+
result.suggestion = trimmedSlug.toLowerCase();
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check first character
|
|
85
|
+
if (!/^[a-z0-9]/.test(trimmedSlug)) {
|
|
86
|
+
result.valid = false;
|
|
87
|
+
result.error = 'Slug must start with a letter or number (not a hyphen or underscore)';
|
|
88
|
+
// Suggest removing leading special chars
|
|
89
|
+
const cleaned = trimmedSlug.replace(/^[-_]+/, '');
|
|
90
|
+
if (cleaned.length > 0) {
|
|
91
|
+
result.suggestion = cleaned;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check pattern
|
|
97
|
+
if (!SLUG_PATTERN.test(trimmedSlug)) {
|
|
98
|
+
const invalidChars = trimmedSlug.match(INVALID_SLUG_CHARS);
|
|
99
|
+
if (invalidChars) {
|
|
100
|
+
const uniqueChars = [...new Set(invalidChars)].join(', ');
|
|
101
|
+
result.valid = false;
|
|
102
|
+
result.error = `Slug contains invalid characters: ${uniqueChars}`;
|
|
103
|
+
result.suggestion = trimmedSlug.replace(INVALID_SLUG_CHARS, '-').replace(/--+/g, '-');
|
|
104
|
+
} else {
|
|
105
|
+
result.valid = false;
|
|
106
|
+
result.error = 'Slug format is invalid. Use only lowercase letters, numbers, hyphens, and underscores.';
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validate a URL format.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} url - The URL to validate
|
|
118
|
+
* @param {Object} options - Validation options
|
|
119
|
+
* @param {boolean} options.requireHttps - Require HTTPS protocol (default: false)
|
|
120
|
+
* @param {boolean} options.allowTrailingSlash - Allow trailing slash (default: true)
|
|
121
|
+
* @returns {{
|
|
122
|
+
* valid: boolean,
|
|
123
|
+
* error: string | null,
|
|
124
|
+
* suggestion: string | null,
|
|
125
|
+
* normalized: string | null
|
|
126
|
+
* }}
|
|
127
|
+
*/
|
|
128
|
+
export function validateUrl(url, options = {}) {
|
|
129
|
+
const { requireHttps = false, allowTrailingSlash = true } = options;
|
|
130
|
+
|
|
131
|
+
const result = {
|
|
132
|
+
valid: true,
|
|
133
|
+
error: null,
|
|
134
|
+
suggestion: null,
|
|
135
|
+
normalized: null
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Check for empty/null/undefined
|
|
139
|
+
if (!url || typeof url !== 'string') {
|
|
140
|
+
result.valid = false;
|
|
141
|
+
result.error = 'URL is required and must be a string';
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const trimmedUrl = url.trim();
|
|
146
|
+
|
|
147
|
+
// Check for empty after trim
|
|
148
|
+
if (trimmedUrl.length === 0) {
|
|
149
|
+
result.valid = false;
|
|
150
|
+
result.error = 'URL cannot be empty';
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check length
|
|
155
|
+
if (trimmedUrl.length > MAX_LENGTHS.siteUrl) {
|
|
156
|
+
result.valid = false;
|
|
157
|
+
result.error = `URL must be ${MAX_LENGTHS.siteUrl} characters or less`;
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Check for protocol
|
|
162
|
+
if (!trimmedUrl.startsWith('http://') && !trimmedUrl.startsWith('https://')) {
|
|
163
|
+
result.valid = false;
|
|
164
|
+
result.error = 'URL must start with http:// or https://';
|
|
165
|
+
// Suggest adding https://
|
|
166
|
+
result.suggestion = `https://${trimmedUrl}`;
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check for HTTPS requirement
|
|
171
|
+
if (requireHttps && trimmedUrl.startsWith('http://')) {
|
|
172
|
+
result.valid = false;
|
|
173
|
+
result.error = 'URL must use HTTPS for security';
|
|
174
|
+
result.suggestion = trimmedUrl.replace('http://', 'https://');
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Try to parse as URL
|
|
179
|
+
try {
|
|
180
|
+
const parsed = new URL(trimmedUrl);
|
|
181
|
+
|
|
182
|
+
// Normalize the URL (remove trailing slash if not allowed)
|
|
183
|
+
let normalized = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
184
|
+
if (!allowTrailingSlash && normalized.endsWith('/') && normalized.length > parsed.protocol.length + 3) {
|
|
185
|
+
normalized = normalized.slice(0, -1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add back query string and hash if present
|
|
189
|
+
if (parsed.search) {
|
|
190
|
+
normalized += parsed.search;
|
|
191
|
+
}
|
|
192
|
+
if (parsed.hash) {
|
|
193
|
+
normalized += parsed.hash;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
result.normalized = normalized;
|
|
197
|
+
} catch (err) {
|
|
198
|
+
result.valid = false;
|
|
199
|
+
result.error = `Invalid URL format: ${err.message}`;
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Validate app name format.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} appName - The app name to validate
|
|
210
|
+
* @returns {{
|
|
211
|
+
* valid: boolean,
|
|
212
|
+
* error: string | null
|
|
213
|
+
* }}
|
|
214
|
+
*/
|
|
215
|
+
export function validateAppName(appName) {
|
|
216
|
+
const result = {
|
|
217
|
+
valid: true,
|
|
218
|
+
error: null
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Check for empty/null/undefined
|
|
222
|
+
if (!appName || typeof appName !== 'string') {
|
|
223
|
+
result.valid = false;
|
|
224
|
+
result.error = 'App name is required and must be a string';
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const trimmed = appName.trim();
|
|
229
|
+
|
|
230
|
+
// Check for empty after trim
|
|
231
|
+
if (trimmed.length === 0) {
|
|
232
|
+
result.valid = false;
|
|
233
|
+
result.error = 'App name cannot be empty or contain only whitespace';
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check length
|
|
238
|
+
if (trimmed.length > MAX_LENGTHS.appName) {
|
|
239
|
+
result.valid = false;
|
|
240
|
+
result.error = `App name must be ${MAX_LENGTHS.appName} characters or less (currently ${trimmed.length})`;
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Check if a string looks like it contains a JavaScript expression.
|
|
249
|
+
* Used to detect template literals or expressions in config values.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} value - The value to check
|
|
252
|
+
* @returns {{
|
|
253
|
+
* isExpression: boolean,
|
|
254
|
+
* type: 'template_literal' | 'variable' | 'function_call' | null
|
|
255
|
+
* }}
|
|
256
|
+
*/
|
|
257
|
+
export function detectExpression(value) {
|
|
258
|
+
const result = {
|
|
259
|
+
isExpression: false,
|
|
260
|
+
type: null
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
if (!value || typeof value !== 'string') {
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check for template literal syntax
|
|
268
|
+
if (value.includes('${') && value.includes('}')) {
|
|
269
|
+
result.isExpression = true;
|
|
270
|
+
result.type = 'template_literal';
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check for variable reference patterns
|
|
275
|
+
if (/^(process\.env\.|env\.|import\.meta\.)/i.test(value)) {
|
|
276
|
+
result.isExpression = true;
|
|
277
|
+
result.type = 'variable';
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check for function call patterns
|
|
282
|
+
if (/\([^)]*\)$/.test(value)) {
|
|
283
|
+
result.isExpression = true;
|
|
284
|
+
result.type = 'function_call';
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Sanitize a string to be safe for use in file paths.
|
|
293
|
+
*
|
|
294
|
+
* @param {string} input - The string to sanitize
|
|
295
|
+
* @returns {string} - Sanitized string
|
|
296
|
+
*/
|
|
297
|
+
export function sanitizeForPath(input) {
|
|
298
|
+
if (!input || typeof input !== 'string') {
|
|
299
|
+
return '';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return input
|
|
303
|
+
.toLowerCase()
|
|
304
|
+
.replace(/[^a-z0-9-_]/g, '-') // Replace invalid chars with hyphen
|
|
305
|
+
.replace(/--+/g, '-') // Replace multiple hyphens with single
|
|
306
|
+
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
|
307
|
+
.substring(0, MAX_LENGTHS.slug);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Validate that a value is not empty after various processing.
|
|
312
|
+
*
|
|
313
|
+
* @param {*} value - The value to check
|
|
314
|
+
* @param {string} fieldName - Name of the field for error messages
|
|
315
|
+
* @returns {{valid: boolean, error: string | null}}
|
|
316
|
+
*/
|
|
317
|
+
export function validateNotEmpty(value, fieldName) {
|
|
318
|
+
const result = {
|
|
319
|
+
valid: true,
|
|
320
|
+
error: null
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
if (value === null || value === undefined) {
|
|
324
|
+
result.valid = false;
|
|
325
|
+
result.error = `${fieldName} is required`;
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (typeof value === 'string' && value.trim().length === 0) {
|
|
330
|
+
result.valid = false;
|
|
331
|
+
result.error = `${fieldName} cannot be empty`;
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
336
|
+
result.valid = false;
|
|
337
|
+
result.error = `${fieldName} cannot be empty`;
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Format validation errors for display.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} fieldName - Name of the field
|
|
348
|
+
* @param {string} error - Error message
|
|
349
|
+
* @param {string | null} suggestion - Suggested fix
|
|
350
|
+
* @returns {string} - Formatted error message
|
|
351
|
+
*/
|
|
352
|
+
export function formatValidationError(fieldName, error, suggestion = null) {
|
|
353
|
+
let message = `Invalid ${fieldName}: ${error}`;
|
|
354
|
+
|
|
355
|
+
if (suggestion) {
|
|
356
|
+
message += `\n Suggestion: ${suggestion}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return message;
|
|
360
|
+
}
|