create-planke 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Espen
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,58 @@
1
+ # planke
2
+
3
+ **The Unified Toolchain Starter for Node.js**
4
+
5
+ Creates a new Node.js TypeScript project using the same opinionated toolchain as [Vite+](https://github.com/voidzero-dev/vite-plus), but for non-web projects.
6
+
7
+ ## Create a new project with planke
8
+
9
+ ```bash
10
+ pnpm create planke@latest
11
+ npm create planke@latest
12
+ ```
13
+
14
+ ## What you get
15
+
16
+ The project follows most of the same toolchain as Vite+.
17
+
18
+ | Tool | Role |
19
+ | -------------------------------------------------- | ------------------ |
20
+ | [Vitest](https://vitest.dev) | Testing |
21
+ | [Oxlint](https://oxc.rs/docs/guide/usage/linter) | Linting |
22
+ | [Oxfmt](https://oxc.rs/docs/guide/usage/formatter) | Formatting |
23
+ | [tsdown](https://tsdown.dev) | Build & bundle |
24
+ | [tsx](https://tsx.is) | Dev-mode execution |
25
+
26
+ ## Backend framework
27
+
28
+ You can also choose one of the following backend frameworks:
29
+
30
+ - **None** — where you don't need a API framework, or you want to pick your own.
31
+ - **Hono** — lightweight, modern API framework
32
+ - **Fastify** — fast and low overhead
33
+ - **Express** — familiar and widely supported
34
+
35
+ ## Scripts
36
+
37
+ Every generated project includes:
38
+
39
+ ```bash
40
+ pnpm dev # Run with tsx (no build step)
41
+ pnpm build # Bundle with tsdown
42
+ pnpm test # Run Vitest
43
+ pnpm check # Lint + format check + type check
44
+ pnpm fmt # Format
45
+ pnpm fmt:check # Check formatting without writing
46
+ pnpm lint # Lint
47
+ pnpm lint:fix # Lint with auto-fix
48
+ ```
49
+
50
+ The `pnpm` commands are just examples, you can also use `npm` or `yarn` or `bun` (depending on your package manager).
51
+
52
+ ## License
53
+
54
+ MIT
55
+
56
+ ## Author
57
+
58
+ - [Espen Steen](https://github.com/ehs5)
package/dist/index.mjs ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import spawn from "cross-spawn";
6
+ import mri from "mri";
7
+ import * as p from "@clack/prompts";
8
+ import pc from "picocolors";
9
+ //#region src/frameworks.ts
10
+ const FRAMEWORKS = [
11
+ {
12
+ value: "none",
13
+ label: "None"
14
+ },
15
+ {
16
+ value: "hono",
17
+ label: "Hono",
18
+ hint: "recommended"
19
+ },
20
+ {
21
+ value: "fastify",
22
+ label: "Fastify"
23
+ },
24
+ {
25
+ value: "express",
26
+ label: "Express"
27
+ }
28
+ ];
29
+ const FRAMEWORK_DEPS = {
30
+ none: {
31
+ deps: [],
32
+ devDeps: []
33
+ },
34
+ hono: {
35
+ deps: ["hono", "@hono/node-server"],
36
+ devDeps: []
37
+ },
38
+ fastify: {
39
+ deps: ["fastify"],
40
+ devDeps: []
41
+ },
42
+ express: {
43
+ deps: ["express"],
44
+ devDeps: ["@types/express"]
45
+ }
46
+ };
47
+ const FRAMEWORK_INDEX = {
48
+ none: `console.log('Hello from planke!')\n`,
49
+ hono: `import { Hono } from 'hono'
50
+ import { serve } from '@hono/node-server'
51
+
52
+ const app = new Hono()
53
+
54
+ app.get('/', (c) => c.text('Hello World'))
55
+
56
+ serve(app, (info) => {
57
+ console.log(\`Server running on http://localhost:\${info.port}\`)
58
+ })
59
+ `,
60
+ fastify: `import Fastify from 'fastify'
61
+
62
+ const fastify = Fastify({ logger: true })
63
+
64
+ fastify.get('/', async () => ({ hello: 'world' }))
65
+
66
+ fastify.listen({ port: 3000 })
67
+ `,
68
+ express: `import express from 'express'
69
+
70
+ const app = express()
71
+
72
+ app.get('/', (req, res) => {
73
+ res.send('Hello World')
74
+ })
75
+
76
+ app.listen(3000, () => {
77
+ console.log('Server running on http://localhost:3000')
78
+ })
79
+ `
80
+ };
81
+ //#endregion
82
+ //#region src/index.ts
83
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
84
+ /** Creates a colored gradient text effect */
85
+ function gradient(text, stops, whiteRange) {
86
+ const chars = [...text];
87
+ return chars.map((char, i) => {
88
+ if (whiteRange && i >= whiteRange[0] && i < whiteRange[1]) return `\x1b[38;2;255;255;255m${char}`;
89
+ const t = chars.length === 1 ? 0 : i / (chars.length - 1);
90
+ const seg = Math.min(Math.floor(t * (stops.length - 1)), stops.length - 2);
91
+ const segT = t * (stops.length - 1) - seg;
92
+ const [r1, g1, b1] = stops[seg];
93
+ const [r2, g2, b2] = stops[seg + 1];
94
+ return `\x1b[38;2;${Math.round(r1 + (r2 - r1) * segT)};${Math.round(g1 + (g2 - g1) * segT)};${Math.round(b1 + (b2 - b1) * segT)}m${char}`;
95
+ }).join("") + "\x1B[0m";
96
+ }
97
+ /** Detects the package manager used to invoke the CLI via npm_config_user_agent. */
98
+ function detectPkgManager() {
99
+ const ua = process.env.npm_config_user_agent ?? "";
100
+ if (ua.startsWith("pnpm")) return "pnpm";
101
+ if (ua.startsWith("bun")) return "bun";
102
+ if (ua.startsWith("yarn")) return "yarn";
103
+ return "npm";
104
+ }
105
+ /** Builds the install/add command args for the given package manager. */
106
+ function addArgs(pkgManager, packages, dev) {
107
+ const cmd = pkgManager === "npm" ? "install" : "add";
108
+ return dev ? [
109
+ cmd,
110
+ {
111
+ npm: "--save-dev",
112
+ pnpm: "-D",
113
+ yarn: "--dev",
114
+ bun: "-d"
115
+ }[pkgManager] ?? "--save-dev",
116
+ ...packages
117
+ ] : [cmd, ...packages];
118
+ }
119
+ /** Runs a command synchronously, exiting the process if it fails. */
120
+ function run(cmd, args, opts) {
121
+ const result = spawn.sync(cmd, args, {
122
+ stdio: "inherit",
123
+ ...opts
124
+ });
125
+ if (result.status != null && result.status !== 0) process.exit(result.status);
126
+ if (result.error) throw result.error;
127
+ }
128
+ /** Recursively copies a directory, renaming _gitignore to .gitignore. */
129
+ function copyDir(src, dest) {
130
+ fs.mkdirSync(dest, { recursive: true });
131
+ for (const entry of fs.readdirSync(src)) {
132
+ const srcPath = path.join(src, entry);
133
+ const destPath = path.join(dest, entry === "_gitignore" ? ".gitignore" : entry);
134
+ if (fs.statSync(srcPath).isDirectory()) copyDir(srcPath, destPath);
135
+ else fs.copyFileSync(srcPath, destPath);
136
+ }
137
+ }
138
+ /** Returns true if a directory doesn't exist or contains only a .git folder. */
139
+ function isEmpty(dir) {
140
+ if (!fs.existsSync(dir)) return true;
141
+ const files = fs.readdirSync(dir);
142
+ return files.length === 0 || files.length === 1 && files[0] === ".git";
143
+ }
144
+ /** Prompts for a project name, or reads it from the first CLI argument. */
145
+ async function promptProjectName(argv) {
146
+ const fromArg = argv._[0] ?? "";
147
+ if (fromArg) return fromArg;
148
+ const answer = await p.text({
149
+ message: "Project name:",
150
+ placeholder: "planke-project",
151
+ defaultValue: "planke-project"
152
+ });
153
+ if (p.isCancel(answer)) {
154
+ p.cancel("Cancelled");
155
+ process.exit(0);
156
+ }
157
+ return answer || "planke-project";
158
+ }
159
+ /** Prompts for a backend framework, or reads it from the --template flag. */
160
+ async function promptFramework(argv) {
161
+ const templateArg = argv.template;
162
+ if (templateArg && FRAMEWORK_DEPS[templateArg]) return templateArg;
163
+ const answer = await p.select({
164
+ message: "Backend framework:",
165
+ options: FRAMEWORKS,
166
+ initialValue: "none"
167
+ });
168
+ if (p.isCancel(answer)) {
169
+ p.cancel("Cancelled");
170
+ process.exit(0);
171
+ }
172
+ return answer;
173
+ }
174
+ /** Asks the user to confirm before wiping a non-empty target directory. */
175
+ async function confirmOverwrite(projectName, targetDir) {
176
+ if (isEmpty(targetDir)) return;
177
+ const overwrite = await p.confirm({ message: `${pc.yellow(projectName)} is not empty. Remove existing files and continue?` });
178
+ if (p.isCancel(overwrite) || !overwrite) {
179
+ p.cancel("Cancelled");
180
+ process.exit(0);
181
+ }
182
+ fs.rmSync(targetDir, {
183
+ recursive: true,
184
+ force: true
185
+ });
186
+ }
187
+ /** Copies the base template, sets the package name, and writes the framework starter code. */
188
+ function scaffoldFiles(projectName, framework, targetDir) {
189
+ copyDir(path.join(__dirname, "..", "template"), targetDir);
190
+ const pkgJsonPath = path.join(targetDir, "package.json");
191
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
192
+ pkg.name = projectName;
193
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + "\n");
194
+ fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
195
+ fs.writeFileSync(path.join(targetDir, "src", "index.ts"), FRAMEWORK_INDEX[framework]);
196
+ }
197
+ /** Installs shared dev dependencies and any framework-specific packages. */
198
+ function installDependencies(pkgManager, framework, targetDir) {
199
+ p.log.step(`Installing dependencies with ${pkgManager}...`);
200
+ run(pkgManager, addArgs(pkgManager, [
201
+ "@types/node",
202
+ "oxfmt",
203
+ "oxlint",
204
+ "tsdown",
205
+ "tsx",
206
+ "typescript",
207
+ "vitest"
208
+ ], true), { cwd: targetDir });
209
+ /** Install framework dependencies */
210
+ if (framework !== "none") {
211
+ p.log.step(`Installing ${framework}...`);
212
+ const frameworkDeps = FRAMEWORK_DEPS[framework];
213
+ if (frameworkDeps.deps.length > 0) run(pkgManager, addArgs(pkgManager, frameworkDeps.deps, false), { cwd: targetDir });
214
+ if (frameworkDeps.devDeps.length > 0) run(pkgManager, addArgs(pkgManager, frameworkDeps.devDeps, true), { cwd: targetDir });
215
+ }
216
+ }
217
+ /** Prints the gradient outro with next steps. */
218
+ function showOutro(projectName, framework, pkgManager) {
219
+ const frameworkLabel = FRAMEWORKS.find((f) => f.value === framework)?.label ?? framework;
220
+ const outroStops = [[
221
+ 168,
222
+ 85,
223
+ 247
224
+ ], [
225
+ 99,
226
+ 102,
227
+ 241
228
+ ]];
229
+ const outroText = frameworkLabel !== "None" ? `Created ${projectName} with ${frameworkLabel}` : `Created ${projectName}`;
230
+ const nameStart = 8;
231
+ const outro = gradient(outroText, outroStops, [nameStart, nameStart + projectName.length]);
232
+ const devCmd = pkgManager === "npm" ? "npm run dev" : `${pkgManager} dev`;
233
+ p.outro(`${outro}\n\n ${pc.dim("Now run:")}\n cd ${projectName}\n ${devCmd}`);
234
+ }
235
+ async function main() {
236
+ const argv = mri(process.argv.slice(2), {
237
+ string: ["template"],
238
+ alias: { t: "template" }
239
+ });
240
+ p.intro(pc.bold(gradient("planke - The Unified Toolchain Starter for Node.js", [
241
+ [
242
+ 255,
243
+ 255,
244
+ 255
245
+ ],
246
+ [
247
+ 168,
248
+ 85,
249
+ 247
250
+ ],
251
+ [
252
+ 99,
253
+ 102,
254
+ 241
255
+ ]
256
+ ], [0, 6])));
257
+ const projectName = await promptProjectName(argv);
258
+ const framework = await promptFramework(argv);
259
+ const targetDir = path.resolve(process.cwd(), projectName);
260
+ const pkgManager = detectPkgManager();
261
+ await confirmOverwrite(projectName, targetDir);
262
+ scaffoldFiles(projectName, framework, targetDir);
263
+ run("git", [
264
+ "init",
265
+ "-b",
266
+ "main"
267
+ ], {
268
+ cwd: targetDir,
269
+ stdio: "ignore"
270
+ });
271
+ p.log.step("Initializing git repository");
272
+ installDependencies(pkgManager, framework, targetDir);
273
+ p.log.step("Formatting code");
274
+ run(pkgManager, [
275
+ "exec",
276
+ "oxlint",
277
+ "--",
278
+ "--init"
279
+ ], {
280
+ cwd: targetDir,
281
+ stdio: "ignore"
282
+ });
283
+ run(pkgManager, [
284
+ "exec",
285
+ "oxfmt",
286
+ "--",
287
+ "--init"
288
+ ], {
289
+ cwd: targetDir,
290
+ stdio: "ignore"
291
+ });
292
+ run(pkgManager, ["exec", "oxfmt"], {
293
+ cwd: targetDir,
294
+ stdio: "ignore"
295
+ });
296
+ showOutro(projectName, framework, pkgManager);
297
+ }
298
+ main().catch((err) => {
299
+ console.error(err);
300
+ process.exit(1);
301
+ });
302
+ //#endregion
303
+ export {};
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "create-planke",
3
+ "version": "0.0.1",
4
+ "description": "The Unified Toolchain Starter for Node.js",
5
+ "keywords": [
6
+ "backend",
7
+ "create-planke",
8
+ "express",
9
+ "fastify",
10
+ "framework",
11
+ "hono",
12
+ "oxfmt",
13
+ "oxlint",
14
+ "planke",
15
+ "scaffolder",
16
+ "toolchain",
17
+ "tsdown",
18
+ "tsx",
19
+ "typescript",
20
+ "vite+",
21
+ "vite-plus",
22
+ "viteplus",
23
+ "vitest"
24
+ ],
25
+ "license": "MIT",
26
+ "author": "Espen Steen",
27
+ "bin": {
28
+ "create-planke": "./dist/index.mjs"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "template"
33
+ ],
34
+ "type": "module",
35
+ "dependencies": {
36
+ "@clack/prompts": "^1.1.0",
37
+ "cross-spawn": "^7.0.6",
38
+ "mri": "^1.2.0",
39
+ "picocolors": "^1.1.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/cross-spawn": "^6.0.6",
43
+ "@types/node": "^25.5.0",
44
+ "oxfmt": "^0.43.0",
45
+ "oxlint": "^1.58.0",
46
+ "tsdown": "^0.21.7",
47
+ "tsx": "^4.21.0",
48
+ "typescript": "^6.0.2",
49
+ "vitest": "^4.1.2"
50
+ },
51
+ "scripts": {
52
+ "test": "vitest",
53
+ "lint": "oxlint",
54
+ "lint:fix": "oxlint --fix",
55
+ "fmt": "oxfmt",
56
+ "fmt:check": "oxfmt --check",
57
+ "check": "oxlint && oxfmt --check && tsc --noEmit",
58
+ "dev": "tsx src/index.ts",
59
+ "build": "tsdown src/index.ts"
60
+ }
61
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "planke-project",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "tsx src/index.ts",
7
+ "build": "tsdown src/index.ts",
8
+ "test": "vitest",
9
+ "lint": "oxlint",
10
+ "lint:fix": "oxlint --fix",
11
+ "fmt": "oxfmt",
12
+ "fmt:check": "oxfmt --check",
13
+ "check": "oxlint && oxfmt --check && tsc --noEmit"
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "strict": true,
6
+ "skipLibCheck": true,
7
+ "types": ["node"],
8
+ "verbatimModuleSyntax": true,
9
+ "noUnusedLocals": true,
10
+ "sourceMap": true,
11
+ "outDir": "./dist"
12
+ },
13
+ "exclude": ["node_modules"]
14
+ }