From acf4c22d1313155701cb80099967a52210d260cf Mon Sep 17 00:00:00 2001 From: Christian Svensson Date: Fri, 12 Sep 2025 20:19:46 +0200 Subject: [PATCH] feat: allow initalizing with "src" dir --- docs/latest/examples/use-src-dir.md | 63 +++++++++++++++++++++++++++++ docs/toc.ts | 1 + packages/init/src/init.ts | 58 +++++++++++++++++--------- packages/init/src/init_test.ts | 39 ++++++++++++++++++ packages/init/src/mod.ts | 2 + 5 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 docs/latest/examples/use-src-dir.md diff --git a/docs/latest/examples/use-src-dir.md b/docs/latest/examples/use-src-dir.md new file mode 100644 index 00000000000..081b0f98c3e --- /dev/null +++ b/docs/latest/examples/use-src-dir.md @@ -0,0 +1,63 @@ +--- +description: | + Change the source directory to effectively manage your project. +--- + +To reduce the number of files in the root project directory, it's possible to +configure a `"src"` directory when initalizing Fresh. + +```sh Terminal +# Move code into "src" folder +deno run -Ar jsr:@fresh/init --src-dir +``` + +It's also possible to use a different folder name: + +```sh Terminal +deno run -Ar jsr:@fresh/init --src-dir=app +``` + +## Files structure + +When initializing the project with `--src-dir`, the structure will look roughly +like below: + +```txt-files Project structure + +├── src/ +│ ├── components/ +│ │ └── Button.tsx +│ ├── islands/ +│ │ └── Counter.tsx +│ ├── routes/ +│ │   └── index.tsx +│ ├── static/ +│ ├── client.ts +│ └── main.ts +├── deno.json +└── vite.config.ts +``` + +## Updating an existing project + +To migrate an existing Fresh project using Vite, to use a `src` directory follow +the steps below. + +1. Move all code related to Fresh into the `src` folder + - Move `components/`, `routes/`, `islands/`, `main.ts`, `client.ts` and any + other related code into the `src` folder + - Leave `deno.json` & `vite.config.ts` in the project root +2. Update `vite.config.ts` and add `root: "src",` + ```diff + export default defineConfig({ + + root: "src", + plugins: [fresh()], + }); + ``` +3. Update the `"start"` task in `deno.json` to the new `_fresh` location + ```diff + - "start": "deno serve -A _fresh/server.js", + + "start": "deno serve -A src/_fresh/server.js", + ``` + +Success! The project should now run with `deno task`. diff --git a/docs/toc.ts b/docs/toc.ts index c88187f46bc..c2ba64c7397 100644 --- a/docs/toc.ts +++ b/docs/toc.ts @@ -91,6 +91,7 @@ const toc: RawTableOfContents = { pages: [ ["migration-guide", "Migration Guide", "link:latest"], ["daisyui", "daisyUI", "link:latest"], + ["use-src-dir", "Using a src folder", "link:latest"], ["markdown", "Rendering Markdown", "link:latest"], ["rendering-raw-html", "Rendering raw HTML", "link:latest"], [ diff --git a/packages/init/src/init.ts b/packages/init/src/init.ts index 54b6298c46e..6815f8c4386 100644 --- a/packages/init/src/init.ts +++ b/packages/init/src/init.ts @@ -62,6 +62,7 @@ ${colors.rgb8("OPTIONS:", 3)} ${colors.rgb8("--vscode", 2)} Setup project for VS Code ${colors.rgb8("--docker", 2)} Setup Project to use Docker ${colors.rgb8("--builder", 2)} Setup with builder instead of vite + ${colors.rgb8("--src-dir", 2)} Setup with "src" directory (Vite only) ${colors.rgb8("--help, -h", 2)} Show this help message `; @@ -84,6 +85,7 @@ export async function initProject( tailwind?: boolean | null; vscode?: boolean | null; builder?: boolean | null; + "src-dir"?: string | null; help?: boolean | null; h?: boolean | null; } = {}, @@ -138,6 +140,16 @@ export async function initProject( } } + let src = ""; + if (flags.builder && typeof flags["src-dir"] === "string") { + error("The --src-dir flag is only supported with Vite."); + } else if (typeof flags["src-dir"] === "string") { + // The --src-dir flag was passed (empty or non-empty string) + src = flags["src-dir"] || "src"; + if (!src.endsWith("/")) src += "/"; + } + const srcDir = path.join(projectDir, src); + const useVite = !flags.builder; const useDocker = flags.docker; @@ -162,6 +174,8 @@ export async function initProject( | ReadableStream | Record, ) => await writeProjectFile(projectDir, pathname, content); + const writeSrcFile = async (...args: Parameters) => + await writeProjectFile(srcDir, ...args); const GITIGNORE = `# dotenv environment variable files .env @@ -354,14 +368,14 @@ ${GRADIENT_CSS}`; const cssStyles = useTailwind ? TAILWIND_CSS : NO_TAILWIND_STYLES; if (useVite) { - await writeFile("assets/styles.css", cssStyles); - await writeFile( + await writeSrcFile("assets/styles.css", cssStyles); + await writeSrcFile( "client.ts", `// Import CSS files here for hot module reloading to work. import "./assets/styles.css";`, ); } else { - await writeFile("static/styles.css", cssStyles); + await writeSrcFile("static/styles.css", cssStyles); } // deno-fmt-ignore const STATIC_LOGO = @@ -384,12 +398,12 @@ import "./assets/styles.css";`, fill="#fff" /> `; - await writeFile("static/logo.svg", STATIC_LOGO); + await writeSrcFile("static/logo.svg", STATIC_LOGO); try { const res = await fetch("https://fresh.deno.dev/favicon.ico"); const buf = await res.arrayBuffer(); - await writeFile("static/favicon.ico", new Uint8Array(buf)); + await writeSrcFile("static/favicon.ico", new Uint8Array(buf)); } catch { // Skip this and be silent if there is a network issue. } @@ -424,7 +438,7 @@ app.use(exampleLoggerMiddleware); // Include file-system based routes here app.fsRoutes();`; - await writeFile("main.ts", MAIN_TS); + await writeSrcFile("main.ts", MAIN_TS); const COMPONENTS_BUTTON_TSX = `import type { ComponentChildren } from "preact"; @@ -444,7 +458,7 @@ export function Button(props: ButtonProps) { /> ); }`; - await writeFile("components/Button.tsx", COMPONENTS_BUTTON_TSX); + await writeSrcFile("components/Button.tsx", COMPONENTS_BUTTON_TSX); const UTILS_TS = `import { createDefine } from "fresh"; @@ -455,7 +469,7 @@ export interface State { } export const define = createDefine();`; - await writeFile("utils.ts", UTILS_TS); + await writeSrcFile("utils.ts", UTILS_TS); const ROUTES_HOME = `import { useSignal } from "@preact/signals"; import { Head } from "fresh/runtime"; @@ -490,7 +504,7 @@ export default define.page(function Home(ctx) { ); });`; - await writeFile("routes/index.tsx", ROUTES_HOME); + await writeSrcFile("routes/index.tsx", ROUTES_HOME); const APP_WRAPPER = `import { define } from "../utils.ts"; @@ -510,7 +524,7 @@ export default define.page(function App({ Component }) { ); });`; - await writeFile("routes/_app.tsx", APP_WRAPPER); + await writeSrcFile("routes/_app.tsx", APP_WRAPPER); const API_NAME = `import { define } from "../../utils.ts"; @@ -522,7 +536,7 @@ export const handler = define.handlers({ ); }, });`; - await writeFile("routes/api/[name].tsx", API_NAME); + await writeSrcFile("routes/api/[name].tsx", API_NAME); const ISLANDS_COUNTER_TSX = `import type { Signal } from "@preact/signals"; import { Button } from "../components/Button.tsx"; @@ -540,7 +554,7 @@ export default function Counter(props: CounterProps) { ); }`; - await writeFile("islands/Counter.tsx", ISLANDS_COUNTER_TSX); + await writeSrcFile("islands/Counter.tsx", ISLANDS_COUNTER_TSX); const DEV_TS = `#!/usr/bin/env -S deno run -A --watch=static/,routes/ ${useTailwind ? `import { tailwind } from "@fresh/plugin-tailwind";\n` : ""} @@ -555,16 +569,16 @@ if (Deno.args.includes("build")) { }`; if (!useVite) { - await writeFile("dev.ts", DEV_TS); + await writeSrcFile("dev.ts", DEV_TS); } const denoJson = { nodeModulesDir: "auto", tasks: { check: "deno fmt --check . && deno lint . && deno check", - dev: "deno run -A --watch=static/,routes/ dev.ts", - build: "deno run -A dev.ts build", - start: "deno serve -A _fresh/server.js", + dev: `deno run -A --watch=static/,routes/ ${src}dev.ts`, + build: `deno run -A ${src}dev.ts build`, + start: `deno serve -A ${src}_fresh/server.js`, update: "deno run -A -r jsr:@fresh/update .", }, lint: { @@ -639,9 +653,15 @@ import { fresh } from "@fresh/plugin-vite";\n`; viteConfig += `import tailwindcss from "@tailwindcss/vite";\n`; } - viteConfig += `\nexport default defineConfig({ - plugins: [fresh()${useTailwind ? ", tailwindcss()" : ""}], -});`; + viteConfig += "\nexport default defineConfig({\n"; + + if (src.length > 0) { + viteConfig += ` root: "${src.slice(0, -1)}",\n`; + } + + viteConfig += ` plugins: [fresh()${ + useTailwind ? ", tailwindcss()" : "" + }],\n});`; await writeFile("vite.config.ts", viteConfig); } diff --git a/packages/init/src/init_test.ts b/packages/init/src/init_test.ts index 21ad07c8e68..c9d20581a73 100644 --- a/packages/init/src/init_test.ts +++ b/packages/init/src/init_test.ts @@ -4,6 +4,7 @@ import { CONFIRM_VITE_MESSAGE, CONFIRM_VSCODE_MESSAGE, HELP_TEXT, + InitError, initProject, } from "./init.ts"; import * as path from "@std/path"; @@ -119,6 +120,44 @@ Deno.test("init - create project dir", async () => { await expectProjectFile(root, "static/styles.css"); }); +Deno.test("init - create project dir with 'src'", async () => { + await using tmp = await withTmpDir(); + const dir = tmp.dir; + using _promptStub = stubPrompt("."); + using _confirmStub = stubConfirm(); + await initProject(dir, [], { "src-dir": "" }); + + const src = path.join(dir, "src"); + await expectProjectFile(dir, ".gitignore"); + await expectProjectFile(dir, "deno.json"); + await expectProjectFile(dir, "vite.config.ts"); + await expectProjectFile(src, "main.ts"); + await expectProjectFile(src, "client.ts"); + await expectProjectFile(src, "routes/index.tsx"); + await expectProjectFile(src, "assets/styles.css"); + await expectProjectFile(src, "static/logo.svg"); +}); + +Deno.test("init - create project dir with custom source dir", async () => { + await using tmp = await withTmpDir(); + const dir = tmp.dir; + using _promptStub = stubPrompt("."); + using _confirmStub = stubConfirm(); + await initProject(dir, [], { "src-dir": "packages/fresh" }); + + const src = path.join(dir, "packages", "fresh"); + await expectProjectFile(dir, "deno.json"); + await expectProjectFile(src, "main.ts"); +}); + +Deno.test("init - fail with src dir and builder", async () => { + await using tmp = await withTmpDir(); + const dir = tmp.dir; + + const promise = initProject(dir, ["."], { "src-dir": "", builder: true }); + await expect(promise).rejects.toBeInstanceOf(InitError); +}); + Deno.test("init - with tailwind", async () => { await using tmp = await withTmpDir(); const dir = tmp.dir; diff --git a/packages/init/src/mod.ts b/packages/init/src/mod.ts index ce6cc9f49c0..0d4a3113e82 100644 --- a/packages/init/src/mod.ts +++ b/packages/init/src/mod.ts @@ -4,12 +4,14 @@ import { InitError } from "./init.ts"; const flags = parseArgs(Deno.args, { boolean: ["force", "tailwind", "vscode", "docker", "help", "builder"], + string: ["src-dir"], default: { force: null, tailwind: null, vscode: null, docker: null, builder: null, + "src-dir": null, }, alias: { help: "h",