@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 +7 -0
- package/.oxfmtrc.json +5 -0
- package/.oxlintrc.json +3 -0
- package/.vscode/extensions.json +3 -0
- package/.vscode/settings.json +27 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bun.lock +106 -0
- package/bunfig.toml +9 -0
- package/package.json +23 -0
- package/src/index.ts +12 -0
- package/src/scaffold.ts +355 -0
- package/src/templates/backend.ts +193 -0
- package/src/templates/frontend.ts +234 -0
- package/src/templates/root.ts +174 -0
- package/tsconfig.json +17 -0
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
package/.oxlintrc.json
ADDED
|
@@ -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
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
|
+
}
|
package/src/scaffold.ts
ADDED
|
@@ -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
|
+
}
|