Update scripts/check-template-combinations for better missing file checking 737e084f
Maximilian Leodolter · 2025-08-07 13:44 4 file(s) · +427 −372
scripts/check-template-combinations.ts +308 −371
6 6
import path from "node:path";
7 7
8 8
// Define the possible boolean options from ProjectOptions type
9 -
const BOOLEAN_OPTIONS = ["tailwind", "shadcn", "rpc", "tanstackQuery"] as const;
9 +
const BOOLEAN_OPTIONS = [
10 +
  "tailwind",
11 +
  "shadcn",
12 +
  "rpc",
13 +
  "tanstackQuery",
14 +
  "reactRouter",
15 +
] as const;
16 +
17 +
// Package dependency rules
18 +
const PACKAGE_DEPENDENCIES: Record<string, string[]> = {
19 +
  shadcn: ["tailwind"], // shadcn requires tailwind
20 +
  // Add more dependencies here as needed
21 +
  // example: somePackage: ["requiredPackage1", "requiredPackage2"]
22 +
};
23 +
24 +
// Mutually exclusive groups (only one option from each group can be selected)
25 +
const MUTUALLY_EXCLUSIVE_GROUPS: string[][] = [
26 +
  // Add mutually exclusive groups here as needed
27 +
  // example: ["option1", "option2", "option3"]
28 +
];
29 +
30 +
// Check if a combination is valid based on dependencies and mutual exclusivity
31 +
function isValidCombination(combination: Record<string, boolean>): boolean {
32 +
  // Skip combinations with no packages selected
33 +
  const hasAnyPackage = Object.values(combination).some((value) => value);
34 +
  if (!hasAnyPackage) {
35 +
    return false;
36 +
  }
37 +
38 +
  // Skip combinations that only contain shadcn and/or tailwind (they're cloned from repo)
39 +
  const enabledPackages = Object.keys(combination).filter(
40 +
    (key) => combination[key],
41 +
  );
42 +
  const onlyShadcnTailwind = enabledPackages.every(
43 +
    (pkg) => pkg === "shadcn" || pkg === "tailwind",
44 +
  );
45 +
  if (onlyShadcnTailwind) {
46 +
    return false;
47 +
  }
10 48
11 -
// Simulate nameGenerator function locally
12 -
const nameGenerator = (
13 -
	basename: string,
14 -
	possibleOptions: Record<string, boolean>,
15 -
) => {
16 -
	const dotIndex = basename.lastIndexOf(".");
17 -
	const filename = dotIndex === -1 ? basename : basename.substring(0, dotIndex);
18 -
	const extension = dotIndex === -1 ? "" : basename.substring(dotIndex + 1);
49 +
  // Check package dependencies
50 +
  for (const [packageName, dependencies] of Object.entries(
51 +
    PACKAGE_DEPENDENCIES,
52 +
  )) {
53 +
    if (combination[packageName]) {
54 +
      // If this package is enabled, all its dependencies must also be enabled
55 +
      for (const dependency of dependencies) {
56 +
        if (!combination[dependency]) {
57 +
          return false;
58 +
        }
59 +
      }
60 +
    }
61 +
  }
19 62
20 -
	const selectedOptions = Object.keys(possibleOptions)
21 -
		.filter((opt) => possibleOptions[opt])
22 -
		.map((opt) => opt.toLowerCase())
23 -
		.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
63 +
  // Check mutual exclusivity
64 +
  for (const group of MUTUALLY_EXCLUSIVE_GROUPS) {
65 +
    const selectedInGroup = group.filter((option) => combination[option]);
66 +
    if (selectedInGroup.length > 1) {
67 +
      return false; // More than one option selected in mutually exclusive group
68 +
    }
69 +
  }
24 70
25 -
	if (selectedOptions.length > 0) {
26 -
		const suffix = ["-with", ...selectedOptions].join("-");
27 -
		return extension
28 -
			? `${filename}${suffix}.${extension}`
29 -
			: `${filename}${suffix}`;
30 -
	}
31 -
	return basename;
32 -
};
71 +
  return true;
72 +
}
33 73
34 -
// Generate all possible combinations of boolean options
74 +
// Generate all possible combinations of boolean options with filtering
35 75
function generateAllCombinations(
36 -
	options: readonly string[],
76 +
  options: readonly string[],
37 77
): Array<Record<string, boolean>> {
38 -
	const combinations: Array<Record<string, boolean>> = [];
39 -
	const totalCombinations = Math.pow(2, options.length);
78 +
  const combinations: Array<Record<string, boolean>> = [];
79 +
  const totalCombinations = Math.pow(2, options.length);
40 80
41 -
	for (let i = 0; i < totalCombinations; i++) {
42 -
		const combination: Record<string, boolean> = {};
43 -
		for (let j = 0; j < options.length; j++) {
44 -
			combination[options[j]] = Boolean(i & (1 << j));
45 -
		}
46 -
		combinations.push(combination);
47 -
	}
81 +
  for (let i = 0; i < totalCombinations; i++) {
82 +
    const combination: Record<string, boolean> = {};
83 +
    for (let j = 0; j < options.length; j++) {
84 +
      combination[options[j]] = Boolean(i & (1 << j));
85 +
    }
86 +
87 +
    // Only include valid combinations
88 +
    if (isValidCombination(combination)) {
89 +
      combinations.push(combination);
90 +
    }
91 +
  }
48 92
49 -
	return combinations;
93 +
  return combinations;
50 94
}
51 95
96 +
// Simulate nameGenerator function locally
97 +
const nameGenerator = (
98 +
  basename: string,
99 +
  possibleOptions: Record<string, boolean>,
100 +
) => {
101 +
  const dotIndex = basename.lastIndexOf(".");
102 +
  const filename = dotIndex === -1 ? basename : basename.substring(0, dotIndex);
103 +
  const extension = dotIndex === -1 ? "" : basename.substring(dotIndex + 1);
104 +
105 +
  const selectedOptions = Object.keys(possibleOptions)
106 +
    .filter((opt) => possibleOptions[opt])
107 +
    .map((opt) => opt.toLowerCase())
108 +
    .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
109 +
110 +
  if (selectedOptions.length > 0) {
111 +
    const suffix = ["-with", ...selectedOptions].join("-");
112 +
    return extension
113 +
      ? `${filename}${suffix}.${extension}`
114 +
      : `${filename}${suffix}`;
115 +
  }
116 +
  return basename;
117 +
};
118 +
52 119
// Parse installer files to find nameGenerator calls and hardcoded template patterns
53 120
// Recursively find all .ts files in installers directory
54 121
async function findInstallerFiles(dir: string): Promise<string[]> {
55 -
	const files: string[] = [];
56 -
	const entries = await fs.readdir(dir, { withFileTypes: true });
122 +
  const files: string[] = [];
123 +
  const entries = await fs.readdir(dir, { withFileTypes: true });
57 124
58 -
	for (const entry of entries) {
59 -
		const fullPath = path.join(dir, entry.name);
60 -
		if (entry.isDirectory()) {
61 -
			files.push(...(await findInstallerFiles(fullPath)));
62 -
		} else if (entry.isFile() && entry.name.endsWith(".ts")) {
63 -
			files.push(fullPath);
64 -
		}
65 -
	}
125 +
  for (const entry of entries) {
126 +
    const fullPath = path.join(dir, entry.name);
127 +
    if (entry.isDirectory()) {
128 +
      files.push(...(await findInstallerFiles(fullPath)));
129 +
    } else if (entry.isFile() && entry.name.endsWith(".ts")) {
130 +
      files.push(fullPath);
131 +
    }
132 +
  }
66 133
67 -
	return files;
134 +
  return files;
68 135
}
69 136
70 137
async function parseInstallerFiles(): Promise<
71 -
	Array<{
72 -
		file: string;
73 -
		basename: string;
74 -
		usedOptions: string[];
75 -
		templatePath: string;
76 -
		type: "nameGenerator" | "hardcoded";
77 -
	}>
