create-nowaki 0.12.0 → 0.14.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/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // `npm create nowaki` のスキャフォールダ。依存ゼロ(Node 組み込みのみ)。
3
- // 対話ウィザード(TTY 時): プロジェクト名・パッケージマネージャ・git・依存インストール。
4
- // 非対話(CI / `-y` / パイプ)では既定値で黙って生成する。
2
+ // Scaffolder for `npm create nowaki`. Zero dependencies (Node built-ins only).
3
+ // Interactive wizard (on a TTY): project name, package manager, git, dependency install.
4
+ // Non-interactive (CI / `-y` / pipe): no prompts, generates with defaults.
5
5
  //
6
- // フラグ: -t/--template <basics|minimal>, -y/--yes(質問を飛ばす),
7
- // --install(非対話でも入れる), --no-install, --no-git, --pm <npm|pnpm|yarn|bun>
6
+ // Flags: -t/--template <basics|minimal>, -y/--yes (skip prompts),
7
+ // --install (install even when non-interactive), --no-install, --no-git, --pm <npm|pnpm|yarn|bun>
8
8
 
9
9
  import { cp, mkdir, readdir, readFile, writeFile, rename } from "node:fs/promises";
10
10
  import path from "node:path";
@@ -15,7 +15,7 @@ import process from "node:process";
15
15
 
16
16
  const { stdin, stdout, env, argv, platform } = process;
17
17
 
18
- // --- 色(NO_COLOR と非 TTY を尊重)---
18
+ // Respects NO_COLOR and non-TTY output.
19
19
  const useColor = !!stdout.isTTY && !env.NO_COLOR;
20
20
  const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : `${s}`);
21
21
  const cyan = wrap("36");
@@ -25,9 +25,8 @@ const green = wrap("32");
25
25
  const red = wrap("31");
26
26
  const yellow = wrap("33");
27
27
 
28
- // --- 引数 ---
29
28
  const args = argv.slice(2);
30
- // 値をとるフラグ。これらの直後の引数は positional ではない。
29
+ // Flags that take a value. The argument right after one is not positional.
31
30
  const VALUE_FLAGS = new Set(["--pm", "--template", "-t"]);
