create-lumen 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,33 +1,12 @@
1
1
  # create-lumen
2
2
 
3
- Scaffold a React + Vite project with your choice of **feature-based** or **component-based** architecture.
3
+ A scaffolder that generates production-ready React + Vite projects (feature-based or component-based).
4
4
 
5
- ```bash
6
- npm create lumen
7
- ```
8
-
9
- ## What it does
10
-
11
- Creates a production-ready React + Vite project with:
5
+ Note: this repository is the CLI scaffolder itself, not a generated app. Generated projects live in the target folder you create.
12
6
 
13
- - **Architecture choice** — feature-based (domain-driven) or component-based (grouped by type)
14
- - **TypeScript or JavaScript**
15
- - **CSS framework** — Tailwind CSS, Bootstrap, or vanilla CSS
16
- - **State management** — Zustand, Redux Toolkit, or none
17
- - **React Router** — client-side routing (optional)
18
- - **Testing** — Vitest or Jest (optional)
19
- - **Linter** — ESLint (default) or Oxlint
20
- - **Icon library** — Lucide React, Huge Icons, or none
21
- - **Axios** — pre-configured HTTP client (optional)
22
- - **README + LICENSE** — auto-generated (MIT)
23
- - **Git init** — auto-initialize a git repo
24
- - **Path alias** — `@/` imports configured out of the box
7
+ ## Quick usage
25
8
 
26
- Your choices are cached and used as defaults next time.
27
-
28
- ## Quick start
29
-
30
- ### From npm (recommended)
9
+ From npm (recommended):
31
10
 
32
11
  ```bash
33
12
  npm create lumen my-app
@@ -35,81 +14,145 @@ cd my-app
35
14
  npm run dev
36
15
  ```
37
16
 
38
- ### From this repo
17
+ Non-interactive Quick Setup (TypeScript + Tailwind + Feature-based + Router + ESLint + Prettier + Vitest), no prompts:
18
+
19
+ ```bash
20
+ npm create lumen my-app -y
21
+ ```
22
+
23
+ Run the scaffolder locally (development / testing the generator):
39
24
 
40
25
  ```bash
41
- git clone https://github.com/Its-JrDev/create-lumen.git
42
- cd create-lumen
26
+ node bin/cli.js # run interactively
43
27
  npm install
44
- npm link
45
- npx create-lumen my-app
46
- cd my-app
47
- npm run dev
28
+ npm link # install globally for testing
29
+ create-lumen my-app # after npm link
48
30
  ```
49
31
 
50
- ### Quick setup defaults
32
+ ## Features
33
+
34
+ - Architecture choice: feature-based or component-based
35
+ - TypeScript or JavaScript output
36
+ - CSS frameworks: Tailwind, Bootstrap, or none
37
+ - Optional state management: Zustand or Redux Toolkit
38
+ - Optional router (React Router)
39
+ - Optional testing: Vitest or Jest
40
+ - Linting: ESLint (default) or Oxlint
41
+ - Code formatter: Prettier or Oxfmt (chosen after the linter; `eslint-config-prettier` is auto-wired for the ESLint + Prettier combo)
42
+ - Optional Axios setup and icon libraries
43
+ - Auto `git init`, generated `README` (with a project description reflecting the chosen tooling) and `LICENSE`
44
+ - `.env.example` scaffolded, and the generated `.gitignore` excludes env files (`.env`, `.env.*`) while keeping `.env.example`
45
+ - `@/` path alias configured (`tsconfig`/`jsconfig` + Vite)
51
46
 
52
- TypeScript · Tailwind CSS · Feature-based · Router · ESLint · Vitest
47
+ Your selected options are cached and offered as defaults on subsequent runs.
53
48
 
54
- ## Architecture
49
+ ## Developing the scaffolder
55
50
 
56
- ### Feature-based
51
+ - Entry point: `bin/cli.js` → `src/main.js` (ESM-only)
52
+ - Templates: `templates/` (architectures + conditional overlays)
53
+ - Injector copies architecture, then overlays conditionals based on choices
54
+ - Config cache: `~/.lumen-config.json` stores previous choices
55
+ - This tool uses Node >= 18 and `execa` for subprocesses
57
56
 
58
- Organized by domain. Each feature is self-contained:
57
+ Run locally (interactive):
59
58
 
59
+ ```bash
60
+ node bin/cli.js
60
61
  ```
61
- src/
62
- ├── app/ # App root
63
- ├── features/
64
- │ └── home/
65
- │ ├── pages/
66
- │ ├── components/
67
- │ └── services/
68
- ├── layouts/
69
- ├── pages/
70
- ├── routes/
71
- ├── store/
72
- ├── hooks/
73
- └── utils/
62
+
63
+ Install globally for testing:
64
+
65
+ ```bash
66
+ npm link
67
+ create-lumen my-app
74
68
  ```
75
69
 
76
- Scaffold new features instantly:
70
+ ## Testing the scaffolder
71
+
72
+ The CLI repo has no app-level test suite, but `npm test` runs the unit tests under `tests/unit/` plus the offline smoke tests under `tests/smoke/`, and two harnesses in `tests/e2e/` and `tests/smoke/` drive the real generator against generated scaffolds. See `docs/tests/README.md` (and `harness.md`) for details, scope, and what they verify.
77
73
 
