create-prodkit 1.0.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 +119 -0
- package/bin/index.js +10 -0
- package/package.json +66 -0
- package/src/cli.js +35 -0
- package/src/features/absoluteImports.js +23 -0
- package/src/features/husky.js +26 -0
- package/src/features/index.js +13 -0
- package/src/features/prettier.js +36 -0
- package/src/features/releaseIt.js +34 -0
- package/src/features/tailwindUtils.js +30 -0
- package/src/installer.js +66 -0
- package/src/prompts.js +106 -0
- package/src/scaffold.js +105 -0
- package/src/utils/logger.js +20 -0
- package/src/utils/modifyPackageJson.js +58 -0
- package/src/utils/runStep.js +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lalit Kakkar
|
|
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,119 @@
|
|
|
1
|
+
# create-prodkit
|
|
2
|
+
|
|
3
|
+
A CLI to scaffold a production-ready Next.js project with only the tools you actually want — no bloat, no manual setup every time.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-prodkit my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or scaffold into the current folder:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx create-prodkit .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What it sets up
|
|
18
|
+
|
|
19
|
+
When you run the CLI it asks a few questions then sets everything up in one shot:
|
|
20
|
+
|
|
21
|
+
**Package manager** — choose between npm, yarn, or pnpm
|
|
22
|
+
|
|
23
|
+
**Features** — pick what you need:
|
|
24
|
+
|
|
25
|
+
| Feature | What it does |
|
|
26
|
+
| ----------------- | ------------------------------------------------------- |
|
|
27
|
+
| Tailwind CSS | Installed via `create-next-app` + `cn()` utility helper |
|
|
28
|
+
| ESLint + Prettier | Prettier config with import sorting |
|
|
29
|
+
| Husky | Git hooks with lint-staged pre-commit |
|
|
30
|
+
| release-it | Changelog generation + semantic versioning |
|
|
31
|
+
| Absolute imports | `@/*`, `@components/*`, `@lib/*`, `@hooks/*` |
|
|
32
|
+
|
|
33
|
+
## Project structure
|
|
34
|
+
|
|
35
|
+
Projects are scaffolded with the `app/` directory at the root (no `src/` wrapper):
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
my-app/
|
|
39
|
+
├── app/
|
|
40
|
+
│ ├── globals.css
|
|
41
|
+
│ ├── layout.tsx
|
|
42
|
+
│ └── page.tsx
|
|
43
|
+
├── components/ ← added if absolute-imports selected
|
|
44
|
+
├── lib/
|
|
45
|
+
│ └── utils.ts ← cn() helper (added if Tailwind selected)
|
|
46
|
+
├── hooks/
|
|
47
|
+
├── public/
|
|
48
|
+
├── next.config.ts
|
|
49
|
+
└── package.json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Feature details
|
|
53
|
+
|
|
54
|
+
### Tailwind CSS
|
|
55
|
+
|
|
56
|
+
Installed via the `create-next-app` `--tailwind` flag. When selected, two utilities are also added:
|
|
57
|
+
|
|
58
|
+
- `clsx` — conditional class helper
|
|
59
|
+
- `tailwind-merge` — merges Tailwind classes without conflicts
|
|
60
|
+
|
|
61
|
+
A `lib/utils.ts` file is created with a ready-to-use `cn()` helper:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { cn } from '@/lib/utils'
|
|
65
|
+
|
|
66
|
+
// Conditional classes, deduplication, conflict resolution — all handled
|
|
67
|
+
<div className={cn('px-4 py-2', isActive && 'bg-blue-500', 'text-sm')} />
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### ESLint + Prettier
|
|
71
|
+
|
|
72
|
+
Adds `.prettierrc` with sane defaults — single quotes, no semicolons, 100 char print width. Also adds `@trivago/prettier-plugin-sort-imports` which auto-sorts your imports on save:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { useState } from "react";
|
|
76
|
+
|
|
77
|
+
import Link from "next/link";
|
|
78
|
+
|
|
79
|
+
import axios from "axios";
|
|
80
|
+
|
|
81
|
+
import { Button } from "@/components/ui/button";
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Husky
|
|
85
|
+
|
|
86
|
+
Sets up `.husky/` with your choice of hooks:
|
|
87
|
+
|
|
88
|
+
- `pre-commit` — runs lint-staged on changed files only before every commit
|
|
89
|
+
- `commit-msg` — runs commitlint to enforce conventional commit format (installs `@commitlint/cli` + `@commitlint/config-conventional` automatically)
|
|
90
|
+
|
|
91
|
+
### release-it
|
|
92
|
+
|
|
93
|
+
Replaces the deprecated `standard-version`. Adds `npm run release` scripts and a `.release-it.json` config with conventional changelog:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm run release # auto bump + generate CHANGELOG.md
|
|
97
|
+
npm run release:patch # force patch bump
|
|
98
|
+
npm run release:minor # force minor bump
|
|
99
|
+
npm run release:major # force major bump
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Absolute imports
|
|
103
|
+
|
|
104
|
+
Extends `tsconfig.json` with path aliases so you never write `../../../` again:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { Button } from "@/components/ui/button";
|
|
108
|
+
import { useAuth } from "@hooks/useAuth";
|
|
109
|
+
import { formatDate } from "@lib/utils";
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
- Node.js 18+
|
|
115
|
+
- Git installed (required if Husky selected)
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
package/bin/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-prodkit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for scaffolding production-ready Next.js applications with TypeScript, Tailwind CSS, ESLint, Prettier and modern project tooling.",
|
|
5
|
+
"main": "src/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-prodkit": "bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "node bin/index.js",
|
|
16
|
+
"prepare": "husky",
|
|
17
|
+
"release": "release-it",
|
|
18
|
+
"release:patch": "release-it patch",
|
|
19
|
+
"release:minor": "release-it minor",
|
|
20
|
+
"release:major": "release-it major"
|
|
21
|
+
},
|
|
22
|
+
"lint-staged": {
|
|
23
|
+
"**/*.js": [
|
|
24
|
+
"prettier --write"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@release-it/conventional-changelog": "^11.0.1",
|
|
29
|
+
"husky": "^9.0.0",
|
|
30
|
+
"lint-staged": "^15.2.0",
|
|
31
|
+
"release-it": "^20.2.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"nextjs",
|
|
35
|
+
"cli",
|
|
36
|
+
"scaffold",
|
|
37
|
+
"starter-template",
|
|
38
|
+
"tailwindcss",
|
|
39
|
+
"typescript",
|
|
40
|
+
"eslint",
|
|
41
|
+
"prettier",
|
|
42
|
+
"nextjs-boilerplate",
|
|
43
|
+
"developer-tools"
|
|
44
|
+
],
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"author": "Lalit Kakkar <lalitkakkar50@gmail.com>",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/kakkar2/create-prodkit.git"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/kakkar2/create-prodkit#readme",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/kakkar2/create-prodkit/issues"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"commander": "^12.1.0",
|
|
57
|
+
"execa": "^8.0.1",
|
|
58
|
+
"fs-extra": "^11.3.5",
|
|
59
|
+
"inquirer": "^9.3.8",
|
|
60
|
+
"ora": "^8.2.0",
|
|
61
|
+
"picocolors": "^1.1.1"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { getProjectOptions } from "./prompts.js";
|
|
4
|
+
import { createProject } from "./scaffold.js";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const { version } = require("../package.json");
|
|
9
|
+
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
export async function run() {
|
|
13
|
+
program
|
|
14
|
+
.name("create-prodkit")
|
|
15
|
+
.description("Scaffold a production-ready project")
|
|
16
|
+
.version(version)
|
|
17
|
+
.argument("[project-name]", "Name of the project folder to create")
|
|
18
|
+
.action(async (projectName) => {
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(pc.bold(pc.cyan(" >> create-prodkit")));
|
|
21
|
+
console.log(pc.dim(" A production-ready project scaffold\n"));
|
|
22
|
+
|
|
23
|
+
const options = await getProjectOptions(projectName); // ← add this
|
|
24
|
+
|
|
25
|
+
if (!options.confirmed) {
|
|
26
|
+
// ← add this
|
|
27
|
+
console.log(pc.dim("\n Cancelled.\n"));
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await createProject(options); // ← pass full options now
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
program.parse();
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { runStep } from "../utils/runStep.js";
|
|
4
|
+
|
|
5
|
+
export async function setupAbsoluteImports(targetDir) {
|
|
6
|
+
await runStep("Configuring absolute imports...", async () => {
|
|
7
|
+
const tsconfigPath = path.join(targetDir, "tsconfig.json");
|
|
8
|
+
const tsconfig = await fs.readJson(tsconfigPath);
|
|
9
|
+
|
|
10
|
+
tsconfig.compilerOptions = tsconfig.compilerOptions || {};
|
|
11
|
+
tsconfig.compilerOptions.baseUrl = ".";
|
|
12
|
+
// Merge with any existing paths (create-next-app sets @/* already)
|
|
13
|
+
tsconfig.compilerOptions.paths = {
|
|
14
|
+
...(tsconfig.compilerOptions.paths || {}),
|
|
15
|
+
"@/*": ["./*"], // root alias
|
|
16
|
+
"@components/*": ["./components/*"],
|
|
17
|
+
"@lib/*": ["./lib/*"],
|
|
18
|
+
"@hooks/*": ["./hooks/*"],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
await fs.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import { runStep } from "../utils/runStep.js";
|
|
5
|
+
|
|
6
|
+
export async function setupHusky({ targetDir, huskyHooks }) {
|
|
7
|
+
await runStep("Setting up Husky hooks...", async () => {
|
|
8
|
+
await execa("npx", ["husky", "init"], { cwd: targetDir, stdio: "pipe" });
|
|
9
|
+
|
|
10
|
+
if (huskyHooks.includes("pre-commit")) {
|
|
11
|
+
const hookPath = path.join(targetDir, ".husky", "pre-commit");
|
|
12
|
+
await fs.outputFile(hookPath, "npx lint-staged\n");
|
|
13
|
+
// husky v9 sets executable bit automatically during init — no chmod needed
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (huskyHooks.includes("commit-msg")) {
|
|
17
|
+
const hookPath = path.join(targetDir, ".husky", "commit-msg");
|
|
18
|
+
await fs.outputFile(hookPath, "npx --no -- commitlint --edit $1\n");
|
|
19
|
+
// commitlint config — uses conventional commit rules
|
|
20
|
+
await fs.outputFile(
|
|
21
|
+
path.join(targetDir, "commitlint.config.js"),
|
|
22
|
+
`export default { extends: ['@commitlint/config-conventional'] }\n`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { setupHusky } from "./husky.js";
|
|
2
|
+
import { setupReleaseIt } from "./releaseIt.js";
|
|
3
|
+
import { setupPrettier } from "./prettier.js";
|
|
4
|
+
import { setupAbsoluteImports } from "./absoluteImports.js";
|
|
5
|
+
import { setupTailwindUtils } from "./tailwindUtils.js";
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
setupHusky,
|
|
9
|
+
setupReleaseIt,
|
|
10
|
+
setupPrettier,
|
|
11
|
+
setupAbsoluteImports,
|
|
12
|
+
setupTailwindUtils,
|
|
13
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { runStep } from "../utils/runStep.js";
|
|
4
|
+
|
|
5
|
+
export async function setupPrettier(targetDir) {
|
|
6
|
+
await runStep("Adding Prettier config...", async () => {
|
|
7
|
+
await fs.writeJson(
|
|
8
|
+
path.join(targetDir, ".prettierrc"),
|
|
9
|
+
{
|
|
10
|
+
semi: false,
|
|
11
|
+
singleQuote: true,
|
|
12
|
+
tabWidth: 2,
|
|
13
|
+
trailingComma: "es5",
|
|
14
|
+
printWidth: 100,
|
|
15
|
+
plugins: ["@trivago/prettier-plugin-sort-imports"],
|
|
16
|
+
importOrder: [
|
|
17
|
+
"^react(.*)$",
|
|
18
|
+
"^next(.*)$",
|
|
19
|
+
"<THIRD_PARTY_MODULES>",
|
|
20
|
+
"^@/(.*)$",
|
|
21
|
+
"^[./]",
|
|
22
|
+
],
|
|
23
|
+
importOrderSeparation: true,
|
|
24
|
+
importOrderSortSpecifiers: true,
|
|
25
|
+
},
|
|
26
|
+
{ spaces: 2 },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const eslintPath = path.join(targetDir, ".eslintrc.json");
|
|
30
|
+
if (await fs.pathExists(eslintPath)) {
|
|
31
|
+
const eslint = await fs.readJson(eslintPath);
|
|
32
|
+
eslint.extends = [...(eslint.extends || []), "prettier"];
|
|
33
|
+
await fs.writeJson(eslintPath, eslint, { spaces: 2 });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { runStep } from "../utils/runStep.js";
|
|
4
|
+
|
|
5
|
+
export async function setupReleaseIt(targetDir) {
|
|
6
|
+
await runStep("Configuring release-it...", async () => {
|
|
7
|
+
const config = {
|
|
8
|
+
git: {
|
|
9
|
+
commitMessage: "chore: release v${version}",
|
|
10
|
+
tagName: "v${version}",
|
|
11
|
+
requireCleanWorkingDir: false,
|
|
12
|
+
},
|
|
13
|
+
npm: {
|
|
14
|
+
publish: false,
|
|
15
|
+
},
|
|
16
|
+
github: {
|
|
17
|
+
release: false,
|
|
18
|
+
},
|
|
19
|
+
plugins: {
|
|
20
|
+
"@release-it/conventional-changelog": {
|
|
21
|
+
preset: {
|
|
22
|
+
name: "angular",
|
|
23
|
+
},
|
|
24
|
+
infile: "CHANGELOG.md",
|
|
25
|
+
header: "# Changelog\n\nAll notable changes are documented here.\n",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
await fs.writeJson(path.join(targetDir, ".release-it.json"), config, {
|
|
31
|
+
spaces: 2,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { runStep } from "../utils/runStep.js";
|
|
4
|
+
|
|
5
|
+
export async function setupTailwindUtils(targetDir) {
|
|
6
|
+
await runStep("Adding clsx + tailwind-merge utils...", async () => {
|
|
7
|
+
// Create lib/ directory at project root (no src/ dir)
|
|
8
|
+
const libDir = path.join(targetDir, "lib");
|
|
9
|
+
await fs.ensureDir(libDir);
|
|
10
|
+
|
|
11
|
+
// cn() helper — merges Tailwind classes safely, deduplicates conflicts
|
|
12
|
+
const utilsContent = `import { type ClassValue, clsx } from 'clsx'
|
|
13
|
+
import { twMerge } from 'tailwind-merge'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Merge Tailwind CSS classes safely.
|
|
17
|
+
* Resolves class conflicts (e.g. p-4 + p-2 → p-2) and
|
|
18
|
+
* supports conditional classes via clsx syntax.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* cn('px-4 py-2', isActive && 'bg-blue-500', 'text-sm')
|
|
22
|
+
*/
|
|
23
|
+
export function cn(...inputs: ClassValue[]) {
|
|
24
|
+
return twMerge(clsx(inputs))
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
await fs.writeFile(path.join(libDir, "utils.ts"), utilsContent, "utf-8");
|
|
29
|
+
});
|
|
30
|
+
}
|
package/src/installer.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { logger } from "./utils/logger.js";
|
|
5
|
+
import {
|
|
6
|
+
setupAbsoluteImports,
|
|
7
|
+
setupPrettier,
|
|
8
|
+
setupReleaseIt,
|
|
9
|
+
setupHusky,
|
|
10
|
+
setupTailwindUtils,
|
|
11
|
+
} from "./features/index.js";
|
|
12
|
+
import { runStep } from "./utils/runStep.js";
|
|
13
|
+
import { modifyPackageJson } from "./utils/modifyPackageJson.js";
|
|
14
|
+
|
|
15
|
+
export async function installFeatures(options) {
|
|
16
|
+
const { targetDir, packageManager, features, huskyHooks } = options;
|
|
17
|
+
|
|
18
|
+
// ── 1. Modify package.json before install ─────────────────────
|
|
19
|
+
// Pass huskyHooks so commitlint deps are added when commit-msg is selected
|
|
20
|
+
await modifyPackageJson(targetDir, features, packageManager, huskyHooks);
|
|
21
|
+
|
|
22
|
+
// ── 2. git init (must happen before husky setup) ──────────────
|
|
23
|
+
if (features.includes("husky")) {
|
|
24
|
+
await runStep("Initializing git repo...", async () => {
|
|
25
|
+
await execa("git", ["init"], { cwd: targetDir });
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── 3. Single install pass (picks up all deps we just added) ──
|
|
30
|
+
await runStep(
|
|
31
|
+
`Installing dependencies with ${packageManager}...`,
|
|
32
|
+
async () => {
|
|
33
|
+
await execa(packageManager, ["install"], {
|
|
34
|
+
cwd: targetDir,
|
|
35
|
+
stdio: "pipe",
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// ── 4. Husky setup ────────────────────────────────────────────
|
|
41
|
+
if (features.includes("husky")) {
|
|
42
|
+
await setupHusky({ targetDir, huskyHooks, packageManager });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── 5. release-it config ──────────────────────────────────────
|
|
46
|
+
if (features.includes("release-it")) {
|
|
47
|
+
await setupReleaseIt(targetDir);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── 6. Prettier config ────────────────────────────────────────
|
|
51
|
+
if (features.includes("eslint")) {
|
|
52
|
+
await setupPrettier(targetDir);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── 7. Tailwind utils (clsx + tailwind-merge cn helper) ───────
|
|
56
|
+
if (features.includes("tailwind")) {
|
|
57
|
+
await setupTailwindUtils(targetDir);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── 8. Absolute imports ───────────────────────────────────────
|
|
61
|
+
if (features.includes("absolute-imports")) {
|
|
62
|
+
await setupAbsoluteImports(targetDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logger.success("All features installed\n");
|
|
66
|
+
}
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
|
|
5
|
+
export async function getProjectOptions(projectNameArg) {
|
|
6
|
+
const answers = await inquirer.prompt([
|
|
7
|
+
// ── 1. Project name ──────────────────────────────────────────
|
|
8
|
+
{
|
|
9
|
+
type: "input",
|
|
10
|
+
name: "projectName",
|
|
11
|
+
message: "Project name:",
|
|
12
|
+
default: projectNameArg || "my-next-app",
|
|
13
|
+
when: !projectNameArg,
|
|
14
|
+
validate: (input) => {
|
|
15
|
+
if (!input.trim()) return "Project name cannot be empty";
|
|
16
|
+
if (!/^[a-z0-9-_]+$/.test(input))
|
|
17
|
+
return "Only lowercase letters, numbers, - and _ allowed";
|
|
18
|
+
return true;
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// ── 2. Package manager ───────────────────────────────────────
|
|
23
|
+
{
|
|
24
|
+
type: "list",
|
|
25
|
+
name: "packageManager",
|
|
26
|
+
message: "Package manager:",
|
|
27
|
+
choices: ["npm", "yarn", "pnpm"],
|
|
28
|
+
default: "npm",
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// ── 3. Feature selection ─────────────────────────────────────
|
|
32
|
+
{
|
|
33
|
+
type: "checkbox",
|
|
34
|
+
name: "features",
|
|
35
|
+
message: "Select features to include:",
|
|
36
|
+
choices: [
|
|
37
|
+
{
|
|
38
|
+
name: "Tailwind CSS (+ clsx & tailwind-merge cn helper)",
|
|
39
|
+
value: "tailwind",
|
|
40
|
+
checked: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "ESLint + Prettier",
|
|
44
|
+
value: "eslint",
|
|
45
|
+
checked: true,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "Husky (git hooks + pre-commit lint-staged)",
|
|
49
|
+
value: "husky",
|
|
50
|
+
checked: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "release-it (changelog + semantic versioning)",
|
|
54
|
+
value: "release-it",
|
|
55
|
+
checked: false,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "Absolute imports (@/* → root, @components/*, @lib/*, @hooks/*)",
|
|
59
|
+
value: "absolute-imports",
|
|
60
|
+
checked: true,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// ── 4. Husky hook selection ───────────────────────────────────
|
|
66
|
+
{
|
|
67
|
+
type: "checkbox",
|
|
68
|
+
name: "huskyHooks",
|
|
69
|
+
message: "Which git hooks do you want?",
|
|
70
|
+
choices: [
|
|
71
|
+
{
|
|
72
|
+
name: "pre-commit → lint-staged (lint + format changed files)",
|
|
73
|
+
value: "pre-commit",
|
|
74
|
+
checked: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "commit-msg → commitlint (enforce conventional commits)",
|
|
78
|
+
value: "commit-msg",
|
|
79
|
+
checked: false,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
when: (ans) => ans.features.includes("husky"),
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// ── 5. Confirm before doing anything ─────────────────────────
|
|
86
|
+
{
|
|
87
|
+
type: "confirm",
|
|
88
|
+
name: "confirm",
|
|
89
|
+
message: (ans) => {
|
|
90
|
+
const name = projectNameArg || ans.projectName;
|
|
91
|
+
return `Create project "${name}" with selected options?`;
|
|
92
|
+
},
|
|
93
|
+
default: true,
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const projectName = projectNameArg || answers.projectName;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
projectName,
|
|
101
|
+
packageManager: answers.packageManager,
|
|
102
|
+
features: answers.features,
|
|
103
|
+
huskyHooks: answers.huskyHooks || [],
|
|
104
|
+
confirmed: answers.confirm,
|
|
105
|
+
};
|
|
106
|
+
}
|
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { execa } from "execa";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { logger } from "./utils/logger.js";
|
|
7
|
+
import { installFeatures } from "./installer.js";
|
|
8
|
+
|
|
9
|
+
export async function createProject(options) {
|
|
10
|
+
const { projectName, packageManager, features } = options;
|
|
11
|
+
|
|
12
|
+
// ── 1. Resolve target directory ───────────────────────────────
|
|
13
|
+
const isSameDir = projectName === ".";
|
|
14
|
+
const targetDir = isSameDir
|
|
15
|
+
? process.cwd()
|
|
16
|
+
: path.resolve(process.cwd(), projectName);
|
|
17
|
+
|
|
18
|
+
const appName = isSameDir ? path.basename(process.cwd()) : projectName;
|
|
19
|
+
|
|
20
|
+
// ── 2. Check if folder exists and has files ───────────────────
|
|
21
|
+
if (!isSameDir && fs.existsSync(targetDir)) {
|
|
22
|
+
const files = fs.readdirSync(targetDir);
|
|
23
|
+
if (files.length > 0) {
|
|
24
|
+
logger.error(`Folder "${projectName}" already exists and is not empty.`);
|
|
25
|
+
logger.dim("Delete it or choose a different name.");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── 3. Build create-next-app args ─────────────────────────────
|
|
31
|
+
const nextArgs = [
|
|
32
|
+
"create-next-app@latest",
|
|
33
|
+
isSameDir ? "." : projectName,
|
|
34
|
+
"--typescript",
|
|
35
|
+
"--app", // use App Router
|
|
36
|
+
"--no-src-dir", // scaffold app/ at root (not inside src/)
|
|
37
|
+
"--no-git", // we'll init git ourselves if husky selected
|
|
38
|
+
"--no-install", // we'll run install ourselves
|
|
39
|
+
"--import-alias",
|
|
40
|
+
"@/*", // default alias → maps to ./* at root
|
|
41
|
+
`--use-${packageManager}`,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// if tailwind selected, pass the flag — otherwise skip it
|
|
45
|
+
if (features.includes("tailwind")) {
|
|
46
|
+
nextArgs.push("--tailwind");
|
|
47
|
+
} else {
|
|
48
|
+
nextArgs.push("--no-tailwind");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ESLint flag
|
|
52
|
+
if (features.includes("eslint")) {
|
|
53
|
+
nextArgs.push("--eslint");
|
|
54
|
+
} else {
|
|
55
|
+
nextArgs.push("--no-eslint");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── 4. Run create-next-app ────────────────────────────────────
|
|
59
|
+
logger.step("Creating Next.js project...");
|
|
60
|
+
|
|
61
|
+
const spinner = ora({
|
|
62
|
+
text: "Running create-next-app...",
|
|
63
|
+
color: "cyan",
|
|
64
|
+
}).start();
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await execa("npx", nextArgs, {
|
|
68
|
+
cwd: isSameDir ? targetDir : process.cwd(),
|
|
69
|
+
stdio: "pipe", // suppress create-next-app's own output
|
|
70
|
+
});
|
|
71
|
+
spinner.succeed(pc.green("Next.js project created"));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
spinner.fail("create-next-app failed");
|
|
74
|
+
logger.error(err.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── 5. Install selected features ──────────────────────────────
|
|
79
|
+
await installFeatures({
|
|
80
|
+
...options,
|
|
81
|
+
targetDir,
|
|
82
|
+
appName,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// ── 6. Done ───────────────────────────────────────────────────
|
|
86
|
+
printSuccess({ appName, isSameDir, projectName, packageManager });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Success message ────────────────────────────────────────────
|
|
90
|
+
function printSuccess({ appName, isSameDir, projectName, packageManager }) {
|
|
91
|
+
const runCmd =
|
|
92
|
+
packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
|
|
93
|
+
|
|
94
|
+
console.log();
|
|
95
|
+
console.log(pc.bold(pc.green(" ✔ Project ready!")));
|
|
96
|
+
console.log();
|
|
97
|
+
|
|
98
|
+
if (!isSameDir) {
|
|
99
|
+
console.log(pc.dim(" Next steps:"));
|
|
100
|
+
console.log(pc.cyan(` cd ${projectName}`));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(pc.cyan(` ${runCmd}`));
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
|
|
3
|
+
// these render correctly in Git Bash, PowerShell, Mac, Linux
|
|
4
|
+
const S = {
|
|
5
|
+
info: pc.cyan("i"),
|
|
6
|
+
success: pc.green("√"),
|
|
7
|
+
warn: pc.yellow("‼"),
|
|
8
|
+
error: pc.red("×"),
|
|
9
|
+
rocket: pc.cyan(">>"),
|
|
10
|
+
bullet: pc.dim("·"),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const logger = {
|
|
14
|
+
info: (msg) => console.log(S.info + " " + msg),
|
|
15
|
+
success: (msg) => console.log(S.success + " " + msg),
|
|
16
|
+
warn: (msg) => console.log(S.warn + " " + msg),
|
|
17
|
+
error: (msg) => console.log(S.error + " " + msg),
|
|
18
|
+
step: (msg) => console.log(pc.bold(pc.white("\n " + msg))),
|
|
19
|
+
dim: (msg) => console.log(pc.dim(" " + msg)),
|
|
20
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
export async function modifyPackageJson(targetDir, features, packageManager, huskyHooks = []) {
|
|
6
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
7
|
+
const pkg = await fs.readJson(pkgPath);
|
|
8
|
+
|
|
9
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
10
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
11
|
+
pkg.scripts = pkg.scripts || {};
|
|
12
|
+
|
|
13
|
+
// ── Tailwind utils ─────────────────────────────────────────────
|
|
14
|
+
if (features.includes("tailwind")) {
|
|
15
|
+
// Runtime deps — used directly in component code
|
|
16
|
+
pkg.dependencies["clsx"] = "^2.1.0";
|
|
17
|
+
pkg.dependencies["tailwind-merge"] = "^2.3.0";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── ESLint + Prettier ──────────────────────────────────────────
|
|
21
|
+
if (features.includes("eslint")) {
|
|
22
|
+
pkg.devDependencies["prettier"] = "^3.2.0";
|
|
23
|
+
pkg.devDependencies["eslint-config-prettier"] = "^9.1.0";
|
|
24
|
+
pkg.devDependencies["@trivago/prettier-plugin-sort-imports"] = "^4.3.0";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Husky ──────────────────────────────────────────────────────
|
|
28
|
+
if (features.includes("husky")) {
|
|
29
|
+
pkg.devDependencies["husky"] = "^9.0.0";
|
|
30
|
+
pkg.devDependencies["lint-staged"] = "^15.2.0";
|
|
31
|
+
pkg["lint-staged"] = {
|
|
32
|
+
"**/*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
|
|
33
|
+
"**/*.{json,css,md}": ["prettier --write"],
|
|
34
|
+
};
|
|
35
|
+
// prepare runs husky on npm install — safe because husky
|
|
36
|
+
// skips silently when .git is absent (e.g. in Docker/CI)
|
|
37
|
+
pkg.scripts["prepare"] = "husky";
|
|
38
|
+
|
|
39
|
+
// commitlint packages — only needed when commit-msg hook is selected
|
|
40
|
+
if (huskyHooks.includes("commit-msg")) {
|
|
41
|
+
pkg.devDependencies["@commitlint/cli"] = "^19.0.0";
|
|
42
|
+
pkg.devDependencies["@commitlint/config-conventional"] = "^19.0.0";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── release-it ────────────────────────────────────────────────
|
|
47
|
+
if (features.includes("release-it")) {
|
|
48
|
+
pkg.devDependencies["release-it"] = "^17.0.0";
|
|
49
|
+
pkg.devDependencies["@release-it/conventional-changelog"] = "^8.0.0";
|
|
50
|
+
pkg.scripts["release"] = "release-it";
|
|
51
|
+
pkg.scripts["release:patch"] = "release-it patch";
|
|
52
|
+
pkg.scripts["release:minor"] = "release-it minor";
|
|
53
|
+
pkg.scripts["release:major"] = "release-it major";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
57
|
+
logger.success("package.json updated");
|
|
58
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
|
|
5
|
+
// ASCII spinner frames — work on every terminal including Git Bash
|
|
6
|
+
const spinner_frames =
|
|
7
|
+
process.platform === "win32"
|
|
8
|
+
? ["-", "\\", "|", "/"] // ASCII fallback for Windows
|
|
9
|
+
: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; // nice on Mac/Linux
|
|
10
|
+
|
|
11
|
+
export async function runStep(message, fn) {
|
|
12
|
+
const spinner = ora({
|
|
13
|
+
text: message,
|
|
14
|
+
color: "cyan",
|
|
15
|
+
spinner: {
|
|
16
|
+
interval: 80,
|
|
17
|
+
frames: spinner_frames,
|
|
18
|
+
},
|
|
19
|
+
}).start();
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await fn();
|
|
23
|
+
spinner.succeed(pc.green(message.replace("...", " done")));
|
|
24
|
+
} catch (err) {
|
|
25
|
+
spinner.fail(pc.red(message.replace("...", " failed")));
|
|
26
|
+
logger.error(err.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
}
|