138 +
  Array<{
139 +
    file: string;
140 +
    basename: string;
141 +
    usedOptions: string[];
142 +
    templatePath: string;
143 +
    type: "nameGenerator" | "hardcoded";
144 +
  }>
78 145
> {
79 -
	const installerFiles = await findInstallerFiles("src/installers");
80 -
	const templateCalls: Array<{
81 -
		file: string;
82 -
		basename: string;
83 -
		usedOptions: string[];
84 -
		templatePath: string;
85 -
		type: "nameGenerator" | "hardcoded";
86 -
	}> = [];
146 +
  const installerFiles = await findInstallerFiles("src/installers");
147 +
  const templateCalls: Array<{
148 +
    file: string;
149 +
    basename: string;
150 +
    usedOptions: string[];
151 +
    templatePath: string;
152 +
    type: "nameGenerator" | "hardcoded";
153 +
  }> = [];
87 154
88 -
	for (const file of installerFiles) {
89 -
		const content = await fs.readFile(file, "utf-8");
155 +
  for (const file of installerFiles) {
156 +
    const content = await fs.readFile(file, "utf-8");
90 157
91 -
		// Find nameGenerator calls with regex
92 -
		const nameGeneratorRegex =
93 -
			/nameGenerator\s*\(\s*["']([^"']+)["']\s*,\s*\{([^}]+)\}/g;
94 -
		let match;
158 +
    // Find nameGenerator calls with regex
159 +
    const nameGeneratorRegex =
160 +
      /nameGenerator\s*\(\s*["']([^"']+)["']\s*,\s*\{([^}]+)\}/g;
161 +
    let match;
95 162
96 -
		while ((match = nameGeneratorRegex.exec(content)) !== null) {
97 -
			const basename = match[1];
98 -
			const optionsStr = match[2];
163 +
    while ((match = nameGeneratorRegex.exec(content)) !== null) {
164 +
      const basename = match[1];
165 +
      const optionsStr = match[2];
99 166
100 -
			// Extract the options used in this call
101 -
			const usedOptions = BOOLEAN_OPTIONS.filter((option) =>
102 -
				optionsStr.includes(option),
103 -
			);
167 +
      // Extract the options used in this call
168 +
      const usedOptions = BOOLEAN_OPTIONS.filter((option) =>
169 +
        optionsStr.includes(option),
170 +
      );
104 171
105 -
			// Try to find the template path from the surrounding context
106 -
			const lines = content.split("\n");
107 -
			const matchLine =
108 -
				content.substring(0, match.index).split("\n").length - 1;
172 +
      // Try to find the template path from the surrounding context
173 +
      const lines = content.split("\n");
174 +
      const matchLine =
175 +
        content.substring(0, match.index).split("\n").length - 1;
109 176
110 -
			let templatePath = "client/src"; // default based on common pattern
177 +
      let templatePath = "client/src"; // default based on common pattern
111 178
112 -
			// Look for path.join with EXTRAS_DIR in nearby lines (could be multi-line)
113 -
			for (
114 -
				let i = Math.max(0, matchLine - 10);
115 -
				i <= Math.min(lines.length - 1, matchLine + 10);
116 -
				i++
117 -
			) {
118 -
				const line = lines[i].trim();
119 -
				if (line.includes("path.join") && line.includes("EXTRAS_DIR")) {
120 -
					// Find the complete path.join statement (might span multiple lines)
121 -
					let pathJoinContent = "";
122 -
					let j = i;
123 -
					let parenCount = 0;
124 -
					let foundStart = false;
179 +
      // Look for path.join with EXTRAS_DIR in nearby lines (could be multi-line)
180 +
      for (
181 +
        let i = Math.max(0, matchLine - 10);
182 +
        i <= Math.min(lines.length - 1, matchLine + 10);
183 +
        i++
184 +
      ) {
185 +
        const line = lines[i].trim();
186 +
        if (line.includes("path.join") && line.includes("EXTRAS_DIR")) {
187 +
          // Find the complete path.join statement (might span multiple lines)
188 +
          let pathJoinContent = "";
189 +
          let j = i;
190 +
          let parenCount = 0;
191 +
          let foundStart = false;
125 192
126 -
					while (j < lines.length) {
127 -
						const currentLine = lines[j].trim();
128 -
						pathJoinContent += currentLine + " ";
193 +
          while (j < lines.length) {
194 +
            const currentLine = lines[j].trim();
195 +
            pathJoinContent += currentLine + " ";
129 196
130 -
						if (currentLine.includes("path.join")) {
131 -
							foundStart = true;
132 -
						}
197 +
            if (currentLine.includes("path.join")) {
198 +
              foundStart = true;
199 +
            }
133 200
134 -
						if (foundStart) {
135 -
							parenCount += (currentLine.match(/\(/g) || []).length;
136 -
							parenCount -= (currentLine.match(/\)/g) || []).length;
201 +
            if (foundStart) {
202 +
              parenCount += (currentLine.match(/\(/g) || []).length;
203 +
              parenCount -= (currentLine.match(/\)/g) || []).length;
137 204
138 -
							if (parenCount === 0) {
139 -
								break;
140 -
							}
141 -
						}
142 -
						j++;
143 -
					}
205 +
              if (parenCount === 0) {
206 +
                break;
207 +
              }
208 +
            }
209 +
            j++;
210 +
          }
144 211
145 -
					// Extract path components from the complete path.join
146 -
					const pathMatch = pathJoinContent.match(/EXTRAS_DIR[^,]*,([^)]+)\)/);
147 -
					if (pathMatch) {
148 -
						templatePath = pathMatch[1]
149 -
							.split(",")
150 -
							.map((s) =>
151 -
								s
152 -
									.trim()
153 -
									.replace(/['"]/g, "")
154 -
									.replace(/nameGenerator[^,]*/, basename),
155 -
							)
156 -
							.filter((s) => s && s !== basename)
157 -
							.join("/");
158 -
					}
159 -
					break;
160 -
				}
161 -
			}
212 +
          // Extract path components from the complete path.join
213 +
          const pathMatch = pathJoinContent.match(/EXTRAS_DIR[^,]*,([^)]+)\)/);
214 +
          if (pathMatch) {
215 +
            templatePath = pathMatch[1]
216 +
              .split(",")
217 +
              .map((s) =>
218 +
                s
219 +
                  .trim()
220 +
                  .replace(/['"]/g, "")
221 +
                  .replace(/nameGenerator[^,]*/, basename),
222 +
              )
223 +
              .filter((s) => s && s !== basename)
224 +
              .join("/");
225 +
          }
226 +
          break;
227 +
        }
228 +
      }
162 229
163 -
			templateCalls.push({
164 -
				file,
165 -
				basename,
166 -
				usedOptions,
167 -
				templatePath,
168 -
				type: "nameGenerator",
169 -
			});
170 -
		}
230 +
      templateCalls.push({
231 +
        file,
232 +
        basename,
233 +
        usedOptions,
234 +
        templatePath,
235 +
        type: "nameGenerator",
236 +
      });
237 +
    }
171 238
172 -
		// Find hardcoded template patterns like: `App-with${tailwind ? "-tailwind" : ""}${shadcn ? "-shadcn" : ""}${rpc ? "-rpc" : ""}.tsx`
173 -
		const hardcodedRegex = /`([^`]*)-with\${[^`]+\$\{[^`]+}\.[^`]+`/g;
174 -
		let hardcodedMatch;
239 +
    // Find hardcoded template patterns like: `App-with${tailwind ? "-tailwind" : ""}${shadcn ? "-shadcn" : ""}${rpc ? "-rpc" : ""}.tsx`
240 +
    const hardcodedRegex = /`([^`]*)-with\${[^`]+\$\{[^`]+}\.[^`]+`/g;
241 +
    let hardcodedMatch;
175 242
176 -
		while ((hardcodedMatch = hardcodedRegex.exec(content)) !== null) {
177 -
			const templatePattern = hardcodedMatch[1];
243 +
    while ((hardcodedMatch = hardcodedRegex.exec(content)) !== null) {
244 +
      const templatePattern = hardcodedMatch[1];
178 245
179 -
			// Extract basename from pattern (everything before "-with")
180 -
			const basenameMatch = templatePattern.match(/^([^-]+)/);
181 -
			if (basenameMatch) {
182 -
				const basename = `${basenameMatch[1]}.tsx`; // Add .tsx extension
246 +
      // Extract basename from pattern (everything before "-with")
247 +
      const basenameMatch = templatePattern.match(/^([^-]+)/);
248 +
      if (basenameMatch) {
249 +
        const basename = `${basenameMatch[1]}.tsx`; // Add .tsx extension
183 250
184 -
				// Extract options from the template pattern
185 -
				const usedOptions = BOOLEAN_OPTIONS.filter((option) =>
186 -
					hardcodedMatch[0].includes(option),
187 -
				);
251 +
        // Extract options from the template pattern
252 +
        const usedOptions = BOOLEAN_OPTIONS.filter((option) =>
253 +
          hardcodedMatch[0].includes(option),
254 +
        );
188 255
189 -
				// Find template path similar to nameGenerator
190 -
				const lines = content.split("\n");
191 -
				const matchLine =
192 -
					content.substring(0, hardcodedMatch.index).split("\n").length - 1;
256 +
        // Find template path similar to nameGenerator
257 +
        const lines = content.split("\n");
258 +
        const matchLine =
259 +
          content.substring(0, hardcodedMatch.index).split("\n").length - 1;
193 260
194 -
				let templatePath = "client/src";
261 +
        let templatePath = "client/src";
195 262
196 -
				for (
197 -
					let i = Math.max(0, matchLine - 10);
198 -
					i <= Math.min(lines.length - 1, matchLine + 10);
199 -
					i++
200 -
				) {
201 -
					const line = lines[i].trim();
202 -
					if (line.includes("path.join") && line.includes("EXTRAS_DIR")) {
203 -
						let pathJoinContent = "";
204 -
						let j = i;
205 -
						let parenCount = 0;
206 -
						let foundStart = false;
263 +
        for (
264 +
          let i = Math.max(0, matchLine - 10);
265 +
          i <= Math.min(lines.length - 1, matchLine + 10);
266 +
          i++
267 +
        ) {
268 +
          const line = lines[i].trim();
269 +
          if (line.includes("path.join") && line.includes("EXTRAS_DIR")) {
270 +
            let pathJoinContent = "";
271 +
            let j = i;
272 +
            let parenCount = 0;
273 +
            let foundStart = false;
207 274
208 -
						while (j < lines.length) {
209 -
							const currentLine = lines[j].trim();
210 -
							pathJoinContent += currentLine + " ";
275 +
            while (j < lines.length) {
276 +
              const currentLine = lines[j].trim();
277 +
              pathJoinContent += currentLine + " ";
211 278
212 -
							if (currentLine.includes("path.join")) {
213 -
								foundStart = true;
214 -
							}
279 +
              if (currentLine.includes("path.join")) {
280 +
                foundStart = true;
281 +
              }
215 282
216 -
							if (foundStart) {
217 -
								parenCount += (currentLine.match(/\(/g) || []).length;
218 -
								parenCount -= (currentLine.match(/\)/g) || []).length;
283 +
              if (foundStart) {
284 +
                parenCount += (currentLine.match(/\(/g) || []).length;
285 +
                parenCount -= (currentLine.match(/\)/g) || []).length;
219 286
220 -
								if (parenCount === 0) {
221 -
									break;
222 -
								}
223 -
							}
224 -
							j++;
225 -
						}
287 +
                if (parenCount === 0) {
288 +
                  break;
289 +
                }
290 +
              }
291 +
              j++;
292 +
            }
226 293
227 -
						const pathMatch = pathJoinContent.match(
228 -
							/EXTRAS_DIR[^,]*,([^)]+)\)/,
229 -
						);
230 -
						if (pathMatch) {
231 -
							templatePath = pathMatch[1]
232 -
								.split(",")
233 -
								.map((s) =>
234 -
									s
235 -
										.trim()
236 -
										.replace(/['"]/g, "")
237 -
										.replace(/selectedTemplate[^,]*/, basename),
238 -
								)
239 -
								.filter(
240 -
									(s) => s && s !== basename && !s.includes("selectedTemplate"),
241 -
								)
242 -
								.join("/");
243 -
						}
244 -
						break;
245 -
					}
246 -
				}
294 +
            const pathMatch = pathJoinContent.match(
295 +
              /EXTRAS_DIR[^,]*,([^)]+)\)/,
296 +
            );
297 +
            if (pathMatch) {
298 +
              templatePath = pathMatch[1]
299 +
                .split(",")
300 +
                .map((s) =>
301 +
                  s
302 +
                    .trim()
303 +
                    .replace(/['"]/g, "")
304 +
                    .replace(/selectedTemplate[^,]*/, basename),
305 +
                )
306 +
                .filter(
307 +
                  (s) => s && s !== basename && !s.includes("selectedTemplate"),
308 +
                )
309 +
                .join("/");
310 +
            }
311 +
            break;
312 +
          }
313 +
        }
247 314
248 -
				templateCalls.push({
249 -
					file,
250 -
					basename,
251 -
					usedOptions,
252 -
					templatePath,
253 -
					type: "hardcoded",
254 -
				});
255 -
			}
256 -
		}
257 -
	}
315 +
        templateCalls.push({
316 +
          file,
317 +
          basename,
318 +
          usedOptions,
319 +
          templatePath,
320 +
          type: "hardcoded",
321 +
        });
322 +
      }
323 +
    }
