Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions docs/latest/examples/use-src-dir.md
Original file line number Diff line number Diff line change
@@ -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
<project root>
├── 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",
Copy link
Copy Markdown
Contributor

@fry69 fry69 Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a good idea to put generated artifacts into the src/ folder? Even when those get gitignored, they do not belong there IMHO.

```

Success! The project should now run with `deno task`.
1 change: 1 addition & 0 deletions docs/toc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
[
Expand Down
58 changes: 39 additions & 19 deletions packages/init/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
`;

Expand All @@ -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;
} = {},
Expand Down Expand Up @@ -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;
Expand All @@ -162,6 +174,8 @@ export async function initProject(
| ReadableStream<Uint8Array>
| Record<string, unknown>,
) => await writeProjectFile(projectDir, pathname, content);
const writeSrcFile = async (...args: Parameters<typeof writeFile>) =>
await writeProjectFile(srcDir, ...args);

const GITIGNORE = `# dotenv environment variable files
.env
Expand Down Expand Up @@ -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 =
Expand All @@ -384,12 +398,12 @@ import "./assets/styles.css";`,
fill="#fff"
/>
</svg>`;
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.
}
Expand Down Expand Up @@ -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";
Expand All @@ -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";

Expand All @@ -455,7 +469,7 @@ export interface State {
}

export const define = createDefine<State>();`;
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";
Expand Down Expand Up @@ -490,7 +504,7 @@ export default define.page(function Home(ctx) {
</div>
);
});`;
await writeFile("routes/index.tsx", ROUTES_HOME);
await writeSrcFile("routes/index.tsx", ROUTES_HOME);

const APP_WRAPPER = `import { define } from "../utils.ts";

Expand All @@ -510,7 +524,7 @@ export default define.page(function App({ Component }) {
</html>
);
});`;
await writeFile("routes/_app.tsx", APP_WRAPPER);
await writeSrcFile("routes/_app.tsx", APP_WRAPPER);

const API_NAME = `import { define } from "../../utils.ts";

Expand All @@ -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";
Expand All @@ -540,7 +554,7 @@ export default function Counter(props: CounterProps) {
</div>
);
}`;
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` : ""}
Expand All @@ -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: {
Expand Down Expand Up @@ -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);
}
Expand Down
39 changes: 39 additions & 0 deletions packages/init/src/init_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
CONFIRM_VITE_MESSAGE,
CONFIRM_VSCODE_MESSAGE,
HELP_TEXT,
InitError,
initProject,
} from "./init.ts";
import * as path from "@std/path";
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/init/src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading