create-swift-rust 0.4.0 → 0.7.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/dist/index.js +74 -938
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/templates/default/README.md +29 -0
- package/templates/default/_env.example +5 -0
- package/templates/default/_env.local +5 -0
- package/templates/default/_gitignore +14 -0
- package/templates/default/biome.json +12 -0
- package/templates/default/globals.d.ts +19 -0
- package/templates/default/package.json +30 -0
- package/templates/default/postcss.config.mjs +7 -0
- package/templates/default/public/blog/introducing-swift-rust.svg +12 -0
- package/templates/default/public/og-image.svg +25 -0
- package/templates/default/public/samples/axelborg-towers-modern-architecture.jpg +0 -0
- package/templates/default/public/samples/brown-high-skyscrapers.jpg +0 -0
- package/templates/default/public/samples/close-up-portrait-gorgeous-young-woman.jpg +0 -0
- package/templates/default/public/samples/full-shot-woman-taking-selfie.jpg +0 -0
- package/templates/default/public/samples/happy-black-parents-with-kids-making-video-call-smart-phone-home.jpg +0 -0
- package/templates/default/public/samples/metaverse-concept-collage-design.jpg +0 -0
- package/templates/default/src/app/about/layout.tsx +5 -0
- package/templates/default/src/app/about/page.tsx +74 -0
- package/templates/default/src/app/about/team/page.tsx +88 -0
- package/templates/default/src/app/api/contact/route.ts +15 -0
- package/templates/default/src/app/api/health/route.ts +7 -0
- package/templates/default/src/app/api/posts/[id]/route.ts +33 -0
- package/templates/default/src/app/api/posts/route.ts +29 -0
- package/templates/default/src/app/api/search/route.ts +15 -0
- package/templates/default/src/app/api/stats/route.ts +16 -0
- package/templates/default/src/app/blog/[slug]/page.tsx +116 -0
- package/templates/default/src/app/blog/page.tsx +101 -0
- package/templates/default/src/app/blog/tag/[tag]/page.tsx +82 -0
- package/templates/default/src/app/contact/page.tsx +131 -0
- package/templates/default/src/app/dashboard/analytics/page.tsx +103 -0
- package/templates/default/src/app/dashboard/layout.tsx +11 -0
- package/templates/default/src/app/dashboard/loading.tsx +18 -0
- package/templates/default/src/app/dashboard/page.tsx +181 -0
- package/templates/default/src/app/dashboard/posts/[id]/not-found.tsx +23 -0
- package/templates/default/src/app/dashboard/posts/[id]/page.tsx +87 -0
- package/templates/default/src/app/dashboard/posts/new/page.tsx +103 -0
- package/templates/default/src/app/dashboard/posts/page.tsx +81 -0
- package/templates/default/src/app/dashboard/settings/page.tsx +119 -0
- package/templates/default/src/app/error.tsx +42 -0
- package/templates/default/src/app/favicon.ico +0 -0
- package/templates/default/src/app/favicon.svg +8 -0
- package/templates/default/src/app/fonts/page.tsx +610 -0
- package/templates/default/src/app/globals.css +238 -0
- package/templates/default/src/app/images/page.tsx +56 -0
- package/templates/default/src/app/layout.tsx +44 -0
- package/templates/default/src/app/loading.tsx +13 -0
- package/templates/default/src/app/not-found.tsx +25 -0
- package/templates/default/src/app/page.tsx +410 -0
- package/templates/default/src/app/videos/page.tsx +182 -0
- package/templates/default/src/components/font-studio.tsx +378 -0
- package/templates/default/src/components/footer.tsx +75 -0
- package/templates/default/src/components/logo.tsx +26 -0
- package/templates/default/src/components/nav.tsx +102 -0
- package/templates/default/src/components/sidebar.tsx +109 -0
- package/templates/default/src/lib/blur.ts +5 -0
- package/templates/default/src/lib/format.ts +31 -0
- package/templates/default/src/lib/posts.ts +132 -0
- package/templates/default/src/lib/site.config.ts +39 -0
- package/templates/default/src/lib/stats.ts +25 -0
- package/templates/default/swift-rust.config.json +8 -0
- package/templates/default/tsconfig.json +29 -0
- package/templates/default/vercel.json +23 -0
package/dist/index.js
CHANGED
|
@@ -1,8 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { cp, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import * as p from "@clack/prompts";
|
|
5
7
|
import pc from "picocolors";
|
|
8
|
+
/** Locate the bundled template dir, both from src/ (dev) and dist/ (published). */
|
|
9
|
+
function templateDir() {
|
|
10
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
for (const candidate of [
|
|
12
|
+
resolve(here, "..", "templates", "default"),
|
|
13
|
+
resolve(here, "templates", "default"),
|
|
14
|
+
]) {
|
|
15
|
+
if (existsSync(candidate))
|
|
16
|
+
return candidate;
|
|
17
|
+
}
|
|
18
|
+
return resolve(here, "..", "templates", "default");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Copy the full-demo template into `target`, restoring underscore-prefixed
|
|
22
|
+
* dotfiles and substituting the project name placeholder.
|
|
23
|
+
*/
|
|
24
|
+
async function copyTemplate(target, projectName) {
|
|
25
|
+
const src = templateDir();
|
|
26
|
+
if (!existsSync(src)) {
|
|
27
|
+
throw new Error(`Template not found at ${src}. Run \`node scripts/sync-template.mjs\` before publishing.`);
|
|
28
|
+
}
|
|
29
|
+
await mkdir(target, { recursive: true });
|
|
30
|
+
await cp(src, target, { recursive: true });
|
|
31
|
+
// Restore real dotfile names (npm mangles dotfiles inside packages).
|
|
32
|
+
const dotfiles = [
|
|
33
|
+
["_gitignore", ".gitignore"],
|
|
34
|
+
["_env.example", ".env.example"],
|
|
35
|
+
["_env.local", ".env.local"],
|
|
36
|
+
];
|
|
37
|
+
for (const [from, to] of dotfiles) {
|
|
38
|
+
const fromPath = join(target, from);
|
|
39
|
+
if (existsSync(fromPath))
|
|
40
|
+
await rename(fromPath, join(target, to));
|
|
41
|
+
}
|
|
42
|
+
// Substitute the project name in package.json + README.
|
|
43
|
+
const displayName = projectName === "." ? basename(resolve(target)) : projectName;
|
|
44
|
+
for (const file of ["package.json", "README.md"]) {
|
|
45
|
+
const path = join(target, file);
|
|
46
|
+
if (!existsSync(path))
|
|
47
|
+
continue;
|
|
48
|
+
const contents = await readFile(path, "utf8");
|
|
49
|
+
await writeFile(path, contents.split("__PROJECT_NAME__").join(displayName));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
6
52
|
async function pathExists(path) {
|
|
7
53
|
try {
|
|
8
54
|
await stat(path);
|
|
@@ -27,31 +73,16 @@ function normalizeProjectName(name) {
|
|
|
27
73
|
return ".";
|
|
28
74
|
return t.replace(/\/+$/, "");
|
|
29
75
|
}
|
|
30
|
-
function isValidAlias(alias) {
|
|
31
|
-
return /^[a-zA-Z@][a-zA-Z0-9_/*-]*$/.test(alias);
|
|
32
|
-
}
|
|
33
76
|
const HELP = `
|
|
34
77
|
${pc.bold("create-swift-rust")} - scaffold a new swift-rust project
|
|
35
78
|
|
|
36
79
|
${pc.bold("Usage")}
|
|
37
80
|
${pc.cyan("bun create swift-rust@latest")} [project-name] [options]
|
|
38
81
|
|
|
82
|
+
Scaffolds the full-demo starter: blog, dashboard, API routes, and the
|
|
83
|
+
Image / PDF / Video / Font components — TypeScript, Tailwind, Biome, src/app.
|
|
84
|
+
|
|
39
85
|
${pc.bold("Options")}
|
|
40
|
-
${pc.yellow("--ts, --typescript")} Use TypeScript (default)
|
|
41
|
-
${pc.yellow("--js, --javascript")} Use JavaScript
|
|
42
|
-
${pc.yellow("--tailwind")} Install Tailwind CSS
|
|
43
|
-
${pc.yellow("--no-tailwind")} Skip Tailwind CSS
|
|
44
|
-
${pc.yellow("--shadcn")} Install shadcn-style UI components (default)
|
|
45
|
-
${pc.yellow("--no-shadcn")} Skip shadcn UI components
|
|
46
|
-
${pc.yellow("--src-dir")} Use src/ under app/ (default)
|
|
47
|
-
${pc.yellow("--no-src-dir")} Use top-level app/
|
|
48
|
-
${pc.yellow("--biome")} Use Biome for linting (default)
|
|
49
|
-
${pc.yellow("--eslint")} Use ESLint for linting
|
|
50
|
-
${pc.yellow("--import-alias <prefix>")} Set import alias (default "@/*")
|
|
51
|
-
${pc.yellow("--ssr")} Server-side rendering
|
|
52
|
-
${pc.yellow("--ssr-wasm")} SSR with WASM hydration (default)
|
|
53
|
-
${pc.yellow("--ssr-htmx")} SSR with HTMX
|
|
54
|
-
${pc.yellow("--wasm")} Full WASM SPA
|
|
55
86
|
${pc.yellow("--install")} Run bun install after scaffolding
|
|
56
87
|
${pc.yellow("--no-install")} Skip bun install (default)
|
|
57
88
|
${pc.yellow("--yes, -y")} Skip prompts, use defaults
|
|
@@ -59,8 +90,8 @@ ${pc.bold("Options")}
|
|
|
59
90
|
|
|
60
91
|
${pc.bold("Examples")}
|
|
61
92
|
${pc.cyan("bun create swift-rust@latest")}
|
|
62
|
-
${pc.cyan("bun create swift-rust@latest my-app --
|
|
63
|
-
${pc.cyan("bun create swift-rust@latest ./
|
|
93
|
+
${pc.cyan("bun create swift-rust@latest my-app --install")}
|
|
94
|
+
${pc.cyan("bun create swift-rust@latest ./")}
|
|
64
95
|
`;
|
|
65
96
|
function parseFlags(args) {
|
|
66
97
|
const flags = {};
|
|
@@ -113,24 +144,6 @@ function parseFlags(args) {
|
|
|
113
144
|
}
|
|
114
145
|
return flags;
|
|
115
146
|
}
|
|
116
|
-
const RENDERER_OPTIONS = [
|
|
117
|
-
{
|
|
118
|
-
value: "ssr-wasm",
|
|
119
|
-
label: "SSR + WASM hydration",
|
|
120
|
-
hint: "Default. Server-rendered HTML, WASM-hydrated interactive islands.",
|
|
121
|
-
},
|
|
122
|
-
{ value: "ssr", label: "SSR only", hint: "Server-rendered HTML, no client JavaScript at all." },
|
|
123
|
-
{
|
|
124
|
-
value: "ssr-htmx",
|
|
125
|
-
label: "SSR + HTMX",
|
|
126
|
-
hint: "Progressive enhancement with HTMX-style interactions.",
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
value: "wasm",
|
|
130
|
-
label: "Full WASM SPA",
|
|
131
|
-
hint: "Single-page app compiled entirely to WebAssembly.",
|
|
132
|
-
},
|
|
133
|
-
];
|
|
134
147
|
async function askQuestions(flags, positional) {
|
|
135
148
|
const yesMode = flags.yes === true;
|
|
136
149
|
const projectName = positional ??
|
|
@@ -144,7 +157,7 @@ async function askQuestions(flags, positional) {
|
|
|
144
157
|
if (!v)
|
|
145
158
|
return "Project name is required";
|
|
146
159
|
if (!isCurrentDir(v) && !isValidName(normalizeProjectName(v)))
|
|
147
|
-
return
|
|
160
|
+
return 'Invalid name. Use letters, numbers, dashes, and underscores — or "." for the current directory.';
|
|
148
161
|
return undefined;
|
|
149
162
|
},
|
|
150
163
|
}));
|
|
@@ -152,130 +165,8 @@ async function askQuestions(flags, positional) {
|
|
|
152
165
|
p.cancel("Aborted.");
|
|
153
166
|
return null;
|
|
154
167
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
languageAnswer = flags.language;
|
|
158
|
-
}
|
|
159
|
-
else if (yesMode) {
|
|
160
|
-
languageAnswer = "ts";
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
const languageOptions = [
|
|
164
|
-
{ value: "ts", label: "TypeScript", hint: "Type-safe, recommended" },
|
|
165
|
-
{ value: "js", label: "JavaScript", hint: "No type checking" },
|
|
166
|
-
];
|
|
167
|
-
const result = await p.select({
|
|
168
|
-
message: "Which language would you like to use?",
|
|
169
|
-
options: languageOptions,
|
|
170
|
-
initialValue: "ts",
|
|
171
|
-
});
|
|
172
|
-
languageAnswer = result;
|
|
173
|
-
}
|
|
174
|
-
if (p.isCancel(languageAnswer)) {
|
|
175
|
-
p.cancel("Aborted.");
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
const language = languageAnswer;
|
|
179
|
-
let renderer = flags.renderer ?? "ssr-wasm";
|
|
180
|
-
if (!flags.renderer && !yesMode) {
|
|
181
|
-
const r = (await p.select({
|
|
182
|
-
message: "Which rendering mode do you want?",
|
|
183
|
-
options: RENDERER_OPTIONS,
|
|
184
|
-
initialValue: "ssr-wasm",
|
|
185
|
-
}));
|
|
186
|
-
if (p.isCancel(r)) {
|
|
187
|
-
p.cancel("Aborted.");
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
renderer = r;
|
|
191
|
-
}
|
|
192
|
-
let linterAnswer;
|
|
193
|
-
if (flags.linter) {
|
|
194
|
-
linterAnswer = flags.linter;
|
|
195
|
-
}
|
|
196
|
-
else if (yesMode) {
|
|
197
|
-
linterAnswer = "biome";
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
const linterOptions = [
|
|
201
|
-
{ value: "biome", label: "Biome", hint: "Fast Rust-based all-in-one tool (recommended)" },
|
|
202
|
-
{ value: "eslint", label: "ESLint", hint: "The classic, more configurable" },
|
|
203
|
-
];
|
|
204
|
-
const result = await p.select({
|
|
205
|
-
message: "Which linter would you like to use?",
|
|
206
|
-
options: linterOptions,
|
|
207
|
-
initialValue: "biome",
|
|
208
|
-
});
|
|
209
|
-
linterAnswer = result;
|
|
210
|
-
}
|
|
211
|
-
if (p.isCancel(linterAnswer)) {
|
|
212
|
-
p.cancel("Aborted.");
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
const linter = linterAnswer;
|
|
216
|
-
const tailwindAnswer = typeof flags.tailwind === "boolean"
|
|
217
|
-
? flags.tailwind
|
|
218
|
-
: yesMode
|
|
219
|
-
? false
|
|
220
|
-
: ((await p.confirm({
|
|
221
|
-
message: "Would you like to use Tailwind CSS?",
|
|
222
|
-
initialValue: true,
|
|
223
|
-
})) ?? false);
|
|
224
|
-
if (p.isCancel(tailwindAnswer)) {
|
|
225
|
-
p.cancel("Aborted.");
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
const tailwind = tailwindAnswer === true;
|
|
229
|
-
const useShadcnAnswer = typeof flags.useShadcn === "boolean"
|
|
230
|
-
? flags.useShadcn
|
|
231
|
-
: yesMode
|
|
232
|
-
? true
|
|
233
|
-
: ((await p.confirm({
|
|
234
|
-
message: "Would you like to use shadcn-style UI components?",
|
|
235
|
-
initialValue: true,
|
|
236
|
-
})) ?? false);
|
|
237
|
-
if (p.isCancel(useShadcnAnswer)) {
|
|
238
|
-
p.cancel("Aborted.");
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
const useShadcn = useShadcnAnswer === true;
|
|
242
|
-
const srcDirAnswer = typeof flags.srcDir === "boolean"
|
|
243
|
-
? flags.srcDir
|
|
244
|
-
: yesMode
|
|
245
|
-
? true
|
|
246
|
-
: ((await p.confirm({
|
|
247
|
-
message: "Would you like to use a `src/` directory for your app code?",
|
|
248
|
-
initialValue: true,
|
|
249
|
-
})) ?? true);
|
|
250
|
-
if (p.isCancel(srcDirAnswer)) {
|
|
251
|
-
p.cancel("Aborted.");
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
const srcDir = srcDirAnswer === true;
|
|
255
|
-
const importAliasAnswer = flags.importAlias ??
|
|
256
|
-
(yesMode
|
|
257
|
-
? "@/*"
|
|
258
|
-
: (await p.text({
|
|
259
|
-
message: "Would you like to customize the import alias?",
|
|
260
|
-
placeholder: "@/*",
|
|
261
|
-
defaultValue: "@/*",
|
|
262
|
-
validate: (v) => {
|
|
263
|
-
if (!v)
|
|
264
|
-
return "Import alias is required";
|
|
265
|
-
if (!isValidAlias(v))
|
|
266
|
-
return "Invalid alias. Use letters, numbers, @, /, *, -, _.";
|
|
267
|
-
return undefined;
|
|
268
|
-
},
|
|
269
|
-
})));
|
|
270
|
-
if (p.isCancel(importAliasAnswer)) {
|
|
271
|
-
p.cancel("Aborted.");
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
const importAlias = importAliasAnswer;
|
|
275
|
-
if (!importAlias) {
|
|
276
|
-
p.cancel("Aborted.");
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
168
|
+
// The scaffold is the full-demo template (TypeScript + src/ + Tailwind +
|
|
169
|
+
// Biome). The only remaining choice is whether to install dependencies.
|
|
279
170
|
const installAnswer = typeof flags.install === "boolean"
|
|
280
171
|
? flags.install
|
|
281
172
|
: yesMode
|
|
@@ -288,763 +179,24 @@ async function askQuestions(flags, positional) {
|
|
|
288
179
|
p.cancel("Aborted.");
|
|
289
180
|
return null;
|
|
290
181
|
}
|
|
291
|
-
const install = installAnswer === true;
|
|
292
182
|
return {
|
|
293
183
|
projectName: normalizeProjectName(String(projectName)),
|
|
294
|
-
language,
|
|
295
|
-
renderer,
|
|
296
|
-
linter,
|
|
297
|
-
tailwind,
|
|
298
|
-
srcDir,
|
|
299
|
-
importAlias,
|
|
300
|
-
useShadcn,
|
|
301
|
-
install,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
async function writeProjectFiles(target, answers) {
|
|
305
|
-
const { projectName: rawName, language, renderer, linter, tailwind, srcDir, importAlias, useShadcn } = answers;
|
|
306
|
-
// When scaffolding into the current directory ("."), name the package after
|
|
307
|
-
// the directory itself (like create-next-app), since "." is not a valid name.
|
|
308
|
-
const projectName = rawName === "." ? basename(resolve(target)) : rawName;
|
|
309
|
-
const appDir = join(target, srcDir ? "app/src" : "app");
|
|
310
|
-
const componentsDir = join(target, "components");
|
|
311
|
-
const libDir = join(target, "lib");
|
|
312
|
-
const uiDir = join(componentsDir, "ui");
|
|
313
|
-
await mkdir(target, { recursive: true });
|
|
314
|
-
await mkdir(appDir, { recursive: true });
|
|
315
|
-
await mkdir(componentsDir, { recursive: true });
|
|
316
|
-
await mkdir(libDir, { recursive: true });
|
|
317
|
-
await mkdir(join(target, "public"), { recursive: true });
|
|
318
|
-
if (useShadcn) {
|
|
319
|
-
await mkdir(uiDir, { recursive: true });
|
|
320
|
-
}
|
|
321
|
-
const fileExt = (lang) => (lang === "ts" ? "ts" : "js");
|
|
322
|
-
const componentExt = (lang) => (lang === "ts" ? "tsx" : "jsx");
|
|
323
|
-
const pkg = {
|
|
324
|
-
name: projectName,
|
|
325
|
-
version: "0.1.0",
|
|
326
|
-
private: true,
|
|
327
|
-
type: "module",
|
|
328
|
-
scripts: {
|
|
329
|
-
dev: "swift-rust dev",
|
|
330
|
-
build: "swift-rust build",
|
|
331
|
-
start: "swift-rust start",
|
|
332
|
-
lint: linter === "biome" ? "biome check ." : "eslint .",
|
|
333
|
-
test: "bun test",
|
|
334
|
-
...(language === "ts" ? { typecheck: "tsc --noEmit" } : {}),
|
|
335
|
-
format: linter === "biome" ? "biome format --write ." : "prettier --write .",
|
|
336
|
-
},
|
|
337
|
-
dependencies: {
|
|
338
|
-
"swift-rust": "^1.0.0",
|
|
339
|
-
react: "^19.0.0",
|
|
340
|
-
"react-dom": "^19.0.0",
|
|
341
|
-
...(useShadcn
|
|
342
|
-
? {
|
|
343
|
-
clsx: "^2.1.1",
|
|
344
|
-
"tailwind-merge": "^2.5.5",
|
|
345
|
-
"class-variance-authority": "^0.7.1",
|
|
346
|
-
"lucide-react": "^0.460.0",
|
|
347
|
-
"tw-animate-css": "^1.0.0",
|
|
348
|
-
}
|
|
349
|
-
: {}),
|
|
350
|
-
},
|
|
351
|
-
devDependencies: {
|
|
352
|
-
...(language === "ts"
|
|
353
|
-
? { typescript: "^6.0.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0" }
|
|
354
|
-
: {}),
|
|
355
|
-
...(linter === "biome" ? { "@biomejs/biome": "^1.9.0" } : { eslint: "^9.0.0" }),
|
|
356
|
-
...(tailwind
|
|
357
|
-
? { tailwindcss: "^4.0.0", "@tailwindcss/postcss": "^4.0.0", postcss: "^8.4.0" }
|
|
358
|
-
: {}),
|
|
359
|
-
...(useShadcn ? { shadcn: "^4.0.0" } : {}),
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
|
-
await writeFile(join(target, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
|
|
363
|
-
const config = {
|
|
364
|
-
rendering: renderer,
|
|
365
|
-
image: { domains: [], formats: ["image/avif", "image/webp"] },
|
|
366
|
-
font: { subsets: ["latin"], display: "swap" },
|
|
367
|
-
pdf: { defaultPageSize: "A4" },
|
|
368
|
-
};
|
|
369
|
-
await writeFile(join(target, "swift-rust.config.json"), `${JSON.stringify(config, null, 2)}\n`);
|
|
370
|
-
if (language === "ts") {
|
|
371
|
-
const tsconfig = {
|
|
372
|
-
extends: "swift-rust/tsconfig.base.json",
|
|
373
|
-
compilerOptions: {
|
|
374
|
-
jsx: "preserve",
|
|
375
|
-
baseUrl: ".",
|
|
376
|
-
paths: { [importAlias]: ["./*"] },
|
|
377
|
-
},
|
|
378
|
-
include: [
|
|
379
|
-
srcDir ? "app/src/**/*" : "app/**/*",
|
|
380
|
-
"components/**/*",
|
|
381
|
-
"lib/**/*",
|
|
382
|
-
"globals.d.ts",
|
|
383
|
-
".swift-rust/types/**/*",
|
|
384
|
-
],
|
|
385
|
-
exclude: ["node_modules", "dist", ".swift-rust", ".turbo"],
|
|
386
|
-
};
|
|
387
|
-
await writeFile(join(target, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`);
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
const jsconfig = {
|
|
391
|
-
compilerOptions: {
|
|
392
|
-
baseUrl: ".",
|
|
393
|
-
paths: { [importAlias]: ["./*"] },
|
|
394
|
-
},
|
|
395
|
-
include: [srcDir ? "app/src/**/*" : "app/**/*", "components/**/*", "lib/**/*"],
|
|
396
|
-
};
|
|
397
|
-
await writeFile(join(target, "jsconfig.json"), `${JSON.stringify(jsconfig, null, 2)}\n`);
|
|
398
|
-
}
|
|
399
|
-
const gitignore = [
|
|
400
|
-
"node_modules/",
|
|
401
|
-
".swift-rust/",
|
|
402
|
-
".vercel/",
|
|
403
|
-
"dist/",
|
|
404
|
-
".turbo/",
|
|
405
|
-
"target/",
|
|
406
|
-
"*.log",
|
|
407
|
-
".DS_Store",
|
|
408
|
-
"",
|
|
409
|
-
"# env files — ignore everything except the example",
|
|
410
|
-
".env",
|
|
411
|
-
".env.*",
|
|
412
|
-
"!.env.example",
|
|
413
|
-
].join("\n");
|
|
414
|
-
await writeFile(join(target, ".gitignore"), `${gitignore}\n`);
|
|
415
|
-
const vercelConfig = {
|
|
416
|
-
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
417
|
-
buildCommand: "bun run build",
|
|
418
|
-
installCommand: "bun install --frozen-lockfile",
|
|
419
|
-
outputDirectory: ".vercel/output",
|
|
420
|
-
framework: null,
|
|
421
|
-
trailingSlash: false,
|
|
422
|
-
cleanUrls: true,
|
|
423
|
-
headers: [
|
|
424
|
-
{
|
|
425
|
-
source: "/_swift-rust/static/(.*)",
|
|
426
|
-
headers: [
|
|
427
|
-
{ key: "Cache-Control", value: "public, max-age=31536000, immutable" },
|
|
428
|
-
],
|
|
429
|
-
},
|
|
430
|
-
{
|
|
431
|
-
source: "/fonts/(.*)",
|
|
432
|
-
headers: [
|
|
433
|
-
{ key: "Cache-Control", value: "public, max-age=31536000, immutable" },
|
|
434
|
-
],
|
|
435
|
-
},
|
|
436
|
-
],
|
|
184
|
+
language: "ts",
|
|
185
|
+
renderer: "ssr-wasm",
|
|
186
|
+
linter: "biome",
|
|
187
|
+
tailwind: true,
|
|
188
|
+
srcDir: true,
|
|
189
|
+
importAlias: "@/*",
|
|
190
|
+
useShadcn: false,
|
|
191
|
+
install: installAnswer === true,
|
|
437
192
|
};
|
|
438
|
-
await writeFile(join(target, "vercel.json"), `${JSON.stringify(vercelConfig, null, 2)}\n`);
|
|
439
|
-
if (tailwind) {
|
|
440
|
-
const postcss = `const config = {
|
|
441
|
-
plugins: {
|
|
442
|
-
"@tailwindcss/postcss": {},
|
|
443
|
-
},
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
export default config;
|
|
447
|
-
`;
|
|
448
|
-
await writeFile(join(target, "postcss.config.mjs"), postcss);
|
|
449
|
-
const shadcnVars = useShadcn
|
|
450
|
-
? `
|
|
451
|
-
|
|
452
|
-
/* shadcn design tokens (https://ui.shadcn.com/docs/theming) */
|
|
453
|
-
:root {
|
|
454
|
-
--ui-bg: #ffffff;
|
|
455
|
-
--ui-fg: #09090b;
|
|
456
|
-
--ui-card: #ffffff;
|
|
457
|
-
--ui-card-fg: #09090b;
|
|
458
|
-
--ui-popover: #ffffff;
|
|
459
|
-
--ui-popover-fg: #09090b;
|
|
460
|
-
--ui-primary: #18181b;
|
|
461
|
-
--ui-primary-fg: #fafafa;
|
|
462
|
-
--ui-secondary: #f4f4f5;
|
|
463
|
-
--ui-secondary-fg: #18181b;
|
|
464
|
-
--ui-muted: #f4f4f5;
|
|
465
|
-
--ui-muted-fg: #71717a;
|
|
466
|
-
--ui-accent: #f4f4f5;
|
|
467
|
-
--ui-accent-fg: #18181b;
|
|
468
|
-
--ui-destructive: #ef4444;
|
|
469
|
-
--ui-destructive-fg: #fafafa;
|
|
470
|
-
--ui-border: #e4e4e7;
|
|
471
|
-
--ui-input: #e4e4e7;
|
|
472
|
-
--ui-ring: #18181b;
|
|
473
|
-
--ui-radius: 0.5rem;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
.dark {
|
|
477
|
-
--ui-bg: #09090b;
|
|
478
|
-
--ui-fg: #fafafa;
|
|
479
|
-
--ui-card: #09090b;
|
|
480
|
-
--ui-card-fg: #fafafa;
|
|
481
|
-
--ui-popover: #09090b;
|
|
482
|
-
--ui-popover-fg: #fafafa;
|
|
483
|
-
--ui-primary: #fafafa;
|
|
484
|
-
--ui-primary-fg: #18181b;
|
|
485
|
-
--ui-secondary: #27272a;
|
|
486
|
-
--ui-secondary-fg: #fafafa;
|
|
487
|
-
--ui-muted: #27272a;
|
|
488
|
-
--ui-muted-fg: #a1a1aa;
|
|
489
|
-
--ui-accent: #27272a;
|
|
490
|
-
--ui-accent-fg: #fafafa;
|
|
491
|
-
--ui-destructive: #7f1d1d;
|
|
492
|
-
--ui-destructive-fg: #fafafa;
|
|
493
|
-
--ui-border: #27272a;
|
|
494
|
-
--ui-input: #27272a;
|
|
495
|
-
--ui-ring: #d4d4d8;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
@media (prefers-color-scheme: dark) {
|
|
499
|
-
:root:not(.light) {
|
|
500
|
-
--ui-bg: #09090b;
|
|
501
|
-
--ui-fg: #fafafa;
|
|
502
|
-
--ui-card: #09090b;
|
|
503
|
-
--ui-card-fg: #fafafa;
|
|
504
|
-
--ui-popover: #09090b;
|
|
505
|
-
--ui-popover-fg: #fafafa;
|
|
506
|
-
--ui-primary: #fafafa;
|
|
507
|
-
--ui-primary-fg: #18181b;
|
|
508
|
-
--ui-secondary: #27272a;
|
|
509
|
-
--ui-secondary-fg: #fafafa;
|
|
510
|
-
--ui-muted: #27272a;
|
|
511
|
-
--ui-muted-fg: #a1a1aa;
|
|
512
|
-
--ui-accent: #27272a;
|
|
513
|
-
--ui-accent-fg: #fafafa;
|
|
514
|
-
--ui-destructive: #7f1d1d;
|
|
515
|
-
--ui-destructive-fg: #fafafa;
|
|
516
|
-
--ui-border: #27272a;
|
|
517
|
-
--ui-input: #27272a;
|
|
518
|
-
--ui-ring: #d4d4d8;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
@theme inline {
|
|
523
|
-
--color-background: var(--ui-bg);
|
|
524
|
-
--color-foreground: var(--ui-fg);
|
|
525
|
-
--color-card: var(--ui-card);
|
|
526
|
-
--color-card-foreground: var(--ui-card-fg);
|
|
527
|
-
--color-popover: var(--ui-popover);
|
|
528
|
-
--color-popover-foreground: var(--ui-popover-fg);
|
|
529
|
-
--color-primary: var(--ui-primary);
|
|
530
|
-
--color-primary-foreground: var(--ui-primary-fg);
|
|
531
|
-
--color-secondary: var(--ui-secondary);
|
|
532
|
-
--color-secondary-foreground: var(--ui-secondary-fg);
|
|
533
|
-
--color-muted: var(--ui-muted);
|
|
534
|
-
--color-muted-foreground: var(--ui-muted-fg);
|
|
535
|
-
--color-accent: var(--ui-accent);
|
|
536
|
-
--color-accent-foreground: var(--ui-accent-fg);
|
|
537
|
-
--color-destructive: var(--ui-destructive);
|
|
538
|
-
--color-destructive-foreground: var(--ui-destructive-fg);
|
|
539
|
-
--color-border: var(--ui-border);
|
|
540
|
-
--color-input: var(--ui-input);
|
|
541
|
-
--color-ring: var(--ui-ring);
|
|
542
|
-
--radius: var(--ui-radius);
|
|
543
|
-
}
|
|
544
|
-
`
|
|
545
|
-
: "";
|
|
546
|
-
const css = `@import "tailwindcss";
|
|
547
|
-
${useShadcn ? '@import "tw-animate-css";' : ""}
|
|
548
|
-
|
|
549
|
-
@theme {
|
|
550
|
-
--color-bg: #ffffff;
|
|
551
|
-
--color-fg: #09090b;
|
|
552
|
-
--color-accent: ${useShadcn ? "#f97316" : "#0070f3"};
|
|
553
|
-
--font-sans: var(--font-geist-sans), system-ui, sans-serif;
|
|
554
|
-
--font-mono: var(--font-geist-mono), ui-monospace, monospace;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
@variant dark (&:where(.dark, .dark *));
|
|
558
|
-
|
|
559
|
-
@media (prefers-color-scheme: dark) {
|
|
560
|
-
:root:not(.light) {
|
|
561
|
-
--color-bg: #09090b;
|
|
562
|
-
--color-fg: #fafafa;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
body {
|
|
567
|
-
background: var(--color-bg);
|
|
568
|
-
color: var(--color-fg);
|
|
569
|
-
font-family: var(--font-sans);
|
|
570
|
-
}
|
|
571
|
-
${shadcnVars}`;
|
|
572
|
-
await writeFile(join(appDir, `globals.${fileExt(language)}`), language === "ts" ? `import "./globals.css";\n` : `import "./globals.css";\n`);
|
|
573
|
-
await writeFile(join(appDir, "globals.css"), css);
|
|
574
|
-
}
|
|
575
|
-
const layoutImports = `import type { ReactNode } from "react";
|
|
576
|
-
import { Geist, Geist_Mono } from "swift-rust/font/google";
|
|
577
|
-
${tailwind ? `import "./globals.css";\n` : ""}
|
|
578
|
-
const geistSans = Geist({ subsets: ["latin"], display: "swap", variable: true });
|
|
579
|
-
const geistMono = Geist_Mono({ subsets: ["latin"], display: "swap", variable: true });
|
|
580
|
-
|
|
581
|
-
export const metadata = {
|
|
582
|
-
title: {
|
|
583
|
-
template: "%s | ${projectName}",
|
|
584
|
-
default: "${projectName}",
|
|
585
|
-
},
|
|
586
|
-
description: "Built with swift-rust — the React framework powered with Rust + Bun. 10x faster than Next.js.",
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
590
|
-
return (
|
|
591
|
-
<html
|
|
592
|
-
lang="en"
|
|
593
|
-
className={\`\${geistSans.variable} \${geistMono.variable} h-full antialiased\`}
|
|
594
|
-
>
|
|
595
|
-
<head>
|
|
596
|
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
597
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
598
|
-
</head>
|
|
599
|
-
<body className="${tailwind ? "min-h-full flex flex-col" : ""}">${tailwind ? "{children}" : "\\n {children}\\n "}</body>
|
|
600
|
-
</html>
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
`;
|
|
604
|
-
await writeFile(join(appDir, `layout.${componentExt(language)}`), layoutImports);
|
|
605
|
-
const homePage = tailwind
|
|
606
|
-
? useShadcn
|
|
607
|
-
? `import { buttonVariants } from "@/components/ui/button";
|
|
608
|
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
609
|
-
import { Badge } from "@/components/ui/badge";
|
|
610
|
-
|
|
611
|
-
export default function Home() {
|
|
612
|
-
return (
|
|
613
|
-
<main className="min-h-screen flex items-center justify-center px-6 py-12">
|
|
614
|
-
<Card className="max-w-2xl w-full">
|
|
615
|
-
<CardHeader>
|
|
616
|
-
<Badge variant="secondary" className="w-fit mb-2">Welcome</Badge>
|
|
617
|
-
<CardTitle className="text-3xl sm:text-5xl">${projectName}</CardTitle>
|
|
618
|
-
<CardDescription>
|
|
619
|
-
Get started by editing <code>${srcDir ? "app/src/" : "app/"}page.${componentExt(language)}</code>.
|
|
620
|
-
</CardDescription>
|
|
621
|
-
</CardHeader>
|
|
622
|
-
<CardContent className="flex gap-2">
|
|
623
|
-
<a
|
|
624
|
-
href="https://swift-rust.dev/docs"
|
|
625
|
-
className={buttonVariants({ variant: "default" })}
|
|
626
|
-
>
|
|
627
|
-
Read the docs →
|
|
628
|
-
</a>
|
|
629
|
-
<a
|
|
630
|
-
href="https://github.com/swift-rust/swift-rust"
|
|
631
|
-
className={buttonVariants({ variant: "outline" })}
|
|
632
|
-
>
|
|
633
|
-
GitHub
|
|
634
|
-
</a>
|
|
635
|
-
</CardContent>
|
|
636
|
-
</Card>
|
|
637
|
-
</main>
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
`
|
|
641
|
-
: `export default function Home() {
|
|
642
|
-
return (
|
|
643
|
-
<main className="min-h-screen flex items-center justify-center px-6">
|
|
644
|
-
<div className="max-w-2xl text-center">
|
|
645
|
-
<div className="inline-block mb-4 px-3 py-1 text-xs font-medium rounded-full bg-accent/10 text-accent">
|
|
646
|
-
Welcome
|
|
647
|
-
</div>
|
|
648
|
-
<h1 className="text-5xl sm:text-7xl font-bold tracking-tight mb-6">
|
|
649
|
-
${projectName}
|
|
650
|
-
</h1>
|
|
651
|
-
<p className="text-lg text-fg-secondary mb-8">
|
|
652
|
-
Get started by editing <code>${srcDir ? "app/src/" : "app/"}page.${componentExt(language)}</code>.
|
|
653
|
-
</p>
|
|
654
|
-
<a
|
|
655
|
-
href="https://swift-rust.dev/docs"
|
|
656
|
-
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-fg text-bg font-medium text-sm hover:opacity-90 transition-opacity"
|
|
657
|
-
>
|
|
658
|
-
Read the docs →
|
|
659
|
-
</a>
|
|
660
|
-
</div>
|
|
661
|
-
</main>
|
|
662
|
-
);
|
|
663
|
-
}
|
|
664
|
-
`
|
|
665
|
-
: `export default function Home() {
|
|
666
|
-
return (
|
|
667
|
-
<main style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", padding: "2rem" }}>
|
|
668
|
-
<div style={{ maxWidth: "32rem", textAlign: "center" }}>
|
|
669
|
-
<h1>${projectName}</h1>
|
|
670
|
-
<p>Get started by editing <code>${srcDir ? "app/src/" : "app/"}page.${componentExt(language)}</code>.</p>
|
|
671
|
-
<a href="https://swift-rust.dev/docs">Read the docs →</a>
|
|
672
|
-
</div>
|
|
673
|
-
</main>
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
`;
|
|
677
|
-
await writeFile(join(appDir, `page.${componentExt(language)}`), homePage);
|
|
678
|
-
const notFoundPage = tailwind
|
|
679
|
-
? `export const metadata = { title: "Not found" };
|
|
680
|
-
|
|
681
|
-
export default function NotFound() {
|
|
682
|
-
return (
|
|
683
|
-
<main className="min-h-screen flex items-center justify-center px-6">
|
|
684
|
-
<div className="text-center">
|
|
685
|
-
<h1 className="text-6xl font-bold mb-4">404</h1>
|
|
686
|
-
<p className="text-fg-secondary">Page not found</p>
|
|
687
|
-
<a href="/" className="text-accent mt-6 inline-block">← Go home</a>
|
|
688
|
-
</div>
|
|
689
|
-
</main>
|
|
690
|
-
);
|
|
691
|
-
}
|
|
692
|
-
`
|
|
693
|
-
: `export default function NotFound() {
|
|
694
|
-
return (
|
|
695
|
-
<main>
|
|
696
|
-
<h1>404</h1>
|
|
697
|
-
<p>Page not found</p>
|
|
698
|
-
<a href="/">← Go home</a>
|
|
699
|
-
</main>
|
|
700
|
-
);
|
|
701
|
-
}
|
|
702
|
-
`;
|
|
703
|
-
await writeFile(join(appDir, `not-found.${componentExt(language)}`), notFoundPage);
|
|
704
|
-
if (linter === "biome") {
|
|
705
|
-
const biome = {
|
|
706
|
-
$schema: "https://biomejs.dev/schemas/1.9.0/schema.json",
|
|
707
|
-
vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
|
|
708
|
-
files: { ignore: [".swift-rust", "node_modules", "dist", ".turbo"] },
|
|
709
|
-
formatter: { enabled: true, indentStyle: "space", indentWidth: 2 },
|
|
710
|
-
organizeImports: { enabled: true },
|
|
711
|
-
linter: {
|
|
712
|
-
enabled: true,
|
|
713
|
-
rules: { recommended: true, a11y: { noSvgWithoutTitle: "off" } },
|
|
714
|
-
},
|
|
715
|
-
javascript: { formatter: { quoteStyle: "double", semicolons: "always" } },
|
|
716
|
-
};
|
|
717
|
-
await writeFile(join(target, "biome.json"), `${JSON.stringify(biome, null, 2)}\n`);
|
|
718
|
-
}
|
|
719
|
-
else {
|
|
720
|
-
const eslint = `import swiftRust from "eslint-config-swift-rust";
|
|
721
|
-
|
|
722
|
-
export default [
|
|
723
|
-
...swiftRust,
|
|
724
|
-
{
|
|
725
|
-
ignores: [".swift-rust/**", "node_modules/**", "dist/**", ".turbo/**"],
|
|
726
|
-
},
|
|
727
|
-
];
|
|
728
|
-
`;
|
|
729
|
-
await writeFile(join(target, "eslint.config.mjs"), eslint);
|
|
730
|
-
}
|
|
731
|
-
const readme = `# ${projectName}
|
|
732
|
-
|
|
733
|
-
Built with [swift-rust](https://swift-rust.dev) — the React framework powered with Rust + Bun. 10x faster than Next.js.
|
|
734
|
-
|
|
735
|
-
## Scripts
|
|
736
|
-
|
|
737
|
-
- \`bun run dev\` — start the dev server
|
|
738
|
-
- \`bun run build\` — build for production
|
|
739
|
-
- \`bun run start\` — start the production server
|
|
740
|
-
- \`bun run lint\` — run the linter
|
|
741
|
-
- \`bun run typecheck\` — run TypeScript checks (if TS)
|
|
742
|
-
- \`bun run test\` — run tests
|
|
743
|
-
- \`bun run format\` — format the code
|
|
744
|
-
|
|
745
|
-
## Project structure
|
|
746
|
-
|
|
747
|
-
${srcDir
|
|
748
|
-
? `\`\`\`\napp/\n src/\n layout.${componentExt(language)}\n page.${componentExt(language)}\n not-found.${componentExt(language)}\ncomponents/\nlib/\n\`\`\``
|
|
749
|
-
: `\`\`\`\napp/\n layout.${componentExt(language)}\n page.${componentExt(language)}\n not-found.${componentExt(language)}\ncomponents/\nlib/\n\`\`\``}
|
|
750
|
-
|
|
751
|
-
## Learn more
|
|
752
|
-
|
|
753
|
-
- [Documentation](https://swift-rust.dev/docs)
|
|
754
|
-
- [Examples](https://github.com/swift-rust/swift-rust/tree/main/examples)
|
|
755
|
-
- [Discord](https://discord.gg/swift-rust)
|
|
756
|
-
|
|
757
|
-
## Deploy to Vercel
|
|
758
|
-
|
|
759
|
-
The fastest way to deploy is to push to GitHub and import the repo on Vercel:
|
|
760
|
-
|
|
761
|
-
\`\`\`bash
|
|
762
|
-
git init && git add -A && git commit -m "init"
|
|
763
|
-
git remote add origin https://github.com/you/${projectName}.git
|
|
764
|
-
git push -u origin main
|
|
765
|
-
\`\`\`
|
|
766
|
-
|
|
767
|
-
Then on [vercel.com/new](https://vercel.com/new), import the repo. No configuration needed — \`vercel.json\` is included. Your site will be live at \`https://${projectName}.vercel.app\`.
|
|
768
|
-
|
|
769
|
-
For custom domains and ISR / serverless functions, see the [deploy guide](https://swift-rust.dev/docs/guides/deploying).
|
|
770
|
-
`;
|
|
771
|
-
await writeFile(join(target, "README.md"), readme);
|
|
772
|
-
const envExample = `# Example environment variables. Copy values you need into .env.local.
|
|
773
|
-
# .env.local is git-ignored; .env.example is committed.
|
|
774
|
-
# SWIFT_RUST_RUNTIME=/path/to/bun
|
|
775
|
-
`;
|
|
776
|
-
await writeFile(join(target, ".env.example"), envExample);
|
|
777
|
-
// Local env file (git-ignored) so the app works out of the box.
|
|
778
|
-
const envLocal = `# Local environment variables (git-ignored). Not committed.
|
|
779
|
-
# SWIFT_RUST_RUNTIME=/path/to/bun
|
|
780
|
-
`;
|
|
781
|
-
await writeFile(join(target, ".env.local"), envLocal);
|
|
782
|
-
const favicon = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#0a0a0a"/><g stroke="#ffffff" stroke-width="1.9" stroke-linecap="round" fill="none"><circle cx="16" cy="16" r="6.4"/><path d="M16 4.4v2.6M16 25v2.6M4.4 16h2.6M25 16h2.6M7.9 7.9l1.8 1.8M22.3 22.3l1.8 1.8M7.9 24.1l1.8-1.8M22.3 9.7l1.8-1.8"/></g><path d="M17.8 9.3 L11.6 17 H15.2 L14.2 22.8 L20.4 15 H16.8 Z" fill="#fb923c"/></svg>
|
|
783
|
-
`;
|
|
784
|
-
// Metadata icons live in the app directory (Next.js style): swift-rust
|
|
785
|
-
// serves app/favicon.* from the site root and auto-links them in <head>.
|
|
786
|
-
await writeFile(join(appDir, "favicon.svg"), favicon);
|
|
787
|
-
// 32x32 favicon.ico (PNG-encoded) matching favicon.svg, so browsers that
|
|
788
|
-
// request /favicon.ico get a real icon instead of a 404.
|
|
789
|
-
const faviconIco = "AAABAAEAICAAAAEAIAAZBQAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAgAAAAIAgGAAAAc3p69AAAAAZiS0dEAP8A/wD/oL2nkwAABM5JREFUWIXFl19sU2UYxn/f6cbsoTsFoqywPw5BMzodpGNxhUIINMREgssEg5ELI7tUs/HnYiExRI1wwRKWXRBiMGEmphqykKl3JYOOBLMy3U7nthDI6LqRzii6ullit75eYJtNtnWWoU/yJc15z/s8z/f2y/ueTzEDNpttdTKZfB94FXgeWM7SYBK4BXyraVrLxMTET6mASv2wWq0HgAtKqfwlEp0TIvI78E48Hr+UNmC1Wg8opb6caegJQ0TkjXg8fknZbLbV09PTt7PZeU1NDQCXL1/OxkRM07QNStf1j4ET2TCMjIwAUFRUlE06wEc5wN5ss3Nzc7NNTWGvBqx/XJZsISIbNMA2V1DTNPr6+vD7/RiGMSdBR0cHHR0dc8YMw8Dv99PX14emaXO+o5TKnzsCJJNJotEobreb9vb2WSaUUpSXl9Pa2srFixdxOp2PiLe3t+N2u4lGoySTyflkQNd1mW85HA7p6uoSEZGWlhax2+1y7NgxiUQiMhPJ+2EJh8Ny5MgRsdvt0tLSIiIiXV1d4nA45uXXdV2Urusyv72Hu2lsbOTKlSscP34cj8dDMpmku7ubUCjEyulfeHlljDWvf4hSis7OTs6cOcPu3bs5deoUsVhsIfqFK5BadrtdOjs7RUSkp6dHqqurRdd1KXo6X26frJL3dj0rbrdbTNMUEZFAICCGYWTk1XVd5j0DM1FXV4fH46G3t5c9e/Zgmia5FsXnh9bz7Ko8+qNxent78Xq9hEIhtm/fzuHDhxdDTca/QCnFwMAAhYWFbNu2DdM0AWh6rZg69zMAlJzsZfzBNACbN2/m+vXrRCIRnE4nIgvSk7ECGzdupLi4mJs3b6bF33StSouP/PZnWhygp6eH7u5uSkpKKCsry0Q/20BNTQ2RSITR0VFGR0fx+XyUlpYCpMWrSpbTXFuSzilasYzx0y7GT7u4deIl1hq56XdLS0vx+Xxpvkgkkp4fi65AqoSaplGQn0vroefIy3k0LTEtvP3FEPdiiXTjyVR+WMQZcDqdBINBgsEgO3funBXrb3yRQvsyABouD/PZdz8DEAgEqKyspLKyksHBwQUNZKzAwMAAw8PDbNmyhU2bNqWfG09ZWGs8FPf9cD8t7nK5cLlchMPhjOKLMiAiNDc3o5Ti/Pnz2GwPR0e5w4pSYN77g/q2YQBsNhvnzp1DKcXZs2czigOLb0SBQEBEREzTlK1bt8q7u0rk3ifV8sLaFaLrung8HgmFQiIicvXq1UU3on/Vio8ePcqOHTsQEcbaPuD7X62MWdZQUVGBy+VCKcW1a9doamrC6/U+fiv+5zAyDEMaGhokHA5Lcmxw1kC6e/eu1NfXi2EYSzeM/H4/brebYDDIvn37Zu2mrKyMdevWATA0NDTrwKXGcVVVFTdu3MDr9c5bgHkNaJqGaZpEo1Fqa2vnLKXP5wPg4MGDj8QMw6CtrQ2Hw0FFRcW83wTKarXGsr0LjI2NAVBQUJBNOkBMU0rdyTZ7CXBHA77JNjuRSJBIJB7HwNf/58Vk3GKxbEhdzfYrpb7iv72a7Y/H420WgKmpqf6cnJwflVKvAHlPWHxcRN6Kx+NtAJbU06mpqf68vLwLIvIAyP97LVsi0QmgD/jUYrEcmpyc7E4F/gIrAoFO2P2bCQAAAABJRU5ErkJggg==";
|
|
790
|
-
await writeFile(join(appDir, "favicon.ico"), Buffer.from(faviconIco, "base64"));
|
|
791
|
-
if (useShadcn) {
|
|
792
|
-
await scaffoldShadcn({ target, uiDir, libDir, componentExt: componentExt(language), srcDir });
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
async function scaffoldShadcn(options) {
|
|
796
|
-
const { target, uiDir, libDir, componentExt, srcDir } = options;
|
|
797
|
-
const utilsContent = `import { clsx, type ClassValue } from "clsx";
|
|
798
|
-
import { twMerge } from "tailwind-merge";
|
|
799
|
-
|
|
800
|
-
export function cn(...inputs: ClassValue[]) {
|
|
801
|
-
return twMerge(clsx(inputs));
|
|
802
|
-
}
|
|
803
|
-
`;
|
|
804
|
-
const buttonContent = `import * as React from "react";
|
|
805
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
806
|
-
import { cn } from "@/lib/utils";
|
|
807
|
-
|
|
808
|
-
const buttonVariants = cva(
|
|
809
|
-
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-ring)] focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
810
|
-
{
|
|
811
|
-
variants: {
|
|
812
|
-
variant: {
|
|
813
|
-
default: "bg-[var(--ui-primary)] text-[var(--ui-primary-fg)] hover:opacity-90",
|
|
814
|
-
destructive: "bg-[var(--ui-destructive)] text-white hover:opacity-90",
|
|
815
|
-
outline:
|
|
816
|
-
"border border-[var(--ui-border)] bg-[var(--ui-bg)] hover:bg-[var(--ui-accent)] hover:text-[var(--ui-accent-fg)]",
|
|
817
|
-
secondary: "bg-[var(--ui-secondary)] text-[var(--ui-secondary-fg)] hover:opacity-90",
|
|
818
|
-
ghost: "hover:bg-[var(--ui-accent)] hover:text-[var(--ui-accent-fg)]",
|
|
819
|
-
link: "text-[var(--ui-primary)] underline-offset-4 hover:underline",
|
|
820
|
-
},
|
|
821
|
-
size: {
|
|
822
|
-
default: "h-9 px-4 py-2",
|
|
823
|
-
sm: "h-8 rounded-md px-3 text-xs",
|
|
824
|
-
lg: "h-10 rounded-md px-8",
|
|
825
|
-
icon: "h-9 w-9",
|
|
826
|
-
},
|
|
827
|
-
},
|
|
828
|
-
defaultVariants: {
|
|
829
|
-
variant: "default",
|
|
830
|
-
size: "default",
|
|
831
|
-
},
|
|
832
|
-
},
|
|
833
|
-
);
|
|
834
|
-
|
|
835
|
-
export interface ButtonProps
|
|
836
|
-
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
837
|
-
VariantProps<typeof buttonVariants> {}
|
|
838
|
-
|
|
839
|
-
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
840
|
-
({ className, variant, size, ...props }, ref) => (
|
|
841
|
-
<button ref={ref} className={cn(buttonVariants({ variant, size, className }))} {...props} />
|
|
842
|
-
),
|
|
843
|
-
);
|
|
844
|
-
Button.displayName = "Button";
|
|
845
|
-
|
|
846
|
-
export { buttonVariants };
|
|
847
|
-
`;
|
|
848
|
-
const cardContent = `import * as React from "react";
|
|
849
|
-
import { cn } from "@/lib/utils";
|
|
850
|
-
|
|
851
|
-
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
852
|
-
({ className, ...props }, ref) => (
|
|
853
|
-
<div
|
|
854
|
-
ref={ref}
|
|
855
|
-
className={cn(
|
|
856
|
-
"rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] text-[var(--ui-card-fg)] shadow-sm",
|
|
857
|
-
className,
|
|
858
|
-
)}
|
|
859
|
-
{...props}
|
|
860
|
-
/>
|
|
861
|
-
),
|
|
862
|
-
);
|
|
863
|
-
Card.displayName = "Card";
|
|
864
|
-
|
|
865
|
-
export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
866
|
-
({ className, ...props }, ref) => (
|
|
867
|
-
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
868
|
-
),
|
|
869
|
-
);
|
|
870
|
-
CardHeader.displayName = "CardHeader";
|
|
871
|
-
|
|
872
|
-
export const CardTitle = React.forwardRef<
|
|
873
|
-
HTMLDivElement,
|
|
874
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
875
|
-
>(({ className, ...props }, ref) => (
|
|
876
|
-
<div
|
|
877
|
-
ref={ref}
|
|
878
|
-
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
879
|
-
{...props}
|
|
880
|
-
/>
|
|
881
|
-
));
|
|
882
|
-
CardTitle.displayName = "CardTitle";
|
|
883
|
-
|
|
884
|
-
export const CardDescription = React.forwardRef<
|
|
885
|
-
HTMLDivElement,
|
|
886
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
887
|
-
>(({ className, ...props }, ref) => (
|
|
888
|
-
<div
|
|
889
|
-
ref={ref}
|
|
890
|
-
className={cn("text-sm text-[var(--ui-muted-fg)]", className)}
|
|
891
|
-
{...props}
|
|
892
|
-
/>
|
|
893
|
-
));
|
|
894
|
-
CardDescription.displayName = "CardDescription";
|
|
895
|
-
|
|
896
|
-
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
897
|
-
({ className, ...props }, ref) => (
|
|
898
|
-
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
899
|
-
),
|
|
900
|
-
);
|
|
901
|
-
CardContent.displayName = "CardContent";
|
|
902
|
-
|
|
903
|
-
export const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
904
|
-
({ className, ...props }, ref) => (
|
|
905
|
-
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
|
906
|
-
),
|
|
907
|
-
);
|
|
908
|
-
CardFooter.displayName = "CardFooter";
|
|
909
|
-
`;
|
|
910
|
-
const inputContent = `import * as React from "react";
|
|
911
|
-
import { cn } from "@/lib/utils";
|
|
912
|
-
|
|
913
|
-
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
|
914
|
-
|
|
915
|
-
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
916
|
-
({ className, type, ...props }, ref) => (
|
|
917
|
-
<input
|
|
918
|
-
type={type}
|
|
919
|
-
ref={ref}
|
|
920
|
-
className={cn(
|
|
921
|
-
"flex h-9 w-full rounded-md border border-[var(--ui-border)] bg-transparent px-3 py-1 text-sm shadow-sm transition-colors",
|
|
922
|
-
"file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-[var(--ui-fg)]",
|
|
923
|
-
"placeholder:text-[var(--ui-muted-fg)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-ring)] focus-visible:ring-offset-1",
|
|
924
|
-
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
925
|
-
className,
|
|
926
|
-
)}
|
|
927
|
-
{...props}
|
|
928
|
-
/>
|
|
929
|
-
),
|
|
930
|
-
);
|
|
931
|
-
Input.displayName = "Input";
|
|
932
|
-
`;
|
|
933
|
-
const labelContent = `import * as React from "react";
|
|
934
|
-
import { cn } from "@/lib/utils";
|
|
935
|
-
|
|
936
|
-
export const Label = React.forwardRef<
|
|
937
|
-
HTMLLabelElement,
|
|
938
|
-
React.LabelHTMLAttributes<HTMLLabelElement>
|
|
939
|
-
>(({ className, ...props }, ref) => (
|
|
940
|
-
<label
|
|
941
|
-
ref={ref}
|
|
942
|
-
className={cn(
|
|
943
|
-
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
944
|
-
className,
|
|
945
|
-
)}
|
|
946
|
-
{...props}
|
|
947
|
-
/>
|
|
948
|
-
));
|
|
949
|
-
Label.displayName = "Label";
|
|
950
|
-
`;
|
|
951
|
-
const badgeContent = `import * as React from "react";
|
|
952
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
953
|
-
import { cn } from "@/lib/utils";
|
|
954
|
-
|
|
955
|
-
const badgeVariants = cva(
|
|
956
|
-
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--ui-ring)] focus:ring-offset-2",
|
|
957
|
-
{
|
|
958
|
-
variants: {
|
|
959
|
-
variant: {
|
|
960
|
-
default:
|
|
961
|
-
"border-transparent bg-[var(--ui-primary)] text-[var(--ui-primary-fg)] hover:opacity-90",
|
|
962
|
-
secondary:
|
|
963
|
-
"border-transparent bg-[var(--ui-secondary)] text-[var(--ui-secondary-fg)] hover:opacity-90",
|
|
964
|
-
destructive:
|
|
965
|
-
"border-transparent bg-[var(--ui-destructive)] text-white hover:opacity-90",
|
|
966
|
-
outline: "text-[var(--ui-fg)] border-[var(--ui-border)]",
|
|
967
|
-
},
|
|
968
|
-
},
|
|
969
|
-
defaultVariants: {
|
|
970
|
-
variant: "default",
|
|
971
|
-
},
|
|
972
|
-
},
|
|
973
|
-
);
|
|
974
|
-
|
|
975
|
-
export interface BadgeProps
|
|
976
|
-
extends React.HTMLAttributes<HTMLDivElement>,
|
|
977
|
-
VariantProps<typeof badgeVariants> {}
|
|
978
|
-
|
|
979
|
-
export function Badge({ className, variant, ...props }: BadgeProps) {
|
|
980
|
-
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
export { badgeVariants };
|
|
984
|
-
`;
|
|
985
|
-
const componentsJson = `{
|
|
986
|
-
"$schema": "https://ui.shadcn.com/schema.json",
|
|
987
|
-
"style": "new-york",
|
|
988
|
-
"rsc": true,
|
|
989
|
-
"tsx": ${componentExt === "tsx"},
|
|
990
|
-
"tailwind": {
|
|
991
|
-
"config": "",
|
|
992
|
-
"css": "${srcDir ? "app/src/globals.css" : "app/globals.css"}",
|
|
993
|
-
"baseColor": "zinc",
|
|
994
|
-
"cssVariables": true,
|
|
995
|
-
"prefix": ""
|
|
996
|
-
},
|
|
997
|
-
"aliases": {
|
|
998
|
-
"components": "@/components",
|
|
999
|
-
"utils": "@/lib/utils",
|
|
1000
|
-
"ui": "@/components/ui",
|
|
1001
|
-
"lib": "@/lib",
|
|
1002
|
-
"hooks": "@/hooks"
|
|
1003
|
-
},
|
|
1004
|
-
"iconLibrary": "lucide"
|
|
1005
|
-
}
|
|
1006
|
-
`;
|
|
1007
|
-
await writeFile(join(libDir, "utils.ts"), utilsContent);
|
|
1008
|
-
await writeFile(join(uiDir, `button.${componentExt}`), buttonContent);
|
|
1009
|
-
await writeFile(join(uiDir, `card.${componentExt}`), cardContent);
|
|
1010
|
-
await writeFile(join(uiDir, `input.${componentExt}`), inputContent);
|
|
1011
|
-
await writeFile(join(uiDir, `label.${componentExt}`), labelContent);
|
|
1012
|
-
await writeFile(join(uiDir, `badge.${componentExt}`), badgeContent);
|
|
1013
|
-
await writeFile(join(target, "components.json"), `${componentsJson}\n`);
|
|
1014
193
|
}
|
|
1015
194
|
async function runInstall(target) {
|
|
1016
195
|
const { spawn } = await import("node:child_process");
|
|
1017
|
-
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
resolve();
|
|
1022
|
-
else
|
|
1023
|
-
reject(new Error(`bun install exited with code ${code}`));
|
|
1024
|
-
});
|
|
1025
|
-
proc.on("error", reject);
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
const SHADCN_DEFAULT_COMPONENTS = ["button", "card", "input", "label", "badge"];
|
|
1029
|
-
async function runShadcnAdd(target, components) {
|
|
1030
|
-
const { spawn } = await import("node:child_process");
|
|
1031
|
-
return new Promise((resolve) => {
|
|
1032
|
-
const args = [
|
|
1033
|
-
"--bun",
|
|
1034
|
-
"shadcn@latest",
|
|
1035
|
-
"add",
|
|
1036
|
-
...components,
|
|
1037
|
-
"--yes",
|
|
1038
|
-
"--overwrite",
|
|
1039
|
-
"--cwd",
|
|
1040
|
-
target,
|
|
1041
|
-
"--silent",
|
|
1042
|
-
];
|
|
1043
|
-
const proc = spawn("bunx", args, { cwd: target, stdio: "pipe" });
|
|
1044
|
-
proc.on("exit", (code) => {
|
|
1045
|
-
resolve(code === 0);
|
|
1046
|
-
});
|
|
1047
|
-
proc.on("error", () => resolve(false));
|
|
196
|
+
await new Promise((resolvePromise, reject) => {
|
|
197
|
+
const child = spawn("bun", ["install"], { cwd: target, stdio: "inherit" });
|
|
198
|
+
child.on("error", reject);
|
|
199
|
+
child.on("close", (code) => code === 0 ? resolvePromise() : reject(new Error(`bun install exited with code ${code}`)));
|
|
1048
200
|
});
|
|
1049
201
|
}
|
|
1050
202
|
async function main() {
|
|
@@ -1066,7 +218,7 @@ async function main() {
|
|
|
1066
218
|
const answers = await askQuestions(flags, positional);
|
|
1067
219
|
if (!answers)
|
|
1068
220
|
process.exit(1);
|
|
1069
|
-
const { projectName,
|
|
221
|
+
const { projectName, install } = answers;
|
|
1070
222
|
const target = resolve(process.cwd(), projectName);
|
|
1071
223
|
if (projectName === ".") {
|
|
1072
224
|
if ((await listDir(target)).length > 0) {
|
|
@@ -1098,13 +250,10 @@ async function main() {
|
|
|
1098
250
|
}
|
|
1099
251
|
const summary = [
|
|
1100
252
|
`${pc.cyan("•")} Project: ${pc.bold(projectName === "." ? resolve(target) : projectName)}`,
|
|
1101
|
-
`${pc.cyan("•")}
|
|
1102
|
-
`${pc.cyan("•")}
|
|
1103
|
-
`${pc.cyan("•")}
|
|
1104
|
-
`${pc.cyan("•")}
|
|
1105
|
-
`${pc.cyan("•")} shadcn UI: ${answers.useShadcn ? "Yes" : "No"}`,
|
|
1106
|
-
`${pc.cyan("•")} src/ dir: ${srcDir ? "Yes (app/src/)" : "No"}`,
|
|
1107
|
-
`${pc.cyan("•")} Import as: ${answers.importAlias}`,
|
|
253
|
+
`${pc.cyan("•")} Template: Full demo (blog, dashboard, API, Image/PDF/Video/Font)`,
|
|
254
|
+
`${pc.cyan("•")} Language: TypeScript`,
|
|
255
|
+
`${pc.cyan("•")} Styling: Tailwind CSS`,
|
|
256
|
+
`${pc.cyan("•")} Linter: Biome`,
|
|
1108
257
|
`${pc.cyan("•")} Install: ${install ? "Yes" : "No"}`,
|
|
1109
258
|
].join("\n");
|
|
1110
259
|
if (quiet) {
|
|
@@ -1123,31 +272,18 @@ async function main() {
|
|
|
1123
272
|
}
|
|
1124
273
|
console.log(`Scaffolding ${pc.cyan(projectName === "." ? "." : projectName)}…`);
|
|
1125
274
|
try {
|
|
1126
|
-
await
|
|
275
|
+
await copyTemplate(target, projectName);
|
|
1127
276
|
console.log(`✓ Scaffolded project into ${pc.green(target)}`);
|
|
1128
|
-
let installSucceeded = false;
|
|
1129
277
|
if (install) {
|
|
1130
278
|
console.log("Running bun install…");
|
|
1131
279
|
try {
|
|
1132
280
|
await runInstall(target);
|
|
1133
281
|
console.log("✓ Installed dependencies");
|
|
1134
|
-
installSucceeded = true;
|
|
1135
282
|
}
|
|
1136
283
|
catch (_err) {
|
|
1137
284
|
console.error("Failed to install dependencies. Run `bun install` manually.");
|
|
1138
285
|
}
|
|
1139
286
|
}
|
|
1140
|
-
if (answers.useShadcn && installSucceeded) {
|
|
1141
|
-
console.log("Fetching canonical shadcn components…");
|
|
1142
|
-
const ok = await runShadcnAdd(target, SHADCN_DEFAULT_COMPONENTS);
|
|
1143
|
-
if (ok) {
|
|
1144
|
-
console.log(`✓ Added ${SHADCN_DEFAULT_COMPONENTS.length} shadcn components from the registry`);
|
|
1145
|
-
}
|
|
1146
|
-
else {
|
|
1147
|
-
const cwdArg = projectName === "." ? "." : `./${projectName}`;
|
|
1148
|
-
console.log(`${pc.yellow("!")} Could not fetch shadcn components from the registry. Using bundled shadcn-style components as a fallback. You can retry later with: ${pc.cyan(`bunx --bun shadcn@latest add ${SHADCN_DEFAULT_COMPONENTS.join(" ")} --cwd ${cwdArg}`)}`);
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
287
|
const next = projectName === "." ? "bun run dev" : `cd ${projectName}\nbun run dev`;
|
|
1152
288
|
console.log(`\n${pc.green("Done!")} Next steps:\n${pc.cyan(next)}`);
|
|
1153
289
|
}
|