32
31
  const flagAt = (...names) => {
33
32
  const i = args.findIndex((a) => names.includes(a));
@@ -37,7 +36,7 @@ const flags = new Set(args.filter((a) => a.startsWith("-")));
37
36
  const positionals = [];
38
37
  for (let i = 0; i < args.length; i++) {
39
38
  if (args[i].startsWith("-")) {
40
- if (VALUE_FLAGS.has(args[i])) i++; // フラグの値を飛ばす
39
+ if (VALUE_FLAGS.has(args[i])) i++;
41
40
  continue;
42
41
  }
43
42
  positionals.push(args[i]);
@@ -51,13 +50,11 @@ const noGit = flags.has("--no-git");
51
50
  const interactive = !!stdin.isTTY && !!stdout.isTTY && !yes;
52
51
 
53
52
  const templatesDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "templates");
54
- // 利用可能なテンプレート(templates/<name>、_shared は共通設定)。
55
53
  const TEMPLATES = [
56
54
  { value: "basics", hint: "starter with an interactive island" },
57
55
  { value: "minimal", hint: "a single page, zero islands" },
58
56
  ];
59
57
 
60
- // --- パッケージマネージャ検出(`npm create` の user-agent から)---
61
58
  function detectPM() {
62
59
  const ua = env.npm_config_user_agent || "";
63
60
  if (ua.startsWith("pnpm")) return "pnpm";
@@ -66,7 +63,6 @@ function detectPM() {
66
63
  return "npm";
67
64
  }
68
65
 
69
- // --- バナー ---
70
66
  console.log();
71
67
  console.log(` ${cyan(bold("Nowaki"))} ${dim("野分")} ${dim("· create a new app")}`);
72
68
  console.log();
@@ -83,7 +79,6 @@ async function confirm(question, def = true) {
83
79
  if (!a) return def;
84
80
  return a === "y" || a === "yes";
85
81
  }
86
- // 番号 or 名前で選ぶ簡易メニュー(依存なし)。
87
82
  async function select(question, options, def) {
88
83
  if (!rl) return def;
89
84
  console.log(` ${question}`);
@@ -98,14 +93,12 @@ async function select(question, options, def) {
98
93
  return options.some((o) => o.value === a) ? a : def;
99
94
  }
100
95
 
101
- // --- 質問 ---
102
96
  let name = positionals[0] || (await ask("Project name?", interactive ? "my-app" : "nowaki-app"));
103
97
  name = name.trim() || "nowaki-app";
104
- // package.json の name は valid な npm 名に正規化(ディレクトリ名は入力のまま)
98
+ // Keep the directory name as typed; normalize only package.json's name to a valid npm name.
105
99
  const pkgName = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "") || "nowaki-app";
106
100
  const dest = path.resolve(process.cwd(), name);
107
101
 
108
- // 既存の空でないディレクトリは中止
109
102
  try {
110
103
  const existing = await readdir(dest);
111
104
  if (existing.length > 0) {
@@ -113,11 +106,8 @@ try {
113
106
  rl?.close();
114
107
  process.exit(1);
115
108
  }
116
- } catch {
117
- // 無ければOK
118
- }
109
+ } catch {}
119
110
 
120
- // テンプレート選択(-t/--template or 対話メニュー、既定 basics)。
121
111
  let template = templateFlag || (interactive ? await select("Template?", TEMPLATES, "basics") : "basics");
122
112
  template = String(template).toLowerCase();
123
113
  if (!TEMPLATES.some((t) => t.value === template)) {
@@ -132,8 +122,7 @@ let pm = pmFlag || detectPM();
132
122
  if (interactive && !pmFlag) pm = (await ask("Package manager?", pm)).trim() || pm;
133
123
  if (!["npm", "pnpm", "yarn", "bun"].includes(pm)) pm = "npm";
134
124
  const doGit = noGit ? false : await confirm("Initialize a git repository?", false);
135
- // 依存インストール: 対話なら確認(既定 Yes)。非対話(-y / CI / パイプ)は自動実行しない
136
- //(驚き防止)。非対話で入れたいときは --install。--no-install は常にスキップ。
125
+ // Non-interactive (-y / CI / pipe): to avoid surprises, don't install unless --install is given.
137
126
  const doInstall = noInstall
138
127
  ? false
139
128
  : forceInstall
@@ -143,34 +132,27 @@ const doInstall = noInstall
143
132
  : false;
144
133
  rl?.close();
145
134
 
146
- // --- 生成 ---
147
- // 共通設定(_shared)→ 選択テンプレートの順に重ねる。
135
+ // Layer the shared config (_shared), then the chosen template.
148
136
  await mkdir(dest, { recursive: true });
149
137
  await cp(path.join(templatesDir, "_shared"), dest, { recursive: true });
150
138
  await cp(path.join(templatesDir, template), dest, { recursive: true });
151
139
 
152
- // npm は公開時に .gitignore を除外するので、テンプレートでは _gitignore で持ち、ここで戻す。
140
+ // npm strips .gitignore on publish, so templates ship _gitignore and it's renamed back here.
153
141
  try {
154
142
  await rename(path.join(dest, "_gitignore"), path.join(dest, ".gitignore"));
155
- } catch {
156
- // _gitignore が無ければ素通し
157
- }
143
+ } catch {}
158
144
 
159
- // package.json の name を設定
160
145
  try {
161
146
  const pkgPath = path.join(dest, "package.json");
162
147
  const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
163
148
  pkg.name = pkgName;
164
149
  await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
165
- } catch {
166
- // package.json が無ければ素通し
167
- }
150
+ } catch {}
168
151
 
169
152
  console.log(
170
153
  `\n ${green("✓")} created ${bold(name)} ${dim(`(${template} template · ${path.relative(process.cwd(), dest) || "."})`)}`,
171
154
  );
172
155
 
173
- // git init(任意)
174
156
  if (doGit) {
175
157
  try {
176
158
  execFileSync("git", ["init", "-q"], { cwd: dest, stdio: "ignore" });
@@ -180,7 +162,6 @@ if (doGit) {
180
162
  }
181
163
  }
182
164
 
183
- // 依存インストール(任意)
184
165
  let installed = false;
185
166
  if (doInstall) {
186
167
  console.log(`\n ${dim(`installing dependencies with ${pm}…`)}\n`);
@@ -193,7 +174,6 @@ if (doInstall) {
193
174
  }
194
175
  }
195
176
 
196
- // --- 次のステップ ---
197
177
  const runDev = pm === "npm" || pm === "bun" ? `${pm} run dev` : `${pm} dev`;
198
178
  console.log(`\n ${bold("Next steps:")}`);
199
179
  console.log(` cd ${name}`);
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "create-nowaki",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Scaffold a new Nowaki app",
5
5
  "license": "MIT",
6
- "author": "VorEdge <dev@voredge.com>",
6
+ "author": "Tettu0530",
7
7
  "homepage": "https://nowaki.dev",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/nowaki-dev/nowaki.git",
10
+ "url": "git+https://github.com/Tettu0530/nowaki.git",
11
11
  "directory": "packages/create-nowaki"
12
12
  },
13
- "bugs": "https://github.com/nowaki-dev/nowaki/issues",
13
+ "bugs": "https://github.com/Tettu0530/nowaki/issues",
14
14
  "keywords": [
15
15
  "nowaki",
16
16
  "create",
@@ -71,6 +71,17 @@ nowaki.config.mjs plugins (optional)
71
71
  return <button onClick={() => setN(n + 1)}>count: {n}</button>;
72
72
  }
73
73
  ```
74
+ **Lazy hydration** (Astro-style `client:*` directives) controls *when* an
75
+ island hydrates — add a directive where you use it:
76
+ ```tsx
77
+ <Counter client:load /> // default — hydrate on load
78
+ <Counter client:idle /> // hydrate on requestIdleCallback
79
+ <Counter client:visible /> // hydrate when scrolled into view
80
+ <Chart client:media="(min-width: 768px)" /> // hydrate when the query matches
81
+ <Map client:only /> // skip SSR, render only on the client
82
+ ```
83
+ Prefer `client:visible` / `client:idle` for below-the-fold or non-critical
84
+ islands to cut work on first load. The directives are typed (no prop leaks).
74
85
  7. **Server functions** — a module with a top-of-file `"use server"` directive
75
86
  becomes RPC; the client gets a tiny fetch proxy, the implementation stays on
76
87
  the server. Validate arguments; read auth via `getContext()`:
@@ -91,6 +102,15 @@ nowaki.config.mjs plugins (optional)
91
102
  `error.tsx`, mark the message element `data-nowaki-error` and the retry button
92
103
  `data-nowaki-reset`. Visited pages are kept in a ~30s Router Cache, so
93
104
  back/forward is instant. These are server components, not islands.
105
+ 10. **Typed navigation** — `dev`/`build` generate `.nowaki/types.d.ts` from your
106
+ routes (run `nowaki typegen` to refresh manually). Build links type-safely
107
+ from `@nowaki-dev/runtime/navigation`:
108
+ ```tsx
109
+ import { route, Link } from "@nowaki-dev/runtime/navigation";
110
+ <Link href="/about">About</Link> // static path, checked
111
+ <a href={route("/blog/[slug]", { slug })}>Post</a> // dynamic, params typed
112
+ ```
113
+ Unknown paths, missing params, and wrong param keys are compile errors.
94
114
 
95
115
  ## Don't
96
116
 
@@ -1,6 +1,8 @@
1
1
  node_modules/
2
2
  dist/
3
3
  .cache/
4
+ .nowaki/
5
+ .vercel/
4
6
 
5
7
  # env (keep secrets out of git; only PUBLIC_* reaches the client)
6
8
  .env
@@ -1,5 +1,5 @@
1
- // Nowaki アンビエント型。CSS / アセット / 仮想モジュールの import をエディタが理解できるように。
2
- // 編集不要(フレームワークの import 規約を型に伝えるだけ)。
1
+ // Nowaki ambient types, so editors understand CSS / asset / virtual-module imports.
2
+ // No need to edit (it only teaches the type checker the framework's import conventions).
3
3
 
4
4
  declare module "*.module.css" {
5
5
  const classes: Record<string, string>;
@@ -10,7 +10,7 @@ declare module "*.css" {
10
10
  export default css;
11
11
  }
12
12
 
13
- // アセット import は配信 URL(文字列)になる。
13
+ // An asset import becomes its served URL (a string).
14
14
  declare module "*.svg" { const url: string; export default url; }
15
15
  declare module "*.png" { const url: string; export default url; }
16
16
  declare module "*.jpg" { const url: string; export default url; }
@@ -29,14 +29,36 @@ declare module "*.mp3" { const url: string; export default url; }
29
29
  declare module "*.wav" { const url: string; export default url; }
30
30
  declare module "*.pdf" { const url: string; export default url; }
31
31
 
32
- // プラグインの仮想モジュール(nowaki.config の resolveId/load)。任意の export を許す。
32
+ // Plugin virtual modules (resolveId/load in nowaki.config). Any export is allowed.
33
33
  declare module "virtual:*";
34
34
 
35
- // import.meta.env(PUBLIC_* と MODE がビルド時に inline される)。
36
- interface ImportMetaEnv {
37
- readonly MODE: string;
38
- readonly [key: `PUBLIC_${string}`]: string | undefined;
35
+ // Extends preact's JSX.IntrinsicAttributes so client:* directives can go on any component.
36
+ // This import makes the file a module, so the global types below are exposed via declare global.
37
+ import "preact";
38
+ declare module "preact" {
39
+ namespace JSX {
40
+ interface IntrinsicAttributes {
41
+ /** Hydrate on page load (default). */
42
+ "client:load"?: boolean;
43
+ /** Hydrate when idle, via requestIdleCallback. */
44
+ "client:idle"?: boolean;
45
+ /** Hydrate once visible, via IntersectionObserver. */
46
+ "client:visible"?: boolean;
47
+ /** Hydrate when the media query matches. e.g. client:media="(min-width: 768px)" */
48
+ "client:media"?: string;
49
+ /** Skip SSR and render on the client only (for islands using browser-only APIs). */
50
+ "client:only"?: boolean;
51
+ }
52
+ }
39
53
  }
40
- interface ImportMeta {
41
- readonly env: ImportMetaEnv;
54
+
55
+ declare global {
56
+ // import.meta.env (PUBLIC_* and MODE are inlined at build time).
57
+ interface ImportMetaEnv {
58
+ readonly MODE: string;
59
+ readonly [key: `PUBLIC_${string}`]: string | undefined;
60
+ }
61
+ interface ImportMeta {
62
+ readonly env: ImportMetaEnv;
63
+ }
42
64
  }
@@ -9,11 +9,11 @@
9
9
  "prerender": "nowaki prerender"
10
10
  },
11
11
  "dependencies": {
12
- "@nowaki-dev/runtime": "^0.10.0",
12
+ "@nowaki-dev/runtime": "^0.12.0",
13
13
  "preact": "^10.25.4",
14
- "preact-render-to-string": "^6.5.13"
14
+ "preact-render-to-string": "^6.8.2"
15
15
  },
16
16
  "devDependencies": {
17
- "nowaki": "^0.11.0"
17
+ "nowaki": "^0.13.0"
18
18
  }
19
19
  }
@@ -23,6 +23,7 @@
23
23
  },
24
24
  "include": [
25
25
  "nowaki-env.d.ts",
26
+ ".nowaki/types.d.ts",
26
27
  "nowaki.config.*",
27
28
  "routes",
28
29
  "islands",
@@ -1,6 +1,6 @@
1
1
  import { useState } from "preact/hooks";
2
2
 
3
- // クライアントでハイドレートする唯一のコンポーネント(島)。スタイルは routes/index.tsx の <style> に。
3
+ // The only component (island) hydrated on the client. Styles live in the <style> in routes/index.tsx.
4
4
  export default function Counter({ start = 0 }: { start?: number }) {
5
5
  const [count, setCount] = useState(start);
6
6
  return (