webnek 0.1.3 → 0.1.5

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/bin/cli.mjs CHANGED
@@ -101,7 +101,8 @@ function copyTemplate(from, to, vars) {
101
101
  for (const name of readdirSync(from)) {
102
102
  if (name === ".gitkeep" && existsSync(join(to, name))) continue;
103
103
  const src = join(from, name);
104
- 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";
105
106
  const dest = join(to, destName);
106
107
  if (statSync(src).isDirectory()) {
107
108
  copyTemplate(src, dest, vars);
@@ -116,6 +117,25 @@ function copyTemplate(from, to, vars) {
116
117
  }
117
118
  }
118
119
 
120
+ // Vercel's Rust builder (@vercel/rust) parses .cargo/config.toml whenever it
121
+ // exists and reads `build.target` without checking that a [build] table is
122
+ // present. An alias-only config kills every deploy with
123
+ // "Cannot read properties of undefined (reading 'target')".
124
+ // Guarantee scaffolded apps never ship that footgun.
125
+ function healCargoConfig(appDir) {
126
+ const p = join(appDir, ".cargo", "config.toml");
127
+ if (!existsSync(p)) return;
128
+ const text = readFileSync(p, "utf8");
129
+ const hasBuild = text
130
+ .split("\n")
131
+ .some((l) => /^\s*(\[build[\].]|build\.)/.test(l));
132
+ if (hasBuild) return;
133
+ writeFileSync(
134
+ p,
135
+ `${text.endsWith("\n") ? text : `${text}\n`}\n# Vercel's Rust builder requires a [build] table to exist when this file does.\n[build]\n`
136
+ );
137
+ }
138
+
119
139
  function stripNodeManifests(appDir) {
120
140
  for (const leftover of [
121
141
  "package.json",
@@ -185,6 +205,7 @@ function scaffold(targetArg) {
185
205
  PROJECT_NAME: pkgName,
186
206
  });
187
207
  stripNodeManifests(appDir);
208
+ healCargoConfig(appDir);
188
209
 
189
210
  const rel = relative(process.cwd(), appDir) || ".";
190
211
  const built = cargoBuild(appDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webnek",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -1,2 +1,6 @@
1
1
  [alias]
2
2
  dev = "run"
3
+
4
+ # Vercel's Rust builder requires a [build] table to exist when this file does
5
+ # (it reads `build.target` unconditionally). Keep this even if empty.
6
+ [build]
@@ -3,3 +3,6 @@ target/**
3
3
  !target/x86_64-unknown-linux-gnu/release/**
4
4
  !target/aarch64-unknown-linux-gnu/release/**
5
5
  .git
6
+ # Local cargo config (the `cargo dev` alias) — a config without a [build]
7
+ # table crashes @vercel/rust, and none of it is needed on Vercel.
8
+ .cargo
@@ -13,3 +13,7 @@ cargo dev
13
13
  ```bash
14
14
  vercel
15
15
  ```
16
+
17
+ Vercel compiles `api/index.rs` into a Rust function; everything else rewrites to it (`vercel.json`).
18
+
19
+ Note: `.cargo/config.toml` must keep a `[build]` table (even empty) — Vercel's Rust builder crashes on configs without one. `.vercelignore` also keeps `.cargo` out of deploys.
package/template/build.rs CHANGED
@@ -1,53 +1,111 @@
1
1
  //! Compile CSS and client JS into OUT_DIR on every `cargo build`.
2
+ //! Tailwind lives in ~/.cargo/webnek/cache (no project node_modules).
2
3
 
3
4
  use std::env;
4
5
  use std::fs;
5
6
  use std::path::{Path, PathBuf};
6
- use std::process::Command;
7
+ use std::process::{Command, Stdio};
8
+
9
+ const TW: &str = "4.3.3";
7
10
 
8
11
  fn main() {
9
12
  println!("cargo:rerun-if-changed=src/styles.css");
13
+ println!("cargo:rerun-if-changed=src/client/index.js");
10
14
  println!("cargo:rerun-if-changed=src/client/index.ts");
11
15
 
16
+ let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".into()));
12
17
  let out = PathBuf::from(env::var("OUT_DIR").unwrap());
13
- write_css(&out.join("styles.css"));
14
- write_js(&out.join("client.js"));
18
+ write_css(&manifest, &out.join("styles.css"));
19
+ write_js(&manifest, &out.join("client.js"));
15
20
  }
16
21
 
17
- fn write_css(dest: &Path) {
18
- let out = dest.to_str().unwrap_or("styles.css");
19
- if run(
20
- "bunx",
21
- &[
22
- "@tailwindcss/cli@4.3.3",
23
- "-i",
24
- "src/styles.css",
25
- "-o",
26
- out,
27
- "--minify",
28
- ],
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}");
37
+ }
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,
29
50
  ) || run(
30
- "npx",
31
- &[
32
- "--yes",
33
- "@tailwindcss/cli@4.3.3",
34
- "-i",
35
- "src/styles.css",
36
- "-o",
37
- out,
38
- "--minify",
39
- ],
40
- ) {
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(""),
85
+ "-i",
86
+ "src/styles.css",
87
+ "-o",
88
+ &out,
89
+ "--minify",
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) {
41
96
  return;
42
97
  }
43
- let fallback = fs::read_to_string("src/styles.css")
44
- .unwrap_or_else(|_| "/* styles.css: install bun or npx to compile Tailwind */\n".into());
45
- let _ = fs::write(dest, fallback);
98
+ fail();
46
99
  }
47
100
 
48
- fn write_js(dest: &Path) {
49
- let out = dest.to_str().unwrap_or("client.js");
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() {
104
+ return;
105
+ }
106
+ let out = dest.to_string_lossy().into_owned();
50
107
  if run(
108
+ manifest,
51
109
  "bunx",
52
110
  &[
53
111
  "esbuild",
@@ -55,9 +113,11 @@ fn write_js(dest: &Path) {
55
113
  "--bundle",
56
114
  "--format=esm",
57
115
  "--outfile",
58
- out,
116
+ &out,
59
117
  ],
118
+ None,
60
119
  ) || run(
120
+ manifest,
61
121
  "npx",
62
122
  &[
63
123
  "--yes",
@@ -66,19 +126,36 @@ fn write_js(dest: &Path) {
66
126
  "--bundle",
67
127
  "--format=esm",
68
128
  "--outfile",
69
- out,
129
+ &out,
70
130
  ],
131
+ None,
71
132
  ) {
72
133
  return;
73
134
  }
74
- let fallback = fs::read_to_string("src/client/index.ts").unwrap_or_else(|_| "// client\n".into());
75
- let _ = fs::write(dest, fallback);
135
+ let _ = fs::write(dest, "// client\n");
136
+ }
137
+
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'")
143
+ }
144
+
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)
76
154
  }
77
155
 
78
- fn run(bin: &str, args: &[&str]) -> bool {
79
- Command::new(bin)
80
- .args(args)
81
- .status()
82
- .map(|s| s.success())
83
- .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
+ );
84
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
  "+"