create-nowaki 0.13.0 → 0.15.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,30 @@ 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 {}
144
+ try {
145
+ await rename(path.join(dest, "env.example"), path.join(dest, ".env.example"));
146
+ } catch {}
158
147
 
159
- // package.json の name を設定
160
148
  try {
161
149
  const pkgPath = path.join(dest, "package.json");
162
150
  const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
163
151
  pkg.name = pkgName;
164
152
  await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
165
- } catch {
166
- // package.json が無ければ素通し
167
- }
153
+ } catch {}
168
154
 
169
155
  console.log(
170
156
  `\n ${green("✓")} created ${bold(name)} ${dim(`(${template} template · ${path.relative(process.cwd(), dest) || "."})`)}`,
171
157
  );
172
158
 
173
- // git init(任意)
174
159
  if (doGit) {
175
160
  try {
176
161
  execFileSync("git", ["init", "-q"], { cwd: dest, stdio: "ignore" });
@@ -180,7 +165,6 @@ if (doGit) {
180
165
  }
181
166
  }
182
167
 
183
- // 依存インストール(任意)
184
168
  let installed = false;
185
169
  if (doInstall) {
186
170
  console.log(`\n ${dim(`installing dependencies with ${pm}…`)}\n`);
@@ -193,7 +177,6 @@ if (doInstall) {
193
177
  }
194
178
  }
195
179
 
196
- // --- 次のステップ ---
197
180
  const runDev = pm === "npm" || pm === "bun" ? `${pm} run dev` : `${pm} dev`;
198
181
  console.log(`\n ${bold("Next steps:")}`);
199
182
  console.log(` cd ${name}`);
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "create-nowaki",
3
- "version": "0.13.0",
3
+ "version": "0.15.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",
@@ -84,13 +84,22 @@ nowaki.config.mjs plugins (optional)
84
84
  islands to cut work on first load. The directives are typed (no prop leaks).
85
85
  7. **Server functions** — a module with a top-of-file `"use server"` directive
86
86
  becomes RPC; the client gets a tiny fetch proxy, the implementation stays on
87
- the server. Validate arguments; read auth via `getContext()`:
87
+ the server. Validate the input with `serverFn(schema, impl)` (any Standard
88
+ Schema library: zod, valibot, arktype); `nowaki build` warns about exports that
89
+ take arguments without it. Read auth via `getContext()`, and throw
90
+ `ServerFnError` for messages the client may see (other errors are a generic
91
+ 500 in production):
88
92
  ```ts
89
93
  // actions/todos.ts
90
94
  "use server";
91
- import { getContext } from "@nowaki-dev/runtime";
92
- export async function addTodo(text: string) { /* runs on the server */ }
95
+ import { z } from "zod";
96
+ import { getContext, serverFn, ServerFnError } from "@nowaki-dev/runtime";
97
+ export const addTodo = serverFn(z.string().min(1).max(200), async (text) => {
98
+ if (!getContext()?.cookies?.user) throw new ServerFnError("Sign in first", { status: 401 });
99
+ /* runs on the server with a validated string */
100
+ });
93
101
  ```
102
+ The **basics** template includes `actions/echo.ts` as a minimal validated example.
94
103
  Call it from an island like a normal async function:
95
104
  `import { addTodo } from "../actions/todos.ts";`
96
105
  8. **Jetstream islands** (server-reactive, zero client JS) — give an island
@@ -112,6 +121,17 @@ nowaki.config.mjs plugins (optional)
112
121
  ```
113
122
  Unknown paths, missing params, and wrong param keys are compile errors.
114
123
 
124
+ ## Security
125
+
126
+ - **`nowaki dev` is for local development.** Do not expose it to untrusted networks.
127
+ Use `nowaki start` or a deploy adapter output in production.
128
+ - **Secrets stay server-side.** Only `PUBLIC_*` (and `MODE`) reach the browser. Copy
129
+ `.env.example` to `.env` and keep database URLs, API keys, and tokens unprefixed.
130
+ - **Server functions and API routes are public endpoints.** Validate and bound every
131
+ argument; authorize with `getContext()` (cookies/headers) before mutating data.
132
+ - **Do not embed secrets** in client islands, loaders returned to the page, or static
133
+ prerender output. ISR pages must not include per-user secrets.
134
+
115
135
  ## Don't
116
136
 
117
137
  - Don't add a build tool (Vite/webpack/Babel) or a `tsconfig` paths setup — the
@@ -8,6 +8,7 @@ components under `islands/` ship JavaScript and hydrate.
8
8
 
9
9
  ```bash
10
10
  npm install
11
+ cp .env.example .env # optional — only PUBLIC_* vars belong in client bundles
11
12
  npm run dev # http://localhost:3000
12
13
  ```
13
14
 
@@ -43,6 +44,7 @@ actions/ "use server" RPC modules (optional)
43
44
  `preact/compat`, so many React libraries work.)
44
45
  - Use **explicit file extensions** in relative imports: `../islands/Counter.tsx`.
45
46
  - A route's `loader` runs only on the server; `action` handles non-GET requests.
47
+ - `routes/api/health.ts` is a minimal JSON API example; `routes/_404.tsx` handles unknown paths.
46
48
  - `AGENTS.md` documents the conventions for AI coding agents.
47
49
 
48
50
  Docs: <https://nowaki.dev/docs>
@@ -0,0 +1,7 @@
1
+ # Copy to `.env` for local development. Never commit `.env` (see `.gitignore`).
2
+ #
3
+ # Only variables prefixed with PUBLIC_ are inlined into client bundles.
4
+ # Everything else stays on the server (loaders, actions, server functions).
5
+ #
6
+ # PUBLIC_SITE_NAME=My App
7
+ # DATABASE_URL=postgres://... # server-only — do not prefix with PUBLIC_
@@ -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,32 +29,31 @@ 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
- // 遅延ハイドレーション指令(Astro 互換の client:*)を任意のコンポーネントに付けられるよう、
36
- // preact の JSX.IntrinsicAttributes を拡張する。<Counter client:visible /> 等が型チェックを通る。
37
- // (この import で本ファイルはモジュールになる=下のグローバル型は declare global で公開する。)
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.
38
37
  import "preact";
39
38
  declare module "preact" {
40
39
  namespace JSX {
41
40
  interface IntrinsicAttributes {
42
- /** ページ読み込み時にハイドレート(既定)。 */
41
+ /** Hydrate on page load (default). */
43
42
  "client:load"?: boolean;
44
- /** requestIdleCallback でアイドル時にハイドレート。 */
43
+ /** Hydrate when idle, via requestIdleCallback. */
45
44
  "client:idle"?: boolean;
46
- /** IntersectionObserver で可視になったらハイドレート。 */
45
+ /** Hydrate once visible, via IntersectionObserver. */
47
46
  "client:visible"?: boolean;
48
- /** 指定メディアクエリが一致したらハイドレート。例: client:media="(min-width: 768px)" */
47
+ /** Hydrate when the media query matches. e.g. client:media="(min-width: 768px)" */
49
48
  "client:media"?: string;
50
- /** SSR せず、クライアントだけで描画(ブラウザ専用 API を使う島向け)。 */
49
+ /** Skip SSR and render on the client only (for islands using browser-only APIs). */
51
50
  "client:only"?: boolean;
52
51
  }
53
52
  }
54
53
  }
55
54
 
56
55
  declare global {
57
- // import.meta.env(PUBLIC_* と MODE がビルド時に inline される)。
56
+ // import.meta.env (PUBLIC_* and MODE are inlined at build time).
58
57
  interface ImportMetaEnv {
59
58
  readonly MODE: string;
60
59
  readonly [key: `PUBLIC_${string}`]: string | undefined;
@@ -9,11 +9,11 @@
9
9
  "prerender": "nowaki prerender"
10
10
  },
11
11
  "dependencies": {
12
- "@nowaki-dev/runtime": "^0.11.0",
12
+ "@nowaki-dev/runtime": "^0.13.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.12.0"
17
+ "nowaki": "^0.14.0"
18
18
  }
19
19
  }
@@ -0,0 +1,22 @@
1
+ "use server";
2
+
3
+ import { getContext, serverFn } from "@nowaki-dev/runtime";
4
+
5
+ // Hand-written Standard Schema (no zod/valibot) — same pattern as examples/hello/actions/todos.ts.
6
+ const message = {
7
+ "~standard": {
8
+ version: 1 as const,
9
+ vendor: "example",
10
+ validate: (v: unknown) =>
11
+ typeof v === "string" && v.length <= 120
12
+ ? { value: v.trim() }
13
+ : { issues: [{ message: "message must be a string of at most 120 characters" }] },
14
+ },
15
+ };
16
+
17
+ /** Example server function — validated input and getContext() for auth in real apps. */
18
+ export const echo = serverFn(message, async (text) => ({
19
+ ok: true,
20
+ echo: text,
21
+ hasRequest: getContext() != null,
22
+ }));
@@ -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 (
@@ -0,0 +1,13 @@
1
+ export const title = "404 — Not found";
2
+
3
+ export default function NotFound({ url }: { url: URL }) {
4
+ return (
5
+ <main>
6
+ <h1>404</h1>
7
+ <p>
8
+ Page not found: <code>{url.pathname}</code>
9
+ </p>
10
+ <a href="/">Back to home</a>
11
+ </main>
12
+ );
13
+ }
@@ -0,0 +1,6 @@
1
+ // Example API route (server-only). No secrets in responses; validate inputs on real endpoints.
2
+ type Ctx = { json: (data: unknown) => unknown };
3
+
4
+ export function GET(ctx: Ctx) {
5
+ return ctx.json({ ok: true });
6
+ }