@magentrix-corp/magentrix-cli 1.3.10 → 1.3.11

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.
@@ -0,0 +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
+ }