"));
-
} else if (auth === "lucia") {
- console.log(chalk.yellow("Lucia Auth requires a database adapter. Installing core package..."));
- run(`${packageManager} install lucia`, projectPath, false);
- console.log(chalk.yellow("Please follow the docs to set up your specific adapter: https://lucia-auth.com/getting-started/"));
+ console.log(
+ chalk.yellow("Lucia Auth requires a database adapter. Installing core package..."),
+ );
+ const luciaInstallResult = await run(
+ `${packageManager} install lucia`,
+ projectPath,
+ false,
+ 3,
+ 2000,
+ );
+ if (!luciaInstallResult.success) {
+ console.error(chalk.bold.red("Failed to install Lucia."));
+ if (luciaInstallResult.stderr) {
+ console.error(chalk.red("Error details:"));
+ console.error(chalk.red(luciaInstallResult.stderr));
+ }
+ throw new Error("Lucia installation failed.");
+ }
+ console.log(
+ chalk.yellow(
+ "Please follow the docs to set up your specific adapter: https://lucia-auth.com/getting-started/",
+ ),
+ );
}
console.log(chalk.bold.green("Auth dependencies installed"));
}
@@ -930,7 +1175,7 @@ export const config = {
console.log(chalk.bold.hex("#23f0bcff")("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"));
console.log();
console.log(chalk.bold.white("-> Next steps:"));
- if (projectName !== '.') {
+ if (projectName !== ".") {
console.log(chalk.cyan(` cd ${chalk.bold.white(projectName)}`));
}
console.log(chalk.cyan(` ${packageManager} ${chalk.bold.white(`run dev`)}`));
@@ -941,11 +1186,20 @@ export const config = {
process.exit(0);
})().catch((error) => {
console.error(chalk.bold.red(`\nAn unexpected error occurred: ${error.message}`));
- if (projectPath && fs.existsSync(projectPath) && createdProjectDir && projectPath !== process.cwd()) {
+ if (
+ projectPath &&
+ fs.existsSync(projectPath) &&
+ createdProjectDir &&
+ projectPath !== process.cwd()
+ ) {
console.error(chalk.yellow(`Cleaning up incomplete project directory: ${projectPath}`));
deleteFolder(projectPath);
} else {
- console.error(chalk.yellow(`Cleanup skipped or not needed (project directory not created by this process or is CWD).`));
+ console.error(
+ chalk.yellow(
+ `Cleanup skipped or not needed (project directory not created by this process or is CWD).`,
+ ),
+ );
}
process.exit(1);
});
diff --git a/src/lib/colors.js b/src/lib/colors.js
index 9335391..9070dce 100644
--- a/src/lib/colors.js
+++ b/src/lib/colors.js
@@ -58,7 +58,7 @@ const createBuilder = (currentStyles = []) => {
// Basic hex-to-ANSI approximation
builder.hex = (hex) => {
// Remove hash if present
- const cleanHex = hex.replace(/^#/, '');
+ const cleanHex = hex.replace(/^#/, "");
// Parse r, g, b
let r, g, b;
@@ -80,9 +80,8 @@ const createBuilder = (currentStyles = []) => {
const b6 = Math.round((b / 255) * 5);
const ansiCode = 16 + 36 * r6 + 6 * g6 + b6;
- const open = `\x1b[38;5;${ansiCode}m`;
-
- return createBuilder([...currentStyles, `\x1b[38;5;${ansiCode}m`]);
+ const _open = `\x1b[38;5;${ansiCode}m`;
+ return createBuilder([...currentStyles, _open]);
};
return builder;
diff --git a/src/lib/prompts.js b/src/lib/prompts.js
index 3251a08..97df66c 100644
--- a/src/lib/prompts.js
+++ b/src/lib/prompts.js
@@ -1,126 +1,129 @@
-import readline from 'readline';
-import chalk from './colors.js';
+import readline from "node:readline";
+import chalk from "./colors.js";
let rl;
const getRl = () => {
- if (!rl) {
- rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout
- });
- }
- return rl;
+ if (!rl) {
+ rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+ }
+ return rl;
};
const ask = (query) => {
- return new Promise((resolve) => getRl().question(query, resolve));
+ return new Promise((resolve) => getRl().question(query, resolve));
};
const processQuestion = async (question) => {
- let answer;
- const { type, name, message, default: defaultVal, choices, validate, filter } = question;
-
- const displayMessage = `${message}${defaultVal !== undefined ? ` (default: ${type === 'confirm' ? (defaultVal ? 'Yes' : 'No') : defaultVal})` : ''} `;
-
- const confirmHint = defaultVal ? '(Y/n)' : '(y/N)';
- if (type === 'confirm') {
- while (true) {
- const input = await ask(`${chalk.green('?')} ${chalk.bold(message)} ${chalk.dim(confirmHint)} `);
- const trimmed = input.trim().toLowerCase();
- if (trimmed === '') {
- answer = defaultVal !== undefined ? defaultVal : false;
- break;
- }
- if (trimmed === 'y' || trimmed === 'yes') {
- answer = true;
- break;
- }
- if (trimmed === 'n' || trimmed === 'no') {
- answer = false;
- break;
- }
- }
- } else if (type === 'list') {
- console.log(`${chalk.green('?')} ${chalk.bold(message)}`);
- choices.forEach((choice, index) => {
- const isDefault = choice === defaultVal;
- console.log(` ${index + 1}) ${choice} ${isDefault ? chalk.dim('(default)') : ''}`);
- });
-
- while (true) {
- const input = await ask(` ${chalk.dim('Answer:')} `);
- const trimmed = input.trim();
-
- if (trimmed === '' && defaultVal !== undefined) {
- // defaultVal might be the value string, finding its index
- const defaultIndex = choices.indexOf(defaultVal);
- if (defaultIndex !== -1) {
- answer = defaultVal;
- break;
- }
- }
-
- const num = parseInt(trimmed, 10);
- if (!isNaN(num) && num > 0 && num <= choices.length) {
- answer = choices[num - 1];
- break;
- }
-
- // Try to match by string content
- const matchedChoice = choices.find(c => c.toLowerCase() === trimmed.toLowerCase());
- if (matchedChoice) {
- answer = matchedChoice;
- break;
- }
-
- console.log(chalk.red(` Please enter a number between 1 and ${choices.length} or the option name`));
- }
-
- } else {
- // type === 'input' or default
- while (true) {
- const input = await ask(`${chalk.green('?')} ${chalk.bold(displayMessage)}`);
- let val = input.trim();
-
- if (val === '' && defaultVal !== undefined) {
- val = defaultVal;
- }
-
- if (filter) {
- val = filter(val);
- }
-
- const validated = validate ? validate(val) : true;
- if (validated === true) {
- answer = val;
- break;
- } else {
- console.log(typeof validated === 'string' ? validated : chalk.red('Invalid input'));
- }
+ let answer;
+ const { type, name, message, default: defaultVal, choices, validate, filter } = question;
+
+ const displayMessage = `${message}${defaultVal !== undefined ? ` (default: ${type === "confirm" ? (defaultVal ? "Yes" : "No") : defaultVal})` : ""} `;
+
+ const confirmHint = defaultVal ? "(Y/n)" : "(y/N)";
+ if (type === "confirm") {
+ while (true) {
+ const input = await ask(
+ `${chalk.green("?")} ${chalk.bold(message)} ${chalk.dim(confirmHint)} `,
+ );
+ const trimmed = input.trim().toLowerCase();
+ if (trimmed === "") {
+ answer = defaultVal !== undefined ? defaultVal : false;
+ break;
+ }
+ if (trimmed === "y" || trimmed === "yes") {
+ answer = true;
+ break;
+ }
+ if (trimmed === "n" || trimmed === "no") {
+ answer = false;
+ break;
+ }
+ }
+ } else if (type === "list") {
+ console.log(`${chalk.green("?")} ${chalk.bold(message)}`);
+ choices.forEach((choice, index) => {
+ const isDefault = choice === defaultVal;
+ console.log(` ${index + 1}) ${choice} ${isDefault ? chalk.dim("(default)") : ""}`);
+ });
+
+ while (true) {
+ const input = await ask(` ${chalk.dim("Answer:")} `);
+ const trimmed = input.trim();
+
+ if (trimmed === "" && defaultVal !== undefined) {
+ // defaultVal might be the value string, finding its index
+ const defaultIndex = choices.indexOf(defaultVal);
+ if (defaultIndex !== -1) {
+ answer = defaultVal;
+ break;
}
+ }
+
+ const num = parseInt(trimmed, 10);
+ if (!Number.isNaN(num) && num > 0 && num <= choices.length) {
+ answer = choices[num - 1];
+ break;
+ }
+
+ // Try to match by string content
+ const matchedChoice = choices.find((c) => c.toLowerCase() === trimmed.toLowerCase());
+ if (matchedChoice) {
+ answer = matchedChoice;
+ break;
+ }
+
+ console.log(
+ chalk.red(` Please enter a number between 1 and ${choices.length} or the option name`),
+ );
+ }
+ } else {
+ // type === 'input' or default
+ while (true) {
+ const input = await ask(`${chalk.green("?")} ${chalk.bold(displayMessage)}`);
+ let val = input.trim();
+
+ if (val === "" && defaultVal !== undefined) {
+ val = defaultVal;
+ }
+
+ if (filter) {
+ val = filter(val);
+ }
+
+ const validated = validate ? validate(val) : true;
+ if (validated === true) {
+ answer = val;
+ break;
+ } else {
+ console.log(typeof validated === "string" ? validated : chalk.red("Invalid input"));
+ }
}
+ }
- return { [name]: answer };
+ return { [name]: answer };
};
const prompt = async (questions) => {
- const answers = {};
- for (const question of questions) {
- const answer = await processQuestion(question);
- Object.assign(answers, answer);
- }
- return answers;
+ const answers = {};
+ for (const question of questions) {
+ const answer = await processQuestion(question);
+ Object.assign(answers, answer);
+ }
+ return answers;
};
const close = () => {
- if (rl) {
- rl.close();
- rl = null;
- }
+ if (rl) {
+ rl.close();
+ rl = null;
+ }
};
export default {
- prompt,
- close
+ prompt,
+ close,
};
diff --git a/src/lib/templates.js b/src/lib/templates.js
index ac3180b..61706ef 100644
--- a/src/lib/templates.js
+++ b/src/lib/templates.js
@@ -1,13 +1,12 @@
-import { writeFile, createFolder } from './utils.js';
-import path from 'path';
+import { writeFile, createFolder } from "./utils.js";
+import path from "node:path";
export const createPages = (pagesPath, pages, useTypeScript, useAppDir) => {
- const extension = useTypeScript ? '.tsx' : '.js';
+ const extension = useTypeScript ? ".tsx" : ".js";
pages.forEach((page) => {
const pageName = page.charAt(0).toUpperCase() + page.slice(1);
- const pageContent =
- `export default function ${pageName}() {
+ const pageContent = `export default function ${pageName}() {
return (
${pageName}
@@ -17,7 +16,7 @@ export const createPages = (pagesPath, pages, useTypeScript, useAppDir) => {
let pagePath;
if (useAppDir) {
- if (page.toLowerCase() === 'index' || page.toLowerCase() === 'home') {
+ if (page.toLowerCase() === "index" || page.toLowerCase() === "home") {
pagePath = path.join(pagesPath, `page${extension}`);
} else {
const pageDir = path.join(pagesPath, page.toLowerCase());
@@ -25,7 +24,7 @@ export const createPages = (pagesPath, pages, useTypeScript, useAppDir) => {
pagePath = path.join(pageDir, `page${extension}`);
}
} else {
- if (page.toLowerCase() === 'index' || page.toLowerCase() === 'home') {
+ if (page.toLowerCase() === "index" || page.toLowerCase() === "home") {
pagePath = path.join(pagesPath, `index${extension}`);
} else {
pagePath = path.join(pagesPath, `${page.toLowerCase()}${extension}`);
@@ -88,5 +87,4 @@ export default function RootLayout({ children }) {
createFolder(appDir);
writeFile(layoutPath, layoutContent);
-
-};
\ No newline at end of file
+};
diff --git a/src/lib/utils.js b/src/lib/utils.js
index 1363404..2788650 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -1,58 +1,72 @@
-import { execSync } from "child_process";
-import fs from "fs";
-import path from "path";
+import { execSync } from "node:child_process";
+import fs from "node:fs";
+import path from "node:path";
import chalk from "./colors.js";
-const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
-
-export const run = (cmd, cwd = process.cwd(), silent = false, retries = 0, retryDelay = 1000) => {
- for (let i = 0; i <= retries; i++) {
- try {
- if (!silent) {
- console.log(`\nRunning: ${cmd}`);
- }
- const output = execSync(cmd, { stdio: silent ? "pipe" : "inherit", cwd });
- return { success: true, stdout: output ? output.toString() : "" };
- } catch (error) {
- if (i < retries) {
- console.log(chalk.yellow(`Command failed. Retrying in ${retryDelay / 1000}s... (Attempt ${i + 1}/${retries})`));
- sleep(retryDelay);
- } else {
- return { success: false, error: error.message, stdout: error.stdout ? error.stdout.toString() : "", stderr: error.stderr ? error.stderr.toString() : "" };
- }
- }
+const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+
+export const run = async (
+ cmd,
+ cwd = process.cwd(),
+ silent = false,
+ retries = 0,
+ retryDelay = 1000,
+) => {
+ for (let i = 0; i <= retries; i++) {
+ try {
+ if (!silent) {
+ console.log(`\nRunning: ${cmd}`);
+ }
+ const output = execSync(cmd, { stdio: silent ? "pipe" : "inherit", cwd });
+ return { success: true, stdout: output ? output.toString() : "" };
+ } catch (error) {
+ if (i < retries) {
+ console.log(
+ chalk.yellow(
+ `Command failed. Retrying in ${retryDelay / 1000}s... (Attempt ${i + 1}/${retries})`,
+ ),
+ );
+ await sleep(retryDelay);
+ } else {
+ return {
+ success: false,
+ error: error.message,
+ stdout: error.stdout ? error.stdout.toString() : "",
+ stderr: error.stderr ? error.stderr.toString() : "",
+ };
+ }
}
+ }
};
-
export const writeFile = (filePath, content) => {
- const dir = path.dirname(filePath);
- if (!fs.existsSync(dir)) {
- fs.mkdirSync(dir, { recursive: true });
- }
- fs.writeFileSync(filePath, content);
+ const dir = path.dirname(filePath);
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ fs.writeFileSync(filePath, content);
};
export const readFile = (filePath) => {
- return fs.readFileSync(filePath, "utf-8");
+ return fs.readFileSync(filePath, "utf-8");
};
export const fileExists = (filePath) => {
- return fs.existsSync(filePath);
+ return fs.existsSync(filePath);
};
export const createFolder = (folderPath) => {
- fs.mkdirSync(folderPath, { recursive: true });
+ fs.mkdirSync(folderPath, { recursive: true });
};
export const deleteFile = (filePath) => {
- if (fs.existsSync(filePath)) {
- fs.unlinkSync(filePath);
- }
+ if (fs.existsSync(filePath)) {
+ fs.unlinkSync(filePath);
+ }
};
export const deleteFolder = (folderPath) => {
- if (fs.existsSync(folderPath)) {
- fs.rmSync(folderPath, { recursive: true, force: true });
- }
-};
\ No newline at end of file
+ if (fs.existsSync(folderPath)) {
+ fs.rmSync(folderPath, { recursive: true, force: true });
+ }
+};
diff --git a/src/test/generate-tests.js b/src/test/generate-tests.js
index b2b946b..4a824b8 100644
--- a/src/test/generate-tests.js
+++ b/src/test/generate-tests.js
@@ -1,21 +1,20 @@
-import fs from 'fs';
-import path from 'path';
+import fs from "node:fs";
+import path from "node:path";
-const indexJsPath = path.join(process.cwd(), 'src', 'index.js');
-const indexJsContent = fs.readFileSync(indexJsPath, 'utf-8');
+const indexJsPath = path.join(process.cwd(), "src", "index.js");
+const indexJsContent = fs.readFileSync(indexJsPath, "utf-8");
const extractedPrompts = [];
const inquirerPromptRegex = /inquirer\.prompt\(\[([\s\S]*?)\]\)/g;
-let promptArrayMatch;
-
-while ((promptArrayMatch = inquirerPromptRegex.exec(indexJsContent)) !== null) {
+let promptArrayMatch = inquirerPromptRegex.exec(indexJsContent);
+while (promptArrayMatch !== null) {
const promptArrayString = promptArrayMatch[1];
- const promptObjectRegex = /{\s*type:\s*"(.*?)",\s*name:\s*"(.*?)",\s*message:\s*"(.*?)"(?:,\s*choices:\s*\[([\s\S]*?)\])?(?:,\s*default:\s*(.*?))?[\s\S]*?}/g;
- let promptMatch;
-
- while ((promptMatch = promptObjectRegex.exec(promptArrayString)) !== null) {
+ const promptObjectRegex =
+ /{\s*type:\s*"(.*?)",\s*name:\s*"(.*?)",\s*message:\s*"(.*?)"(?:,\s*choices:\s*\[([\s\S]*?)\])?(?:,\s*default:\s*(.*?))?[\s\S]*?}/g;
+ let promptMatch = promptObjectRegex.exec(promptArrayString);
+ while (promptMatch !== null) {
const prompt = {
type: promptMatch[1],
name: promptMatch[2],
@@ -25,43 +24,73 @@ while ((promptArrayMatch = inquirerPromptRegex.exec(indexJsContent)) !== null) {
};
if (promptMatch[4]) {
- prompt.choices = promptMatch[4].split(',').map(c => c.trim().replace(/['"]/g, '')).filter(c => c !== '');
+ prompt.choices = promptMatch[4]
+ .split(",")
+ .map((c) => c.trim().replace(/['"]/g, ""))
+ .filter((c) => c !== "");
}
if (promptMatch[5]) {
const defaultValue = promptMatch[5].trim();
- if (defaultValue === 'true') {
+ if (defaultValue === "true") {
prompt.default = true;
- } else if (defaultValue === 'false') {
+ } else if (defaultValue === "false") {
prompt.default = false;
- } else if (defaultValue === '' || defaultValue === '""') {
- prompt.default = '';
- } else if (defaultValue !== '' && !isNaN(defaultValue)) {
- prompt.default = Number(defaultValue);
+ } else if (defaultValue === "" || defaultValue === '""') {
+ prompt.default = "";
} else {
- prompt.default = defaultValue.replace(/['"]/g, '');
+ const parsed = Number(defaultValue);
+ if (defaultValue !== "" && !Number.isNaN(parsed)) {
+ prompt.default = parsed;
+ } else {
+ prompt.default = defaultValue.replace(/['"]/g, "");
+ }
}
}
extractedPrompts.push(prompt);
+ promptMatch = promptObjectRegex.exec(promptArrayString);
}
+ promptArrayMatch = inquirerPromptRegex.exec(indexJsContent);
+}
+
+// Validation: Count expected prompts in src/index.js and warn if mismatch
+const expectedPromptCount = (indexJsContent.match(/type:\s*["'](confirm|list|input)["']/g) || [])
+ .length;
+if (extractedPrompts.length < expectedPromptCount) {
+ console.warn(
+ `ā ļø Warning: Extracted ${extractedPrompts.length} prompts but found ${expectedPromptCount} prompt definitions in src/index.js.`,
+ );
+ console.warn(
+ ` This may indicate that the promptObjectRegex is missing some prompts due to property reordering.`,
+ );
+ console.warn(
+ ` Consider updating the regex to be order-agnostic or reviewing the prompt definitions in src/index.js.`,
+ );
}
const testCases = [];
+// Helper function to resolve prompt default values
+const getPromptDefault = (prompt) => {
+ if (prompt.default !== undefined) {
+ return prompt.default;
+ }
+ return prompt.type === "confirm" ? false : "";
+};
+
for (const prompt of extractedPrompts) {
- if (prompt.name === 'projectName') continue;
+ if (prompt.name === "projectName") continue;
- if (prompt.type === 'confirm') {
+ if (prompt.type === "confirm") {
const enableTestCase = {
description: `should create a project with ${prompt.name} enabled`,
options: {},
expectedFiles: [],
- assertions: "assert.ok(fs.existsSync(projectPath), 'Project directory should exist');",
};
- extractedPrompts.forEach(p => {
- if (p.name !== 'projectName') {
- enableTestCase.options[p.name] = p.default !== undefined ? p.default : (p.type === 'confirm' ? false : '');
+ extractedPrompts.forEach((p) => {
+ if (p.name !== "projectName") {
+ enableTestCase.options[p.name] = getPromptDefault(p);
}
});
enableTestCase.options[prompt.name] = true;
@@ -72,62 +101,64 @@ for (const prompt of extractedPrompts) {
description: `should create a project with ${prompt.name} disabled`,
options: {},
expectedFiles: [],
- assertions: "assert.ok(fs.existsSync(projectPath), 'Project directory should exist');",
};
- extractedPrompts.forEach(p => {
- if (p.name !== 'projectName') {
- disableTestCase.options[p.name] = p.default !== undefined ? p.default : (p.type === 'confirm' ? false : '');
+ extractedPrompts.forEach((p) => {
+ if (p.name !== "projectName") {
+ disableTestCase.options[p.name] = getPromptDefault(p);
}
});
disableTestCase.options[prompt.name] = false;
testCases.push(disableTestCase);
}
- } else if (prompt.type === 'list') {
- prompt.choices.forEach(choice => {
+ } else if (prompt.type === "list") {
+ prompt.choices.forEach((choice) => {
const listTestCase = {
description: `should create a project with ${prompt.name} set to ${choice}`,
options: {},
expectedFiles: [],
- assertions: "assert.ok(fs.existsSync(projectPath), 'Project directory should exist');",
};
- extractedPrompts.forEach(p => {
- if (p.name !== 'projectName') {
- listTestCase.options[p.name] = p.default !== undefined ? p.default : (p.type === 'confirm' ? false : '');
+ extractedPrompts.forEach((p) => {
+ if (p.name !== "projectName") {
+ listTestCase.options[p.name] = getPromptDefault(p);
}
});
listTestCase.options[prompt.name] = choice;
testCases.push(listTestCase);
});
- } else if (prompt.type === 'input') {
+ } else if (prompt.type === "input") {
const inputTestCase = {
description: `should create a project with custom ${prompt.name}`,
options: {},
expectedFiles: [],
- assertions: "assert.ok(fs.existsSync(projectPath), 'Project directory should exist');",
};
- extractedPrompts.forEach(p => {
- if (p.name !== 'projectName') {
- inputTestCase.options[p.name] = p.default !== undefined ? p.default : (p.type === 'confirm' ? false : '');
+ extractedPrompts.forEach((p) => {
+ if (p.name !== "projectName") {
+ inputTestCase.options[p.name] = getPromptDefault(p);
}
});
- inputTestCase.options[prompt.name] = prompt.default || (prompt.name === 'pages' ? 'testpage' : 'custom');
+ inputTestCase.options[prompt.name] =
+ prompt.default || (prompt.name === "pages" ? "testpage" : "custom");
testCases.push(inputTestCase);
}
}
-const generatedTestCasesPath = path.join(process.cwd(), 'src', 'test', 'generated-test-cases.js');
+const generatedTestCasesPath = path.join(process.cwd(), "src", "test", "generated-test-cases.js");
const fileContent = `// This file is auto-generated by test/generate-tests.js
// Do not edit this file directly. Run 'npm run test:generate' to regenerate.
-import { strict as assert } from 'assert';
-import fs from 'fs';
-import path from 'path';
+import { strict as assert } from 'node:assert';
+import fs from 'node:fs';
+
+// Default assertion function for all test cases
+const defaultAssertion = (projectPath) => {
+ assert.ok(fs.existsSync(projectPath), 'Project directory should exist');
+};
export const testCases = ${JSON.stringify(testCases, null, 2)}.map(tc => ({
...tc,
- assertions: (projectPath) => { eval(tc.assertions) }
+ assertions: defaultAssertion
}));
`;
fs.writeFileSync(generatedTestCasesPath, fileContent);
-console.log(`Generated ${testCases.length} test cases in ${generatedTestCasesPath}`);
\ No newline at end of file
+console.log(`Generated ${testCases.length} test cases in ${generatedTestCasesPath}`);
diff --git a/src/test/interactive.test.js b/src/test/interactive.test.js
index e2c7cf8..f57039f 100644
--- a/src/test/interactive.test.js
+++ b/src/test/interactive.test.js
@@ -1,30 +1,30 @@
-import { spawn } from 'child_process';
-import { strict as assert } from 'assert';
-import { fileURLToPath } from 'url';
-import path from 'path';
-import fs from 'fs';
-import { deleteFolder, createFolder, writeFile, readFile } from '../lib/utils.js';
+import { spawn } from "node:child_process";
+import { strict as assert } from "node:assert";
+import { fileURLToPath } from "node:url";
+import path from "node:path";
+import fs from "node:fs";
+import { deleteFolder, readFile } from "../lib/utils.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
-const cliPath = path.join(__dirname, '..', 'index.js');
+const cliPath = path.join(__dirname, "..", "index.js");
-describe('create-next-quick interactive mode', function () {
+describe("create-next-quick interactive mode", function () {
this.timeout(300000);
let currentProjectName;
let currentProjectPath;
- beforeEach(function (done) {
+ beforeEach((done) => {
currentProjectName = `test-interactive-project-${Date.now()}`;
currentProjectPath = path.join(process.cwd(), currentProjectName);
const command = `npx create-next-app@latest ${currentProjectName} --ts --tailwind --src-dir --app --use-npm --yes`;
- const child = spawn(command, { shell: true, stdio: 'inherit' });
+ const child = spawn(command, { shell: true, stdio: "inherit" });
- child.on('close', (code) => {
- assert.strictEqual(code, 0, 'create-next-app should exit with code 0');
+ child.on("close", (code) => {
+ assert.strictEqual(code, 0, "create-next-app should exit with code 0");
done();
});
});
@@ -37,19 +37,14 @@ describe('create-next-quick interactive mode', function () {
}
});
- it('should add a new page in interactive mode', (done) => {
- const answers = [
- 'about',
- '',
- '',
- 'n',
- ];
+ it("should add a new page in interactive mode", (done) => {
+ const answers = ["about", "", "", "n"];
- const child = spawn('node', [cliPath, '-i'], { cwd: currentProjectPath });
+ const child = spawn("node", [cliPath, "-i"], { cwd: currentProjectPath });
let i = 0;
const interval = setInterval(() => {
if (i < answers.length) {
- child.stdin.write(answers[i] + '\n');
+ child.stdin.write(`${answers[i]}\n`);
i++;
} else {
clearInterval(interval);
@@ -57,30 +52,25 @@ describe('create-next-quick interactive mode', function () {
}
}, 8000);
- child.on('close', (code) => {
- assert.strictEqual(code, 0, 'CLI should exit with code 0');
+ child.on("close", (code) => {
+ assert.strictEqual(code, 0, "CLI should exit with code 0");
- const newPagePath = path.join(currentProjectPath, 'src', 'app', 'about', 'page.tsx');
- assert.ok(fs.existsSync(newPagePath), 'New page should be created');
+ const newPagePath = path.join(currentProjectPath, "src", "app", "about", "page.tsx");
+ assert.ok(fs.existsSync(newPagePath), "New page should be created");
done();
});
});
- it('should add biome linter in interactive mode', (done) => {
- const answers = [
- '',
- '3',
- '',
- 'n',
- ];
+ it("should add biome linter in interactive mode", (done) => {
+ const answers = ["", "3", "", "n"];
- const child = spawn('node', [cliPath, '-i'], { cwd: currentProjectPath });
+ const child = spawn("node", [cliPath, "-i"], { cwd: currentProjectPath });
let i = 0;
const interval = setInterval(() => {
if (i < answers.length) {
- child.stdin.write(answers[i] + '\n');
+ child.stdin.write(`${answers[i]}\n`);
i++;
} else {
clearInterval(interval);
@@ -88,31 +78,32 @@ describe('create-next-quick interactive mode', function () {
}
}, 8000);
- child.on('close', (code) => {
- assert.strictEqual(code, 0, 'CLI should exit with code 0');
+ child.on("close", (code) => {
+ assert.strictEqual(code, 0, "CLI should exit with code 0");
- const packageJson = JSON.parse(readFile(path.join(currentProjectPath, 'package.json')));
- assert.ok(packageJson.devDependencies['@biomejs/biome'], 'Biome should be in devDependencies');
- assert.ok(fs.existsSync(path.join(currentProjectPath, 'biome.json')), 'biome.json should be created');
+ const packageJson = JSON.parse(readFile(path.join(currentProjectPath, "package.json")));
+ assert.ok(
+ packageJson.devDependencies["@biomejs/biome"],
+ "Biome should be in devDependencies",
+ );
+ assert.ok(
+ fs.existsSync(path.join(currentProjectPath, "biome.json")),
+ "biome.json should be created",
+ );
done();
});
});
- it('should add prisma orm in interactive mode', (done) => {
- const answers = [
- '',
- '',
- '2',
- 'n',
- ];
+ it("should add prisma orm in interactive mode", (done) => {
+ const answers = ["", "", "2", "n"];
- const child = spawn('node', [cliPath, '-i'], { cwd: currentProjectPath });
+ const child = spawn("node", [cliPath, "-i"], { cwd: currentProjectPath });
let i = 0;
const interval = setInterval(() => {
if (i < answers.length) {
- child.stdin.write(answers[i] + '\n');
+ child.stdin.write(`${answers[i]}\n`);
i++;
} else {
clearInterval(interval);
@@ -120,32 +111,33 @@ describe('create-next-quick interactive mode', function () {
}
}, 8000);
- child.on('close', (code) => {
- assert.strictEqual(code, 0, 'CLI should exit with code 0');
+ child.on("close", (code) => {
+ assert.strictEqual(code, 0, "CLI should exit with code 0");
- const packageJson = JSON.parse(readFile(path.join(currentProjectPath, 'package.json')));
- assert.ok(packageJson.devDependencies['prisma'], 'Prisma should be in devDependencies');
- assert.ok(packageJson.dependencies['@prisma/client'], 'Prisma client should be in dependencies');
- assert.ok(fs.existsSync(path.join(currentProjectPath, 'prisma', 'schema.prisma')), 'prisma/schema.prisma should be created');
+ const packageJson = JSON.parse(readFile(path.join(currentProjectPath, "package.json")));
+ assert.ok(packageJson.devDependencies.prisma, "Prisma should be in devDependencies");
+ assert.ok(
+ packageJson.dependencies["@prisma/client"],
+ "Prisma client should be in dependencies",
+ );
+ assert.ok(
+ fs.existsSync(path.join(currentProjectPath, "prisma", "schema.prisma")),
+ "prisma/schema.prisma should be created",
+ );
done();
});
});
- it('should add shadcn ui in interactive mode', (done) => {
- const answers = [
- '',
- '',
- '',
- 'y',
- ];
+ it("should add shadcn ui in interactive mode", (done) => {
+ const answers = ["", "", "", "y"];
- const child = spawn('node', [cliPath, '-i'], { cwd: currentProjectPath });
+ const child = spawn("node", [cliPath, "-i"], { cwd: currentProjectPath });
let i = 0;
const interval = setInterval(() => {
if (i < answers.length) {
- child.stdin.write(answers[i] + '\n');
+ child.stdin.write(`${answers[i]}\n`);
i++;
} else {
clearInterval(interval);
@@ -153,32 +145,36 @@ describe('create-next-quick interactive mode', function () {
}
}, 8000);
- child.on('close', (code) => {
- assert.strictEqual(code, 0, 'CLI should exit with code 0');
-
- const packageJson = JSON.parse(readFile(path.join(currentProjectPath, 'package.json')));
- assert.ok(packageJson.devDependencies['tailwindcss-animate'], 'tailwindcss-animate should be in devDependencies');
- assert.ok(packageJson.devDependencies['class-variance-authority'], 'class-variance-authority should be in devDependencies');
- assert.ok(fs.existsSync(path.join(currentProjectPath, 'components.json')), 'components.json should be created');
+ child.on("close", (code) => {
+ assert.strictEqual(code, 0, "CLI should exit with code 0");
+
+ const packageJson = JSON.parse(readFile(path.join(currentProjectPath, "package.json")));
+ assert.ok(
+ packageJson.devDependencies["tailwindcss-animate"],
+ "tailwindcss-animate should be in devDependencies",
+ );
+ assert.ok(
+ packageJson.devDependencies["class-variance-authority"],
+ "class-variance-authority should be in devDependencies",
+ );
+ assert.ok(
+ fs.existsSync(path.join(currentProjectPath, "components.json")),
+ "components.json should be created",
+ );
done();
});
});
- it('should skip installation if feature already exists', (done) => {
- const firstRunAnswers = [
- '',
- '3',
- '',
- 'n',
- ];
+ it("should skip installation if feature already exists", (done) => {
+ const firstRunAnswers = ["", "3", "", "n"];
- const firstRun = spawn('node', [cliPath, '-i'], { cwd: currentProjectPath });
+ const firstRun = spawn("node", [cliPath, "-i"], { cwd: currentProjectPath });
let i = 0;
const firstInterval = setInterval(() => {
if (i < firstRunAnswers.length) {
- firstRun.stdin.write(firstRunAnswers[i] + '\n');
+ firstRun.stdin.write(`${firstRunAnswers[i]}\n`);
i++;
} else {
clearInterval(firstInterval);
@@ -186,27 +182,22 @@ describe('create-next-quick interactive mode', function () {
}
}, 8000);
- firstRun.on('close', (code) => {
- assert.strictEqual(code, 0, 'First run should exit with code 0');
+ firstRun.on("close", (code) => {
+ assert.strictEqual(code, 0, "First run should exit with code 0");
- const secondRunAnswers = [
- '',
- '3',
- '',
- 'n',
- ];
+ const secondRunAnswers = ["", "3", "", "n"];
- const secondRun = spawn('node', [cliPath, '-i'], { cwd: currentProjectPath });
- let stdout = '';
+ const secondRun = spawn("node", [cliPath, "-i"], { cwd: currentProjectPath });
+ let stdout = "";
- secondRun.stdout.on('data', (data) => {
+ secondRun.stdout.on("data", (data) => {
stdout += data.toString();
});
let j = 0;
const secondInterval = setInterval(() => {
if (j < secondRunAnswers.length) {
- secondRun.stdin.write(secondRunAnswers[j] + '\n');
+ secondRun.stdin.write(`${secondRunAnswers[j]}\n`);
j++;
} else {
clearInterval(secondInterval);
@@ -214,11 +205,14 @@ describe('create-next-quick interactive mode', function () {
}
}, 8000);
- secondRun.on('close', (secondCode) => {
- assert.strictEqual(secondCode, 0, 'Second run should exit with code 0');
- assert.ok(stdout.includes('Biome is already installed. Skipping setup.'), 'Should show "already installed" message');
+ secondRun.on("close", (secondCode) => {
+ assert.strictEqual(secondCode, 0, "Second run should exit with code 0");
+ assert.ok(
+ stdout.includes("Biome is already installed. Skipping setup."),
+ 'Should show "already installed" message',
+ );
done();
});
});
});
-});
\ No newline at end of file
+});
diff --git a/src/test/test-cases.js b/src/test/test-cases.js
index b599d3a..594bec9 100644
--- a/src/test/test-cases.js
+++ b/src/test/test-cases.js
@@ -1,87 +1,101 @@
-
-import { strict as assert } from 'assert';
-import fs from 'fs';
-import path from 'path';
+import { strict as assert } from "node:assert";
+import fs from "node:fs";
+import path from "node:path";
export const testCases = [
{
- description: 'should create a new project with default options',
+ description: "should create a new project with default options",
options: {
useTypeScript: true,
useTailwind: true,
useSrcDir: true,
useAppDir: true,
- pages: '',
- linter: 'none',
- orm: 'none',
+ pages: "",
+ linter: "none",
+ orm: "none",
useShadcn: false,
- testing: 'none',
- auth: 'none',
+ testing: "none",
+ auth: "none",
docker: false,
},
assertions: (projectPath) => {
- assert.ok(fs.existsSync(path.join(projectPath, 'package.json')), 'package.json should exist');
- assert.ok(!fs.existsSync(path.join(projectPath, 'app')), 'app folder should not exist at root when using src');
- assert.ok(fs.existsSync(path.join(projectPath, 'src', 'app')), 'app folder should exist in src');
+ assert.ok(fs.existsSync(path.join(projectPath, "package.json")), "package.json should exist");
+ assert.ok(
+ !fs.existsSync(path.join(projectPath, "app")),
+ "app folder should not exist at root when using src",
+ );
+ assert.ok(
+ fs.existsSync(path.join(projectPath, "src", "app")),
+ "app folder should exist in src",
+ );
},
},
{
- description: 'should create a new project with TypeScript disabled',
+ description: "should create a new project with TypeScript disabled",
options: {
useTypeScript: false,
useTailwind: true,
useSrcDir: true,
useAppDir: true,
- pages: '',
- linter: 'none',
- orm: 'none',
+ pages: "",
+ linter: "none",
+ orm: "none",
useShadcn: false,
- testing: 'none',
- auth: 'none',
+ testing: "none",
+ auth: "none",
docker: false,
},
assertions: (projectPath) => {
- assert.ok(fs.existsSync(path.join(projectPath, 'jsconfig.json')), 'jsconfig.json should exist');
+ assert.ok(
+ fs.existsSync(path.join(projectPath, "jsconfig.json")),
+ "jsconfig.json should exist",
+ );
},
},
{
- description: 'should create a new project with a specific page',
+ description: "should create a new project with a specific page",
options: {
useTypeScript: true,
useTailwind: true,
useSrcDir: true,
useAppDir: true,
- pages: 'about',
- linter: 'none',
- orm: 'none',
+ pages: "about",
+ linter: "none",
+ orm: "none",
useShadcn: false,
- testing: 'none',
- auth: 'none',
+ testing: "none",
+ auth: "none",
docker: false,
},
assertions: (projectPath) => {
- assert.ok(fs.existsSync(path.join(projectPath, 'src', 'app', 'about', 'page.tsx')), 'about page should exist');
+ assert.ok(
+ fs.existsSync(path.join(projectPath, "src", "app", "about", "page.tsx")),
+ "about page should exist",
+ );
},
},
{
- description: 'should create a new project without src directory',
+ description: "should create a new project without src directory",
options: {
useTypeScript: true,
useTailwind: true,
useSrcDir: false,
useAppDir: true,
- pages: '',
- linter: 'none',
- orm: 'none',
+ pages: "",
+ linter: "none",
+ orm: "none",
useShadcn: false,
- testing: 'none',
- auth: 'none',
+ testing: "none",
+ auth: "none",
docker: false,
},
assertions: (projectPath) => {
- assert.ok(fs.existsSync(path.join(projectPath, 'package.json')), 'package.json should exist');
- assert.ok(fs.existsSync(path.join(projectPath, 'app')), 'app folder should exist at root');
- assert.ok(!fs.existsSync(path.join(projectPath, 'src', 'app')), 'app folder should not exist in src');
+ assert.ok(fs.existsSync(path.join(projectPath, "package.json")), "package.json should exist");
+ assert.ok(fs.existsSync(path.join(projectPath, "app")), "app folder should exist at root");
+ assert.ok(
+ !fs.existsSync(path.join(projectPath, "src", "app")),
+ "app folder should not exist in src",
+ );
},
},
];
diff --git a/src/test/test.js b/src/test/test.js
index 362c987..eead730 100644
--- a/src/test/test.js
+++ b/src/test/test.js
@@ -1,18 +1,16 @@
-import { spawn } from 'child_process';
-import { strict as assert } from 'assert';
-import { fileURLToPath } from 'url';
-import path from 'path';
-import fs from 'fs';
-import { deleteFolder } from '../lib/utils.js';
+import { spawn } from "node:child_process";
+import { strict as assert } from "node:assert";
+import { fileURLToPath } from "node:url";
+import path from "node:path";
+import fs from "node:fs";
+import { deleteFolder } from "../lib/utils.js";
+import { testCases } from "./test-cases.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
+const cliPath = path.join(__dirname, "..", "index.js");
-const cliPath = path.join(__dirname, '..', 'index.js');
-
-import { testCases } from './test-cases.js';
-
-describe('create-next-quick', function () {
+describe("create-next-quick", function () {
this.timeout(0);
let currentProjectName;
@@ -27,64 +25,129 @@ describe('create-next-quick', function () {
deleteFolder(currentProjectPath);
});
+ /**
+ * Maps a desired boolean value to the appropriate confirm prompt answer string.
+ * For confirm prompts:
+ * - If desiredValue matches defaultValue: return "" (press Enter to accept default)
+ * - If desiredValue is true and defaultValue is false: return "y"
+ * - If desiredValue is false and defaultValue is true: return "n"
+ */
+ const mapConfirmAnswer = (desiredValue, defaultValue) => {
+ if (desiredValue === defaultValue) {
+ return "";
+ }
+ return desiredValue ? "y" : "n";
+ };
+
const runTest = (answers, assertions, done) => {
deleteFolder(currentProjectPath);
- const child = spawn('node', [cliPath]);
- let stdout = '';
- let stderr = '';
+ const child = spawn("node", [cliPath]);
+ let _stdout = "";
+ let _stderr = "";
+ let interval = null;
- child.stdout.on('data', (data) => {
+ child.stdout.on("data", (data) => {
console.log(data.toString());
- stdout += data.toString();
+ _stdout += data.toString();
});
- child.stderr.on('data', (data) => {
+ child.stderr.on("data", (data) => {
console.error(data.toString());
- stderr += data.toString();
+ _stderr += data.toString();
+ });
+
+ // Error handler for child process
+ child.on("error", (err) => {
+ if (interval) clearInterval(interval);
+ console.error("Child process error:", err);
+ done(err);
+ });
+
+ // Error handler for stdin
+ child.stdin.on("error", (err) => {
+ if (interval) clearInterval(interval);
+ console.error("Child stdin error:", err);
});
let i = 0;
- const interval = setInterval(() => {
+ interval = setInterval(() => {
if (i < answers.length) {
- child.stdin.write(answers[i] + '\n');
- i++;
+ // Check if child is still alive and stdin is writable
+ if (!child.killed && child.stdin && child.stdin.writable) {
+ try {
+ child.stdin.write(`${answers[i]}\n`);
+ i++;
+ } catch (err) {
+ console.error("Error writing to stdin:", err);
+ clearInterval(interval);
+ }
+ } else {
+ clearInterval(interval);
+ }
} else {
clearInterval(interval);
- child.stdin.end();
+ child.stdin?.end();
}
}, 3000);
- child.on('close', (code) => {
- assert.strictEqual(code, 0, 'CLI should exit with code 0');
+ child.on("close", (code) => {
+ if (interval) clearInterval(interval);
+ if (code !== 0) {
+ console.log("Full stdout:", _stdout);
+ console.error("Full stderr:", _stderr);
+ }
+ assert.strictEqual(code, 0, "CLI should exit with code 0");
assertions();
done();
});
+
+ child.on("exit", () => {
+ if (interval) clearInterval(interval);
+ });
};
testCases.forEach((testCase) => {
it(testCase.description, (done) => {
const answers = [
currentProjectName,
- '', // Package manager (default variables)
- testCase.options.useTypeScript ? '' : 'n',
- testCase.options.useTailwind ? '' : 'n',
- testCase.options.useSrcDir ? '' : 'n',
- testCase.options.useAppDir ? '' : 'n',
+ "", // Package manager (default variables)
+ mapConfirmAnswer(testCase.options.useTypeScript, true),
+ mapConfirmAnswer(testCase.options.useTailwind, true),
+ mapConfirmAnswer(testCase.options.useSrcDir, true),
+ mapConfirmAnswer(testCase.options.useAppDir, true),
testCase.options.pages,
- (testCase.options.linter === 'none' ? '' : (testCase.options.linter === 'eslint' ? '\u001b[B' : '\u001b[B\u001b[B')), // Linter: none (default), eslint (1 down), biome (2 down)
- (testCase.options.orm === 'none' ? '' : (testCase.options.orm === 'prisma' ? '\u001b[B' : '\u001b[B\u001b[B')), // ORM: none (default), prisma (1 down), drizzle (2 down)
- testCase.options.useShadcn ? '' : 'n',
- testCase.options.testing === 'none' ? '' : (testCase.options.testing === 'vitest' ? '\u001b[B' : '\u001b[B\u001b[B'), // testing
- testCase.options.auth === 'none' ? '' : (testCase.options.auth === 'next-auth' ? '\u001b[B' : (testCase.options.auth === 'clerk' ? '\u001b[B\u001b[B' : '\u001b[B\u001b[B\u001b[B')), // auth
- testCase.options.docker ? 'y' : '', // docker (default false, so enter is false. but to be safe, explicit? No, confirm default false means Enter -> false.)
+ testCase.options.linter === "none"
+ ? ""
+ : testCase.options.linter === "eslint"
+ ? "\u001b[B"
+ : "\u001b[B\u001b[B", // Linter: none (default), eslint (1 down), biome (2 down)
+ testCase.options.orm === "none"
+ ? ""
+ : testCase.options.orm === "prisma"
+ ? "\u001b[B"
+ : "\u001b[B\u001b[B", // ORM: none (default), prisma (1 down), drizzle (2 down)
+ mapConfirmAnswer(testCase.options.useShadcn, true),
+ testCase.options.testing === "none"
+ ? ""
+ : testCase.options.testing === "vitest"
+ ? "\u001b[B"
+ : "\u001b[B\u001b[B", // testing
+ testCase.options.auth === "none"
+ ? ""
+ : testCase.options.auth === "next-auth"
+ ? "\u001b[B"
+ : testCase.options.auth === "clerk"
+ ? "\u001b[B\u001b[B"
+ : "\u001b[B\u001b[B\u001b[B", // auth
+ mapConfirmAnswer(testCase.options.docker, false),
];
const assertions = () => {
- assert.ok(fs.existsSync(currentProjectPath), 'Project directory should exist');
+ assert.ok(fs.existsSync(currentProjectPath), "Project directory should exist");
testCase.assertions(currentProjectPath);
};
runTest(answers, assertions, done);
});
});
-});
\ No newline at end of file
+});