webnek 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/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # webnek
2
+
3
+ Installer for the Rust web framework. Run it with `bunx` / `npx` — it is **not** a dependency of your app.
4
+
5
+ Generated apps have **no** `package.json`, **no** lockfile, **no** `node_modules`. `Cargo.toml` is the only manifest (`[package.metadata.npm]` for JS packages, cached in `~/.cargo/webnek/cache/`).
6
+
7
+ ## Create an app
8
+
9
+ ```bash
10
+ bunx webnek my-app
11
+ # or
12
+ npx webnek my-app
13
+ ```
14
+
15
+ ```
16
+ my-app/
17
+ Cargo.toml # only manifest (Rust crates + [package.metadata.npm])
18
+ Cargo.lock
19
+ public/
20
+ src/
21
+ ```
22
+
23
+ Then:
24
+
25
+ ```bash
26
+ cd my-app
27
+ cargo webnek dev
28
+ cargo webnek add canvas-confetti --npm
29
+ vercel
30
+ ```
31
+
32
+ Do not `bun add webnek` inside the app. That is the two-ecosystem split this installer exists to avoid.
33
+
34
+ ## Commands
35
+
36
+ | Command | What it does |
37
+ |---|---|
38
+ | `webnek [dir]` / `webnek new [dir]` | Scaffold a project |
39
+ | `webnek init` | Scaffold into the current directory |
40
+ | `webnek dev` | `cargo webnek dev` |
41
+ | `webnek build` | `cargo webnek build` |
42
+ | `webnek add <pkg>` | `cargo webnek add` |
43
+
44
+ Requires [Rust](https://rustup.rs/). `cargo webnek` is the `cargo-webnek` binary (`cargo install --path crates/cli` from this repo).
package/bin/cli.mjs ADDED
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ rmSync,
9
+ statSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { dirname, join, relative, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const here = dirname(fileURLToPath(import.meta.url));
16
+ const pkgRoot = resolve(here, "..");
17
+ const templateRoot = join(pkgRoot, "template");
18
+
19
+ const args = process.argv.slice(2);
20
+ const cmd = args[0];
21
+
22
+ const IGNORE_WHEN_EMPTY = new Set([
23
+ ".git",
24
+ ".gitignore",
25
+ ".ds_store",
26
+ "node_modules",
27
+ "package.json",
28
+ "package-lock.json",
29
+ "bun.lock",
30
+ "bun.lockb",
31
+ "pnpm-lock.yaml",
32
+ "yarn.lock",
33
+ "index.ts",
34
+ "index.js",
35
+ "tsconfig.json",
36
+ "readme.md",
37
+ "claude.md",
38
+ "agents.md",
39
+ ]);
40
+
41
+ function help() {
42
+ console.log(`
43
+ webnek 0.1.0
44
+
45
+ webnek [dir] Scaffold a new app
46
+ webnek new [dir] Same as above
47
+ webnek init Scaffold into the current directory
48
+ webnek dev [...args] cargo webnek dev
49
+ webnek build [...args] cargo webnek build
50
+ webnek add [...args] cargo webnek add
51
+ webnek export [...args] cargo webnek export
52
+ `);
53
+ }
54
+
55
+ function rustPackageName(dirName) {
56
+ const n = dirName.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^-+/, "");
57
+ return n || "webnek-app";
58
+ }
59
+
60
+ function findFrameworkRoot() {
61
+ if (process.env.WEBNEK_HOME && existsSync(join(process.env.WEBNEK_HOME, "crates/framework/Cargo.toml"))) {
62
+ return resolve(process.env.WEBNEK_HOME);
63
+ }
64
+ let dir = process.cwd();
65
+ for (let i = 0; i < 8; i++) {
66
+ if (existsSync(join(dir, "crates/framework/Cargo.toml"))) return dir;
67
+ const parent = dirname(dir);
68
+ if (parent === dir) break;
69
+ dir = parent;
70
+ }
71
+ dir = pkgRoot;
72
+ for (let i = 0; i < 6; i++) {
73
+ if (existsSync(join(dir, "crates/framework/Cargo.toml"))) return dir;
74
+ const parent = dirname(dir);
75
+ if (parent === dir) break;
76
+ dir = parent;
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function frameworkDep(appDir) {
82
+ const root = findFrameworkRoot();
83
+ if (root) {
84
+ const fw = resolve(root, "crates/framework");
85
+ let rel = relative(appDir, fw);
86
+ if (!rel.startsWith(".") && !rel.startsWith("/")) rel = `./${rel}`;
87
+ // Cargo.toml paths work with forward slashes on all platforms.
88
+ rel = rel.replaceAll("\\", "/");
89
+ return `webnek = { path = "${rel}", features = ["ssr"] }`;
90
+ }
91
+ return `webnek = { version = "0.1.0", features = ["ssr"] }`;
92
+ }
93
+
94
+ function isEffectivelyEmpty(dir) {
95
+ if (!existsSync(dir)) return true;
96
+ const names = readdirSync(dir);
97
+ return names.every((n) => IGNORE_WHEN_EMPTY.has(n.toLowerCase()));
98
+ }
99
+
100
+ function copyTemplate(from, to, vars) {
101
+ mkdirSync(to, { recursive: true });
102
+ for (const name of readdirSync(from)) {
103
+ if (name === ".gitkeep" && existsSync(join(to, name))) continue;
104
+ const src = join(from, name);
105
+ const destName = name.endsWith(".tmpl") ? name.slice(0, -5) : name;
106
+ const dest = join(to, destName);
107
+ if (statSync(src).isDirectory()) {
108
+ copyTemplate(src, dest, vars);
109
+ continue;
110
+ }
111
+ let text = readFileSync(src, "utf8");
112
+ for (const [k, v] of Object.entries(vars)) {
113
+ text = text.replaceAll(`{{${k}}}`, v);
114
+ }
115
+ mkdirSync(dirname(dest), { recursive: true });
116
+ writeFileSync(dest, text);
117
+ }
118
+ }
119
+
120
+ function stripNodeManifests(appDir) {
121
+ for (const leftover of [
122
+ "package.json",
123
+ "package-lock.json",
124
+ "bun.lock",
125
+ "bun.lockb",
126
+ "pnpm-lock.yaml",
127
+ "yarn.lock",
128
+ "index.ts",
129
+ "index.js",
130
+ "CLAUDE.md",
131
+ ]) {
132
+ const p = join(appDir, leftover);
133
+ if (existsSync(p)) rmSync(p, { force: true });
134
+ }
135
+ const nm = join(appDir, "node_modules");
136
+ if (existsSync(nm)) rmSync(nm, { recursive: true, force: true });
137
+ }
138
+
139
+ function scaffold(targetArg) {
140
+ const appDir = resolve(process.cwd(), targetArg || ".");
141
+ const pkgName = rustPackageName(
142
+ appDir === process.cwd() ? (process.cwd().split(/[/\\]/).pop() || "app") : targetArg.replace(/\/+$/, "").split(/[/\\]/).pop()
143
+ );
144
+
145
+ if (existsSync(join(appDir, "Cargo.toml"))) {
146
+ console.error(` ✖ ${appDir} already has a Cargo.toml`);
147
+ process.exit(1);
148
+ }
149
+ if (existsSync(appDir) && !isEffectivelyEmpty(appDir)) {
150
+ console.error(
151
+ ` ✖ ${appDir} is not empty. Use an empty folder (node_modules / lockfiles are ok).`
152
+ );
153
+ process.exit(1);
154
+ }
155
+
156
+ mkdirSync(appDir, { recursive: true });
157
+ stripNodeManifests(appDir);
158
+ const vars = {
159
+ PACKAGE_NAME: pkgName,
160
+ FRAMEWORK_DEP: frameworkDep(appDir),
161
+ PROJECT_NAME: pkgName,
162
+ };
163
+ copyTemplate(templateRoot, appDir, vars);
164
+ stripNodeManifests(appDir);
165
+
166
+ const rel = relative(process.cwd(), appDir) || ".";
167
+ console.log(`
168
+ webnek created ${rel}
169
+
170
+ src/ source (Rust, TS islands, styles.css)
171
+ public/ static files you add
172
+ target/web/ generated CSS/JS (gitignored)
173
+
174
+ Next:
175
+ cd ${rel === "." ? "." : rel}
176
+ cargo webnek dev
177
+ `);
178
+ }
179
+
180
+ function which(bin) {
181
+ const r = spawnSync("sh", ["-c", `command -v ${bin}`], { encoding: "utf8" });
182
+ return r.status === 0 ? r.stdout.trim() : "";
183
+ }
184
+
185
+ function ensureCargoWeb() {
186
+ const probe = spawnSync("cargo", ["webnek", "--help"], {
187
+ encoding: "utf8",
188
+ stdio: "pipe",
189
+ });
190
+ if (probe.status === 0) return "cargo";
191
+
192
+ if (which("cargo-webnek")) {
193
+ return "cargo-webnek";
194
+ }
195
+
196
+ const root = findFrameworkRoot();
197
+ if (root && existsSync(join(root, "crates/cli/Cargo.toml"))) {
198
+ console.log(" webnek installing cargo-webnek from local crates/cli …");
199
+ const ins = spawnSync(
200
+ "cargo",
201
+ ["install", "--path", join(root, "crates/cli"), "--force"],
202
+ { stdio: "inherit" }
203
+ );
204
+ if (ins.status === 0) return "cargo";
205
+ }
206
+
207
+ console.error(`
208
+ ✖ cargo-webnek is not installed.
209
+
210
+ cargo install cargo-webnek
211
+ # or, from this repo:
212
+ cargo install --path crates/cli
213
+ `);
214
+ process.exit(1);
215
+ }
216
+
217
+ function proxyCargoWeb(forward) {
218
+ ensureCargoWeb();
219
+ const result = spawnSync("cargo", ["webnek", ...forward], {
220
+ stdio: "inherit",
221
+ cwd: process.cwd(),
222
+ });
223
+ process.exit(result.status ?? 1);
224
+ }
225
+
226
+ const createInvoked =
227
+ process.argv[1] && process.argv[1].includes("create-webnek");
228
+
229
+ if (args.length === 0 && createInvoked) {
230
+ scaffold(".");
231
+ } else if (args.length === 0) {
232
+ if (isEffectivelyEmpty(process.cwd()) && !existsSync(join(process.cwd(), "Cargo.toml"))) {
233
+ scaffold(".");
234
+ } else {
235
+ help();
236
+ }
237
+ } else if (cmd === "-h" || cmd === "--help" || cmd === "help") {
238
+ help();
239
+ } else if (cmd === "new" || cmd === "create" || cmd === "init") {
240
+ scaffold(cmd === "init" ? args[1] || "." : args[1] || ".");
241
+ } else if (["dev", "build", "add", "export"].includes(cmd)) {
242
+ proxyCargoWeb(args);
243
+ } else if (cmd.startsWith("-")) {
244
+ help();
245
+ process.exit(1);
246
+ } else {
247
+ // `webnek my-app`
248
+ scaffold(cmd);
249
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "webnek",
3
+ "version": "0.1.0",
4
+ "description": "Next-gen fullstack Rust web framework with React ergonomics. Scaffold with bunx webnek, then cargo webnek dev.",
5
+ "type": "module",
6
+ "license": "MIT OR Apache-2.0",
7
+ "author": "Grenish Rai <mrcoder2033d@gmail.com>",
8
+ "bin": {
9
+ "webnek": "./bin/cli.mjs",
10
+ "create-webnek": "./bin/cli.mjs"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "template",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "rust",
19
+ "ssr",
20
+ "fullstack",
21
+ "tailwind",
22
+ "framework",
23
+ "create-webnek"
24
+ ],
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/Grenish/webnek"
31
+ }
32
+ }
@@ -0,0 +1,5 @@
1
+ target/**
2
+ !target/release
3
+ !target/x86_64-unknown-linux-gnu/release/**
4
+ !target/aarch64-unknown-linux-gnu/release/**
5
+ .git
@@ -0,0 +1,4 @@
1
+ {
2
+ "rust-analyzer.procMacro.enable": true,
3
+ "rust-analyzer.cargo.buildScripts.enable": true
4
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "lsp": {
3
+ "rust-analyzer": {
4
+ "initialization_options": {
5
+ "procMacro": { "enable": true },
6
+ "cargo": { "buildScripts": { "enable": true } }
7
+ }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,18 @@
1
+ [package]
2
+ name = "{{PACKAGE_NAME}}"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [workspace]
7
+
8
+ [dependencies]
9
+ {{FRAMEWORK_DEP}}
10
+ tokio = { version = "1.38", features = ["full"] }
11
+
12
+ [[bin]]
13
+ name = "index"
14
+ path = "api/index.rs"
15
+
16
+ [package.metadata.npm]
17
+ # Frontend NPM packages installed to ~/.cargo/webnek/cache/
18
+ # Add new packages with: cargo webnek add <package> --npm
@@ -0,0 +1,19 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ Fullstack Rust with React ergonomics.
4
+
5
+ ```bash
6
+ cargo webnek dev
7
+ ```
8
+
9
+ - Edit `src/styles.css` (Tailwind v4). Compiled CSS goes to `target/web/`.
10
+ - `public/` is for files you add (favicon, images).
11
+ - JS packages: `cargo webnek add <pkg> --npm` (global cache, no local node_modules).
12
+
13
+ ## Deploy to Vercel
14
+
15
+ ```bash
16
+ vercel
17
+ ```
18
+
19
+ Same app, no `package.json`. Vercel compiles `api/index.rs`. CSS and client JS are built during that compile.
@@ -0,0 +1,12 @@
1
+ use webnek::prelude::*;
2
+
3
+ #[path = "../src/assets.rs"]
4
+ mod assets;
5
+ #[path = "../src/routes/mod.rs"]
6
+ mod routes;
7
+
8
+ #[tokio::main]
9
+ async fn main() -> Result<(), Box<dyn std::error::Error>> {
10
+ assets::install();
11
+ serve_vercel(routes::router(), Some(routes::metadata()), None, None).await
12
+ }
@@ -0,0 +1,107 @@
1
+ //! Compile CSS and client JS into OUT_DIR so the Vercel function can embed them.
2
+ //!
3
+ //! Local `cargo webnek dev` still serves from `target/web/` (hot reload).
4
+ //! On Vercel, `npx` compiles Tailwind and esbuild.
5
+
6
+ use std::env;
7
+ use std::fs;
8
+ use std::path::{Path, PathBuf};
9
+ use std::process::Command;
10
+
11
+ fn main() {
12
+ println!("cargo:rerun-if-changed=src/styles.css");
13
+ println!("cargo:rerun-if-changed=src/client/index.ts");
14
+
15
+ let out = PathBuf::from(env::var("OUT_DIR").unwrap());
16
+ write_css(&out.join("styles.css"));
17
+ write_js(&out.join("client.js"));
18
+ }
19
+
20
+ fn write_css(dest: &Path) {
21
+ if copy_generated("styles.css", dest) {
22
+ return;
23
+ }
24
+ if on_vercel() && npx_ok(&[
25
+ "--yes",
26
+ "@tailwindcss/cli@4.3.3",
27
+ "-i",
28
+ "src/styles.css",
29
+ "-o",
30
+ dest.to_str().unwrap_or("styles.css"),
31
+ "--minify",
32
+ ]) {
33
+ return;
34
+ }
35
+ let fallback = fs::read_to_string("src/styles.css").unwrap_or_else(|_| {
36
+ "/* styles.css missing — run cargo webnek dev */\n".to_string()
37
+ });
38
+ let _ = fs::write(dest, fallback);
39
+ }
40
+
41
+ fn write_js(dest: &Path) {
42
+ if copy_generated("pkg/client.js", dest) {
43
+ return;
44
+ }
45
+ if on_vercel()
46
+ && npx_ok(&[
47
+ "--yes",
48
+ "esbuild",
49
+ "src/client/index.ts",
50
+ "--bundle",
51
+ "--format=esm",
52
+ "--outfile",
53
+ dest.to_str().unwrap_or("client.js"),
54
+ ])
55
+ {
56
+ return;
57
+ }
58
+ if on_vercel()
59
+ && npx_ok(&[
60
+ "--yes",
61
+ "esbuild",
62
+ "src/client/index.ts",
63
+ "--format=esm",
64
+ "--outfile",
65
+ dest.to_str().unwrap_or("client.js"),
66
+ ])
67
+ {
68
+ return;
69
+ }
70
+ let fallback = fs::read_to_string("src/client/index.ts")
71
+ .unwrap_or_else(|_| "// client\n".to_string());
72
+ let _ = fs::write(dest, fallback);
73
+ }
74
+
75
+ fn copy_generated(relative: &str, dest: &Path) -> bool {
76
+ let pkg = match env::var("CARGO_PKG_NAME") {
77
+ Ok(p) => p,
78
+ Err(_) => return false,
79
+ };
80
+ let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into()));
81
+ let mut candidates = Vec::new();
82
+ if let Ok(td) = env::var("CARGO_TARGET_DIR") {
83
+ candidates.push(PathBuf::from(td).join("web").join(&pkg).join(relative));
84
+ }
85
+ candidates.push(manifest.join("target/web").join(&pkg).join(relative));
86
+ if let Some(parent) = manifest.parent() {
87
+ candidates.push(parent.join("target/web").join(&pkg).join(relative));
88
+ }
89
+ for src in candidates {
90
+ if src.is_file() && fs::copy(&src, dest).is_ok() {
91
+ return true;
92
+ }
93
+ }
94
+ false
95
+ }
96
+
97
+ fn on_vercel() -> bool {
98
+ env::var_os("VERCEL").is_some()
99
+ }
100
+
101
+ fn npx_ok(args: &[&str]) -> bool {
102
+ Command::new("npx")
103
+ .args(args)
104
+ .status()
105
+ .map(|s| s.success())
106
+ .unwrap_or(false)
107
+ }
@@ -0,0 +1 @@
1
+ User static files (favicon, images). Generated CSS/JS live in target/web/.
@@ -0,0 +1,8 @@
1
+ /// CSS and JS compiled by `build.rs` into the binary.
2
+ /// On Vercel the function serves these when `public/` is empty.
3
+ pub fn install() {
4
+ webnek::prelude::embed_assets(
5
+ include_str!(concat!(env!("OUT_DIR"), "/styles.css")),
6
+ include_str!(concat!(env!("OUT_DIR"), "/client.js")),
7
+ );
8
+ }
@@ -0,0 +1,19 @@
1
+ console.log("⚡ [webnek] Client island initialized");
2
+
3
+ document.addEventListener("click", (e) => {
4
+ const target = (e.target as HTMLElement).closest("[data-action]");
5
+ if (!target) return;
6
+
7
+ const action = target.getAttribute("data-action");
8
+ const counterEl = document.querySelector("[data-counter-value]");
9
+ const doubleEl = document.querySelector("[data-double-value]");
10
+ if (!counterEl) return;
11
+
12
+ let val = parseInt(counterEl.textContent || "0", 10);
13
+ if (action === "inc") val += 1;
14
+ if (action === "dec") val -= 1;
15
+ if (action === "reset") val = 0;
16
+
17
+ counterEl.textContent = val.toString();
18
+ if (doubleEl) doubleEl.textContent = (val * 2).toString();
19
+ });
@@ -0,0 +1,20 @@
1
+ use webnek::prelude::*;
2
+
3
+ mod assets;
4
+ mod routes;
5
+
6
+ #[tokio::main]
7
+ async fn main() -> Result<(), Box<dyn std::error::Error>> {
8
+ assets::install();
9
+
10
+ let port = std::env::var("PORT").unwrap_or_else(|_| "3030".to_string());
11
+
12
+ serve_router(
13
+ &format!("127.0.0.1:{}", port),
14
+ routes::router(),
15
+ Some(routes::metadata()),
16
+ None,
17
+ None,
18
+ )
19
+ .await
20
+ }
@@ -0,0 +1,86 @@
1
+ use webnek::prelude::*;
2
+
3
+ #[component]
4
+ pub fn about_page() -> View {
5
+ set_title("About · Webnek");
6
+ set_description("How to add a page.");
7
+
8
+ view! {
9
+ <div>
10
+ {super::layout::site_header("src/routes/about.rs")}
11
+ <main className="max-w-2xl mx-auto px-6 pt-24 pb-32">
12
+ <p className="font-display text-[15px] text-mute mb-3">
13
+ "Another route."
14
+ </p>
15
+ <h1 className="text-[2rem] sm:text-[2.5rem] leading-[1.15] tracking-tight font-medium text-ink max-w-md">
16
+ "This is /about."
17
+ </h1>
18
+ <p className="mt-4 text-[15px] leading-relaxed text-mute max-w-md">
19
+ "This URL is "
20
+ <code className="font-mono text-[12px] text-ink">"src/routes/about.rs"</code>
21
+ ", plus one line in "
22
+ <code className="font-mono text-[12px] text-ink">"src/main.rs"</code>
23
+ "."
24
+ </p>
25
+
26
+ <section className="mt-16 pt-10 border-t border-line">
27
+ <h2 className="font-mono text-[11px] uppercase tracking-[0.14em] text-mute mb-8">
28
+ "Add a page"
29
+ </h2>
30
+ <ol className="space-y-8">
31
+ <li className="grid grid-cols-[1.25rem_1fr] gap-x-3">
32
+ <span className="font-mono text-[11px] text-mute pt-0.5">"1"</span>
33
+ <div>
34
+ <p className="text-[15px] text-ink">"Write the page"</p>
35
+ <p className="mt-1 text-[15px] leading-relaxed text-mute">
36
+ "Create "
37
+ <code className="font-mono text-[12px] text-ink">"src/routes/hello.rs"</code>
38
+ ". Return the HTML from a function, same as this file."
39
+ </p>
40
+ </div>
41
+ </li>
42
+ <li className="grid grid-cols-[1.25rem_1fr] gap-x-3">
43
+ <span className="font-mono text-[11px] text-mute pt-0.5">"2"</span>
44
+ <div>
45
+ <p className="text-[15px] text-ink">"Name the file"</p>
46
+ <p className="mt-1 text-[15px] leading-relaxed text-mute">
47
+ "In "
48
+ <code className="font-mono text-[12px] text-ink">"src/main.rs"</code>
49
+ ", add "
50
+ <code className="font-mono text-[12px] text-ink">"pub mod hello;"</code>
51
+ " next to "
52
+ <code className="font-mono text-[12px] text-ink">"about"</code>
53
+ " and "
54
+ <code className="font-mono text-[12px] text-ink">"page"</code>
55
+ "."
56
+ </p>
57
+ </div>
58
+ </li>
59
+ <li className="grid grid-cols-[1.25rem_1fr] gap-x-3">
60
+ <span className="font-mono text-[11px] text-mute pt-0.5">"3"</span>
61
+ <div>
62
+ <p className="text-[15px] text-ink">"Connect the URL"</p>
63
+ <p className="mt-1 text-[15px] leading-relaxed text-mute">
64
+ "Copy the "
65
+ <code className="font-mono text-[12px] text-ink">"/about"</code>
66
+ " line. Change "
67
+ <code className="font-mono text-[12px] text-ink">"\"/about\""</code>
68
+ " to "
69
+ <code className="font-mono text-[12px] text-ink">"\"/hello\""</code>
70
+ " and "
71
+ <code className="font-mono text-[12px] text-ink">"about_page"</code>
72
+ " to "
73
+ <code className="font-mono text-[12px] text-ink">"hello_page"</code>
74
+ "."
75
+ </p>
76
+ </div>
77
+ </li>
78
+ </ol>
79
+ <p className="mt-10 text-[15px] leading-relaxed text-mute">
80
+ "A path with no matching line still 404s."
81
+ </p>
82
+ </section>
83
+ </main>
84
+ </div>
85
+ }
86
+ }
@@ -0,0 +1,28 @@
1
+ use webnek::prelude::*;
2
+
3
+ pub fn site_header(file: &'static str) -> View {
4
+ view! {
5
+ <header className="border-b border-line">
6
+ <div className="max-w-2xl mx-auto px-6 h-14 flex items-center justify-between">
7
+ <a href="/" className="font-display text-[17px] tracking-tight text-ink">
8
+ "Webnek"
9
+ </a>
10
+ <nav className="flex items-center gap-5 font-mono text-[11px] text-mute">
11
+ <a href="/" className="hover:text-ink">"/"</a>
12
+ <a href="/about" className="hover:text-ink">"/about"</a>
13
+ <span className="text-line">"·"</span>
14
+ <span>{file}</span>
15
+ </nav>
16
+ </div>
17
+ </header>
18
+ }
19
+ }
20
+
21
+ #[component]
22
+ pub fn root_layout(children: View) -> View {
23
+ view! {
24
+ <div className="min-h-screen bg-paper text-ink font-sans antialiased">
25
+ {children}
26
+ </div>
27
+ }
28
+ }
@@ -0,0 +1,18 @@
1
+ use webnek::prelude::*;
2
+
3
+ pub mod about;
4
+ pub mod layout;
5
+ pub mod page;
6
+
7
+ pub fn metadata() -> Metadata {
8
+ Metadata::new()
9
+ .title("Webnek")
10
+ .description("A running native web app. SSR, signals, Tailwind.")
11
+ .robots("index, follow")
12
+ }
13
+
14
+ pub fn router() -> Router {
15
+ Router::new()
16
+ .route("/", |_| layout::root_layout(page::home_page()))
17
+ .route("/about", |_| layout::root_layout(about::about_page()))
18
+ }
@@ -0,0 +1,65 @@
1
+ use webnek::prelude::*;
2
+
3
+ #[component]
4
+ pub fn home_page() -> View {
5
+ set_title("Webnek");
6
+ set_description("A running native web app. SSR, signals, Tailwind.");
7
+
8
+ let (count, set_count) = use_state(0);
9
+ let double_count = use_memo(move || count.get() * 2);
10
+
11
+ view! {
12
+ <div>
13
+ {super::layout::site_header("src/routes/page.rs")}
14
+ <main className="max-w-2xl mx-auto px-6 pt-24 pb-32">
15
+ <p className="font-display text-[15px] text-mute mb-3">
16
+ "A running app."
17
+ </p>
18
+ <h1 className="text-[2rem] sm:text-[2.5rem] leading-[1.15] tracking-tight font-medium text-ink max-w-md">
19
+ "Native SSR. Signals. No node_modules."
20
+ </h1>
21
+ <p className="mt-4 text-[15px] leading-relaxed text-mute max-w-sm">
22
+ "This page was rendered on the server. The number below is a reactive signal — edit "
23
+ <code className="font-mono text-[12px] text-ink">"src/routes/page.rs"</code>
24
+ " and save."
25
+ </p>
26
+
27
+ <div className="mt-16 pt-10 border-t border-line">
28
+ <div className="flex items-end justify-between gap-8">
29
+ <div>
30
+ <div className="font-mono text-[11px] uppercase tracking-[0.14em] text-mute mb-3">
31
+ "count"
32
+ </div>
33
+ <div
34
+ data-counter-value="true"
35
+ className="font-mono text-6xl sm:text-7xl tabular-nums tracking-tight text-ink leading-none"
36
+ >
37
+ {count}
38
+ </div>
39
+ <div className="mt-3 font-mono text-[12px] text-mute">
40
+ "×2 "
41
+ <span data-double-value="true" className="text-ink">{double_count}</span>
42
+ </div>
43
+ </div>
44
+ <div className="flex gap-2 pb-1">
45
+ <button
46
+ data-action="dec"
47
+ className="w-10 h-10 border border-line bg-paper text-ink text-lg leading-none hover:bg-white transition-colors"
48
+ onClick={move || set_count.update(|c| *c -= 1)}
49
+ >
50
+ "−"
51
+ </button>
52
+ <button
53
+ data-action="inc"
54
+ className="w-10 h-10 bg-ink text-paper text-lg leading-none hover:bg-patina transition-colors"
55
+ onClick={move || set_count.update(|c| *c += 1)}
56
+ >
57
+ "+"
58
+ </button>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </main>
63
+ </div>
64
+ }
65
+ }
@@ -0,0 +1,30 @@
1
+ @import "tailwindcss";
2
+
3
+ @source "../src";
4
+ @source not "../target";
5
+
6
+ @theme {
7
+ --color-paper: #f6f5f2;
8
+ --color-ink: #111110;
9
+ --color-mute: #73726c;
10
+ --color-line: #e4e2dc;
11
+ --color-patina: #2c5c4f;
12
+ --font-display: ui-serif, "Iowan Old Style", Palatino, Georgia, serif;
13
+ --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
14
+ --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
15
+ }
16
+
17
+ html {
18
+ background: var(--color-paper);
19
+ color: var(--color-ink);
20
+ }
21
+
22
+ ::selection {
23
+ background: var(--color-patina);
24
+ color: var(--color-paper);
25
+ }
26
+
27
+ :focus-visible {
28
+ outline: 2px solid var(--color-ink);
29
+ outline-offset: 3px;
30
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "jsx": "preserve",
8
+ "skipLibCheck": true,
9
+ "baseUrl": ".",
10
+ "paths": {
11
+ "*": ["~/.cargo/webnek/cache/node_modules/*", "*"]
12
+ }
13
+ },
14
+ "include": ["src/client/**/*"]
15
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://openapi.vercel.sh/vercel.json",
3
+ "rewrites": [
4
+ {
5
+ "source": "/(.*)",
6
+ "destination": "/api/index"
7
+ }
8
+ ]
9
+ }