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 +15 -35
- package/package.json +4 -4
- package/templates/_shared/AGENTS.md +20 -0
- package/templates/_shared/_gitignore +2 -0
- package/templates/_shared/nowaki-env.d.ts +32 -10
- package/templates/_shared/package.json +3 -3
- package/templates/_shared/tsconfig.json +1 -0
- package/templates/basics/islands/Counter.tsx +1 -1
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,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
|
|
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.
|
|
3
|
+
"version": "0.14.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",
|
|
@@ -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,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,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
|
-
//
|
|
32
|
+
// Plugin virtual modules (resolveId/load in nowaki.config). Any export is allowed.
|
|
33
33
|
declare module "virtual:*";
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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.
|
|
12
|
+
"@nowaki-dev/runtime": "^0.12.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.13.0"
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -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 (
|