webnek 0.1.2 → 0.1.4

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 CHANGED
@@ -1,23 +1,15 @@
1
1
  # webnek
2
2
 
3
3
  ```bash
4
- bunx webnek@latest my-app
4
+ bun create webnek@latest my-app
5
5
  cd my-app
6
- bunx webnek@latest dev
6
+ cargo dev
7
7
  ```
8
8
 
9
- That last command installs the Rust CLI the first time, then starts the app.
9
+ Create compiles the app (first time on a machine can take a minute). After that `cargo dev` starts the server.
10
10
 
11
- 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/`).
11
+ Same create command: `npm create webnek@latest my-app` or `pnpm create webnek@latest my-app`.
12
12
 
13
- Do not `bun add webnek` inside the app.
14
-
15
- ## Commands
16
-
17
- | Command | What it does |
18
- |---|---|
19
- | `bunx webnek@latest [dir]` | Scaffold a project |
20
- | `bunx webnek@latest dev` | Start the app |
21
- | `bunx webnek@latest add <pkg>` | Add a crate or npm package |
13
+ Generated apps have no `package.json` and no `node_modules`. `cargo build` and `cargo test` work as usual.
22
14
 
23
15
  Requires [Rust](https://rustup.rs/).
package/bin/cli.mjs CHANGED
@@ -15,7 +15,6 @@ import { fileURLToPath } from "node:url";
15
15
  const here = dirname(fileURLToPath(import.meta.url));
16
16
  const pkgRoot = resolve(here, "..");
17
17
  const templateRoot = join(pkgRoot, "template");
18
- const GIT = "https://github.com/Grenish/webnek";
19
18
  const VERSION = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")).version;
20
19
 
21
20
  const args = process.argv.slice(2);
@@ -41,16 +40,15 @@ const IGNORE_WHEN_EMPTY = new Set([
41
40
  ]);
42
41
 
43
42
  function help() {
44
- const run = invokeCmd();
45
43
  console.log(`
46
44
  webnek ${VERSION}
47
45
 
