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 +18 -35
- package/package.json +4 -4
- package/templates/_shared/AGENTS.md +23 -3
- package/templates/_shared/README.md +2 -0
- package/templates/_shared/env.example +7 -0
- package/templates/_shared/nowaki-env.d.ts +12 -13
- package/templates/_shared/package.json +3 -3
- package/templates/basics/actions/echo.ts +22 -0
- package/templates/basics/islands/Counter.tsx +1 -1
- package/templates/basics/routes/_404.tsx +13 -0
- package/templates/basics/routes/api/health.ts +6 -0
package/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `npm create nowaki
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
//
|
|
7
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Scaffold a new Nowaki app",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "
|
|
6
|
+
"author": "Tettu0530",
|
|
7
7
|
"homepage": "https://nowaki.dev",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
10
|
+
"url": "git+https://github.com/Tettu0530/nowaki.git",
|
|
11
11
|
"directory": "packages/create-nowaki"
|
|
12
12
|
},
|
|
13
|
-
"bugs": "https://github.com/
|
|
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
|
|
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 {
|
|
92
|
-
|
|
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
|
|
2
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
32
|
+
// Plugin virtual modules (resolveId/load in nowaki.config). Any export is allowed.
|
|
33
33
|
declare module "virtual:*";
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
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
|
-
/**
|
|
47
|
+
/** Hydrate when the media query matches. e.g. client:media="(min-width: 768px)" */
|
|
49
48
|
"client:media"?: string;
|
|
50
|
-
/** SSR
|
|
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
|
|
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.
|
|
12
|
+
"@nowaki-dev/runtime": "^0.13.0",
|
|
13
13
|
"preact": "^10.25.4",
|
|
14
|
-
"preact-render-to-string": "^6.
|
|
14
|
+
"preact-render-to-string": "^6.8.2"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"nowaki": "^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
|
-
//
|
|
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 (
|