@thomaslorincz/create-project 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/.cursorrules ADDED
@@ -0,0 +1,7 @@
1
+ # General
2
+ - Do not automatically fix or focus on linting errors unless specifically asked. Focus only on functionality
3
+ - Do not build solutions when you are finished. Do not run build commands
4
+
5
+ # TypeScript
6
+ - Prefer interfaces over types unless an interface is not possible
7
+ - Avoid using ReturnType over defining standalone types for return values
package/.oxfmtrc.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "singleQuote": true,
4
+ "ignorePatterns": ["**/*.d.ts"]
5
+ }
package/.oxlintrc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json"
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["oxc.oxc-vscode"]
3
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "biome.enabled": false,
3
+ "oxc.fmt.configPath": ".oxfmtrc.json",
4
+ "editor.defaultFormatter": "oxc.oxc-vscode",
5
+ "editor.formatOnSave": true,
6
+ "[json]": {
7
+ "editor.defaultFormatter": "oxc.oxc-vscode"
8
+ },
9
+ "[javascript]": {
10
+ "editor.defaultFormatter": "oxc.oxc-vscode"
11
+ },
12
+
13
+ "[typescript]": {
14
+ "editor.defaultFormatter": "oxc.oxc-vscode"
15
+ },
16
+
17
+ "[javascriptreact]": {
18
+ "editor.defaultFormatter": "oxc.oxc-vscode"
19
+ },
20
+
21
+ "[typescriptreact]": {
22
+ "editor.defaultFormatter": "oxc.oxc-vscode"
23
+ },
24
+ "[jsonc]": {
25
+ "editor.defaultFormatter": "oxc.oxc-vscode"
26
+ }
27
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thomas Lorincz
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,83 @@
1
+ # create-project
2
+
3
+ An opinionated scaffolder for my full stack web projects.
4
+
5
+ This is the project generator I use as a starting point for my own apps, including
6
+ [thomaslorincz.com](https://thomaslorincz.com) and many of my portfolio projects.
7
+ It will evolve over time as my preferred stack, defaults, and project patterns
8
+ change.
9
+
10
+ The generated app is a full stack Bun workspace with a Vite React frontend and a
11
+ Cloudflare Worker backend. It is designed to be developed locally and deployed to
12
+ Cloudflare Workers.
13
+
14
+ ## What It Creates
15
+
16
+ The generated project includes:
17
+
18
+ - A [React](https://react.dev) + [Vite](https://vite.dev) + [TypeScript](https://www.typescriptlang.org) frontend.
19
+ - A [Cloudflare Workers](https://workers.cloudflare.com) backend using [Hono](https://hono.dev).
20
+ - [Drizzle ORM](https://orm.drizzle.team) configured for [PostgreSQL](https://www.postgresql.org).
21
+ - [Wrangler](https://developers.cloudflare.com/workers/wrangler/) configuration for local development and deployment.
22
+ - [Bun](https://bun.sh) workspace scripts for development, formatting, linting, and deployment.
23
+ - [Cursor](https://cursor.com)-friendly project defaults, including a generated `.cursorrules` file.
24
+
25
+ ## Usage
26
+
27
+ ```sh
28
+ bun create @thomaslorincz/project my-project
29
+ ```
30
+
31
+ ## Generated Project Structure
32
+
33
+ ```text
34
+ my-project/
35
+ ├── .cursorrules
36
+ ├── .gitignore
37
+ ├── .oxfmtrc.json
38
+ ├── .oxlintrc.json
39
+ ├── .vscode/
40
+ │ ├── extensions.json
41
+ │ └── settings.json
42
+ ├── bunfig.toml
43
+ ├── package.json
44
+ ├── README.md
45
+ ├── frontend/
46
+ │ ├── .oxlintrc.json
47
+ │ ├── index.html
48
+ │ ├── package.json
49
+ │ ├── tsconfig.app.json
50
+ │ ├── tsconfig.json
51
+ │ ├── tsconfig.node.json
52
+ │ ├── vite.config.ts
53
+ │ └── src/
54
+ │ ├── App.tsx
55
+ │ ├── index.css
56
+ │ └── main.tsx
57
+ └── backend/
58
+ ├── .dev.vars
59
+ ├── drizzle.config.ts
60
+ ├── package.json
61
+ ├── tsconfig.json
62
+ ├── worker-configuration.d.ts
63
+ ├── wrangler.jsonc
64
+ ├── db/
65
+ │ └── migrations/
66
+ └── src/
67
+ ├── index.ts
68
+ ├── middleware.ts
69
+ ├── schema.ts
70
+ ├── types.ts
71
+ └── routers/
72
+ └── health.ts
73
+ ```
74
+
75
+ `worker-configuration.d.ts` is generated by Wrangler after dependencies are
76
+ installed.
77
+
78
+ ## Future Additions
79
+
80
+ - An option to choose between [shadcn/ui](https://ui.shadcn.com) and a bespoke plain [Tailwind CSS](https://tailwindcss.com) setup.
81
+ - An option to generate Python/ML container scaffolding.
82
+ - An option to include [MapLibre](https://maplibre.org).
83
+ - An option to include [React Three Fiber](https://r3f.docs.pmnd.rs).
package/bun.lock ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "create-project",
7
+ "devDependencies": {
8
+ "bun-types": "^1.3.14",
9
+ "oxfmt": "^0.51.0",
10
+ "oxlint": "^1.66.0",
11
+ "typescript": "^6.0.3",
12
+ },
13
+ },
14
+ },
15
+ "packages": {
16
+ "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.51.0", "", { "os": "android", "cpu": "arm" }, "sha512-Ni0sCqg5CIHaLIYFGj+ncbcumylvNC6FE4rfD0KfdmnWHbPJ+zev0qZCXKxy2hFVa0fYRK0yPzf5nzPbkZou7g=="],
17
+
18
+ "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.51.0", "", { "os": "android", "cpu": "arm64" }, "sha512-eu5lAZjuo0KAkp+M24EhDqfOwA8owQ8d7wyBlOUUGRbDLHpU3IRlDHp8Dif+YqGlxs6jra7yS6WQu/NkPhAxeg=="],
19
+
20
+ "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.51.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6LsUNIdURhhcIfIn8+xsOb61mSTa9msAHTeSGx9Jf4rsP/gN8PGCF+SKWPAQZbND2w/WBkqQ6303jqEEIXzMdQ=="],
21
+
22
+ "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.51.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-9aUMGmVxdHjYMsEAW1tNRoieTJXlVNDFkRvIR1J7LttJXWjVYCu2ekclLij2KJtxBxSQOYSHd12ME/adVGVbZg=="],
23
+
24
+ "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.51.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mkY1nhZTqYb+NHaAWxOCKISN6FwdrwMNsu17vTUA3wzUV2VJ+Paq15ZokRcsMU/2PUdHO73prxyeJpjXQ3MPpQ=="],
25
+
26
+ "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.51.0", "", { "os": "linux", "cpu": "arm" }, "sha512-wtFwNwE4+YCNuPaWoGDZeGsKvD6D1YSUNBJNn/rJBh7CrDBThFE+TBI5kY7vRW9rIOQRsbW2IpyyL3Du4Zqwiw=="],
27
+
28
+ "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.51.0", "", { "os": "linux", "cpu": "arm" }, "sha512-rnOaNx86G7iRKM6lsCIQMux0SMGNC/TEbFR+r7lpruJ12bnrIWgxd5w1PLqOvgR9r8ZJbpK/zfRKctJnh8/Jfg=="],
29
+
30
+ "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.51.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jOgDzSqWcICGRjsp4mc08FxKMN8vzP2Kgs4E0d2HUP99F+nJDQKklRV4Zuj+0gcBgjrzx2CbpqaIdUVPepCojA=="],
31
+
32
+ "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.51.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-KBUCdrH5bwVrAvI9gU/1S55oH6fzXjr++J/oVocdu7bYTks1l7DNNT+rLd/1TDdAEjObGwmfWamn7LC1m8A0DQ=="],
33
+
34
+ "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.51.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NapfjYsABFqTJ1Dn9Efq6sN5esaHconVKwVLbDGNQLrwpOx/g17mkwErHzU72PutL67nf3wNAkbq122H+zLxag=="],
35
+
36
+ "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.51.0", "", { "os": "linux", "cpu": "none" }, "sha512-5dlDt1dUZCVi6elIhiK1PWg9wpTzTcIuj0IZnSurvIoMrhOWqqTcc1dSTxcSkNaBZhfsNqRZdINI1zAgbKkJNQ=="],
37
+
38
+ "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.51.0", "", { "os": "linux", "cpu": "none" }, "sha512-pgdWUJn0S5nulyiVdlFV8DzCUnGXkU99W5PSkkmbaZW+LrZBPxpezun4G0DDHbQaVYuJeCuKsXsGKGo77CkUTQ=="],
39
+
40
+ "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.51.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-2XTFUe97CbDGAI8vjwDfZ1HdakO0XIADyJ24idEg64SC4/K4in/OisXVnrW4NMK7I6TgC7EqRhC0Ln/nKhAemA=="],
41
+
42
+ "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.51.0", "", { "os": "linux", "cpu": "x64" }, "sha512-kQ1OuCqqt/yyf0ZN9VFxW1/JnlgJgii3Dr7pWf9vNBvrX1hv6g39/+mc5oGRHRGJFZtl3zsGDWR9c5N2B/gwBw=="],
43
+
44
+ "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.51.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ARTYqxHF475o96Gbn41hvSWSSRygPlRDXZZgZ9I2scU1y0qiWpCQyZCoefaQa0mwv+wwtZ+luS4YOzsRzM/izg=="],
45
+
46
+ "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.51.0", "", { "os": "none", "cpu": "arm64" }, "sha512-QiC1XrCl6a6BmqMzduO8hdIRMf1m44hCkt2Q68KWkTvUB/E7fd2iomyNh6KnnRca5w6eBrRAAtLFqTh+xjsjJA=="],
47
+
48
+ "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.51.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-NC/hJb9dtU23Zf8L7IVK95xnFjiQ7AfcLO2l5pb69TDEr958qxrtnB2CveeeNSCBFNIkgaTCfd/vHNSoG78l9g=="],
49
+
50
+ "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.51.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-2C45za4Rj36n8YIbhRL1PQbxmXJYf81WEcAgvj5I4ptRROG+A+81hREEN5bmCHADE1UfYaN312U6tkILoZZy6w=="],
51
+
52
+ "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.51.0", "", { "os": "win32", "cpu": "x64" }, "sha512-73RqdAuVKQTkjZIDw08JaDHUM4lav5Qu+CaPwg4QbbA7k8o7LEW0p3UsfZ/F8dsO/pwVYh3RzFcanwLRTTahbQ=="],
53
+
54
+ "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.66.0", "", { "os": "android", "cpu": "arm" }, "sha512-f7kq8N51T4phpzqfBpA2qaVTI/KrkCmNwaj3t/97I/WLTDI+UhlP5GL9eER+zVxBhtlx5rKXWByJU1/zDAvyaw=="],
55
+
56
+ "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.66.0", "", { "os": "android", "cpu": "arm64" }, "sha512-xu6QO71tdDS9mjmLZ3AqhtaVHBvdmsOKkYnReNNDgh+XiwnsipeQOIxbiYOOO0iAXycJ+GK0wdMSZP/2j/AmSg=="],
57
+
58
+ "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.66.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ24VimSOC7mxuEA99e0H2FS0C1yO3+iW13jPRAk+e2njsUs3QeAXsafCDyaIrV/MirdOVez+etQNQsJE43zNQ=="],
59
+
60
+ "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.66.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-awhj8ZvJrrRSnXj7V++rpZvTmnl99L6mi0B7gg7Cp7BN6cKpzuI481bHNLvXGA9GB1/oEgA3ponuyoAc6Md12A=="],
61
+
62
+ "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.66.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KQF0oVV21/FjIqkRuL8Q1vh8ECsE5+ocdH5tcqTQ4ZnYuDVoYibQUNfqBjQaUsP6UIIda5Y75Wpm5p4RgQWiWw=="],
63
+
64
+ "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.66.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9u1rgwZSEXWb30vbFZzQ78HVXBo0WCKNwJ3a2InRUTNMRng+PUDIoSFmA+m4HdUfBaIqftShq8J8qHc+eE/Vig=="],
65
+
66
+ "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.66.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ynot2HR1bHxUaNWoC280MVTDfZuaWuP3XfSMRDhyuZrVjhzoaBCVFlw8h8qeZjWKVUBhPWFIxB7AQTlK8Z2WWg=="],
67
+
68
+ "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.66.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-xCbgzciGgo+A4aQZEknsNrNiIwY7sU5SfRuMmRjPIvZAgdF34cIHiKvwOsS5XRLjlTVSFwitmq6YclTtHTfU+g=="],
69
+
70
+ "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.66.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hmo+ZB/lHkR1HdDmnziNpzSLmulnUSu10VEqX2Yex7OwvoBAbjJQLvy4gIBRV3AAwWnCvAxKp5Nv1GE6LU1QMg=="],
71
+
72
+ "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.66.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-2Invd4Uyy81mVooQC5FBtfxSNrvcX1OxbMlVQ6M2erRrNI2awFYF26YNW2yFxdVFZ4ffNOWKghtMjhnUPsXsVA=="],
73
+
74
+ "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.66.0", "", { "os": "linux", "cpu": "none" }, "sha512-s0iXPDQVdgayE3RGa/N2DZF7tjgg0TwEtD1sGoDxqPDGrIXgo45H0yHknT0f9A0yteASsweYZtDyTuVlM4aSag=="],
75
+
76
+ "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.66.0", "", { "os": "linux", "cpu": "none" }, "sha512-OekL4XFiu7RPK0JIZi8VeHgtIXPREf42t8Cy/rKEsC+P3gcqDgNAAGiyuUOpdbG4wwbfue1q4CHcCO7spSve6w=="],
77
+
78
+ "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.66.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Ga1D0kj1SFslm34ThA/BdkUlyAYEnTsXyRC4pF0C5agZSwtGdHYWMTQWemUfBGp4RCG4QWXgdO+HmmmKqOtlBg=="],
79
+
80
+ "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.66.0", "", { "os": "linux", "cpu": "x64" }, "sha512-p5jfP1wUZe/IC3qpQO84n9DRnf9g3lKRtLBlQq23ykyrDglHcVx7sWmVTlPuU6SBw8mNnPzyOn022G3XZHnlww=="],
81
+
82
+ "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.66.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vUB/sYlYZorDL1ZD+o9mRv7zbsykrrFRtmgS6R8musZqLtrPRQn1gc1eGpuX+sfdccz42STl/AqldY6XRb2upQ=="],
83
+
84
+ "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.66.0", "", { "os": "none", "cpu": "arm64" }, "sha512-yde+6p/F59xRkGR9H1HfngWRif1QRJjynZK349l+UI0H6w9hL3G8/AVaTHFyTtLVQ56qtNbX2/5Dc77n1ovnOg=="],
85
+
86
+ "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.66.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-O9GLucgoTdmOrbBX+EjzNe7o/Ze5TFOvXcib6bzUOtBOmj6cV+zw18NgB+cGKAkDw1Pdqs8vGkfHbbsLuDtXWg=="],
87
+
88
+ "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.66.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-m3Pjwc2MfTcom4E4gOv7DyuGyt7OfGNCbmqDHd+N7EzXmP+ppHuudm2NjcA3AjV5TSeGxaguVF4SbTKHe1USYA=="],
89
+
90
+ "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.66.0", "", { "os": "win32", "cpu": "x64" }, "sha512-/DbBvw8UFBhja6PqudUjV4UtfsJr0Oa7jUjWVKB0g86lj/VwnPrkngn0sFql3c9RDA0O16dh7ozsXb6GjNAzBQ=="],
91
+
92
+ "@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
93
+
94
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
95
+
96
+ "oxfmt": ["oxfmt@0.51.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.51.0", "@oxfmt/binding-android-arm64": "0.51.0", "@oxfmt/binding-darwin-arm64": "0.51.0", "@oxfmt/binding-darwin-x64": "0.51.0", "@oxfmt/binding-freebsd-x64": "0.51.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.51.0", "@oxfmt/binding-linux-arm-musleabihf": "0.51.0", "@oxfmt/binding-linux-arm64-gnu": "0.51.0", "@oxfmt/binding-linux-arm64-musl": "0.51.0", "@oxfmt/binding-linux-ppc64-gnu": "0.51.0", "@oxfmt/binding-linux-riscv64-gnu": "0.51.0", "@oxfmt/binding-linux-riscv64-musl": "0.51.0", "@oxfmt/binding-linux-s390x-gnu": "0.51.0", "@oxfmt/binding-linux-x64-gnu": "0.51.0", "@oxfmt/binding-linux-x64-musl": "0.51.0", "@oxfmt/binding-openharmony-arm64": "0.51.0", "@oxfmt/binding-win32-arm64-msvc": "0.51.0", "@oxfmt/binding-win32-ia32-msvc": "0.51.0", "@oxfmt/binding-win32-x64-msvc": "0.51.0" }, "peerDependencies": { "svelte": "^5.0.0" }, "optionalPeers": ["svelte"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-l/AoAnaEOV7Q5/Z9kHOMDehVJnCgYN7wRoooWCTUMBMi16BJhLZqd9cmCnwcVFfVlzkt53zK2KLPFNp8vSsoDg=="],
97
+
98
+ "oxlint": ["oxlint@1.66.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.66.0", "@oxlint/binding-android-arm64": "1.66.0", "@oxlint/binding-darwin-arm64": "1.66.0", "@oxlint/binding-darwin-x64": "1.66.0", "@oxlint/binding-freebsd-x64": "1.66.0", "@oxlint/binding-linux-arm-gnueabihf": "1.66.0", "@oxlint/binding-linux-arm-musleabihf": "1.66.0", "@oxlint/binding-linux-arm64-gnu": "1.66.0", "@oxlint/binding-linux-arm64-musl": "1.66.0", "@oxlint/binding-linux-ppc64-gnu": "1.66.0", "@oxlint/binding-linux-riscv64-gnu": "1.66.0", "@oxlint/binding-linux-riscv64-musl": "1.66.0", "@oxlint/binding-linux-s390x-gnu": "1.66.0", "@oxlint/binding-linux-x64-gnu": "1.66.0", "@oxlint/binding-linux-x64-musl": "1.66.0", "@oxlint/binding-openharmony-arm64": "1.66.0", "@oxlint/binding-win32-arm64-msvc": "1.66.0", "@oxlint/binding-win32-ia32-msvc": "1.66.0", "@oxlint/binding-win32-x64-msvc": "1.66.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-N4LLxYLd94KEBqXDMDM5f+2PUpItTjDLreXe2Gn5KhjhCK4Qp2YUXaBi8Yu325ryOgKwt22m45fpD7nPOn69Yw=="],
99
+
100
+ "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
101
+
102
+ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
103
+
104
+ "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
105
+ }
106
+ }
package/bunfig.toml ADDED
@@ -0,0 +1,9 @@
1
+ # bunfig.toml
2
+
3
+ [install]
4
+ # Require packages to be at least 10080 minutes old (7 days)
5
+ minimumReleaseAge = 10080
6
+
7
+ # Prevent lifecycle scripts like postinstall/preinstall/install
8
+ # from running automatically
9
+ ignoreScripts = true
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@thomaslorincz/create-project",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "bin": {
6
+ "create-project": "./src/index.ts"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "start": "bun run src/index.ts",
11
+ "check": "tsc --noEmit",
12
+ "lint": "oxlint",
13
+ "lint:fix": "oxlint --fix",
14
+ "fmt": "oxfmt",
15
+ "fmt:check": "oxfmt --check"
16
+ },
17
+ "devDependencies": {
18
+ "bun-types": "^1.3.14",
19
+ "oxfmt": "^0.51.0",
20
+ "oxlint": "^1.66.0",
21
+ "typescript": "^6.0.3"
22
+ }
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { run } from './scaffold.ts';
4
+
5
+ try {
6
+ await run(process.argv.slice(2));
7
+ } catch (error) {
8
+ const message = error instanceof Error ? error.message : String(error);
9
+
10
+ console.error(`\ncreate-project failed: ${message}`);
11
+ process.exit(1);
12
+ }
@@ -0,0 +1,355 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { stdin as input, stdout as output } from 'node:process';
5
+ import readline from 'node:readline/promises';
6
+ import {
7
+ backendPackageJson,
8
+ tsconfig as backendTsconfig,
9
+ devVars,
10
+ drizzleConfig,
11
+ healthRouterTs,
12
+ indexTs,
13
+ middlewareTs,
14
+ schemaTs,
15
+ typesTs,
16
+ wranglerJsonc,
17
+ } from './templates/backend.ts';
18
+ import {
19
+ appTsx,
20
+ oxlintConfig as frontendOxlintConfig,
21
+ frontendPackageJson,
22
+ indexCss,
23
+ indexHtml,
24
+ mainTsx,
25
+ tsconfig,
26
+ tsconfigApp,
27
+ tsconfigNode,
28
+ viteConfig,
29
+ } from './templates/frontend.ts';
30
+ import {
31
+ bunfigToml,
32
+ cursorRules,
33
+ gitignore,
34
+ oxfmtConfig,
35
+ oxlintConfig,
36
+ readme,
37
+ rootPackageJson,
38
+ vscodeExtensions,
39
+ vscodeSettings,
40
+ } from './templates/root.ts';
41
+
42
+ interface CliOptions {
43
+ force: boolean;
44
+ help: boolean;
45
+ install: boolean;
46
+ packageName?: string;
47
+ projectName?: string;
48
+ }
49
+
50
+ interface ScaffoldOptions {
51
+ force: boolean;
52
+ install: boolean;
53
+ packageName: string;
54
+ projectName: string;
55
+ targetDir: string;
56
+ workerName: string;
57
+ }
58
+
59
+ export async function run(argv: string[]) {
60
+ const options = await getOptions(argv);
61
+
62
+ if (options.help) {
63
+ printHelp();
64
+ return;
65
+ }
66
+
67
+ const projectName = options.projectName ?? (await promptProjectName());
68
+ const targetDir = path.resolve(process.cwd(), projectName);
69
+ const packageName = options.packageName ?? toPackageName(path.basename(targetDir));
70
+
71
+ validatePackageName(packageName);
72
+
73
+ const scaffoldOptions: ScaffoldOptions = {
74
+ force: options.force,
75
+ install: options.install,
76
+ packageName,
77
+ projectName,
78
+ targetDir,
79
+ workerName: toWorkerName(packageName),
80
+ };
81
+
82
+ await scaffold(scaffoldOptions);
83
+ printNextSteps(scaffoldOptions);
84
+ }
85
+
86
+ async function getOptions(argv: string[]): Promise<CliOptions> {
87
+ const options: CliOptions = {
88
+ force: false,
89
+ help: false,
90
+ install: true,
91
+ };
92
+
93
+ for (let index = 0; index < argv.length; index += 1) {
94
+ const arg = argv[index];
95
+
96
+ if (arg === '--help' || arg === '-h') {
97
+ options.help = true;
98
+ continue;
99
+ }
100
+
101
+ if (arg === '--force' || arg === '-f') {
102
+ options.force = true;
103
+ continue;
104
+ }
105
+
106
+ if (arg === '--no-install') {
107
+ options.install = false;
108
+ continue;
109
+ }
110
+
111
+ if (arg === '--name') {
112
+ const value = argv[index + 1];
113
+
114
+ if (!value) {
115
+ throw new Error('Expected a value after --name.');
116
+ }
117
+
118
+ options.packageName = value;
119
+ index += 1;
120
+ continue;
121
+ }
122
+
123
+ if (arg.startsWith('--name=')) {
124
+ options.packageName = arg.slice('--name='.length);
125
+ continue;
126
+ }
127
+
128
+ if (arg.startsWith('-')) {
129
+ throw new Error(`Unknown option: ${arg}`);
130
+ }
131
+
132
+ if (options.projectName) {
133
+ throw new Error(`Unexpected argument: ${arg}`);
134
+ }
135
+
136
+ options.projectName = arg;
137
+ }
138
+
139
+ return options;
140
+ }
141
+
142
+ async function promptProjectName() {
143
+ const rl = readline.createInterface({ input, output });
144
+
145
+ try {
146
+ const answer = await rl.question('Project name: ');
147
+ const projectName = answer.trim();
148
+
149
+ if (!projectName) {
150
+ throw new Error('Project name is required.');
151
+ }
152
+
153
+ return projectName;
154
+ } finally {
155
+ rl.close();
156
+ }
157
+ }
158
+
159
+ async function scaffold(options: ScaffoldOptions) {
160
+ await prepareTargetDirectory(options);
161
+ await writeRootFiles(options);
162
+ await scaffoldFrontend(options);
163
+ await writeBackendFiles(options);
164
+
165
+ if (options.install) {
166
+ await runCommand('bun', ['install'], options.targetDir);
167
+ await runPostInstallCommands(options);
168
+ }
169
+ }
170
+
171
+ async function prepareTargetDirectory({ force, targetDir }: ScaffoldOptions) {
172
+ const exists = await pathExists(targetDir);
173
+
174
+ if (!exists) {
175
+ await mkdir(targetDir, { recursive: true });
176
+ return;
177
+ }
178
+
179
+ const entries = await readdir(targetDir);
180
+
181
+ if (entries.length === 0) {
182
+ return;
183
+ }
184
+
185
+ if (!force) {
186
+ throw new Error(`${targetDir} is not empty. Re-run with --force to overwrite it.`);
187
+ }
188
+
189
+ await rm(targetDir, { force: true, recursive: true });
190
+ await mkdir(targetDir, { recursive: true });
191
+ }
192
+
193
+ async function writeRootFiles(options: ScaffoldOptions) {
194
+ await writeFile(path.join(options.targetDir, 'package.json'), rootPackageJson(options));
195
+ await writeFile(path.join(options.targetDir, 'bunfig.toml'), bunfigToml);
196
+ await writeFile(path.join(options.targetDir, '.gitignore'), gitignore);
197
+ await writeFile(path.join(options.targetDir, '.cursorrules'), cursorRules);
198
+ await writeFile(path.join(options.targetDir, '.oxlintrc.json'), oxlintConfig);
199
+ await writeFile(path.join(options.targetDir, '.oxfmtrc.json'), oxfmtConfig);
200
+ await writeFile(path.join(options.targetDir, 'README.md'), readme(options));
201
+
202
+ await mkdir(path.join(options.targetDir, '.vscode'), { recursive: true });
203
+ await writeFile(path.join(options.targetDir, '.vscode', 'settings.json'), vscodeSettings);
204
+ await writeFile(path.join(options.targetDir, '.vscode', 'extensions.json'), vscodeExtensions);
205
+ }
206
+
207
+ async function scaffoldFrontend(options: ScaffoldOptions) {
208
+ await runCommand(
209
+ 'bun',
210
+ ['create', 'vite@latest', 'frontend', '--', '--template', 'react-ts'],
211
+ options.targetDir,
212
+ );
213
+
214
+ const frontendDir = path.join(options.targetDir, 'frontend');
215
+
216
+ await writeFile(path.join(frontendDir, 'package.json'), frontendPackageJson(options));
217
+ await writeFile(path.join(frontendDir, 'vite.config.ts'), viteConfig);
218
+ await writeFile(path.join(frontendDir, 'tsconfig.json'), tsconfig);
219
+ await writeFile(path.join(frontendDir, 'tsconfig.app.json'), tsconfigApp);
220
+ await writeFile(path.join(frontendDir, 'tsconfig.node.json'), tsconfigNode);
221
+ await writeFile(path.join(frontendDir, 'index.html'), indexHtml);
222
+ await writeFile(path.join(frontendDir, '.oxlintrc.json'), frontendOxlintConfig);
223
+
224
+ await rm(path.join(frontendDir, 'src'), { force: true, recursive: true });
225
+ await mkdir(path.join(frontendDir, 'src'), { recursive: true });
226
+ await writeFile(path.join(frontendDir, 'src', 'main.tsx'), mainTsx);
227
+ await writeFile(path.join(frontendDir, 'src', 'App.tsx'), appTsx);
228
+ await writeFile(path.join(frontendDir, 'src', 'index.css'), indexCss);
229
+ }
230
+
231
+ async function writeBackendFiles(options: ScaffoldOptions) {
232
+ const backendDir = path.join(options.targetDir, 'backend');
233
+
234
+ await mkdir(path.join(backendDir, 'src', 'routers'), { recursive: true });
235
+ await mkdir(path.join(backendDir, 'db', 'migrations'), { recursive: true });
236
+
237
+ await writeFile(path.join(backendDir, 'package.json'), backendPackageJson(options));
238
+ await writeFile(path.join(backendDir, 'wrangler.jsonc'), wranglerJsonc(options));
239
+ await writeFile(path.join(backendDir, 'tsconfig.json'), backendTsconfig);
240
+ await writeFile(path.join(backendDir, 'drizzle.config.ts'), drizzleConfig);
241
+ await writeFile(path.join(backendDir, '.dev.vars'), devVars);
242
+ await writeFile(path.join(backendDir, 'src', 'index.ts'), indexTs);
243
+ await writeFile(path.join(backendDir, 'src', 'schema.ts'), schemaTs);
244
+ await writeFile(path.join(backendDir, 'src', 'types.ts'), typesTs);
245
+ await writeFile(path.join(backendDir, 'src', 'middleware.ts'), middlewareTs);
246
+ await writeFile(path.join(backendDir, 'src', 'routers', 'health.ts'), healthRouterTs);
247
+ }
248
+
249
+ async function runPostInstallCommands(options: ScaffoldOptions) {
250
+ await runCommand('bun', ['run', 'fmt'], options.targetDir);
251
+ await runCommand('bun', ['wrangler', 'types'], path.join(options.targetDir, 'backend'));
252
+ }
253
+
254
+ async function runCommand(command: string, args: string[], cwd: string) {
255
+ console.log(`\n> ${[command, ...args].join(' ')}`);
256
+
257
+ await new Promise<void>((resolve, reject) => {
258
+ const child = spawn(command, args, {
259
+ cwd,
260
+ stdio: 'inherit',
261
+ shell: process.platform === 'win32',
262
+ });
263
+
264
+ child.on('error', reject);
265
+ child.on('exit', (code) => {
266
+ if (code === 0) {
267
+ resolve();
268
+ return;
269
+ }
270
+
271
+ reject(new Error(`${command} exited with code ${code ?? 'unknown'}.`));
272
+ });
273
+ });
274
+ }
275
+
276
+ async function pathExists(filePath: string) {
277
+ try {
278
+ await stat(filePath);
279
+ return true;
280
+ } catch {
281
+ return false;
282
+ }
283
+ }
284
+
285
+ function toPackageName(value: string) {
286
+ return value
287
+ .trim()
288
+ .toLowerCase()
289
+ .replace(/[\s_]+/g, '-')
290
+ .replace(/[^a-z0-9-~/@.]/g, '-')
291
+ .replace(/^-+|-+$/g, '');
292
+ }
293
+
294
+ function toWorkerName(packageName: string) {
295
+ const unscopedName = packageName.includes('/')
296
+ ? (packageName.split('/').at(-1) as string)
297
+ : packageName;
298
+
299
+ return unscopedName
300
+ .toLowerCase()
301
+ .replace(/[^a-z0-9-]/g, '-')
302
+ .replace(/^-+|-+$/g, '')
303
+ .slice(0, 63);
304
+ }
305
+
306
+ function validatePackageName(packageName: string) {
307
+ if (!packageName) {
308
+ throw new Error('Could not infer a valid package name.');
309
+ }
310
+
311
+ const validPackageName = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
312
+
313
+ if (!validPackageName.test(packageName)) {
314
+ throw new Error(`Invalid package name: ${packageName}`);
315
+ }
316
+ }
317
+
318
+ function printHelp() {
319
+ console.log(`create-project
320
+
321
+ Usage:
322
+ create-project <project-name> [options]
323
+
324
+ Options:
325
+ --name <package-name> Override the generated package name
326
+ --force, -f Overwrite a non-empty target directory
327
+ --no-install Skip bun install after scaffolding
328
+ --help, -h Show this help message
329
+ `);
330
+ }
331
+
332
+ function printNextSteps(options: ScaffoldOptions) {
333
+ const relativeTarget = path.relative(process.cwd(), options.targetDir) || '.';
334
+ const installSteps = options.install
335
+ ? ''
336
+ : `
337
+ bun install
338
+ bun run fmt
339
+ (cd backend && bun wrangler types)
340
+ `;
341
+
342
+ console.log(`
343
+ Done. Next steps:
344
+
345
+ cd ${relativeTarget}
346
+ ${installSteps}
347
+ bun run --cwd frontend dev
348
+ bun run dev
349
+
350
+ Before deploying:
351
+
352
+ Fill in DATABASE_URL in backend/.dev.vars
353
+ bun run deploy
354
+ `);
355
+ }
@@ -0,0 +1,193 @@
1
+ export interface BackendTemplateOptions {
2
+ packageName: string;
3
+ workerName: string;
4
+ }
5
+
6
+ export function backendPackageJson({ packageName }: BackendTemplateOptions) {
7
+ return `${JSON.stringify(
8
+ {
9
+ name: `${packageName}-backend`,
10
+ version: '0.0.0',
11
+ private: true,
12
+ type: 'module',
13
+ scripts: {
14
+ dev: 'wrangler dev',
15
+ deploy: 'wrangler deploy',
16
+ test: 'vitest',
17
+ },
18
+ dependencies: {
19
+ '@hono/zod-validator': '^0.7.6',
20
+ 'drizzle-orm': '^0.45.2',
21
+ hono: '^4.12.10',
22
+ postgres: '^3.4.8',
23
+ zod: '^4.3.6',
24
+ },
25
+ devDependencies: {
26
+ '@types/node': '^25.5.2',
27
+ 'drizzle-kit': '^0.31.10',
28
+ typescript: '^6.0.2',
29
+ wrangler: '^4.80.0',
30
+ },
31
+ },
32
+ null,
33
+ 2,
34
+ )}\n`;
35
+ }
36
+
37
+ export function wranglerJsonc({ workerName }: BackendTemplateOptions) {
38
+ return `{
39
+ "$schema": "node_modules/wrangler/config-schema.json",
40
+ "name": "${workerName}",
41
+ "main": "src/index.ts",
42
+ "compatibility_date": "2026-04-04",
43
+ "observability": {
44
+ "enabled": true
45
+ },
46
+ "placement": {
47
+ "region": "aws:ca-central-1"
48
+ },
49
+ "assets": {
50
+ "directory": "../frontend/dist",
51
+ "binding": "ASSETS",
52
+ "not_found_handling": "single-page-application"
53
+ },
54
+ "compatibility_flags": ["nodejs_compat"],
55
+ "vars": {
56
+ "ENV": "production"
57
+ }
58
+ }
59
+ `;
60
+ }
61
+
62
+ export const tsconfig = `{
63
+ "compilerOptions": {
64
+ "target": "es2021",
65
+ "lib": ["es2021"],
66
+ "jsx": "react-jsx",
67
+ "module": "es2022",
68
+ "moduleResolution": "Bundler",
69
+ "resolveJsonModule": true,
70
+ "allowJs": true,
71
+ "checkJs": false,
72
+ "noEmit": true,
73
+ "isolatedModules": true,
74
+ "allowSyntheticDefaultImports": true,
75
+ "forceConsistentCasingInFileNames": true,
76
+ "strict": true,
77
+ "skipLibCheck": true,
78
+ "types": ["./worker-configuration.d.ts", "node"],
79
+ "paths": {
80
+ "@/*": ["./src/*"]
81
+ }
82
+ },
83
+ "include": ["worker-configuration.d.ts", "src/**/*.ts"]
84
+ }
85
+ `;
86
+
87
+ export const drizzleConfig = `import { type Config, defineConfig } from 'drizzle-kit';
88
+
89
+ export default defineConfig({
90
+ schema: './src/schema.ts',
91
+ out: './db/migrations',
92
+ dialect: 'postgresql',
93
+ dbCredentials: {
94
+ url: process.env.DATABASE_URL as string,
95
+ },
96
+ }) satisfies Config;
97
+ `;
98
+
99
+ export const schemaTs = `import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
100
+
101
+ export const projects = pgTable('projects', {
102
+ id: text('id').primaryKey(),
103
+ name: text('name').notNull(),
104
+ createdAt: timestamp('created_at').defaultNow().notNull(),
105
+ });
106
+ `;
107
+
108
+ export const typesTs = `import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
109
+ import type * as schema from './schema';
110
+
111
+ export type DB = PostgresJsDatabase<typeof schema>;
112
+
113
+ interface AppVariables {
114
+ db: DB;
115
+ }
116
+
117
+ export interface AppContext {
118
+ Bindings: Env;
119
+ Variables: AppVariables;
120
+ }
121
+ `;
122
+
123
+ export const middlewareTs = `import { drizzle } from 'drizzle-orm/postgres-js';
124
+ import type { Context, Next } from 'hono';
125
+ import postgres from 'postgres';
126
+ import * as schema from './schema';
127
+ import type { AppContext } from './types';
128
+
129
+ export async function dbMiddleware(c: Context<AppContext>, next: Next) {
130
+ const client = postgres(c.env.DATABASE_URL, {
131
+ prepare: false,
132
+ max: 5,
133
+ fetch_types: false,
134
+ });
135
+
136
+ c.set('db', drizzle(client, { schema }));
137
+
138
+ await next();
139
+ }
140
+ `;
141
+
142
+ export const healthRouterTs = `import { Hono } from 'hono';
143
+ import type { AppContext } from '../types';
144
+
145
+ const healthRouter = new Hono<AppContext>();
146
+
147
+ healthRouter.get('/', (c) => {
148
+ return c.json({
149
+ ok: true,
150
+ message: 'API is healthy',
151
+ env: c.env.ENV,
152
+ });
153
+ });
154
+
155
+ export default healthRouter;
156
+ `;
157
+
158
+ export const indexTs = `import { Hono } from 'hono';
159
+ import { HTTPException } from 'hono/http-exception';
160
+ import { ZodError } from 'zod';
161
+ import healthRouter from './routers/health';
162
+ import type { AppContext } from './types';
163
+
164
+ const app = new Hono<AppContext>();
165
+
166
+ app.route('/api/health', healthRouter);
167
+
168
+ app.onError((err, c) => {
169
+ console.error(\`Error in route \${c.req.path}:\`, err);
170
+
171
+ if (err instanceof ZodError) {
172
+ return c.text('Invalid request body', 400);
173
+ }
174
+ if (err instanceof HTTPException) {
175
+ return err.getResponse();
176
+ }
177
+ return c.text('Failed to process request', 500);
178
+ });
179
+
180
+ app.notFound((c) => {
181
+ if (c.req.path.startsWith('/api')) {
182
+ return c.text('Not Found', 404);
183
+ }
184
+
185
+ return c.env.ASSETS.fetch(c.req.raw);
186
+ });
187
+
188
+ export default app;
189
+ `;
190
+
191
+ export const devVars = `ENV=development
192
+ DATABASE_URL=
193
+ `;
@@ -0,0 +1,234 @@
1
+ export interface FrontendTemplateOptions {
2
+ packageName: string;
3
+ }
4
+
5
+ export function frontendPackageJson({ packageName }: FrontendTemplateOptions) {
6
+ return `${JSON.stringify(
7
+ {
8
+ name: `${packageName}-frontend`,
9
+ version: '0.0.0',
10
+ private: true,
11
+ type: 'module',
12
+ scripts: {
13
+ dev: 'vite',
14
+ build: 'tsc -b && vite build',
15
+ lint: 'oxlint',
16
+ 'lint:fix': 'oxlint --fix',
17
+ fmt: 'oxfmt',
18
+ 'fmt:check': 'oxfmt --check',
19
+ },
20
+ dependencies: {
21
+ react: '^19.2.6',
22
+ 'react-dom': '^19.2.6',
23
+ 'react-router': '^7.15.0',
24
+ },
25
+ devDependencies: {
26
+ '@babel/core': '^7.29.0',
27
+ '@rolldown/plugin-babel': '^0.2.3',
28
+ '@tailwindcss/vite': '^4.3.0',
29
+ '@tsconfig/node20': '^20.1.9',
30
+ '@types/babel__core': '^7.20.5',
31
+ '@types/node': '^24.12.4',
32
+ '@types/react': '^19.2.14',
33
+ '@types/react-dom': '^19.2.3',
34
+ '@vitejs/plugin-react': '^6.0.1',
35
+ 'babel-plugin-react-compiler': '^1.0.0',
36
+ globals: '^17.6.0',
37
+ tailwindcss: '^4.3.0',
38
+ typescript: '~6.0.3',
39
+ vite: '^8.0.12',
40
+ },
41
+ engines: {
42
+ node: '>=22.0.0',
43
+ npm: '>=10.0.0',
44
+ },
45
+ },
46
+ null,
47
+ 2,
48
+ )}\n`;
49
+ }
50
+
51
+ export const viteConfig = `import { URL, fileURLToPath } from 'node:url';
52
+ import tailwindcss from '@tailwindcss/vite';
53
+ import react, { reactCompilerPreset } from '@vitejs/plugin-react';
54
+ import babel from '@rolldown/plugin-babel';
55
+
56
+ import { defineConfig } from 'vite';
57
+
58
+ export default defineConfig({
59
+ plugins: [react(), babel({ presets: [reactCompilerPreset()] }), tailwindcss()],
60
+ resolve: {
61
+ alias: {
62
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
63
+ },
64
+ },
65
+ server: {
66
+ proxy: {
67
+ '/api': 'http://localhost:8787',
68
+ },
69
+ },
70
+ });
71
+ `;
72
+
73
+ export const tsconfig = `${JSON.stringify(
74
+ {
75
+ files: [],
76
+ references: [{ path: './tsconfig.app.json' }, { path: './tsconfig.node.json' }],
77
+ },
78
+ null,
79
+ 2,
80
+ )}\n`;
81
+
82
+ export const tsconfigApp = `${JSON.stringify(
83
+ {
84
+ compilerOptions: {
85
+ tsBuildInfoFile: './node_modules/.tmp/tsconfig.app.tsbuildinfo',
86
+ target: 'es2023',
87
+ lib: ['ES2023', 'DOM'],
88
+ module: 'esnext',
89
+ types: ['vite/client'],
90
+ skipLibCheck: true,
91
+ moduleResolution: 'bundler',
92
+ paths: {
93
+ '@/*': ['./src/*'],
94
+ },
95
+ allowImportingTsExtensions: true,
96
+ verbatimModuleSyntax: true,
97
+ moduleDetection: 'force',
98
+ noEmit: true,
99
+ jsx: 'react-jsx',
100
+ noUnusedLocals: true,
101
+ noUnusedParameters: true,
102
+ erasableSyntaxOnly: true,
103
+ noFallthroughCasesInSwitch: true,
104
+ },
105
+ include: ['src'],
106
+ },
107
+ null,
108
+ 2,
109
+ )}\n`;
110
+
111
+ export const tsconfigNode = `${JSON.stringify(
112
+ {
113
+ compilerOptions: {
114
+ tsBuildInfoFile: './node_modules/.tmp/tsconfig.node.tsbuildinfo',
115
+ target: 'es2023',
116
+ lib: ['ES2023'],
117
+ module: 'esnext',
118
+ types: ['node'],
119
+ skipLibCheck: true,
120
+ moduleResolution: 'bundler',
121
+ allowImportingTsExtensions: true,
122
+ verbatimModuleSyntax: true,
123
+ moduleDetection: 'force',
124
+ noEmit: true,
125
+ noUnusedLocals: true,
126
+ noUnusedParameters: true,
127
+ erasableSyntaxOnly: true,
128
+ noFallthroughCasesInSwitch: true,
129
+ },
130
+ include: ['vite.config.ts'],
131
+ },
132
+ null,
133
+ 2,
134
+ )}\n`;
135
+
136
+ export const indexHtml = `<!doctype html>
137
+ <html lang="en">
138
+ <head>
139
+ <meta charset="UTF-8" />
140
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
141
+ <title>Vite + React + Cloudflare</title>
142
+ </head>
143
+ <body>
144
+ <div id="root"></div>
145
+ <script type="module" src="/src/main.tsx"></script>
146
+ </body>
147
+ </html>
148
+ `;
149
+
150
+ export const mainTsx = `import { StrictMode } from 'react';
151
+ import { createRoot } from 'react-dom/client';
152
+ import './index.css';
153
+ import App from './App.tsx';
154
+
155
+ createRoot(document.getElementById('root')!).render(
156
+ <StrictMode>
157
+ <App />
158
+ </StrictMode>,
159
+ );
160
+ `;
161
+
162
+ export const appTsx = `export default function App() {
163
+ async function checkApi() {
164
+ const response = await fetch('/api/health');
165
+ const result = await response.json();
166
+
167
+ alert(result.message);
168
+ }
169
+
170
+ return (
171
+ <main className="min-h-screen bg-zinc-950 px-6 py-16 text-white">
172
+ <section className="mx-auto flex max-w-3xl flex-col gap-6">
173
+ <p className="text-sm uppercase tracking-[0.35em] text-violet-300">
174
+ Bun + Vite + Wrangler
175
+ </p>
176
+ <h1 className="text-5xl font-semibold tracking-tight">
177
+ Your project is ready.
178
+ </h1>
179
+ <p className="text-lg leading-8 text-zinc-300">
180
+ This app uses a React frontend, a Hono Cloudflare Worker backend, and
181
+ Bun workspace scripts that build and deploy them together.
182
+ </p>
183
+ <button
184
+ className="w-fit rounded-full bg-violet-400 px-5 py-3 font-medium text-zinc-950 transition hover:bg-violet-300"
185
+ onClick={checkApi}
186
+ type="button"
187
+ >
188
+ Check API
189
+ </button>
190
+ </section>
191
+ </main>
192
+ );
193
+ }
194
+ `;
195
+
196
+ export const indexCss = `@import 'tailwindcss' important;
197
+
198
+ *,
199
+ *::before,
200
+ *::after {
201
+ box-sizing: border-box;
202
+ }
203
+
204
+ body {
205
+ min-height: 100vh;
206
+ margin: 0;
207
+ color: white;
208
+ background: black;
209
+ font-family:
210
+ Inter,
211
+ -apple-system,
212
+ BlinkMacSystemFont,
213
+ 'Segoe UI',
214
+ Roboto,
215
+ Oxygen,
216
+ Ubuntu,
217
+ Cantarell,
218
+ 'Fira Sans',
219
+ 'Droid Sans',
220
+ 'Helvetica Neue',
221
+ sans-serif;
222
+ text-rendering: optimizeLegibility;
223
+ -webkit-font-smoothing: antialiased;
224
+ -moz-osx-font-smoothing: grayscale;
225
+ }
226
+ `;
227
+
228
+ export const oxlintConfig = `${JSON.stringify(
229
+ {
230
+ extends: ['../.oxlintrc.json'],
231
+ },
232
+ null,
233
+ 2,
234
+ )}\n`;
@@ -0,0 +1,174 @@
1
+ export interface RootTemplateOptions {
2
+ packageName: string;
3
+ }
4
+
5
+ export function rootPackageJson({ packageName }: RootTemplateOptions) {
6
+ return `${JSON.stringify(
7
+ {
8
+ name: packageName,
9
+ private: true,
10
+ workspaces: ['backend', 'frontend'],
11
+ scripts: {
12
+ build: 'bun run --cwd frontend build',
13
+ deploy: 'bun run build && bun run --cwd backend deploy',
14
+ dev: 'bun run --cwd backend dev',
15
+ lint: 'oxlint',
16
+ 'lint:fix': 'oxlint --fix',
17
+ fmt: 'oxfmt',
18
+ 'fmt:check': 'oxfmt --check',
19
+ },
20
+ devDependencies: {
21
+ oxfmt: '^0.49.0',
22
+ oxlint: '^1.64.0',
23
+ },
24
+ },
25
+ null,
26
+ 2,
27
+ )}\n`;
28
+ }
29
+
30
+ export const bunfigToml = `# bunfig.toml
31
+
32
+ [install]
33
+ # Require packages to be at least 10080 minutes old (7 days)
34
+ minimumReleaseAge = 10080
35
+
36
+ # Prevent lifecycle scripts like postinstall/preinstall/install from running automatically
37
+ ignoreScripts = true
38
+ `;
39
+
40
+ export const gitignore = `# Logs
41
+ logs
42
+ *.log
43
+ npm-debug.log*
44
+ yarn-debug.log*
45
+ pnpm-debug.log*
46
+
47
+ node_modules
48
+ .DS_Store
49
+ dist
50
+ dist-ssr
51
+ coverage
52
+ *.local
53
+ *.tsbuildinfo
54
+
55
+ # Editor directories and files
56
+ .idea
57
+ *.suo
58
+ *.ntvs*
59
+ *.njsproj
60
+ *.sln
61
+ *.sw?
62
+
63
+ .env.*
64
+
65
+ # wrangler files
66
+ .wrangler
67
+ .dev.vars*
68
+ !.env.example
69
+ `;
70
+
71
+ export const cursorRules = `# General
72
+ - Do not automatically fix or focus on linting errors unless specifically asked. Focus only on functionality
73
+ - Do not build solutions when you are finished. Do not run build commands
74
+
75
+ # TypeScript
76
+ - Prefer interfaces over types unless an interface is not possible
77
+ - Avoid using ReturnType over defining standalone types for return values
78
+
79
+ # React
80
+ - Prefer export default function Component over having an export at the end of the file
81
+ - Prefer component props to be named just Props instead of ComponentNameProps for simplicity
82
+ - Use React Query when applicable for server state
83
+ - Avoid useEffect unless it is applicable and necessary
84
+ - Avoid useCallback and useMemo excessively as React Compiler applies these automatically
85
+ `;
86
+
87
+ export const oxlintConfig = `${JSON.stringify(
88
+ {
89
+ $schema: './node_modules/oxlint/configuration_schema.json',
90
+ },
91
+ null,
92
+ 2,
93
+ )}\n`;
94
+
95
+ export const oxfmtConfig = `${JSON.stringify(
96
+ {
97
+ $schema: './node_modules/oxfmt/configuration_schema.json',
98
+ singleQuote: true,
99
+ ignorePatterns: ['**/*.d.ts'],
100
+ },
101
+ null,
102
+ 2,
103
+ )}\n`;
104
+
105
+ export const vscodeSettings = `${JSON.stringify(
106
+ {
107
+ 'biome.enabled': false,
108
+ 'oxc.fmt.configPath': '.oxfmtrc.json',
109
+ 'editor.defaultFormatter': 'oxc.oxc-vscode',
110
+ 'editor.formatOnSave': true,
111
+ '[json]': {
112
+ 'editor.defaultFormatter': 'oxc.oxc-vscode',
113
+ },
114
+ '[javascript]': {
115
+ 'editor.defaultFormatter': 'oxc.oxc-vscode',
116
+ },
117
+ '[typescript]': {
118
+ 'editor.defaultFormatter': 'oxc.oxc-vscode',
119
+ },
120
+ '[javascriptreact]': {
121
+ 'editor.defaultFormatter': 'oxc.oxc-vscode',
122
+ },
123
+ '[typescriptreact]': {
124
+ 'editor.defaultFormatter': 'oxc.oxc-vscode',
125
+ },
126
+ '[jsonc]': {
127
+ 'editor.defaultFormatter': 'oxc.oxc-vscode',
128
+ },
129
+ },
130
+ null,
131
+ 2,
132
+ )}\n`;
133
+
134
+ export const vscodeExtensions = `${JSON.stringify(
135
+ {
136
+ recommendations: ['oxc.oxc-vscode'],
137
+ },
138
+ null,
139
+ 2,
140
+ )}\n`;
141
+
142
+ export function readme({ packageName }: RootTemplateOptions) {
143
+ return `# ${packageName}
144
+
145
+ Bun workspace with a Vite React frontend and a Cloudflare Worker backend.
146
+
147
+ ## Prerequisites
148
+
149
+ - Bun
150
+ - Wrangler
151
+ - A Cloudflare account for deployment
152
+
153
+ ## Development
154
+
155
+ \`\`\`sh
156
+ bun install
157
+ bun run --cwd frontend dev
158
+ bun run dev
159
+ \`\`\`
160
+
161
+ The frontend dev server proxies \`/api\` to the Worker on \`http://localhost:8787\`.
162
+
163
+ ## Scripts
164
+
165
+ - \`bun run build\`: build the frontend assets.
166
+ - \`bun run deploy\`: build the frontend and deploy the Worker with Wrangler.
167
+ - \`bun run lint\`: run oxlint.
168
+ - \`bun run fmt\`: run oxfmt.
169
+
170
+ ## Environment
171
+
172
+ Fill in \`DATABASE_URL\` in \`backend/.dev.vars\` for local Worker secrets.
173
+ `;
174
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "types": ["bun-types"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "allowImportingTsExtensions": true,
10
+ "verbatimModuleSyntax": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "noUnusedLocals": true,
14
+ "noUnusedParameters": true
15
+ },
16
+ "include": ["src"]
17
+ }