@launchframe/cli 0.1.6

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,313 @@
1
+ /**
2
+ * Variant Processor
3
+ *
4
+ * Applies variant modifications to generated projects.
5
+ * Strategy: Copy base โ†’ Copy variant FILES โ†’ Insert variant SECTIONS
6
+ *
7
+ * Clear distinction:
8
+ * - SECTIONS: Code snippets inserted into base template files
9
+ * - FILES: Complete files/folders copied to the project
10
+ */
11
+
12
+ const { replaceSection } = require('./section-replacer');
13
+ const { copyDirectory } = require('./file-ops');
14
+ const { replaceVariables } = require('./variable-replacer');
15
+ const { getVariantConfig, getVariantsToApply } = require('../services/variant-config');
16
+ const fs = require('fs-extra');
17
+ const path = require('path');
18
+
19
+ /**
20
+ * Process service with variant modifications
21
+ *
22
+ * @param {string} serviceName - Service to process (backend, admin-portal, etc.)
23
+ * @param {object} variantChoices - User's variant selections (e.g., { tenancy: 'multi-tenant', userModel: 'b2b' })
24
+ * @param {string} destination - Destination directory for generated code
25
+ * @param {object} replacements - Template variable replacements (e.g., {{PROJECT_NAME}})
26
+ * @param {string} templateRoot - Root directory of templates
27
+ */
28
+ async function processServiceVariant(
29
+ serviceName,
30
+ variantChoices,
31
+ destination,
32
+ replacements,
33
+ templateRoot
34
+ ) {
35
+ console.log(`\n๐Ÿ“ฆ Processing ${serviceName} with choices:`, variantChoices);
36
+
37
+ const serviceConfig = getVariantConfig(serviceName);
38
+ if (!serviceConfig) {
39
+ throw new Error(`No variant configuration found for service: ${serviceName}`);
40
+ }
41
+
42
+ const basePath = path.join(templateRoot, serviceConfig.base);
43
+
44
+ // Step 1: Copy base template (minimal - B2B + single-tenant)
45
+ console.log(` ๐Ÿ“ Copying base template from ${serviceConfig.base}`);
46
+ await copyDirectory(basePath, destination, {
47
+ exclude: ['node_modules', '.git', 'dist', '.env', 'templates']
48
+ });
49
+
50
+ // Step 2: Determine which variants to apply
51
+ const variantsToApply = getVariantsToApply(variantChoices);
52
+
53
+ if (variantsToApply.length === 0) {
54
+ console.log(` โœ… Using base template (no variants to apply)`);
55
+ } else {
56
+ console.log(` ๐Ÿ”ง Applying variants: ${variantsToApply.join(', ')}`);
57
+ }
58
+
59
+ // Step 3: Apply each variant
60
+ for (const variantName of variantsToApply) {
61
+ const variantConfig = serviceConfig.variants[variantName];
62
+
63
+ if (!variantConfig) {
64
+ console.warn(` โš ๏ธ No configuration found for variant: ${variantName}, skipping`);
65
+ continue;
66
+ }
67
+
68
+ console.log(`\n โœจ Applying ${variantName} variant:`);
69
+
70
+ // Step 3a: Copy variant FILES
71
+ await copyVariantFiles(
72
+ variantName,
73
+ variantConfig.files || [],
74
+ serviceConfig.filesDir,
75
+ destination,
76
+ templateRoot
77
+ );
78
+
79
+ // Step 3b: Insert variant SECTIONS
80
+ await insertVariantSections(
81
+ variantName,
82
+ variantConfig.sections || {},
83
+ serviceConfig.sectionsDir,
84
+ destination,
85
+ templateRoot
86
+ );
87
+ }
88
+
89
+ // Step 4: Clean up unused section markers
90
+ console.log(`\n ๐Ÿงน Cleaning up unused section markers`);
91
+ await cleanupSectionMarkers(serviceName, serviceConfig, variantsToApply, destination);
92
+
93
+ // Step 5: Replace template variables ({{PROJECT_NAME}}, etc.)
94
+ console.log(`\n ๐Ÿ”ค Replacing template variables`);
95
+ await replaceVariables(destination, replacements);
96
+
97
+ console.log(` โœ… ${serviceName} processing complete\n`);
98
+ }
99
+
100
+ /**
101
+ * Clean up unused section markers from generated files
102
+ * Uses variant configuration to only process files with known sections
103
+ * @param {string} serviceName - Service name (backend, admin-portal, etc.)
104
+ * @param {object} serviceConfig - Service variant configuration
105
+ * @param {string[]} appliedVariants - Variants that were applied
106
+ * @param {string} destination - Destination directory
107
+ */
108
+ async function cleanupSectionMarkers(serviceName, serviceConfig, appliedVariants, destination) {
109
+ const allVariants = Object.keys(serviceConfig.variants || {});
110
+ const unappliedVariants = allVariants.filter(v => !appliedVariants.includes(v));
111
+
112
+ if (unappliedVariants.length === 0) {
113
+ console.log(` โœ“ No unused section markers to clean`);
114
+ return;
115
+ }
116
+
117
+ // Collect all sections from unapplied variants
118
+ const sectionsToRemove = new Map(); // filePath -> Set of section names
119
+
120
+ for (const variantName of unappliedVariants) {
121
+ const variantConfig = serviceConfig.variants[variantName];
122
+ const sections = variantConfig?.sections || {};
123
+
124
+ for (const [filePath, sectionNames] of Object.entries(sections)) {
125
+ if (!sectionsToRemove.has(filePath)) {
126
+ sectionsToRemove.set(filePath, new Set());
127
+ }
128
+ sectionNames.forEach(name => sectionsToRemove.get(filePath).add(name));
129
+ }
130
+ }
131
+
132
+ let totalCleaned = 0;
133
+
134
+ // Process each file that has sections to remove
135
+ for (const [filePath, sectionNames] of sectionsToRemove.entries()) {
136
+ const targetFilePath = path.join(destination, filePath);
137
+
138
+ if (!await fs.pathExists(targetFilePath)) {
139
+ continue;
140
+ }
141
+
142
+ try {
143
+ let content = await fs.readFile(targetFilePath, 'utf-8');
144
+ let modified = false;
145
+
146
+ // Remove each unused section marker
147
+ for (const sectionName of sectionNames) {
148
+ // Try both comment formats
149
+ const regularPattern = new RegExp(
150
+ `\\/\\/ ${sectionName}_START\\n[\\s\\S]*?\\/\\/ ${sectionName}_END\\n?`,
151
+ 'g'
152
+ );
153
+ const jsxPattern = new RegExp(
154
+ `\\{\\/\\* ${sectionName}_START \\*\\/\\}\\n[\\s\\S]*?\\{\\/\\* ${sectionName}_END \\*\\/\\}\\n?`,
155
+ 'g'
156
+ );
157
+
158
+ const beforeRegular = content;
159
+ content = content.replace(regularPattern, '');
160
+ if (content !== beforeRegular) {
161
+ modified = true;
162
+ }
163
+
164
+ const beforeJsx = content;
165
+ content = content.replace(jsxPattern, '');
166
+ if (content !== beforeJsx) {
167
+ modified = true;
168
+ }
169
+ }
170
+
171
+ if (modified) {
172
+ await fs.writeFile(targetFilePath, content, 'utf-8');
173
+ totalCleaned++;
174
+ }
175
+ } catch (error) {
176
+ console.warn(` โš ๏ธ Could not clean markers in ${filePath}:`, error.message);
177
+ }
178
+ }
179
+
180
+ if (totalCleaned > 0) {
181
+ console.log(` โœ“ Cleaned up section markers in ${totalCleaned} file(s)`);
182
+ } else {
183
+ console.log(` โœ“ No unused section markers found`);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Copy variant files to destination
189
+ * @param {string} variantName - Variant name (e.g., 'multi-tenant')
190
+ * @param {string[]} files - List of files/folders to copy
191
+ * @param {string} filesDir - Variant files directory
192
+ * @param {string} destination - Destination directory
193
+ * @param {string} templateRoot - Template root directory
194
+ */
195
+ async function copyVariantFiles(variantName, files, filesDir, destination, templateRoot) {
196
+ if (!files || files.length === 0) {
197
+ console.log(` ๐Ÿ“‚ No files to copy for ${variantName}`);
198
+ return;
199
+ }
200
+
201
+ console.log(` ๐Ÿ“‚ Copying ${files.length} file(s)/folder(s):`);
202
+
203
+ const variantFilesPath = path.join(templateRoot, filesDir, variantName);
204
+
205
+ for (const filePath of files) {
206
+ const sourcePath = path.join(variantFilesPath, filePath);
207
+ const destPath = path.join(destination, filePath);
208
+
209
+ try {
210
+ // Check if source exists
211
+ if (!await fs.pathExists(sourcePath)) {
212
+ console.warn(` โš ๏ธ Source not found: ${filePath}, skipping`);
213
+ continue;
214
+ }
215
+
216
+ // Create parent directory if needed
217
+ await fs.ensureDir(path.dirname(destPath));
218
+
219
+ // Copy file or directory
220
+ await fs.copy(sourcePath, destPath, { overwrite: true });
221
+
222
+ const isDir = (await fs.stat(sourcePath)).isDirectory();
223
+ console.log(` โœ“ Copied ${isDir ? 'folder' : 'file'}: ${filePath}`);
224
+ } catch (error) {
225
+ console.warn(` โš ๏ธ Could not copy ${filePath}:`, error.message);
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Insert variant section content into base template files
232
+ * @param {string} variantName - Variant name (e.g., 'multi-tenant')
233
+ * @param {object} sections - Section configuration { 'file.ts': ['SECTION1', 'SECTION2'] }
234
+ * @param {string} sectionsDir - Variant sections directory
235
+ * @param {string} destination - Destination directory
236
+ * @param {string} templateRoot - Template root directory
237
+ */
238
+ async function insertVariantSections(variantName, sections, sectionsDir, destination, templateRoot) {
239
+ if (!sections || Object.keys(sections).length === 0) {
240
+ console.log(` โœ๏ธ No sections to insert for ${variantName}`);
241
+ return;
242
+ }
243
+
244
+ const sectionFiles = Object.keys(sections);
245
+ console.log(` โœ๏ธ Inserting sections into ${sectionFiles.length} file(s):`);
246
+
247
+ const variantSectionsPath = path.join(templateRoot, sectionsDir, variantName);
248
+
249
+ for (const [filePath, sectionNames] of Object.entries(sections)) {
250
+ const targetFilePath = path.join(destination, filePath);
251
+
252
+ // Check if target file exists
253
+ if (!await fs.pathExists(targetFilePath)) {
254
+ console.warn(` โš ๏ธ Target file not found: ${filePath}, skipping sections`);
255
+ continue;
256
+ }
257
+
258
+ console.log(` ๐Ÿ“ ${filePath}:`);
259
+
260
+ for (const sectionName of sectionNames) {
261
+ try {
262
+ // Read section content from file
263
+ const fileName = path.basename(filePath);
264
+ const sectionFileName = `${fileName}.${sectionName}`;
265
+ const sectionFilePath = path.join(variantSectionsPath, sectionFileName);
266
+
267
+ if (!await fs.pathExists(sectionFilePath)) {
268
+ console.warn(` โš ๏ธ Section file not found: ${sectionFileName}, skipping`);
269
+ continue;
270
+ }
271
+
272
+ const sectionContent = await fs.readFile(sectionFilePath, 'utf-8');
273
+
274
+ // Insert section content into target file
275
+ await replaceSection(targetFilePath, sectionName, sectionContent);
276
+ console.log(` โœ“ Inserted [${sectionName}]`);
277
+ } catch (error) {
278
+ console.warn(` โš ๏ธ Could not insert section ${sectionName}:`, error.message);
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Validate variant choices
286
+ * @param {object} variantChoices - User's variant selections
287
+ * @returns {object} Validation result { valid: boolean, errors: string[] }
288
+ */
289
+ function validateVariantChoices(variantChoices) {
290
+ const errors = [];
291
+
292
+ // Validate tenancy choice
293
+ const tenancy = variantChoices.tenancy;
294
+ if (tenancy && !['multi-tenant', 'single-tenant'].includes(tenancy)) {
295
+ errors.push(`Invalid tenancy choice: ${tenancy}`);
296
+ }
297
+
298
+ // Validate user model choice
299
+ const userModel = variantChoices.userModel;
300
+ if (userModel && !['b2b', 'b2b2c'].includes(userModel)) {
301
+ errors.push(`Invalid user model choice: ${userModel}`);
302
+ }
303
+
304
+ return {
305
+ valid: errors.length === 0,
306
+ errors
307
+ };
308
+ }
309
+
310
+ module.exports = {
311
+ processServiceVariant,
312
+ validateVariantChoices
313
+ };