48
- ${run} [dir] Scaffold a new app
49
- ${run} new [dir] Same as above
50
- ${run} init Scaffold into the current directory
51
- ${run} dev Start the app
52
- ${run} build Production build
53
- ${run} add <pkg> Add a crate or npm package
46
+ bun create webnek@latest [dir]
47
+ npm create webnek@latest [dir]
48
+ pnpm create webnek@latest [dir]
49
+
50
+ Then:
51
+ cargo dev
54
52
  `);
55
53
  }
56
54
 
@@ -86,7 +84,6 @@ function frameworkDep(appDir) {
86
84
  const fw = resolve(root, "crates/framework");
87
85
  let rel = relative(appDir, fw);
88
86
  if (!rel.startsWith(".") && !rel.startsWith("/")) rel = `./${rel}`;
89
- // Cargo.toml paths work with forward slashes on all platforms.
90
87
  rel = rel.replaceAll("\\", "/");
91
88
  return `webnek = { path = "${rel}", features = ["ssr"] }`;
92
89
  }
@@ -104,7 +101,8 @@ function copyTemplate(from, to, vars) {
104
101
  for (const name of readdirSync(from)) {
105
102
  if (name === ".gitkeep" && existsSync(join(to, name))) continue;
106
103
  const src = join(from, name);
107
- const destName = name.endsWith(".tmpl") ? name.slice(0, -5) : name;
104
+ let destName = name.endsWith(".tmpl") ? name.slice(0, -5) : name;
105
+ if (name === "gitignore") destName = ".gitignore";
108
106
  const dest = join(to, destName);
109
107
  if (statSync(src).isDirectory()) {
110
108
  copyTemplate(src, dest, vars);
@@ -138,10 +136,35 @@ function stripNodeManifests(appDir) {
138
136
  if (existsSync(nm)) rmSync(nm, { recursive: true, force: true });
139
137
  }
140
138
 
139
+ function which(bin) {
140
+ const r = spawnSync("sh", ["-c", `command -v ${bin}`], { encoding: "utf8" });
141
+ return r.status === 0 ? r.stdout.trim() : "";
142
+ }
143
+
144
+ function cargoBuild(appDir) {
145
+ if (!which("cargo")) {
146
+ console.error(`
147
+ ✖ cargo is not installed. Install Rust first:
148
+
149
+ https://rustup.rs/
150
+ `);
151
+ return false;
152
+ }
153
+ console.log(" webnek compiling (first time on this computer can take a minute) …\n");
154
+ const r = spawnSync("cargo", ["build"], {
155
+ cwd: appDir,
156
+ stdio: "inherit",
157
+ env: { ...process.env, CARGO_TERM_COLOR: process.env.CARGO_TERM_COLOR || "always" },
158
+ });
159
+ return r.status === 0;
160
+ }
161
+
141
162
  function scaffold(targetArg) {
142
163
  const appDir = resolve(process.cwd(), targetArg || ".");
143
164
  const pkgName = rustPackageName(
144
- appDir === process.cwd() ? (process.cwd().split(/[/\\]/).pop() || "app") : targetArg.replace(/\/+$/, "").split(/[/\\]/).pop()
165
+ appDir === process.cwd()
166
+ ? process.cwd().split(/[/\\]/).pop() || "app"
167
+ : targetArg.replace(/\/+$/, "").split(/[/\\]/).pop()
145
168
  );
146
169
 
147
170
  if (existsSync(join(appDir, "Cargo.toml"))) {
@@ -157,101 +180,32 @@ function scaffold(targetArg) {
157
180
 
158
181
  mkdirSync(appDir, { recursive: true });
159
182
  stripNodeManifests(appDir);
160
- const vars = {
183
+ copyTemplate(templateRoot, appDir, {
161
184
  PACKAGE_NAME: pkgName,
162
185
  FRAMEWORK_DEP: frameworkDep(appDir),
163
186
  PROJECT_NAME: pkgName,
164
- START_CMD: invokeCmd(),
165
- };
166
- copyTemplate(templateRoot, appDir, vars);
187
+ });
167
188
  stripNodeManifests(appDir);
168
189
 
169
190
  const rel = relative(process.cwd(), appDir) || ".";
170
- const run = invokeCmd();
171
- const lines = [` webnek ${VERSION} created ${rel}`, ``, ` Next:`];
172
- if (rel !== ".") {
173
- lines.push(` cd ${rel}`);
174
- }
175
- lines.push(` ${run} dev`);
176
- console.log(`\n${lines.join("\n")}\n`);
177
- }
191
+ const built = cargoBuild(appDir);
178
192
 
179
- function which(bin) {
180
- const r = spawnSync("sh", ["-c", `command -v ${bin}`], { encoding: "utf8" });
181
- return r.status === 0 ? r.stdout.trim() : "";
182
- }
183
-
184
- function invokeCmd() {
185
- const ua = process.env.npm_config_user_agent || "";
186
- const argv0 = process.argv[0] || "";
187
- const argv1 = process.argv[1] || "";
188
- if (ua.includes("bun") || argv0.includes("bun") || argv1.includes("bun")) {
189
- return "bunx webnek@latest";
190
- }
191
- if (ua.includes("npx") || argv1.includes("npx") || argv1.includes("/npm/")) {
192
- return "npx webnek@latest";
193
+ const lines = [` webnek ${VERSION} ${built ? "ready" : "created"} ${rel}`, ``, ` Next:`];
194
+ if (rel !== ".") lines.push(` cd ${rel}`);
195
+ lines.push(` cargo dev`);
196
+ if (!built) {
197
+ lines.push(``, ` cargo build failed. Fix that, then cargo dev.`);
193
198
  }
194
- if (which("webnek")) return "webnek";
195
- return "bunx webnek@latest";
196
- }
197
-
198
- function cargoWebnekOk() {
199
- const probe = spawnSync("cargo", ["webnek", "--help"], {
200
- encoding: "utf8",
201
- stdio: "pipe",
202
- });
203
- return probe.status === 0 || Boolean(which("cargo-webnek"));
204
- }
205
-
206
- function installCargoWebnek() {
207
- const root = findFrameworkRoot();
208
- if (root && existsSync(join(root, "crates/cli/Cargo.toml"))) {
209
- console.log(" webnek installing cargo-webnek …");
210
- const ins = spawnSync(
211
- "cargo",
212
- ["install", "--path", join(root, "crates/cli"), "--force"],
213
- { stdio: "inherit" }
214
- );
215
- return ins.status === 0;
216
- }
217
- console.log(" webnek installing cargo-webnek (once) …");
218
- const ins = spawnSync(
219
- "cargo",
220
- ["install", "webnek-cli"],
221
- { stdio: "inherit" }
222
- );
223
- return ins.status === 0;
199
+ console.log(`\n${lines.join("\n")}\n`);
224
200
  }
225
201
 
226
- function ensureCargoWeb() {
227
- if (cargoWebnekOk()) return;
228
-
202
+ function cargo(args) {
229
203
  if (!which("cargo")) {
230
- console.error(`
231
- ✖ cargo is not installed. Install Rust first:
232
-
233
- https://rustup.rs/
234
- `);
204
+ console.error(" ✖ cargo is not installed. https://rustup.rs/");
235
205
  process.exit(1);
236
206
  }
237
-
238
- if (!installCargoWebnek()) {
239
- console.error(`
240
- ✖ could not install cargo-webnek.
241
-
242
- cargo install webnek-cli
243
- `);
244
- process.exit(1);
245
- }
246
- }
247
-
248
- function proxyCargoWeb(forward) {
249
- ensureCargoWeb();
250
- const result = spawnSync("cargo", ["webnek", ...forward], {
251
- stdio: "inherit",
252
- cwd: process.cwd(),
253
- });
254
- process.exit(result.status ?? 1);
207
+ const r = spawnSync("cargo", args, { stdio: "inherit", cwd: process.cwd() });
208
+ process.exit(r.status ?? 1);
255
209
  }
256
210
 
257
211
  const createInvoked =
@@ -269,12 +223,15 @@ if (args.length === 0 && createInvoked) {
269
223
  help();
270
224
  } else if (cmd === "new" || cmd === "create" || cmd === "init") {
271
225
  scaffold(cmd === "init" ? args[1] || "." : args[1] || ".");
272
- } else if (["dev", "build", "add", "export"].includes(cmd)) {
273
- proxyCargoWeb(args);
226
+ } else if (cmd === "dev") {
227
+ cargo(["run", ...args.slice(1)]);
228
+ } else if (cmd === "build") {
229
+ cargo(["build", "--release", ...args.slice(1)]);
230
+ } else if (cmd === "test") {
231
+ cargo(["test", ...args.slice(1)]);
274
232
  } else if (cmd.startsWith("-")) {
275
233
  help();
276
234
  process.exit(1);
277
235
  } else {
278
- // `webnek my-app`
279
236
  scaffold(cmd);
280
237
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "webnek",
3
- "version": "0.1.2",
4
- "description": "Webnek app installer. bunx webnek@latest my-app && bunx webnek@latest dev",
3
+ "version": "0.1.4",
4
+ "description": "Create a Webnek app. bun create webnek@latest my-app && cargo dev",
5
5
  "type": "module",
6
6
  "license": "MIT OR Apache-2.0",
7
7
  "author": "Grenish Rai <mrcoder2033d@gmail.com>",
@@ -22,6 +22,9 @@
22
22
  "framework",
23
23
  "create-webnek"
24
24
  ],
25
+ "exports": {
26
+ "./bin/cli.mjs": "./bin/cli.mjs"
27
+ },
25
28
  "engines": {
26
29
  "node": ">=18"
27
30
  },
@@ -0,0 +1,2 @@
1
+ [alias]
2
+ dev = "run"
@@ -2,6 +2,7 @@
2
2
  name = "{{PACKAGE_NAME}}"
3
3
  version = "0.1.0"
4
4
  edition = "2021"
5
+ default-run = "{{PACKAGE_NAME}}"
5
6
 
6
7
  [workspace]
7
8
 
@@ -15,4 +16,4 @@ path = "api/index.rs"
15
16
 
16
17
  [package.metadata.npm]
17
18
  # Frontend NPM packages installed to ~/.cargo/webnek/cache/
18
- # Add new packages with: cargo webnek add <package> --npm
19
+ # JS packages: bunx <pkg> is not required. Add crates with cargo add.
@@ -1,12 +1,12 @@
1
1
  # {{PROJECT_NAME}}
2
2
 
3
3
  ```bash