78
74
  ```bash
79
- npm run create:feature auth
75
+ npm test # unit (tests/unit/) + offline smoke (tests/smoke/generate.test.mjs)
76
+ node --import ./register.js tests/smoke/install.mjs # real-install smoke (Quick Setup default; needs network)
77
+ node --import ./register.js tests/e2e/exhaustive.mjs # full option matrix (~11,664 combos, local-only; ~13 min). Use LIMIT=n.
80
78
  ```
81
79
 
82
- ### Component-based
80
+ ## Project structure (generated app examples)
83
81
 
84
- Organized by type. Components grouped by their role:
82
+ Feature-based (example):
85
83
 
86
84
  ```
87
85
  src/
88
- ├── components/
89
- │ ├── common/ # Shared, reusable
90
- │ ├── form/ # Form elements
91
- │ └── layout/ # Layout primitives
92
- ├── pages/
93
- ├── layouts/
94
- ├── routes/
95
- ├── store/
96
- ├── hooks/
97
- └── utils/
86
+ ├── app
87
+ │   ├── App.tsx
88
+ │   ├── providers
89
+ │   │   └── ThemeProvider.tsx
90
+ │   └── router.tsx
91
+ ├── components
92
+ │   └── ui
93
+ ├── config
94
+ │   └── constants.ts
95
+ ├── features
96
+ │   └── logout
97
+ │   ├── components
98
+ │   ├── interfaces
99
+ │   ├── layouts
100
+ │   ├── pages
101
+ │   ├── services
102
+ │   └── store
103
+ ├── hooks
104
+ │   ├── index.ts
105
+ │   └── useLocalStorage.ts
106
+ ├── index.css
107
+ ├── lib
108
+ ├── main.tsx
109
+ ├── stores
110
+ ├── styles
111
+ │   └── globals.css
112
+ ├── test
113
+ │   └── setup.ts
114
+ ├── types
115
+ └── utils
98
116
  ```
99
117
 
100
- ## Path alias
118
+ Component-based (example):
101
119
 
102
- All projects use the `@/` path alias:
103
-
104
- ```tsx
105
- import { Button } from "@/components/common/Button";
106
- import { useAuth } from "@/hooks/useAuth";
107
120
  ```
