create-next-shadcn-kit 0.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/LICENSE +21 -0
- package/README.md +70 -0
- package/bin/index.js +8 -0
- package/package.json +39 -0
- package/src/index.js +80 -0
- package/src/prompts.js +156 -0
- package/src/scaffold.js +382 -0
- package/src/utils.js +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nikunj Sonigara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# create-next-shadcn-kit
|
|
2
|
+
|
|
3
|
+
> Create a Next.js app with **shadcn/ui** pre-integrated — zero config.
|
|
4
|
+
|
|
5
|
+
One command, a fresh Next.js project, Tailwind configured, shadcn/ui initialized, and your favorite components already installed. No juggling two CLIs.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-next-shadcn-kit@latest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or pass a name directly:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx create-next-shadcn-kit my-app
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
1. Scaffolds a fresh Next.js app via `create-next-app@latest` (App Router, Tailwind, Turbopack).
|
|
22
|
+
2. Runs `shadcn@latest init` inside the new project.
|
|
23
|
+
3. Pre-installs the shadcn components you selected.
|
|
24
|
+
4. Wires up state management — **Redux Toolkit** (default) or **Zustand**, with a sample store and (for Redux) a `<Providers>` wrapper already mounted in `app/layout`.
|
|
25
|
+
5. (Optional) Sets up **Husky + lint-staged + Prettier** with a pre-commit hook that runs `eslint --fix` and `prettier --write` on staged files.
|
|
26
|
+
|
|
27
|
+
You skip the "install Next → read shadcn docs → run a second init → add components one by one" ritual.
|
|
28
|
+
|
|
29
|
+
## Options
|
|
30
|
+
|
|
31
|
+
| Flag | Description |
|
|
32
|
+
| --------------------------------------- | ----------------------------------- |
|
|
33
|
+
| `-y, --yes` | Skip prompts, use sensible defaults |
|
|
34
|
+
| `--ts` / `--js` | TypeScript (default) or JavaScript |
|
|
35
|
+
| `--npm` / `--pnpm` / `--yarn` / `--bun` | Pick your package manager |
|
|
36
|
+
| `--no-husky` | Skip Husky + lint-staged setup |
|
|
37
|
+
| `--state=<lib>` | `redux` (default), `zustand`, `none`|
|
|
38
|
+
| `-v, --version` | Print version |
|
|
39
|
+
| `-h, --help` | Show help |
|
|
40
|
+
|
|
41
|
+
## Examples
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Fully interactive
|
|
45
|
+
npx create-next-shadcn-kit
|
|
46
|
+
|
|
47
|
+
# Non-interactive with defaults
|
|
48
|
+
npx create-next-shadcn-kit my-app --yes
|
|
49
|
+
|
|
50
|
+
# Use pnpm
|
|
51
|
+
npx create-next-shadcn-kit my-app --pnpm
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
- Node.js **18.17+**
|
|
57
|
+
- Network access (to fetch `create-next-app` and `shadcn`)
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone <this-repo>
|
|
63
|
+
cd create-next-shadcn-kit
|
|
64
|
+
npm install
|
|
65
|
+
node bin/index.js test-app --yes
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/bin/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-next-shadcn-kit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a Next.js app with shadcn/ui pre-integrated — zero config.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nextjs",
|
|
7
|
+
"next",
|
|
8
|
+
"shadcn",
|
|
9
|
+
"shadcn-ui",
|
|
10
|
+
"cli",
|
|
11
|
+
"scaffold",
|
|
12
|
+
"starter",
|
|
13
|
+
"create-next-app",
|
|
14
|
+
"tailwind"
|
|
15
|
+
],
|
|
16
|
+
"author": "Nikunj Sonigara <nikunjsonigara987@gmail.com>",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"create-next-shadcn-kit": "bin/index.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin",
|
|
24
|
+
"src",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.17.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"start": "node bin/index.js",
|
|
33
|
+
"test:local": "node bin/index.js test-app --yes"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"picocolors": "^1.1.1",
|
|
37
|
+
"prompts": "^2.4.2"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { parseArgs } from "./utils.js";
|
|
4
|
+
import { promptConfig } from "./prompts.js";
|
|
5
|
+
import { scaffold } from "./scaffold.js";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const pkg = require("../package.json");
|
|
9
|
+
|
|
10
|
+
export async function main() {
|
|
11
|
+
const args = parseArgs(process.argv.slice(2));
|
|
12
|
+
|
|
13
|
+
if (args.help) {
|
|
14
|
+
printHelp();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (args.version) {
|
|
19
|
+
console.log(pkg.version);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
assertNodeVersion();
|
|
24
|
+
|
|
25
|
+
console.log();
|
|
26
|
+
console.log(pc.bold(pc.cyan("◆ create-next-shadcn-kit")) + pc.dim(` v${pkg.version} — Next.js + shadcn/ui starter`));
|
|
27
|
+
|
|
28
|
+
const config = await promptConfig(args);
|
|
29
|
+
await scaffold(config);
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(pc.green("✔ Success!") + " Your project is ready.");
|
|
33
|
+
console.log();
|
|
34
|
+
console.log("Next steps:");
|
|
35
|
+
console.log(pc.cyan(` cd ${config.projectName}`));
|
|
36
|
+
console.log(pc.cyan(` ${devCommand(config.packageManager)}`));
|
|
37
|
+
console.log();
|
|
38
|
+
console.log(pc.dim("Docs: https://ui.shadcn.com"));
|
|
39
|
+
console.log();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function devCommand(pm) {
|
|
43
|
+
if (pm === "npm") return "npm run dev";
|
|
44
|
+
if (pm === "yarn") return "yarn dev";
|
|
45
|
+
return `${pm} dev`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function assertNodeVersion() {
|
|
49
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
50
|
+
if (major < 18) {
|
|
51
|
+
throw new Error(`Node.js 18.17+ is required. You are running ${process.versions.node}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function printHelp() {
|
|
56
|
+
console.log(`
|
|
57
|
+
${pc.bold("create-next-shadcn-kit")} — Create a Next.js app with shadcn/ui pre-integrated.
|
|
58
|
+
|
|
59
|
+
${pc.bold("Usage:")}
|
|
60
|
+
npx create-next-shadcn-kit [project-name] [options]
|
|
61
|
+
|
|
62
|
+
${pc.bold("Options:")}
|
|
63
|
+
-y, --yes Skip prompts and use defaults
|
|
64
|
+
--ts, --typescript Use TypeScript (default)
|
|
65
|
+
--js, --javascript Use JavaScript
|
|
66
|
+
--npm Use npm (default)
|
|
67
|
+
--pnpm Use pnpm
|
|
68
|
+
--yarn Use yarn
|
|
69
|
+
--bun Use bun
|
|
70
|
+
--no-husky Skip Husky + lint-staged setup
|
|
71
|
+
--state=<lib> State management: redux (default), zustand, none
|
|
72
|
+
-v, --version Show version
|
|
73
|
+
-h, --help Show this help
|
|
74
|
+
|
|
75
|
+
${pc.bold("Examples:")}
|
|
76
|
+
npx create-next-shadcn-kit
|
|
77
|
+
npx create-next-shadcn-kit my-app
|
|
78
|
+
npx create-next-shadcn-kit my-app --yes --pnpm
|
|
79
|
+
`);
|
|
80
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { isValidProjectName } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
const COMPONENT_CHOICES = [
|
|
6
|
+
{ title: "button", value: "button", selected: true },
|
|
7
|
+
{ title: "card", value: "card", selected: true },
|
|
8
|
+
{ title: "input", value: "input", selected: true },
|
|
9
|
+
{ title: "label", value: "label", selected: true },
|
|
10
|
+
{ title: "form", value: "form", selected: false },
|
|
11
|
+
{ title: "dialog", value: "dialog", selected: false },
|
|
12
|
+
{ title: "dropdown-menu", value: "dropdown-menu", selected: false },
|
|
13
|
+
{ title: "select", value: "select", selected: false },
|
|
14
|
+
{ title: "sonner (toast)", value: "sonner", selected: false },
|
|
15
|
+
{ title: "tabs", value: "tabs", selected: false },
|
|
16
|
+
{ title: "textarea", value: "textarea", selected: false },
|
|
17
|
+
{ title: "tooltip", value: "tooltip", selected: false },
|
|
18
|
+
{ title: "avatar", value: "avatar", selected: false },
|
|
19
|
+
{ title: "badge", value: "badge", selected: false },
|
|
20
|
+
{ title: "separator", value: "separator", selected: false },
|
|
21
|
+
{ title: "skeleton", value: "skeleton", selected: false },
|
|
22
|
+
{ title: "table", value: "table", selected: false },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const onCancel = () => {
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(pc.red("✖") + " Cancelled");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function promptConfig(args) {
|
|
32
|
+
if (args.yes) {
|
|
33
|
+
return {
|
|
34
|
+
projectName: args.projectName || "my-app",
|
|
35
|
+
typescript: args.typescript ?? true,
|
|
36
|
+
eslint: true,
|
|
37
|
+
srcDir: true,
|
|
38
|
+
importAlias: "@/*",
|
|
39
|
+
packageManager: args.packageManager || "npm",
|
|
40
|
+
husky: args.husky ?? true,
|
|
41
|
+
state: args.state || "redux",
|
|
42
|
+
components: ["button", "card", "input", "label"],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const questions = [];
|
|
47
|
+
|
|
48
|
+
if (!args.projectName) {
|
|
49
|
+
questions.push({
|
|
50
|
+
type: "text",
|
|
51
|
+
name: "projectName",
|
|
52
|
+
message: "Project name:",
|
|
53
|
+
initial: "my-app",
|
|
54
|
+
validate: (v) => (isValidProjectName(v) ? true : "Invalid project name"),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (args.typescript === undefined) {
|
|
59
|
+
questions.push({
|
|
60
|
+
type: "toggle",
|
|
61
|
+
name: "typescript",
|
|
62
|
+
message: "Use TypeScript?",
|
|
63
|
+
initial: true,
|
|
64
|
+
active: "yes",
|
|
65
|
+
inactive: "no",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
questions.push(
|
|
70
|
+
{
|
|
71
|
+
type: "toggle",
|
|
72
|
+
name: "eslint",
|
|
73
|
+
message: "Use ESLint?",
|
|
74
|
+
initial: true,
|
|
75
|
+
active: "yes",
|
|
76
|
+
inactive: "no",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: "toggle",
|
|
80
|
+
name: "srcDir",
|
|
81
|
+
message: "Use a `src/` directory?",
|
|
82
|
+
initial: true,
|
|
83
|
+
active: "yes",
|
|
84
|
+
inactive: "no",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: "text",
|
|
88
|
+
name: "importAlias",
|
|
89
|
+
message: "Import alias:",
|
|
90
|
+
initial: "@/*",
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (!args.packageManager) {
|
|
95
|
+
questions.push({
|
|
96
|
+
type: "select",
|
|
97
|
+
name: "packageManager",
|
|
98
|
+
message: "Package manager:",
|
|
99
|
+
choices: [
|
|
100
|
+
{ title: "npm", value: "npm" },
|
|
101
|
+
{ title: "pnpm", value: "pnpm" },
|
|
102
|
+
{ title: "yarn", value: "yarn" },
|
|
103
|
+
{ title: "bun", value: "bun" },
|
|
104
|
+
],
|
|
105
|
+
initial: 0,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (args.husky === undefined) {
|
|
110
|
+
questions.push({
|
|
111
|
+
type: "toggle",
|
|
112
|
+
name: "husky",
|
|
113
|
+
message: "Set up Husky + lint-staged (pre-commit hooks)?",
|
|
114
|
+
initial: true,
|
|
115
|
+
active: "yes",
|
|
116
|
+
inactive: "no",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!args.state) {
|
|
121
|
+
questions.push({
|
|
122
|
+
type: "select",
|
|
123
|
+
name: "state",
|
|
124
|
+
message: "State management:",
|
|
125
|
+
choices: [
|
|
126
|
+
{ title: "Redux Toolkit", value: "redux" },
|
|
127
|
+
{ title: "Zustand", value: "zustand" },
|
|
128
|
+
{ title: "None", value: "none" },
|
|
129
|
+
],
|
|
130
|
+
initial: 0,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
questions.push({
|
|
135
|
+
type: "multiselect",
|
|
136
|
+
name: "components",
|
|
137
|
+
message: "Pre-install components:",
|
|
138
|
+
choices: COMPONENT_CHOICES,
|
|
139
|
+
hint: "(space to toggle, enter to confirm)",
|
|
140
|
+
instructions: false,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const answers = await prompts(questions, { onCancel });
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
projectName: args.projectName || answers.projectName,
|
|
147
|
+
typescript: args.typescript ?? answers.typescript,
|
|
148
|
+
eslint: answers.eslint,
|
|
149
|
+
srcDir: answers.srcDir,
|
|
150
|
+
importAlias: answers.importAlias || "@/*",
|
|
151
|
+
packageManager: args.packageManager || answers.packageManager,
|
|
152
|
+
husky: args.husky ?? answers.husky ?? true,
|
|
153
|
+
state: args.state || answers.state || "redux",
|
|
154
|
+
components: answers.components || [],
|
|
155
|
+
};
|
|
156
|
+
}
|
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { run } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
export async function scaffold(config) {
|
|
7
|
+
const cwd = process.cwd();
|
|
8
|
+
const projectPath = path.resolve(cwd, config.projectName);
|
|
9
|
+
|
|
10
|
+
if (fs.existsSync(projectPath) && fs.readdirSync(projectPath).length > 0) {
|
|
11
|
+
throw new Error(`Directory "${config.projectName}" already exists and is not empty.`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
console.log();
|
|
15
|
+
console.log(pc.cyan("◆") + " Creating Next.js app...");
|
|
16
|
+
console.log();
|
|
17
|
+
|
|
18
|
+
const cnaArgs = [
|
|
19
|
+
"create-next-app@latest",
|
|
20
|
+
config.projectName,
|
|
21
|
+
config.typescript ? "--typescript" : "--javascript",
|
|
22
|
+
config.eslint ? "--eslint" : "--no-eslint",
|
|
23
|
+
"--tailwind",
|
|
24
|
+
"--app",
|
|
25
|
+
config.srcDir ? "--src-dir" : "--no-src-dir",
|
|
26
|
+
"--turbopack",
|
|
27
|
+
`--import-alias=${config.importAlias}`,
|
|
28
|
+
`--use-${config.packageManager}`,
|
|
29
|
+
"--yes",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
await run("npx", cnaArgs);
|
|
33
|
+
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(pc.cyan("◆") + " Initializing shadcn/ui...");
|
|
36
|
+
console.log();
|
|
37
|
+
|
|
38
|
+
const shadcnRunner = runnerFor(config.packageManager);
|
|
39
|
+
|
|
40
|
+
await run(shadcnRunner.cmd, [...shadcnRunner.args, "shadcn@latest", "init", "--yes", "--defaults"], { cwd: projectPath });
|
|
41
|
+
|
|
42
|
+
if (config.components.length > 0) {
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(pc.cyan("◆") + ` Adding components: ${pc.dim(config.components.join(", "))}`);
|
|
45
|
+
console.log();
|
|
46
|
+
|
|
47
|
+
await run(shadcnRunner.cmd, [...shadcnRunner.args, "shadcn@latest", "add", ...config.components, "--yes"], { cwd: projectPath });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (config.state === "redux") {
|
|
51
|
+
await setupRedux(projectPath, config);
|
|
52
|
+
} else if (config.state === "zustand") {
|
|
53
|
+
await setupZustand(projectPath, config);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (config.husky) {
|
|
57
|
+
await setupHusky(projectPath, config);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function appDirFor(projectPath, config) {
|
|
62
|
+
return config.srcDir ? path.join(projectPath, "src", "app") : path.join(projectPath, "app");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function storeDirFor(projectPath, config) {
|
|
66
|
+
return config.srcDir ? path.join(projectPath, "src", "store") : path.join(projectPath, "store");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function setupRedux(projectPath, config) {
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(pc.cyan("◆") + " Setting up Redux Toolkit...");
|
|
72
|
+
console.log();
|
|
73
|
+
|
|
74
|
+
const installer = installerFor(config.packageManager, false);
|
|
75
|
+
await run(installer.cmd, [...installer.args, "@reduxjs/toolkit", "react-redux"], { cwd: projectPath });
|
|
76
|
+
|
|
77
|
+
const storeDir = storeDirFor(projectPath, config);
|
|
78
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
79
|
+
|
|
80
|
+
const ts = config.typescript;
|
|
81
|
+
const storeExt = ts ? "ts" : "js";
|
|
82
|
+
const compExt = ts ? "tsx" : "js";
|
|
83
|
+
|
|
84
|
+
const storeIndex = ts
|
|
85
|
+
? `import { configureStore } from "@reduxjs/toolkit";
|
|
86
|
+
import counterReducer from "./counterSlice";
|
|
87
|
+
|
|
88
|
+
export const store = configureStore({
|
|
89
|
+
reducer: {
|
|
90
|
+
counter: counterReducer,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
95
|
+
export type AppDispatch = typeof store.dispatch;
|
|
96
|
+
`
|
|
97
|
+
: `import { configureStore } from "@reduxjs/toolkit";
|
|
98
|
+
import counterReducer from "./counterSlice";
|
|
99
|
+
|
|
100
|
+
export const store = configureStore({
|
|
101
|
+
reducer: {
|
|
102
|
+
counter: counterReducer,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
`;
|
|
106
|
+
fs.writeFileSync(path.join(storeDir, `index.${storeExt}`), storeIndex);
|
|
107
|
+
|
|
108
|
+
const counterSlice = ts
|
|
109
|
+
? `import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|
110
|
+
|
|
111
|
+
interface CounterState {
|
|
112
|
+
value: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const initialState: CounterState = { value: 0 };
|
|
116
|
+
|
|
117
|
+
const counterSlice = createSlice({
|
|
118
|
+
name: "counter",
|
|
119
|
+
initialState,
|
|
120
|
+
reducers: {
|
|
121
|
+
increment: (state) => {
|
|
122
|
+
state.value += 1;
|
|
123
|
+
},
|
|
124
|
+
decrement: (state) => {
|
|
125
|
+
state.value -= 1;
|
|
126
|
+
},
|
|
127
|
+
incrementByAmount: (state, action: PayloadAction<number>) => {
|
|
128
|
+
state.value += action.payload;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
|
|
134
|
+
export default counterSlice.reducer;
|
|
135
|
+
`
|
|
136
|
+
: `import { createSlice } from "@reduxjs/toolkit";
|
|
137
|
+
|
|
138
|
+
const initialState = { value: 0 };
|
|
139
|
+
|
|
140
|
+
const counterSlice = createSlice({
|
|
141
|
+
name: "counter",
|
|
142
|
+
initialState,
|
|
143
|
+
reducers: {
|
|
144
|
+
increment: (state) => {
|
|
145
|
+
state.value += 1;
|
|
146
|
+
},
|
|
147
|
+
decrement: (state) => {
|
|
148
|
+
state.value -= 1;
|
|
149
|
+
},
|
|
150
|
+
incrementByAmount: (state, action) => {
|
|
151
|
+
state.value += action.payload;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
|
|
157
|
+
export default counterSlice.reducer;
|
|
158
|
+
`;
|
|
159
|
+
fs.writeFileSync(path.join(storeDir, `counterSlice.${storeExt}`), counterSlice);
|
|
160
|
+
|
|
161
|
+
if (ts) {
|
|
162
|
+
const hooks = `import { useDispatch, useSelector } from "react-redux";
|
|
163
|
+
import type { TypedUseSelectorHook } from "react-redux";
|
|
164
|
+
import type { RootState, AppDispatch } from "./index";
|
|
165
|
+
|
|
166
|
+
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
167
|
+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
168
|
+
`;
|
|
169
|
+
fs.writeFileSync(path.join(storeDir, "hooks.ts"), hooks);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const appDir = appDirFor(projectPath, config);
|
|
173
|
+
const providers = ts
|
|
174
|
+
? `"use client";
|
|
175
|
+
|
|
176
|
+
import { Provider } from "react-redux";
|
|
177
|
+
import { store } from "@/store";
|
|
178
|
+
|
|
179
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
180
|
+
return <Provider store={store}>{children}</Provider>;
|
|
181
|
+
}
|
|
182
|
+
`
|
|
183
|
+
: `"use client";
|
|
184
|
+
|
|
185
|
+
import { Provider } from "react-redux";
|
|
186
|
+
import { store } from "@/store";
|
|
187
|
+
|
|
188
|
+
export function Providers({ children }) {
|
|
189
|
+
return <Provider store={store}>{children}</Provider>;
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
fs.writeFileSync(path.join(appDir, `providers.${compExt}`), providers);
|
|
193
|
+
|
|
194
|
+
patchLayoutWithProviders(appDir, ts);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function setupZustand(projectPath, config) {
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(pc.cyan("◆") + " Setting up Zustand...");
|
|
200
|
+
console.log();
|
|
201
|
+
|
|
202
|
+
const installer = installerFor(config.packageManager, false);
|
|
203
|
+
await run(installer.cmd, [...installer.args, "zustand"], { cwd: projectPath });
|
|
204
|
+
|
|
205
|
+
const storeDir = storeDirFor(projectPath, config);
|
|
206
|
+
fs.mkdirSync(storeDir, { recursive: true });
|
|
207
|
+
|
|
208
|
+
const ts = config.typescript;
|
|
209
|
+
const ext = ts ? "ts" : "js";
|
|
210
|
+
const contents = ts
|
|
211
|
+
? `import { create } from "zustand";
|
|
212
|
+
|
|
213
|
+
interface CounterState {
|
|
214
|
+
count: number;
|
|
215
|
+
increment: () => void;
|
|
216
|
+
decrement: () => void;
|
|
217
|
+
reset: () => void;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export const useCounterStore = create<CounterState>((set) => ({
|
|
221
|
+
count: 0,
|
|
222
|
+
increment: () => set((s) => ({ count: s.count + 1 })),
|
|
223
|
+
decrement: () => set((s) => ({ count: s.count - 1 })),
|
|
224
|
+
reset: () => set({ count: 0 }),
|
|
225
|
+
}));
|
|
226
|
+
`
|
|
227
|
+
: `import { create } from "zustand";
|
|
228
|
+
|
|
229
|
+
export const useCounterStore = create((set) => ({
|
|
230
|
+
count: 0,
|
|
231
|
+
increment: () => set((s) => ({ count: s.count + 1 })),
|
|
232
|
+
decrement: () => set((s) => ({ count: s.count - 1 })),
|
|
233
|
+
reset: () => set({ count: 0 }),
|
|
234
|
+
}));
|
|
235
|
+
`;
|
|
236
|
+
fs.writeFileSync(path.join(storeDir, `useCounterStore.${ext}`), contents);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function patchLayoutWithProviders(appDir, ts) {
|
|
240
|
+
const layoutPath = path.join(appDir, ts ? "layout.tsx" : "layout.js");
|
|
241
|
+
if (!fs.existsSync(layoutPath)) {
|
|
242
|
+
console.log(pc.yellow("⚠") + ` Could not find ${path.basename(layoutPath)} — wrap {children} with <Providers> manually.`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let layout = fs.readFileSync(layoutPath, "utf8");
|
|
247
|
+
const original = layout;
|
|
248
|
+
|
|
249
|
+
if (!layout.includes(`from "./providers"`)) {
|
|
250
|
+
if (/import\s+["']\.\/globals\.css["'];?/.test(layout)) {
|
|
251
|
+
layout = layout.replace(
|
|
252
|
+
/(import\s+["']\.\/globals\.css["'];?)/,
|
|
253
|
+
`$1\nimport { Providers } from "./providers";`
|
|
254
|
+
);
|
|
255
|
+
} else {
|
|
256
|
+
layout = `import { Providers } from "./providers";\n${layout}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!layout.includes("<Providers>") && layout.includes("{children}")) {
|
|
261
|
+
layout = layout.replace("{children}", "<Providers>{children}</Providers>");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (layout === original) {
|
|
265
|
+
console.log(pc.yellow("⚠") + " Could not auto-wrap layout — please wrap {children} with <Providers> manually.");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fs.writeFileSync(layoutPath, layout);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function setupHusky(projectPath, config) {
|
|
273
|
+
console.log();
|
|
274
|
+
console.log(pc.cyan("◆") + " Setting up Husky + lint-staged + Prettier...");
|
|
275
|
+
console.log();
|
|
276
|
+
|
|
277
|
+
const installer = installerFor(config.packageManager, true);
|
|
278
|
+
await run(
|
|
279
|
+
installer.cmd,
|
|
280
|
+
[...installer.args, "husky@^8", "lint-staged", "prettier"],
|
|
281
|
+
{ cwd: projectPath }
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const pkgPath = path.join(projectPath, "package.json");
|
|
285
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
286
|
+
pkg.scripts = {
|
|
287
|
+
...pkg.scripts,
|
|
288
|
+
prepare: "husky install",
|
|
289
|
+
format: "prettier --check .",
|
|
290
|
+
"format:fix": "prettier --write .",
|
|
291
|
+
"lint:fix": "eslint --fix",
|
|
292
|
+
...(config.typescript ? { typecheck: "tsc --noEmit" } : {}),
|
|
293
|
+
};
|
|
294
|
+
delete pkg["lint-staged"];
|
|
295
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
296
|
+
|
|
297
|
+
const runner = runnerFor(config.packageManager);
|
|
298
|
+
await run(runner.cmd, [...runner.args, "husky", "install"], {
|
|
299
|
+
cwd: projectPath,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await run("git", ["config", "core.hooksPath", ".husky"], { cwd: projectPath });
|
|
303
|
+
|
|
304
|
+
const huskyDir = path.join(projectPath, ".husky");
|
|
305
|
+
if (!fs.existsSync(huskyDir)) fs.mkdirSync(huskyDir, { recursive: true });
|
|
306
|
+
|
|
307
|
+
const hookCmd = config.typescript
|
|
308
|
+
? "npx tsc --noEmit && npx lint-staged"
|
|
309
|
+
: "npx lint-staged";
|
|
310
|
+
const preCommit = `#!/usr/bin/env sh
|
|
311
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
312
|
+
|
|
313
|
+
${hookCmd}
|
|
314
|
+
`;
|
|
315
|
+
const preCommitPath = path.join(huskyDir, "pre-commit");
|
|
316
|
+
fs.writeFileSync(preCommitPath, preCommit);
|
|
317
|
+
fs.chmodSync(preCommitPath, 0o755);
|
|
318
|
+
|
|
319
|
+
const jsGlob = config.typescript ? "*.{js,jsx,ts,tsx}" : "*.{js,jsx}";
|
|
320
|
+
const lintStagedConfig = {
|
|
321
|
+
[jsGlob]: ["eslint --fix", "prettier --write"],
|
|
322
|
+
"*.{json,css,scss,md,mdx,yml,yaml,html}": ["prettier --write"],
|
|
323
|
+
};
|
|
324
|
+
fs.writeFileSync(
|
|
325
|
+
path.join(projectPath, ".lintstagedrc.json"),
|
|
326
|
+
JSON.stringify(lintStagedConfig, null, 2) + "\n"
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const prettierrc = {
|
|
330
|
+
semi: true,
|
|
331
|
+
singleQuote: false,
|
|
332
|
+
tabWidth: 2,
|
|
333
|
+
trailingComma: "es5",
|
|
334
|
+
printWidth: 100,
|
|
335
|
+
arrowParens: "always",
|
|
336
|
+
endOfLine: "lf",
|
|
337
|
+
};
|
|
338
|
+
fs.writeFileSync(path.join(projectPath, ".prettierrc"), JSON.stringify(prettierrc, null, 2) + "\n");
|
|
339
|
+
|
|
340
|
+
const prettierIgnore = [
|
|
341
|
+
"node_modules",
|
|
342
|
+
".next",
|
|
343
|
+
"out",
|
|
344
|
+
"build",
|
|
345
|
+
"dist",
|
|
346
|
+
"coverage",
|
|
347
|
+
"package-lock.json",
|
|
348
|
+
"pnpm-lock.yaml",
|
|
349
|
+
"yarn.lock",
|
|
350
|
+
"bun.lockb",
|
|
351
|
+
"",
|
|
352
|
+
].join("\n");
|
|
353
|
+
fs.writeFileSync(path.join(projectPath, ".prettierignore"), prettierIgnore);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function installerFor(pm, dev = true) {
|
|
357
|
+
switch (pm) {
|
|
358
|
+
case "pnpm":
|
|
359
|
+
return { cmd: "pnpm", args: dev ? ["add", "-D"] : ["add"] };
|
|
360
|
+
case "yarn":
|
|
361
|
+
return { cmd: "yarn", args: dev ? ["add", "-D"] : ["add"] };
|
|
362
|
+
case "bun":
|
|
363
|
+
return { cmd: "bun", args: dev ? ["add", "-d"] : ["add"] };
|
|
364
|
+
case "npm":
|
|
365
|
+
default:
|
|
366
|
+
return { cmd: "npm", args: dev ? ["install", "-D"] : ["install"] };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function runnerFor(pm) {
|
|
371
|
+
switch (pm) {
|
|
372
|
+
case "pnpm":
|
|
373
|
+
return { cmd: "pnpm", args: ["dlx"] };
|
|
374
|
+
case "yarn":
|
|
375
|
+
return { cmd: "yarn", args: ["dlx"] };
|
|
376
|
+
case "bun":
|
|
377
|
+
return { cmd: "bunx", args: [] };
|
|
378
|
+
case "npm":
|
|
379
|
+
default:
|
|
380
|
+
return { cmd: "npx", args: [] };
|
|
381
|
+
}
|
|
382
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function parseArgs(argv) {
|
|
4
|
+
const positional = [];
|
|
5
|
+
const flags = {};
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < argv.length; i++) {
|
|
8
|
+
const arg = argv[i];
|
|
9
|
+
if (arg.startsWith("--")) {
|
|
10
|
+
const eq = arg.indexOf("=");
|
|
11
|
+
if (eq !== -1) {
|
|
12
|
+
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
|
|
13
|
+
} else {
|
|
14
|
+
const key = arg.slice(2);
|
|
15
|
+
const next = argv[i + 1];
|
|
16
|
+
if (next && !next.startsWith("-")) {
|
|
17
|
+
flags[key] = next;
|
|
18
|
+
i++;
|
|
19
|
+
} else {
|
|
20
|
+
flags[key] = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
24
|
+
flags[arg.slice(1)] = true;
|
|
25
|
+
} else {
|
|
26
|
+
positional.push(arg);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pm = flags.pnpm ? "pnpm" : flags.yarn ? "yarn" : flags.bun ? "bun" : flags.npm ? "npm" : undefined;
|
|
31
|
+
|
|
32
|
+
let ts;
|
|
33
|
+
if (flags.ts || flags.typescript) ts = true;
|
|
34
|
+
else if (flags.js || flags.javascript) ts = false;
|
|
35
|
+
|
|
36
|
+
const state = ["redux", "zustand", "none"].includes(flags.state) ? flags.state : undefined;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
projectName: positional[0],
|
|
40
|
+
yes: Boolean(flags.yes || flags.y),
|
|
41
|
+
help: Boolean(flags.help || flags.h),
|
|
42
|
+
version: Boolean(flags.version || flags.v),
|
|
43
|
+
typescript: ts,
|
|
44
|
+
packageManager: pm,
|
|
45
|
+
husky: flags["no-husky"] ? false : undefined,
|
|
46
|
+
state,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function run(cmd, args, options = {}) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const child = spawn(cmd, args, {
|
|
53
|
+
stdio: "inherit",
|
|
54
|
+
shell: process.platform === "win32",
|
|
55
|
+
...options,
|
|
56
|
+
});
|
|
57
|
+
child.on("error", reject);
|
|
58
|
+
child.on("close", (code) => {
|
|
59
|
+
if (code === 0) resolve();
|
|
60
|
+
else reject(new Error(`Command failed: ${cmd} ${args.join(" ")} (exit ${code})`));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isValidProjectName(name) {
|
|
66
|
+
if (!name || typeof name !== "string") return false;
|
|
67
|
+
if (name.length > 214) return false;
|
|
68
|
+
if (name === "." || name === "..") return false;
|
|
69
|
+
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i.test(name);
|
|
70
|
+
}
|