324 +
  }
258 325
259 -
	return templateCalls;
326 +
  return templateCalls;
260 327
}
261 328
262 329
// Simulate hardcoded template naming (like in RPC installer)
263 330
const hardcodedGenerator = (
264 -
	basename: string,
265 -
	possibleOptions: Record<string, boolean>,
331 +
  basename: string,
332 +
  possibleOptions: Record<string, boolean>,
266 333
) => {
267 -
	const dotIndex = basename.lastIndexOf(".");
268 -
	const filename = dotIndex === -1 ? basename : basename.substring(0, dotIndex);
269 -
	const extension = dotIndex === -1 ? "" : basename.substring(dotIndex + 1);
334 +
  const dotIndex = basename.lastIndexOf(".");
335 +
  const filename = dotIndex === -1 ? basename : basename.substring(0, dotIndex);
336 +
  const extension = dotIndex === -1 ? "" : basename.substring(dotIndex + 1);
270 337
271 -
	let result = filename + "-with";
338 +
  let result = filename + "-with";
272 339
273 -
	// Hardcoded follows specific order: tailwind, shadcn, rpc
274 -
	if (possibleOptions.tailwind) result += "-tailwind";
275 -
	if (possibleOptions.shadcn) result += "-shadcn";
276 -
	if (possibleOptions.rpc) result += "-rpc";
340 +
  // Hardcoded follows specific order: tailwind, shadcn, rpc, tanstackQuery, then routers
341 +
  if (possibleOptions.tailwind) result += "-tailwind";
342 +
  if (possibleOptions.shadcn) result += "-shadcn";
343 +
  if (possibleOptions.rpc) result += "-rpc";
344 +
  if (possibleOptions.tanstackQuery) result += "-tanstackquery";
345 +
  if (possibleOptions.reactRouter) result += "-reactrouter";
277 346
278 -
	return extension ? `${result}.${extension}` : result;
347 +
  return extension ? `${result}.${extension}` : result;
279 348
};
280 349
281 350
// Check if template files exist for all combinations
282 351
async function checkTemplateFiles() {
283 -
	console.log("🔍 Analyzing template patterns in installers...\n");
352 +
  const templateCalls = await parseInstallerFiles();
284 353
285 -
	const templateCalls = await parseInstallerFiles();
354 +
  if (templateCalls.length === 0) {
355 +
    return;
356 +
  }
286 357
287 -
	if (templateCalls.length === 0) {
288 -
		console.log("❌ No template patterns found in installer files!");
289 -
		return;
290 -
	}
358 +
  for (const call of templateCalls) {
359 +
    const allCombinations = generateAllCombinations(call.usedOptions);
360 +
    const templateDir = path.join(
361 +
      path.resolve("src/templates/extras"),
362 +
      call.templatePath.replace(/["']/g, ""),
363 +
      call.basename,
364 +
    );
291 365
292 -
	const extrasDir = path.resolve("src/templates/extras");
366 +
    for (const combination of allCombinations) {
367 +
      const templateName =
368 +
        call.type === "nameGenerator"
369 +
          ? nameGenerator(call.basename, combination)
370 +
          : hardcodedGenerator(call.basename, combination);
371 +
      const fullTemplatePath = path.join(templateDir, templateName);
293 372
294 -
	for (const call of templateCalls) {
295 -
		console.log(`📁 Analyzing: ${call.file} (${call.type})`);
296 -
		console.log(`   Basename: ${call.basename}`);
297 -
		console.log(`   Used Options: [${call.usedOptions.join(", ")}]`);
298 -
		console.log(`   Template Path: ${call.templatePath}`);
299 -
		console.log("");
300 -
301 -
		// Generate all possible combinations for the used options
302 -
		const allCombinations = generateAllCombinations(call.usedOptions);
303 -
		const templateDir = path.join(
304 -
			extrasDir,
305 -
			call.templatePath.replace(/["']/g, ""),
306 -
			call.basename,
307 -
		);
308 -
309 -
		console.log(
310 -
			`   📋 All possible template files for ${call.basename} (${call.type}):`,
311 -
		);
312 -
313 -
		let foundCount = 0;
314 -
		let missingCount = 0;
315 -
316 -
		for (const combination of allCombinations) {
317 -
			const templateName =
318 -
				call.type === "nameGenerator"
319 -
					? nameGenerator(call.basename, combination)
320 -
					: hardcodedGenerator(call.basename, combination);
321 -
			const fullTemplatePath = path.join(templateDir, templateName);
322 -
323 -
			const exists = await fs.pathExists(fullTemplatePath);
324 -
			const status = exists ? "✅" : "❌";
325 -
326 -
			if (exists) {
327 -
				foundCount++;
328 -
			} else {
329 -
				missingCount++;
330 -
			}
331 -
332 -
			// Show combination details
333 -
			const enabledOptions = Object.keys(combination)
334 -
				.filter((key) => combination[key])
335 -
				.join(", ");
336 -
337 -
			console.log(
338 -
				`     ${status} ${templateName} ${enabledOptions ? `(${enabledOptions})` : "(no options)"}`,
339 -
			);
340 -
341 -
			if (!exists) {
342 -
				console.log(`        Missing: ${fullTemplatePath}`);
343 -
			}
344 -
		}
345 -
346 -
		console.log("");
347 -
		console.log(`   📊 Summary: ${foundCount} found, ${missingCount} missing`);
348 -
		console.log("   " + "=".repeat(50));
349 -
		console.log("");
350 -
	}
351 -
352 -
	// Overall statistics
353 -
	console.log("🎯 Overall Analysis Complete!");
354 -
	console.log(
355 -
		`   Found ${templateCalls.length} template patterns in installers`,
356 -
	);
357 -
	console.log(
358 -
		`   - ${templateCalls.filter((c) => c.type === "nameGenerator").length} nameGenerator calls`,
359 -
	);
360 -
	console.log(
361 -
		`   - ${templateCalls.filter((c) => c.type === "hardcoded").length} hardcoded template patterns`,
362 -
	);
363 -
364 -
	// Consistency analysis
365 -
	console.log("");
366 -
	console.log("!  CONSISTENCY ISSUES DETECTED:");
367 -
	console.log(
368 -
		"   The RPC installer uses hardcoded template names with order: tailwind-shadcn-rpc",
369 -
	);
370 -
	console.log(
371 -
		"   The TanStack Query installer uses nameGenerator with alphabetical order: rpc-shadcn-tailwind-tanstackquery",
372 -
	);
373 -
	console.log("   But the actual template files follow the hardcoded pattern!");
374 -
	console.log("");
375 -
	console.log("💡 RECOMMENDATIONS:");
376 -
	console.log(
377 -
		"   1. Standardize on nameGenerator for consistency across all installers",
378 -
	);
379 -
	console.log(
380 -
		"   2. OR rename template files to match nameGenerator's alphabetical sorting",
381 -
	);
382 -
	console.log(
383 -
		"   3. OR update nameGenerator to use the same order as hardcoded pattern",
384 -
	);
385 -
	console.log("");
386 -
	console.log("🔍 MISSING TEMPLATE FILES:");
387 -
	const totalMissing = templateCalls.reduce((acc, call) => {
388 -
		const allCombinations = generateAllCombinations(call.usedOptions);
389 -
		return (
390 -
			acc +
391 -
			allCombinations.filter((combo) => {
392 -
				const templateName =
393 -
					call.type === "nameGenerator"
394 -
						? nameGenerator(call.basename, combo)
395 -
						: hardcodedGenerator(call.basename, combo);
396 -
				const templateDir = path.join(
397 -
					path.resolve("src/templates/extras"),
398 -
					call.templatePath.replace(/["']/g, ""),
399 -
					call.basename,
400 -
				);
401 -
				const fullTemplatePath = path.join(templateDir, templateName);
402 -
				return !require("fs-extra").pathExistsSync(fullTemplatePath);
403 -
			}).length
404 -
		);
405 -
	}, 0);
406 -
	console.log(`   Total missing template files: ${totalMissing}`);
407 -
408 -
	console.log("");
409 -
	console.log("🛠  COMMANDS TO CREATE MISSING FILES:");
410 -
411 -
	for (const call of templateCalls) {
412 -
		const allCombinations = generateAllCombinations(call.usedOptions);
413 -
		const templateDir = path.join(
414 -
			path.resolve("src/templates/extras"),
415 -
			call.templatePath.replace(/["']/g, ""),
416 -
			call.basename,
417 -
		);
418 -
419 -
		const missingFiles: string[] = [];
420 -
421 -
		for (const combination of allCombinations) {
422 -
			const templateName =
423 -
				call.type === "nameGenerator"
424 -
					? nameGenerator(call.basename, combination)
425 -
					: hardcodedGenerator(call.basename, combination);
426 -
			const fullTemplatePath = path.join(templateDir, templateName);
427 -
428 -
			const exists = require("fs-extra").pathExistsSync(fullTemplatePath);
429 -
			if (!exists) {
430 -
				missingFiles.push(`touch "${fullTemplatePath}"`);
431 -
			}
432 -
		}
433 -
434 -
		if (missingFiles.length > 0) {
435 -
			console.log(`   # Missing files for ${call.basename} (${call.type}):`);
436 -
			for (const cmd of missingFiles) {
437 -
				console.log(`   ${cmd}`);
438 -
			}
439 -
			console.log("");
440 -
		}
441 -
	}
373 +
      const exists = await fs.pathExists(fullTemplatePath);
374 +
      if (!exists) {
375 +
        console.log(`touch "${fullTemplatePath}"`);
376 +
      }
377 +
    }
378 +
  }
442 379
}
443 380
444 381
// Run the analysis
src/installers/react-router.ts +58 −0
1 +
import path from "node:path";
2 +
import fs from "fs-extra";
3 +
import type { ProjectOptions } from "@/types";
4 +
import yoctoSpinner from "yocto-spinner";
5 +
import pc from "picocolors";
6 +
import { consola } from "consola";
7 +
import { addPackageDependency } from "@/utils/add-package-dependency";
8 +
import { EXTRAS_DIR } from "@/utils";
9 +
import { nameGenerator } from "@/utils/name-generator";
10 +
11 +
export const reactRouterInstaller = async (
12 +
	options: Required<ProjectOptions>,
13 +
): Promise<boolean> => {
14 +
	const spinner = yoctoSpinner({
15 +
		text: "Setting up React Router...",
16 +
	}).start();
17 +
18 +
	try {
19 +
		const { projectName, rpc, shadcn, tailwind, tanstackQuery } = options;
20 +
21 +
		const projectPath = path.resolve(process.cwd(), projectName);
22 +
		spinner.text = "Installing React Router...";
23 +
		await addPackageDependency({
24 +
			dependencies: ["react-router"],
25 +
			target: "client",
26 +
			projectName,
27 +
		});
28 +
29 +
		const selectedTemplate = nameGenerator("App.tsx", {
30 +
			rpc,
31 +
			shadcn,
32 +
			tailwind,
33 +
			tanstackQuery,
34 +
			reactRouter: true,
35 +
		});
36 +
37 +
		const appTsxSrc = path.join(
38 +
			EXTRAS_DIR,
39 +
			"client",
40 +
			"src",
41 +
			"App.tsx",
42 +
			selectedTemplate,
43 +
		);
44 +
		const appTsxTarget = path.join(projectPath, "client", "src", "App.tsx");
45 +
		fs.copySync(appTsxSrc, appTsxTarget);
46 +
47 +
		spinner.success("React Router setup completed");
48 +
		return true;
49 +
	} catch (err: unknown) {
50 +
		spinner.error("Failed to set up React Router");
51 +
		if (err instanceof Error) {
52 +
			consola.error(pc.red("Error:"), err.message);
53 +
		} else {
54 +
			consola.error(pc.red("Error: Unknown error"));
55 +
		}
56 +
		return false;
57 +
	}
58 +
};
src/installers/tanstack-router.ts (added) +58 −0
1 +
import path from "node:path";
2 +
import fs from "fs-extra";
3 +
import type { ProjectOptions } from "@/types";
4 +
import yoctoSpinner from "yocto-spinner";
5 +
import pc from "picocolors";
6 +
import { consola } from "consola";
7 +
import { addPackageDependency } from "@/utils/add-package-dependency";
8 +
import { EXTRAS_DIR } from "@/utils";
9 +
import { nameGenerator } from "@/utils/name-generator";
10 +
11 +
export const tanstackRouterInstaller = async (
12 +
	options: Required<ProjectOptions>,
13 +
): Promise<boolean> => {
14 +
	const spinner = yoctoSpinner({
15 +
		text: "Setting up TanStack Router...",
16 +
	}).start();
17 +
18 +
	try {
19 +
		const { projectName, rpc, shadcn, tailwind, tanstackQuery } = options;
20 +
21 +
		const projectPath = path.resolve(process.cwd(), projectName);
22 +
		spinner.text = "Installing TanStack Router...";
23 +
		await addPackageDependency({
24 +
			dependencies: [""],
25 +
			target: "client",
26 +
			projectName,
27 +
		});
28 +
29 +
		// const selectedTemplate = nameGenerator("App.tsx", {
30 +
		// 	rpc,
31 +
		// 	shadcn,
32 +
		// 	tailwind,
33 +
		// 	tanstackQuery,
34 +
		// 	reactRouter: true,
35 +
		// });
36 +
37 +
		// const appTsxSrc = path.join(
38 +
		// 	EXTRAS_DIR,
39 +
		// 	"client",
40 +
		// 	"src",
41 +
		// 	"App.tsx",
42 +
		// 	selectedTemplate,
43 +
		// );
44 +
		// const appTsxTarget = path.join(projectPath, "client", "src", "App.tsx");
45 +
		// fs.copySync(appTsxSrc, appTsxTarget);
46 +
		//
47 +
		spinner.success("TanStack Router setup completed");
48 +
		return true;
49 +
	} catch (err: unknown) {
50 +
		spinner.error("Failed to set up TanStack Router");
51 +
		if (err instanceof Error) {
52 +
			consola.error(pc.red("Error:"), err.message);
53 +
		} else {
54 +
			consola.error(pc.red("Error: Unknown error"));
55 +
		}
56 +
		return false;
57 +
	}
58 +
};
src/lib/install-packages.ts +3 −1
22 22
	if (router !== "none") {
23 23
		switch (router) {
24 24
			case "reactrouter": {
25 +
				console.log("Instlling React Router");
25 26
				break;
26 27
			}
27 -
			case: "tanstackrouter": {
28 +
			case "tanstackrouter": {
29 +
				console.log("Instlling TanStack Router");
28 30
				break;
29 31
			}
30 32
		}