121
+ src/
122
+ ├── App.tsx
123
+ ├── components
124
+ │   ├── common
125
+ │   ├── form
126
+ │   └── layout
127
+ ├── config
128
+ │   ├── constants.ts
129
+ │   └── routes.ts
130
+ ├── context
131
+ ├── hooks
132
+ ├── index.css
133
+ ├── layouts
134
+ ├── main.tsx
135
+ ├── pages
136
+ ├── routes
137
+ ├── services
138
+ ├── store
139
+ ├── styles
140
+ ├── test
141
+ └── utils
142
+ ```
143
+
144
+ ## Roadmap
145
+
146
+ See `docs/ROADMAP.md` for planned features and upcoming releases.
108
147
 
109
148
  ## Requirements
110
149
 
111
150
  - Node.js >= 18
112
151
 
152
+ ## Acknowledgements
153
+
154
+ This project is based on [create-vrtw](https://github.com/Avijit07x/create-vrtw) by [@Avijit07x](https://github.com/Avijit07x). Thank you for the original work that inspired this scaffolder.
155
+
113
156
  ## License
114
157
 
115
158
  MIT
package/bin/cli.js CHANGED
@@ -1,17 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { main } from "../src/main.js";
4
3
  import chalk from "chalk";
4
+ import "../register.js";
5
+
6
+ const args = process.argv.slice(2);
7
+ const quickSetup = args.includes("-y");
8
+ const projectName = args.find((a) => !a.startsWith("-"));
5
9
 
6
10
  console.log(
7
11
  chalk.bold.cyan("\n ✦ LUMEN") +
8
- chalk.gray(" v1.0.0") +
12
+ chalk.gray(" v1.1.0") +
9
13
  chalk.gray(
10
14
  "\n Scaffold a React + Vite project with architecture choice\n"
11
15
  )
12
16
  );
13
17
 
14
- main().catch((e) => {
18
+ const { main } = await import("@/main.js");
19
+
20
+ main({ quickSetup, projectName }).catch((e) => {
15
21
  console.error(chalk.red("\nError:"), e.message || e);
16
22
  process.exit(1);
17
23
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lumen",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Scaffold a React + Vite project with architecture choice: feature-based or component-based.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "LICENSE"
15
15
  ],
16
16
  "scripts": {
17
- "test": "echo \"Error: no test specified\" && exit 1"
17
+ "test": "node --import ./register.js --test \"tests/unit/*.test.mjs\" \"tests/smoke/*.test.mjs\""
18
18
  },
19
19
  "keywords": [
20
20
  "scaffold",
package/src/aliases.js ADDED
@@ -0,0 +1,7 @@
1
+ export function getPathAliases() {
2
+ return { "@/*": ["./src/*"] };
3
+ }
4
+
5
+ export function getViteAliases() {
6
+ return [{ find: "@", replacement: "path.resolve(__dirname, \"src\")" }];
7
+ }
package/src/cleanup.js CHANGED
@@ -1,4 +1,4 @@
1
- import { deleteIfExists } from "./utils/fs.js";
1
+ import { deleteIfExists } from "@/utils/fs.js";
2
2
 
3
3
  export async function cleanupBoilerplate(projectPath) {
4
4
  await deleteIfExists(`${projectPath}/public`);
package/src/configure.js CHANGED
@@ -1,20 +1,21 @@
1
1
  import { promises as fsp } from "fs";
2
2
  import path from "path";
3
- import { readFileContent } from "./utils/fs.js";
3
+ import { readFileContent } from "@/utils/fs.js";
4
+ import { getPathAliases, getViteAliases } from "@/aliases.js";
4
5
 
5
6
  export async function configureProject(projectPath, language, cssFramework) {
6
7
  const ext = language === "ts" ? "ts" : "js";
7
8
 
8
- // Set up tsconfig/jsconfig with @/* paths
9
+ // Set up tsconfig/jsconfig with the @/* path alias
9
10
  if (language === "ts") {
10
11
  await configureTsconfig(projectPath);
11
12
  } else {
12
13
  await configureJsconfig(projectPath);
13
14
  }
14
15
 
15
- // Add tsconfigPaths: true to vite.config for non-tailwind setups
16
+ // Add path aliases to vite.config for non-tailwind setups
16
17
  if (cssFramework !== "tailwind") {
17
- await addTsconfigPathsToViteConfig(projectPath, ext);
18
+ await addPathAliasesToViteConfig(projectPath, ext);
18
19
  }
19
20
 
20
21
  // Add scripts to package.json
@@ -22,6 +23,7 @@ export async function configureProject(projectPath, language, cssFramework) {
22
23
  }
23
24
 
24
25
  async function configureTsconfig(projectPath) {
26
+ const paths = getPathAliases();
25
27
  // Create/update tsconfig.app.json with paths
26
28
  const tsconfigAppPath = path.join(projectPath, "tsconfig.app.json");
27
29
  try {
@@ -30,9 +32,7 @@ async function configureTsconfig(projectPath) {
30
32
 
31
33
  if (!config.compilerOptions) config.compilerOptions = {};
32
34
  config.compilerOptions.baseUrl = ".";
33
- config.compilerOptions.paths = {
34
- "@/*": ["./src/*"],
35
- };
35
+ config.compilerOptions.paths = paths;
36
36
 
37
37
  await fsp.writeFile(
38
38
  tsconfigAppPath,
@@ -59,9 +59,7 @@ async function configureTsconfig(projectPath) {
59
59
  noUnusedParameters: true,
60
60
  noFallthroughCasesInSwitch: true,
61
61
  baseUrl: ".",
62
- paths: {
63
- "@/*": ["./src/*"],
64
- },
62
+ paths,
65
63
  },
66
64
  include: ["src"],
67
65
  };
@@ -90,16 +88,14 @@ async function configureJsconfig(projectPath) {
90
88
  const config = {
91
89
  compilerOptions: {
92
90
  baseUrl: ".",
93
- paths: {
94
- "@/*": ["./src/*"],
95
- },
91
+ paths: getPathAliases(),
96
92
  },
97
93
  include: ["src"],
98
94
  };
99
95
  await fsp.writeFile(jsconfigPath, JSON.stringify(config, null, 2) + "\n", "utf8");
100
96
  }
101
97
 
102
- async function addTsconfigPathsToViteConfig(projectPath, ext) {
98
+ async function addPathAliasesToViteConfig(projectPath, ext) {
103
99
  const viteConfigPath = path.join(projectPath, `vite.config.${ext}`);
104
100
  try {
105
101
  let content = await readFileContent(viteConfigPath);
@@ -124,15 +120,21 @@ async function addTsconfigPathsToViteConfig(projectPath, ext) {
124
120
 
125
121
  let result = lines.join("\n");
126
122
 
123
+ const aliasArray = getViteAliases()
124
+ .map(
125
+ (a) =>
126
+ `{ find: "${a.find}", replacement: ${a.replacement} }`
127
+ )
128
+ .join(",\n ");
129
+
130
+ const resolveBlock = `resolve: {\n tsconfigPaths: true,\n alias: [\n ${aliasArray}\n ],\n }`;
131
+
127
132
  if (result.includes("resolve:")) {
128
- result = result.replace(
129
- /resolve:\s*\{/,
130
- 'resolve: {\n tsconfigPaths: true,\n alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],'
131
- );
133
+ result = result.replace(/resolve:\s*\{/, resolveBlock);
132
134
  } else {
133
135
  result = result.replace(
134
136
  /defineConfig\(\{/,
135
- 'defineConfig({\n resolve: {\n tsconfigPaths: true,\n alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],\n },'
137
+ `defineConfig({\n ${resolveBlock},`
136
138
  );
137
139
  }
138
140
 
@@ -1,7 +1,7 @@
1
1
  import { isCancel, select } from "@clack/prompts";
2
2
  import { access, mkdir, readdir, rm } from "fs/promises";
3
3
  import path from "path";
4
- import { onCancel } from "./prompts.js";
4
+ import { onCancel } from "@/prompts.js";
5
5
 
6
6
  export async function confirmEmptyFolder(projectName) {
7
7
  const cwd = process.cwd();
package/src/css.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import { promises as fsp } from "fs";
3
3
  import path from "path";
4
- import { deleteIfExists } from "./utils/fs.js";
4
+ import { deleteIfExists } from "@/utils/fs.js";
5
5
 
6
6
  export async function setupCssFramework({
7
7
  projectPath,
@@ -55,8 +55,8 @@ export async function installAllDeps(responses, pkg, cwd) {
55
55
  deps.push("@hugeicons/react", "@hugeicons/core-free-icons");
56
56
  }
57
57
 
58
- // Axios
59
- if (responses.axios) {
58
+ // API Client
59
+ if (responses.apiClient === "axios") {
60
60
  deps.push("axios");
61
61
  }
62
62
 
@@ -89,6 +89,16 @@ export async function installAllDeps(responses, pkg, cwd) {
89
89
  devDeps.push("oxlint");
90
90
  }
91
91
 
92
+ // Formatter
93
+ if (responses.formatter === "prettier") {
94
+ devDeps.push("prettier");
95
+ if (responses.linter === "eslint") {
96
+ devDeps.push("eslint-config-prettier");
97
+ }
98
+ } else if (responses.formatter === "oxfmt") {
99
+ devDeps.push("oxfmt");
100
+ }
101
+
92
102
  // Always re-install react + react-dom for consistency
93
103
  deps.push("react", "react-dom");
94
104
 
package/src/env.js ADDED
@@ -0,0 +1,27 @@
1
+ import path from "path";
2
+ import { promises as fsp } from "fs";
3
+
4
+ const ENV_GITIGNORE = `
5
+ # Environment variables
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ `;
10
+
11
+ export async function copyEnvExample(projectPath, templatesDir) {
12
+ const src = path.join(templatesDir, ".env.example");
13
+ try {
14
+ await fsp.access(src);
15
+ await fsp.copyFile(src, path.join(projectPath, ".env.example"));
16
+ } catch {}
17
+
18
+ // Ensure the project gitignore excludes env files but keeps .env.example.
19
+ const gitignore = path.join(projectPath, ".gitignore");
20
+ try {
21
+ let content = await fsp.readFile(gitignore, "utf8");
22
+ if (!content.includes("!.env.example")) {
23
+ content = content.replace(/\s*$/, "\n") + ENV_GITIGNORE.trimEnd() + "\n";
24
+ await fsp.writeFile(gitignore, content, "utf8");
25
+ }
26
+ } catch {}
27
+ }
package/src/injector.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { promises as fsp } from "fs";
2
2
  import path from "path";
3
- import { copyDirRecursive, writeFileRecursive } from "./utils/fs.js";
3
+ import { copyDirRecursive, writeFileRecursive } from "@/utils/fs.js";
4
4
 
5
5
  export async function injectArchitecture(projectPath, templatesDir, architecture, language) {
6
6
  const ext = language === "ts" ? "tsx" : "jsx";
@@ -43,6 +43,89 @@ export async function injectArchitecture(projectPath, templatesDir, architecture
43
43
  try { await fsp.rm(oppositeMain, { force: true }); } catch {}
44
44
  }
45
45
 
46
+ export async function injectFormatter(projectPath, templatesDir, responses) {
47
+ if (!responses.formatter || responses.formatter === "none") return;
48
+
49
+ // Copy the formatter config file to the project root
50
+ const configFile =
51
+ responses.formatter === "prettier" ? ".prettierrc" : ".oxfmtrc.json";
52
+ const configSrc = path.join(
53
+ templatesDir,
54
+ "conditional",
55
+ "formatter",
56
+ responses.formatter,
57
+ configFile
58
+ );
59
+ try {
60
+ await fsp.access(configSrc);
61
+ await fsp.copyFile(configSrc, path.join(projectPath, configFile));
62
+ } catch {}
63
+
64
+ // Append format script(s) to package.json
65
+ const pkgPath = path.join(projectPath, "package.json");
66
+ try {
67
+ const content = await fsp.readFile(pkgPath, "utf8");
68
+ const pkg = JSON.parse(content);
69
+ if (!pkg.scripts) pkg.scripts = {};
70
+ if (responses.formatter === "prettier") {
71
+ pkg.scripts.format = "prettier --write .";
72
+ pkg.scripts["format:check"] = "prettier --check .";
73
+ } else if (responses.formatter === "oxfmt") {
74
+ pkg.scripts.format = "oxfmt .";
75
+ }
76
+ await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
77
+ } catch {}
78
+
79
+ // Wire eslint-config-prettier only for the eslint + prettier combination
80
+ if (responses.linter === "eslint" && responses.formatter === "prettier") {
81
+ await wireEslintPrettier(projectPath, responses.language);
82
+ }
83
+ }
84
+
85
+ async function wireEslintPrettier(projectPath, language) {
86
+ const ext = language === "ts" ? "ts" : "js";
87
+ const eslintConfigPath = path.join(projectPath, `eslint.config.${ext}`);
88
+ try {
89
+ let content = await fsp.readFile(eslintConfigPath, "utf8");
90
+
91
+ // Avoid double-injection on re-runs
92
+ if (content.includes("eslint-config-prettier")) return;
93
+
94
+ // Add the import after the last import line
95
+ const lines = content.split("\n");
96
+ let lastImportIdx = -1;
97
+ for (let i = 0; i < lines.length; i++) {
98
+ if (lines[i].startsWith("import ")) lastImportIdx = i;
99
+ }
100
+ lines.splice(
101
+ lastImportIdx + 1,
102
+ 0,
103
+ 'import prettier from "eslint-config-prettier";'
104
+ );
105
+
106
+ let result = lines.join("\n");
107
+
108
+ // Append ...prettier as the LAST element of the eslint config.
109
+ // The JS template exports an array (`export default [...]`); the TS
110
+ // template uses a function call (`export default tseslint.config(...)`).
111
+ const pushLast = (_, head, tail) =>
112
+ head.replace(/,\s*$/, "") + ",\n ...prettier," + tail;
113
+ if (result.includes("tseslint.config(")) {
114
+ result = result.replace(
115
+ /(export default tseslint\.config\([\s\S]*?)(\n\s*\);)/,
116
+ pushLast
117
+ );
118
+ } else {
119
+ result = result.replace(
120
+ /(export default \[[\s\S]*?)(\n\s*\];)/,
121
+ pushLast
122
+ );
123
+ }
124
+
125
+ await fsp.writeFile(eslintConfigPath, result, "utf8");
126
+ } catch {}
127
+ }
128
+
46
129
  export async function injectConditionals(projectPath, templatesDir, responses, architecture, language) {
47
130
  const ext = language === "ts" ? "tsx" : "jsx";
48
131
 
@@ -84,14 +167,21 @@ export async function injectConditionals(projectPath, templatesDir, responses, a
84
167
  );
85
168
  }
86
169
 
87
- // 4. Axios
88
- if (responses.axios) {
170
+ // 4. API Client
171
+ if (responses.apiClient === "axios") {
89
172
  await injectOverlay(
90
173
  projectPath,
91
174
  templatesDir,
92
175
  `conditional/axios/${architecture}`,
93
176
  language
94
177
  );
178
+ } else if (responses.apiClient === "fetch") {
179
+ await injectOverlay(
180
+ projectPath,
181
+ templatesDir,
182
+ `conditional/fetch/${architecture}`,
183
+ language
184
+ );
95
185
  }
96
186
 
97
187
  // 5. Testing
@@ -149,6 +239,16 @@ async function injectTesting(projectPath, templatesDir, framework, language, arc
149
239
  await writeFileRecursive(setupDest, await fsp.readFile(setupSrc, "utf8"));
150
240
  } catch {}
151
241
 
242
+ // Jest keeps its setup at the template root (jest.setup.ts/js), referenced by
243
+ // jest.config as <rootDir>/jest.setup.ts — copy it to the project root.
244
+ if (framework === "jest") {
245
+ const jestSetupSrc = path.join(testingDir, `jest.setup.${configExt}`);
246
+ try {
247
+ await fsp.access(jestSetupSrc);
248
+ await fsp.copyFile(jestSetupSrc, path.join(projectPath, `jest.setup.${configExt}`));
249
+ } catch {}
250
+ }
251
+
152
252
  // Copy test file
153
253
  const testExt = language === "ts" ? "tsx" : "jsx";
154
254
  const testSrc = path.join(testingDir, `src/__tests__/App.test.${testExt}`);
package/src/main.js CHANGED
@@ -3,28 +3,30 @@ import chalk from "chalk";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
5
5
 
6
- import { confirmEmptyFolder } from "./confirm-empty-folder.js";
7
- import { cleanupBoilerplate } from "./cleanup.js";
8
- import { configureProject } from "./configure.js";
9
- import { setupCssFramework } from "./css.js";
10
- import { injectArchitecture, injectConditionals } from "./injector.js";
11
- import { getPkgManager } from "./utils/pkg-manager.js";
12
- import { handleDevServer } from "./utils/dev-server.js";
13
- import { getUserInputs, onCancel } from "./prompts.js";
14
- import { runBaseInstall, runGitInit, runViteCreate } from "./scaffold.js";
15
- import { installAllDeps } from "./dependencies.js";
16
- import { generateReadme } from "./readme.js";
6
+ import { confirmEmptyFolder } from "@/confirm-empty-folder.js";
7
+ import { copyEnvExample } from "@/env.js";
8
+ import { cleanupBoilerplate } from "@/cleanup.js";
9
+ import { configureProject } from "@/configure.js";
10
+ import { setupCssFramework } from "@/css.js";
11
+ import { injectArchitecture, injectConditionals, injectFormatter } from "@/injector.js";
12
+ import { getPkgManager } from "@/utils/pkg-manager.js";
13
+ import { handleDevServer } from "@/utils/dev-server.js";
14
+ import { getUserInputs, onCancel } from "@/prompts.js";
15
+ import { runBaseInstall, runGitInit, runViteCreate } from "@/scaffold.js";
16
+ import { installAllDeps } from "@/dependencies.js";
17
+ import { generateReadme } from "@/readme.js";
17
18
 
18
19
  const __filename = fileURLToPath(import.meta.url);
19
20
  const __dirname = path.dirname(__filename);
20
21
  const TEMPLATES_DIR = path.join(__dirname, "../templates");
21
22
  const CURRENT_DIR = process.cwd();
22
23
 
23
- export async function main() {
24
+ export async function main(options = {}) {
25
+ const { quickSetup = false, projectName: nameArg } = options;
24
26
  const pkg = getPkgManager();
25
27
 
26
28
  // 1. Project name
27
- const cliArg = process.argv[2];
29
+ const cliArg = nameArg || process.argv[2];
28
30
  let projectName;
29
31
 
30
32
  if (cliArg && cliArg.trim()) {
@@ -45,7 +47,7 @@ export async function main() {
45
47
  await confirmEmptyFolder(projectName);
46
48
 
47
49
  // 3. Collect user preferences
48
- const responses = await getUserInputs(projectName);
50
+ const responses = await getUserInputs(projectName, { quickSetup });
49
51
 
50
52
  const projectPath = path.resolve(CURRENT_DIR, projectName);
51
53
 
@@ -119,6 +121,17 @@ export async function main() {
119
121
  throw err;
120
122
  }
121
123
 
124
+ // 8.5 Inject formatter (config file + scripts + eslint-config-prettier wiring)
125
+ const fmtSpin = spinner();
126
+ fmtSpin.start("Configuring code formatter...");
127
+ try {
128
+ await injectFormatter(projectPath, TEMPLATES_DIR, responses);
129
+ fmtSpin.stop(chalk.green("Formatter configured."));
130
+ } catch (err) {
131
+ fmtSpin.stop(chalk.red("Failed to configure formatter."));
132
+ throw err;
133
+ }
134
+
122
135
  // 9. Install all additional dependencies in one batch
123
136
  const extrasSpin = spinner();
124
137
  extrasSpin.start("Installing additional dependencies...");
@@ -172,6 +185,9 @@ export async function main() {
172
185
  // 12. Cleanup boilerplate
173
186
  await cleanupBoilerplate(projectPath);
174
187
 
188
+ // 12.5 Copy .env.example into the project
189
+ await copyEnvExample(projectPath, TEMPLATES_DIR);
190
+
175
191
  // 13. Generate README.md + LICENSE (if requested)
176
192
  if (responses.readme) {
177
193
  const readmeSpin = spinner();
package/src/prompts.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cancel, confirm, isCancel, note, select, text } from "@clack/prompts";
2
2
  import chalk from "chalk";
3
- import { configExists, loadConfig, saveConfig } from "./config-cache.js";
3
+ import { configExists, loadConfig, saveConfig } from "@/config-cache.js";
4
4
 
5
5
  export function onCancel() {
6
6
  console.log(chalk.gray("\n\nOperation cancelled.\n"));
@@ -40,17 +40,39 @@ function formatConfig(responses) {
40
40
  `• ${chalk.bold("Router:")} ${responses.router ? chalk.green("Yes") : chalk.red("No")}`,
41
41
  `• ${chalk.bold("State:")} ${chalk.blue(state)}`,
42
42
  `• ${chalk.bold("Icons:")} ${chalk.blue(responses.iconLibrary === "none" ? "None" : responses.iconLibrary === "lucide" ? "Lucide" : "Huge")}`,
43
- `• ${chalk.bold("Axios:")} ${responses.axios ? chalk.green("Yes") : chalk.red("No")}`,
43
+ `• ${chalk.bold("API Client:")} ${responses.apiClient === "axios" ? chalk.green("Axios") : responses.apiClient === "fetch" ? chalk.yellow("Fetch") : chalk.red("None")}`,
44
44
  `• ${chalk.bold("Linter:")} ${responses.linter === "oxlint" ? chalk.green("Oxlint") : responses.linter === "eslint" ? chalk.yellow("ESLint") : chalk.red("None")}`,
45
+ `• ${chalk.bold("Formatter:")} ${responses.formatter === "oxfmt" ? chalk.green("Oxfmt") : responses.formatter === "prettier" ? chalk.yellow("Prettier") : chalk.red("None")}`,
45
46
  `• ${chalk.bold("Git:")} ${responses.gitInit ? chalk.green("Yes") : chalk.red("No")}`,
46
47
  `• ${chalk.bold("README:")} ${responses.readme ? chalk.green("Yes") : chalk.red("No")}`,
47
48
  ].join("\n");
48
49
  }
49
50
 
50
- export async function getUserInputs(projectName) {
51
+ export async function getUserInputs(projectName, { quickSetup = false } = {}) {
51
52
  let oldConfig = {};
52
53
  let useOldConfig = false;
53
54
 
55
+ // Non-interactive quick setup: apply defaults without prompting.
56
+ if (quickSetup) {
57
+ const responses = {
58
+ projectName,
59
+ architecture: "feature-based",
60
+ language: "ts",
61
+ cssFramework: "tailwind",
62
+ testing: "vitest",
63
+ router: true,
64
+ stateManagement: "none",
65
+ iconLibrary: "none",
66
+ apiClient: "none",
67
+ linter: "eslint",
68
+ formatter: "prettier",
69
+ gitInit: true,
70
+ readme: true,
71
+ };
72
+ await saveConfig(responses);
73
+ return responses;
74
+ }
75
+
54
76
  if (await configExists()) {
55
77
  oldConfig = await loadConfig();
56
78
  oldConfig.projectName = projectName;
@@ -73,19 +95,25 @@ export async function getUserInputs(projectName) {
73
95
  useOldConfig = useExisting;
74
96
 
75
97
  if (useOldConfig) {
98
+ if (oldConfig.apiClient === undefined) {
99
+ oldConfig.apiClient = oldConfig.axios ? "axios" : "none";
100
+ }
101
+ if (oldConfig.formatter === undefined) {
102
+ oldConfig.formatter = "none";
103
+ }
76
104
  return { ...oldConfig, projectName };
77
105
  }
78
106
  }
79
107
 
80
108
  // Quick Setup
81
- const quickSetup = await confirm({
109
+ const quickSetupChoice = await confirm({
82
110
  message:
83
111
  "Quick Setup? (TypeScript + Tailwind + Feature-based + Router + ESLint + Vitest)",
84
112
  initialValue: true,
85
113
  });
86
- if (isCancel(quickSetup)) onCancel();
114
+ if (isCancel(quickSetupChoice)) onCancel();
87
115
 
88
- if (quickSetup) {
116
+ if (quickSetupChoice) {
89
117
  const responses = {
90
118
  projectName,
91
119
  architecture: "feature-based",
@@ -95,9 +123,10 @@ export async function getUserInputs(projectName) {
95
123
  router: true,
96
124
  stateManagement: "none",
97
125
  iconLibrary: "none",
98
- axios: false,
126
+ apiClient: "none",
99
127
  linter: "eslint",
100
- gitInit: false,
128
+ formatter: "prettier",
129
+ gitInit: true,
101
130
  readme: true,
102
131
  };
103
132
  await saveConfig(responses);
@@ -183,11 +212,16 @@ export async function getUserInputs(projectName) {
183
212
  });
184
213
  if (isCancel(iconLibrary)) onCancel();
185
214
 
186
- const axios = await confirm({
187
- message: "Would you like to install Axios?",
188
- initialValue: false,
215
+ const apiClient = await select({
216
+ message: "Which API client do you want to use?",
217
+ options: [
218
+ { label: "None", value: "none" },
219
+ { label: "Axios", value: "axios" },
220
+ { label: "Fetch (native)", value: "fetch" },
221
+ ],
222
+ initialValue: "none",
189
223
  });
190
- if (isCancel(axios)) onCancel();
224
+ if (isCancel(apiClient)) onCancel();
191
225
 
192
226
  const gitInit = await confirm({
193
227
  message: "Would you like to initialize a Git repository?",
@@ -206,6 +240,27 @@ export async function getUserInputs(projectName) {
206
240
  });
207
241
  if (isCancel(linter)) onCancel();
208
242
 
243
+ let formatter = "none";
244
+ if (linter !== "none") {
245
+ const formatterOptions =
246
+ linter === "eslint"
247
+ ? [
248
+ { label: "None", value: "none" },
249
+ { label: "Prettier", value: "prettier" },
250
+ ]
251
+ : [
252
+ { label: "None", value: "none" },
253
+ { label: "Oxfmt (Recommended)", value: "oxfmt" },
254
+ { label: "Prettier", value: "prettier" },
255
+ ];
256
+ formatter = await select({
257
+ message: "Which formatter do you want to use?",
258
+ options: formatterOptions,
259
+ initialValue: linter === "eslint" ? "prettier" : "oxfmt",
260
+ });
261
+ if (isCancel(formatter)) onCancel();
262
+ }
263
+
209
264
  const readme = await confirm({
210
265
  message: "Would you like to generate a README.md and LICENSE?",
211
266
  initialValue: true,
@@ -221,8 +276,9 @@ export async function getUserInputs(projectName) {
221
276
  router,
222
277
  stateManagement,
223
278
  iconLibrary,
224
- axios,
279
+ apiClient,
225
280
  linter,
281
+ formatter,
226
282
  gitInit,
227
283
  readme,
228
284
  };
package/src/readme.js CHANGED
@@ -1,4 +1,4 @@
1
- import { writeFileRecursive } from "./utils/fs.js";
1
+ import { writeFileRecursive } from "@/utils/fs.js";
2
2
  import path from "path";
3
3
 
4
4
  function buildBuiltWith(responses) {
@@ -30,8 +30,10 @@ function buildBuiltWith(responses) {
30
30
  items.push("[Huge Icons](https://hugeicons.com/)");
31
31
  }
32
32
 
33
- if (responses.axios) {
33
+ if (responses.apiClient === "axios") {
34
34
  items.push("[Axios](https://axios-http.com/)");
35
+ } else if (responses.apiClient === "fetch") {
36
+ items.push("Fetch API");
35
37
  }
36
38
 
37
39
  if (responses.testing === "vitest") {
@@ -46,6 +48,12 @@ function buildBuiltWith(responses) {
46
48
  items.push("[Oxlint](https://oxc.rs/)");
47
49
  }
48
50
 
51
+ if (responses.formatter === "prettier") {
52
+ items.push("[Prettier](https://prettier.io/)");
53
+ } else if (responses.formatter === "oxfmt") {
54
+ items.push("[Oxfmt](https://oxc.rs/)");
55
+ }
56
+
49
57
  return items.map((item) => `- ${item}`).join("\n");
50
58
  }
51
59
 
@@ -67,6 +75,10 @@ function buildScripts(responses) {
67
75
  rows.push("| `npm run lint` | Run Oxlint |");
68
76
  }
69
77
 
78
+ if (responses.formatter === "prettier" || responses.formatter === "oxfmt") {
79
+ rows.push("| `npm run format` | Format code |");
80
+ }
81
+
70
82
  return rows.join("\n");
71
83
  }
72
84
 
@@ -123,14 +135,26 @@ SOFTWARE.
123
135
  `;
124
136
  }
125
137
 
138
+ function buildDescription(projectName, responses) {
139
+ const tools = [];
140
+ if (responses.linter === "eslint") tools.push("ESLint");
141
+ else if (responses.linter === "oxlint") tools.push("Oxlint");
142
+ if (responses.formatter === "prettier") tools.push("Prettier");
143
+ else if (responses.formatter === "oxfmt") tools.push("Oxfmt");
144
+
145
+ let suffix = "";
146
+ if (tools.length > 0) {
147
+ suffix = `\n\nThis project is set up with ${tools.join(" + ")} for code quality.`;
148
+ }
149
+ return `# ${projectName}\n\nA React + Vite application built with ${responses.architecture}.${suffix}`;
150
+ }
151
+
126
152
  function buildReadme(projectName, responses) {
127
153
  const builtWith = buildBuiltWith(responses);
128
154
  const scripts = buildScripts(responses);
129
155
  const structure = buildProjectStructure(responses.architecture);
130
156
 
131
- return `# ${projectName}
132
-
133
- <!-- TODO: Add a short description of your project -->
157
+ return `${buildDescription(projectName, responses)}
134
158
 
135
159
  ## Built With
136
160
 
package/src/scaffold.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from "path";
2
2
  import { execa } from "execa";
3
- import { isYarnV1 } from "./utils/yarn-v1.js";
3
+ import { isYarnV1 } from "@/utils/yarn-v1.js";
4
4
 
5
5
  export async function runViteCreate(projectPath, projectName, pkg, language) {
6
6
  const useYarn = pkg === "yarn";
@@ -0,0 +1,4 @@
1
+ # Copy this file to `.env.local` (or `.env`) and fill in your values.
2
+ # See https://vitejs.dev/guide/env-and-mode for how env vars are exposed.
3
+
4
+ VITE_API_URL=https://api.example.com
@@ -0,0 +1,27 @@
1
+ import { API_BASE_URL } from "@/config/constants";
2
+
3
+ const apiClient = async (endpoint, options = {}) => {
4
+ const token = localStorage.getItem("access_token");
5
+
6
+ const headers = {
7
+ "Content-Type": "application/json",
8
+ ...options.headers,
9
+ };
10
+
11
+ if (token) {
12
+ headers["Authorization"] = `Bearer ${token}`;
13
+ }
14
+
15
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
16
+ ...options,
17
+ headers,
18
+ });
19
+
20
+ if (!response.ok) {
21
+ throw new Error(`HTTP error! status: ${response.status}`);
22
+ }
23
+
24
+ return response.json();
25
+ };
26
+
27
+ export default apiClient;
@@ -0,0 +1,27 @@
1
+ import { API_BASE_URL } from "@/config/constants";
2
+
3
+ const apiClient = async (endpoint: string, options: RequestInit = {}) => {
4
+ const token = localStorage.getItem("access_token");
5
+
6
+ const headers: HeadersInit = {
7
+ "Content-Type": "application/json",
8
+ ...options.headers,
9
+ };
10
+
11
+ if (token) {
12
+ headers["Authorization"] = `Bearer ${token}`;
13
+ }
14
+
15
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
16
+ ...options,
17
+ headers,
18
+ });
19
+
20
+ if (!response.ok) {
21
+ throw new Error(`HTTP error! status: ${response.status}`);
22
+ }
23
+
24
+ return response.json();
25
+ };
26
+
27
+ export default apiClient;
@@ -0,0 +1 @@
1
+ export { default as apiClient } from "./api";
@@ -0,0 +1 @@
1
+ export { default as apiClient } from "./api";
@@ -0,0 +1,27 @@
1
+ import { API_BASE_URL } from "@/config/constants";
2
+
3
+ const apiClient = async (endpoint, options = {}) => {
4
+ const token = localStorage.getItem("access_token");
5
+
6
+ const headers = {
7
+ "Content-Type": "application/json",
8
+ ...options.headers,
9
+ };
10
+
11
+ if (token) {
12
+ headers["Authorization"] = `Bearer ${token}`;
13
+ }
14
+
15
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
16
+ ...options,
17
+ headers,
18
+ });
19
+
20
+ if (!response.ok) {
21
+ throw new Error(`HTTP error! status: ${response.status}`);
22
+ }
23
+
24
+ return response.json();
25
+ };
26
+
27
+ export default apiClient;
@@ -0,0 +1,27 @@
1
+ import { API_BASE_URL } from "@/config/constants";
2
+
3
+ const apiClient = async (endpoint: string, options: RequestInit = {}) => {
4
+ const token = localStorage.getItem("access_token");
5
+
6
+ const headers: HeadersInit = {
7
+ "Content-Type": "application/json",
8
+ ...options.headers,
9
+ };
10
+
11
+ if (token) {
12
+ headers["Authorization"] = `Bearer ${token}`;
13
+ }
14
+
15
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
16
+ ...options,
17
+ headers,
18
+ });
19
+
20
+ if (!response.ok) {
21
+ throw new Error(`HTTP error! status: ${response.status}`);
22
+ }
23
+
24
+ return response.json();
25
+ };
26
+
27
+ export default apiClient;
@@ -0,0 +1 @@
1
+ export { default as apiClient } from "./api";
@@ -0,0 +1 @@
1
+ export { default as apiClient } from "./api";
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "printWidth": 80,
4
+ "singleQuote": true,
5
+ "semi": true,
6
+ "trailingComma": "all"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "trailingComma": "es5",
6
+ "printWidth": 80
7
+ }
@@ -2,7 +2,7 @@
2
2
  module.exports = {
3
3
  testEnvironment: "jsdom",
4
4
  roots: ["<rootDir>/src"],
5
- setupFilesAfterSetup: ["<rootDir>/jest.setup.js"],
5
+ setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
6
6
  moduleNameMapper: {
7
7
  "^@/(.*)$": "<rootDir>/src/$1",
8
8
  },
@@ -3,7 +3,7 @@ module.exports = {
3
3
  preset: "ts-jest",
4
4
  testEnvironment: "jsdom",
5
5
  roots: ["<rootDir>/src"],
6
- setupFilesAfterSetup: ["<rootDir>/jest.setup.ts"],
6
+ setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
7
7
  moduleNameMapper: {
8
8
  "^@/(.*)$": "<rootDir>/src/$1",
9
9
  },