4
- {{START_CMD}} dev
4
+ cargo dev
5
5
  ```
6
6
 
7
- - Edit `src/styles.css` (Tailwind v4). Compiled CSS goes to `target/web/`.
7
+ - Edit `src/styles.css` (Tailwind v4).
8
8
  - `public/` is for files you add (favicon, images).
9
- - JS packages: `{{START_CMD}} add <pkg> --npm`
9
+ - `cargo build`, `cargo test`, and `cargo run` work as usual.
10
10
 
11
11
  ## Deploy
12
12
 
package/template/build.rs CHANGED
@@ -1,107 +1,161 @@
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.
1
+ //! Compile CSS and client JS into OUT_DIR on every `cargo build`.
2
+ //! Tailwind lives in ~/.cargo/webnek/cache (no project node_modules).
5
3
 
6
4
  use std::env;
7
5
  use std::fs;
8
6
  use std::path::{Path, PathBuf};
9
- use std::process::Command;
7
+ use std::process::{Command, Stdio};
8
+
9
+ const TW: &str = "4.3.3";
10
10
 
11
11
  fn main() {
12
12
  println!("cargo:rerun-if-changed=src/styles.css");
13
+ println!("cargo:rerun-if-changed=src/client/index.js");
13
14
  println!("cargo:rerun-if-changed=src/client/index.ts");
14
15
 
16
+ let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into()));
15
17
  let out = PathBuf::from(env::var("OUT_DIR").unwrap());
16
- write_css(&out.join("styles.css"));
17
- write_js(&out.join("client.js"));
18
+ write_css(&manifest, &out.join("styles.css"));
19
+ write_js(&manifest, &out.join("client.js"));
18
20
  }
19
21
 
20
- fn write_css(dest: &Path) {
21
- if copy_generated("styles.css", dest) {
22
- return;
22
+ fn cache_dir() -> PathBuf {
23
+ if let Ok(h) = env::var("CARGO_HOME") {
24
+ PathBuf::from(h).join("webnek").join("cache")
25
+ } else if let Ok(h) = env::var("HOME") {
26
+ PathBuf::from(h).join(".cargo").join("webnek").join("cache")
27
+ } else {
28
+ PathBuf::from(".webnek-cache")
29
+ }
30
+ }
31
+
32
+ fn ensure_tailwind(cache: &Path) -> bool {
33
+ let _ = fs::create_dir_all(cache);
34
+ let pkg = cache.join("package.json");
35
+ if !pkg.exists() {
36
+ let _ = fs::write(&pkg, "{\"name\":\"webnek-cache\",\"private\":true}");
23
37
  }
24
- if on_vercel() && npx_ok(&[
25
- "--yes",
26
- "@tailwindcss/cli@4.3.3",
38
+ let cli = cache.join("node_modules/@tailwindcss/cli/dist/index.mjs");
39
+ let tw = cache.join("node_modules/tailwindcss");
40
+ if cli.exists() && tw.exists() {
41
+ return true;
42
+ }
43
+ let spec_tw = format!("tailwindcss@{TW}");
44
+ let spec_cli = format!("@tailwindcss/cli@{TW}");
45
+ run(
46
+ cache,
47
+ "bun",
48
+ &["add", &spec_tw, &spec_cli],
49
+ None,
50
+ ) || run(
51
+ cache,
52
+ "npm",
53
+ &["install", &spec_tw, &spec_cli],
54
+ None,
55
+ )
56
+ }
57
+
58
+ fn link_node_modules(manifest: &Path, cache: &Path) -> bool {
59
+ let dest = manifest.join("node_modules");
60
+ let src = cache.join("node_modules");
61
+ if dest.exists() || !src.exists() {
62
+ return dest.exists();
63
+ }
64
+ #[cfg(unix)]
65
+ {
66
+ std::os::unix::fs::symlink(&src, &dest).is_ok()
67
+ }
68
+ #[cfg(not(unix))]
69
+ {
70
+ std::os::windows::fs::symlink_dir(&src, &dest).is_ok()
71
+ }
72
+ }
73
+
74
+ fn write_css(manifest: &Path, dest: &Path) {
75
+ let cache = cache_dir();
76
+ if !ensure_tailwind(&cache) {
77
+ fail();
78
+ }
79
+ let _ = link_node_modules(manifest, &cache);
80
+ let entry = cache.join("node_modules/@tailwindcss/cli/dist/index.mjs");
81
+ let node_modules = cache.join("node_modules");
82
+ let out = dest.to_string_lossy().into_owned();
83
+ let args = [
84
+ entry.to_str().unwrap_or(""),
27
85
  "-i",
28
86
  "src/styles.css",
29
87
  "-o",
30
- dest.to_str().unwrap_or("styles.css"),
88
+ &out,
31
89
  "--minify",
32
- ]) {
90
+ ];
91
+ let ok = run(manifest, "bun", &args, Some(&node_modules))
92
+ || run(manifest, "node", &args, Some(&node_modules));
93
+
94
+ let css = fs::read_to_string(dest).unwrap_or_default();
95
+ if ok && compiled(&css) {
33
96
  return;
34
97
  }
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);
98
+ fail();
39
99
  }
40
100
 
41
- fn write_js(dest: &Path) {
42
- if copy_generated("pkg/client.js", dest) {
101
+ fn write_js(manifest: &Path, dest: &Path) {
102
+ let js = manifest.join("src/client/index.js");
103
+ if js.exists() && fs::copy(&js, dest).is_ok() {
43
104
  return;
44
105
  }
45
- if on_vercel()
46
- && npx_ok(&[
47
- "--yes",
106
+ let out = dest.to_string_lossy().into_owned();
107
+ if run(
108
+ manifest,
109
+ "bunx",
110
+ &[
48
111
  "esbuild",
49
112
  "src/client/index.ts",
50
113
  "--bundle",
51
114
  "--format=esm",
52
115
  "--outfile",
53
- dest.to_str().unwrap_or("client.js"),
54
- ])
55
- {
56
- return;
57
- }
58
- if on_vercel()
59
- && npx_ok(&[
116
+ &out,
117
+ ],
118
+ None,
119
+ ) || run(
120
+ manifest,
121
+ "npx",
122
+ &[
60
123
  "--yes",
61
124
  "esbuild",
62
125
  "src/client/index.ts",
126
+ "--bundle",
63
127
  "--format=esm",
64
128
  "--outfile",
65
- dest.to_str().unwrap_or("client.js"),
66
- ])
67
- {
129
+ &out,
130
+ ],
131
+ None,
132
+ ) {
68
133
  return;
69
134
  }
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);
135
+ let _ = fs::write(dest, "// client\n");
73
136
  }
74
137
 
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
138
+ fn compiled(css: &str) -> bool {
139
+ let t = css.trim_start();
140
+ !t.is_empty()
141
+ && !t.starts_with("@import \"tailwindcss\"")
142
+ && !t.starts_with("@import 'tailwindcss'")
95
143
  }
96
144
 
97
- fn on_vercel() -> bool {
98
- env::var_os("VERCEL").is_some()
145
+ fn run(dir: &Path, bin: &str, args: &[&str], node_path: Option<&Path>) -> bool {
146
+ let mut cmd = Command::new(bin);
147
+ cmd.args(args)
148
+ .current_dir(dir)
149
+ .stdin(Stdio::null());
150
+ if let Some(p) = node_path {
151
+ cmd.env("NODE_PATH", p);
152
+ }
153
+ cmd.status().map(|s| s.success()).unwrap_or(false)
99
154
  }
100
155
 
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)
156
+ fn fail() -> ! {
157
+ panic!(
158
+ "webnek: could not compile src/styles.css.\n\
159
+ Install bun or npm, then run `cargo build` again."
160
+ );
107
161
  }
@@ -0,0 +1,8 @@
1
+ /target
2
+ /node_modules
3
+ /.vercel
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+ .DS_Store
8
+ *.rs.bk
@@ -1,7 +1,7 @@
1
- console.log("⚡ [webnek] Client island initialized");
2
-
3
1
  document.addEventListener("click", (e) => {
4
- const target = (e.target as HTMLElement).closest("[data-action]");
2
+ const el = e.target;
3
+ if (!(el instanceof Element)) return;
4
+ const target = el.closest("[data-action]");
5
5
  if (!target) return;
6
6
 
7
7
  const action = target.getAttribute("data-action");
@@ -14,6 +14,6 @@ document.addEventListener("click", (e) => {
14
14
  if (action === "dec") val -= 1;
15
15
  if (action === "reset") val = 0;
16
16
 
17
- counterEl.textContent = val.toString();
18
- if (doubleEl) doubleEl.textContent = (val * 2).toString();
17
+ counterEl.textContent = String(val);
18
+ if (doubleEl) doubleEl.textContent = String(val * 2);
19
19
  });
@@ -44,14 +44,14 @@ pub fn home_page() -> View {
44
44
  <div className="flex gap-2 pb-1">
45
45
  <button
46
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"
47
+ className="w-10 h-10 border border-line bg-paper text-ink text-lg leading-none cursor-pointer hover:bg-white transition-colors"
48
48
  onClick={move || set_count.update(|c| *c -= 1)}
49
49
  >
50
50
  "−"
51
51
  </button>
52
52
  <button
53
53
  data-action="inc"
54
- className="w-10 h-10 bg-ink text-paper text-lg leading-none hover:bg-patina transition-colors"
54
+ className="w-10 h-10 bg-ink text-paper text-lg leading-none cursor-pointer hover:bg-patina transition-colors"
55
55
  onClick={move || set_count.update(|c| *c += 1)}
56
56
  >
57
57
  "+"