create-what 0.10.0 → 0.11.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.
Files changed (3) hide show
  1. package/README.md +52 -3
  2. package/index.js +363 -60
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,30 +23,68 @@ bun create what@latest my-app
23
23
  npm create what@latest my-app -- --yes
24
24
  ```
25
25
 
26
+ ### Full-stack template
27
+
28
+ ```bash
29
+ npm create what@latest my-app -- --fullstack
30
+ cd my-app
31
+ npm install
32
+ npm run dev # real SSR + ISR server -> http://localhost:3000
33
+ ```
34
+
35
+ File-routed SSR with server loaders, server actions, client hydration, and
36
+ origin-first ISR (stale-while-revalidate + on-demand revalidation + poll
37
+ regeneration). Buildless: the server serves the client entry and the framework
38
+ as native ES modules — no bundler, works on any Node host, no CDN required.
39
+
40
+ In production, set `WHAT_REVALIDATE_SECRET` (the server refuses to start
41
+ without it when `NODE_ENV=production`).
42
+
26
43
  ## Options
27
44
 
28
45
  The scaffolder prompts you for:
29
46
 
30
47
  1. **Project name** -- directory to create
31
- 2. **React compat** -- include `what-react` for using React libraries (zustand, TanStack Query, etc.)
32
- 3. **CSS approach** -- vanilla CSS, Tailwind CSS v4, or StyleX
48
+ 2. **Template** -- SPA (default) or full-stack (`--fullstack` / `--template=fullstack`)
49
+ 3. **React compat** (SPA only) -- include `what-react` for using React libraries (zustand, TanStack Query, etc.)
50
+ 4. **CSS approach** (SPA only) -- vanilla CSS, Tailwind CSS v4, or StyleX
33
51
 
34
52
  ## What You Get
35
53
 
54
+ ### SPA (default)
55
+
36
56
  ```
37
57
  my-app/
38
58
  src/
39
- main.jsx # App entry point with counter example
59
+ main.jsx # App entry point with counter example
40
60
  styles.css # Styles (vanilla, Tailwind, or StyleX)
41
61
  public/
42
62
  favicon.svg # What Framework logo
43
63
  index.html # HTML entry
44
64
  vite.config.js # Pre-configured Vite (What compiler or React compat)
65
+ eslint.config.js # eslint-plugin-what (compiler preset)
45
66
  tsconfig.json # TypeScript config
46
67
  package.json
47
68
  .gitignore
48
69
  ```
49
70
 
71
+ ### Full-stack (`--fullstack`)
72
+
73
+ ```
74
+ my-app/
75
+ src/
76
+ pages/ # File-routed pages (loader + page config + component)
77
+ actions/ # Server actions (mutations + cache revalidation)
78
+ routes.js # Route table
79
+ entry-client.js # Client hydration entry
80
+ db.js # In-memory demo data (swap for SQLite/Postgres)
81
+ styles.css
82
+ server.js # Node adapter + ISR engine + revalidate webhook
83
+ what.config.js # Deploy adapter + ISR defaults
84
+ eslint.config.js # eslint-plugin-what (recommended preset)
85
+ package.json
86
+ ```
87
+
50
88
  ### With React compat enabled
51
89
 
52
90
  The scaffold includes a working zustand demo showing a React state library running on What's signal engine.
@@ -61,11 +99,22 @@ StyleX is configured via `vite-plugin-stylex`. The counter example uses `stylex.
61
99
 
62
100
  ## Scripts
63
101
 
102
+ ### SPA
103
+
64
104
  | Script | Command |
65
105
  |---|---|
66
106
  | `npm run dev` | Start Vite dev server |
67
107
  | `npm run build` | Production build |
68
108
  | `npm run preview` | Preview production build |
109
+ | `npm run lint` | ESLint (eslint-plugin-what) |
110
+
111
+ ### Full-stack
112
+
113
+ | Script | Command |
114
+ |---|---|
115
+ | `npm run dev` | SSR + ISR server with auto-restart on change |
116
+ | `npm start` | Same server, no watcher (production entry point) |
117
+ | `npm run lint` | ESLint (eslint-plugin-what) |
69
118
 
70
119
  ## Links
71
120
 
package/index.js CHANGED
@@ -131,13 +131,17 @@ async function gatherOptions() {
131
131
  { label: 'Full-stack (SSR + file routes + loaders + actions + ISR)', value: 'fullstack' },
132
132
  ]);
133
133
 
134
- reactCompat = await prompter.confirm('Add React library support? (what-react)');
135
-
136
- cssApproach = await prompter.select('CSS approach:', [
137
- { label: 'None (vanilla CSS)', value: 'none' },
138
- { label: 'Tailwind CSS v4', value: 'tailwind' },
139
- { label: 'StyleX', value: 'stylex' },
140
- ]);
134
+ // The full-stack template is buildless (native ESM served by server.js), so
135
+ // the Vite-based React-compat / CSS tooling options don't apply to it.
136
+ if (template !== 'fullstack') {
137
+ reactCompat = await prompter.confirm('Add React library support? (what-react)');
138
+
139
+ cssApproach = await prompter.select('CSS approach:', [
140
+ { label: 'None (vanilla CSS)', value: 'none' },
141
+ { label: 'Tailwind CSS v4', value: 'tailwind' },
142
+ { label: 'StyleX', value: 'stylex' },
143
+ ]);
144
+ }
141
145
 
142
146
  prompter.close();
143
147
 
@@ -152,19 +156,30 @@ function generatePackageJson(packageName, { reactCompat, cssApproach, template }
152
156
  const deps = {
153
157
  'what-framework': whatVersionRange,
154
158
  };
155
- const devDeps = {
156
- vite: '^6.0.0',
157
- 'what-compiler': whatVersionRange,
158
- 'what-devtools-mcp': whatVersionRange,
159
- '@babel/core': '^7.23.0',
160
- };
161
159
 
162
- // Full-stack apps add the origin-first ISR engine + a Node server entry.
160
+ // Full-stack apps are buildless: server.js does SSR + ISR and serves the
161
+ // client entry as native ES modules, so there is no Vite/compiler toolchain.
162
+ // `npm run dev` runs the real app with auto-restart on change.
163
+ const devDeps = template === 'fullstack'
164
+ ? {
165
+ 'what-devtools-mcp': whatVersionRange,
166
+ eslint: '^9.0.0',
167
+ 'eslint-plugin-what': whatVersionRange,
168
+ }
169
+ : {
170
+ vite: '^6.0.0',
171
+ 'what-compiler': whatVersionRange,
172
+ 'what-devtools-mcp': whatVersionRange,
173
+ '@babel/core': '^7.23.0',
174
+ eslint: '^9.0.0',
175
+ 'eslint-plugin-what': whatVersionRange,
176
+ };
177
+
163
178
  const scripts = template === 'fullstack'
164
- ? { dev: 'vite', build: 'vite build', start: 'node server.js', preview: 'vite preview' }
165
- : { dev: 'vite', build: 'vite build', preview: 'vite preview' };
179
+ ? { dev: 'node --watch server.js', start: 'node server.js', lint: 'eslint .' }
180
+ : { dev: 'vite', build: 'vite build', preview: 'vite preview', lint: 'eslint .' };
166
181
  if (template === 'fullstack') {
167
- deps['what-cache'] = whatVersionRange;
182
+ deps['what-isr'] = whatVersionRange;
168
183
  }
169
184
 
170
185
  if (reactCompat) {
@@ -822,10 +837,17 @@ output {
822
837
  }
823
838
 
824
839
  function generateReadme(packageName, { reactCompat, cssApproach, template }) {
825
- let notes = `- Canonical package name is \`what-framework\`.
840
+ let notes = template === 'fullstack'
841
+ ? `- Canonical package name is \`what-framework\`.
842
+ - Pages are authored with \`h()\` in plain .js so they run isomorphically (Node
843
+ SSR + browser hydration) with no compile step.
844
+ - Event handlers accept both \`onClick\` and \`onclick\`.
845
+ - Lint with \`npm run lint\` (eslint-plugin-what).`
846
+ : `- Canonical package name is \`what-framework\`.
826
847
  - Uses the What compiler for JSX transforms and automatic reactivity.
827
848
  - Vite is preconfigured; use \`npm run dev/build/preview\`.
828
849
  - Event handlers accept both \`onClick\` and \`onclick\`; docs and templates use \`onClick\`.
850
+ - Lint with \`npm run lint\` (eslint-plugin-what).
829
851
  - Bun is also supported: \`bun create what@latest\`, \`bun run dev\`.`;
830
852
 
831
853
  if (reactCompat) {
@@ -848,17 +870,26 @@ function generateReadme(packageName, { reactCompat, cssApproach, template }) {
848
870
  return `# ${packageName}
849
871
 
850
872
  A full-stack What Framework app: file-routed SSR, server loaders, server
851
- actions, and origin-first ISR (stale-while-revalidate + on-demand revalidation +
852
- poll regeneration) — works on any host, no CDN required.
873
+ actions, client hydration, and origin-first ISR (stale-while-revalidate +
874
+ on-demand revalidation + poll regeneration) — works on any host, no CDN
875
+ required. Buildless: the server serves the client entry and the framework as
876
+ native ES modules, so there is no bundle step.
853
877
 
854
878
  ## Run
855
879
 
856
880
  \`\`\`bash
857
881
  npm install
858
- npm start # full-stack SSR server → http://localhost:3000
882
+ npm run dev # SSR server with auto-restart on change → http://localhost:3000
883
+ npm start # same server, no watcher (production entry point)
859
884
  \`\`\`
860
885
 
861
- Or develop the client in isolation with \`npm run dev\` (Vite, http://localhost:5173).
886
+ In production, set \`WHAT_REVALIDATE_SECRET\` (the server refuses to start
887
+ without it when \`NODE_ENV=production\`):
888
+
889
+ \`\`\`bash
890
+ WHAT_REVALIDATE_SECRET=$(node -e "console.log(require('node:crypto').randomBytes(24).toString('hex'))") \\
891
+ NODE_ENV=production npm start
892
+ \`\`\`
862
893
 
863
894
  ## Structure
864
895
 
@@ -867,13 +898,17 @@ Or develop the client in isolation with \`npm run dev\` (Vite, http://localhost:
867
898
  - \`src/actions/*\` — server actions (mutations), served at \`/__what_action\`. Call
868
899
  \`revalidatePath\`/\`revalidateTag\` after a mutation to purge the ISR cache.
869
900
  - \`src/routes.js\` — the route table (loaders/getStaticPaths/page bound per route).
870
- - \`server.js\` — Node adapter + ISR engine + revalidate webhook + poll scheduler.
901
+ - \`src/entry-client.js\` — client hydration entry: re-renders the matched page
902
+ with the server's loader data (\`#__what_data\`) and attaches interactivity.
903
+ - \`server.js\` — Node adapter + ISR engine + revalidate webhook + poll scheduler
904
+ + static serving (client entry, framework modules, \`public/\`).
871
905
  - \`what.config.js\` — deploy adapter + ISR defaults.
872
906
 
873
907
  ### ISR cheat-sheet
874
908
 
875
909
  - \`page = { mode: 'static', revalidate: 60 }\` → cached, regenerated in the
876
- background after 60s (stale-while-revalidate).
910
+ background after 60s (stale-while-revalidate). Check the \`X-What-Cache\`
911
+ response header: MISS on first hit, HIT after.
877
912
  - \`page = { mode: 'server' }\` → always fresh, never cached.
878
913
  - \`revalidatePath('/')\` / \`revalidateTag('posts')\` → on-demand purge.
879
914
  - \`POST /__what_revalidate\` with \`WHAT_REVALIDATE_SECRET\` → CMS-triggered purge.
@@ -955,13 +990,18 @@ export const createPostAction = action(
955
990
  );
956
991
  `);
957
992
 
958
- // Home page — static + ISR, lists posts via a loader.
993
+ // Home page — static + ISR, lists posts via a loader, plus a hydrated
994
+ // counter that proves the client entry attaches interactivity.
959
995
  writeFileSync(resolve(root, 'src', 'pages', 'home.js'), `// Home page.
960
996
  // export const page -> route config (JSON-safe: mode, revalidate, tags)
961
997
  // export const loader -> runs on the server before render; result -> loaderData
962
998
  // export default -> the page component (runs once, no re-renders)
999
+ //
1000
+ // This module is isomorphic: the server renders it to HTML, then
1001
+ // src/entry-client.js hydrates the same component in the browser (reusing the
1002
+ // server DOM and attaching the onclick handler + reactive text below).
963
1003
 
964
- import { h, Head, useLoaderData } from 'what-framework';
1004
+ import { h, Head, signal, useLoaderData } from 'what-framework';
965
1005
  import { listPosts } from '../db.js';
966
1006
 
967
1007
  export const page = { mode: 'static', revalidate: 60, tags: ['posts'] };
@@ -970,13 +1010,19 @@ export const loader = () => ({ posts: listPosts() });
970
1010
 
971
1011
  export default function Home() {
972
1012
  const { posts } = useLoaderData();
1013
+ const likes = signal(0, 'likes');
973
1014
  return h('main', { class: 'container' },
974
1015
  h(Head, {
975
1016
  title: '${packageName}',
976
1017
  meta: [{ name: 'description', content: 'A full-stack app built with What Framework' }],
977
1018
  }),
978
1019
  h('h1', {}, '${packageName}'),
979
- h('p', {}, 'Server-rendered, ISR-cached, hydrated islands.'),
1020
+ h('p', {}, 'Server-rendered, ISR-cached, hydrated on the client.'),
1021
+ h('section', { class: 'like-demo' },
1022
+ h('button', { onclick: () => likes(n => n + 1) }, 'Like'),
1023
+ h('output', {}, () => String(likes())),
1024
+ h('span', { class: 'hint' }, 'hydrated — this button works in the browser')
1025
+ ),
980
1026
  h('ul', {}, posts.map((p) =>
981
1027
  h('li', { key: p.slug }, h('a', { href: \`/blog/\${p.slug}\` }, p.title))
982
1028
  )),
@@ -1018,8 +1064,9 @@ export default function Post() {
1018
1064
  `);
1019
1065
 
1020
1066
  // /new — mode:'server' (never cached), posts to the createPost action.
1021
- writeFileSync(resolve(root, 'src', 'pages', 'new.js'), `// New-post form. mode:'server' so it's always fresh. The form posts to the
1022
- // createPost server action; progressively enhanced (works as a plain POST).
1067
+ writeFileSync(resolve(root, 'src', 'pages', 'new.js'), `// New-post form. mode:'server' so it's always fresh. src/entry-client.js
1068
+ // enhances the form: it submits to the createPost server action as JSON with
1069
+ // the X-What-Action header (the action protocol), then navigates to the post.
1023
1070
 
1024
1071
  import { h, Head } from 'what-framework';
1025
1072
 
@@ -1058,22 +1105,184 @@ export const routes = [
1058
1105
  ];
1059
1106
  `);
1060
1107
 
1061
- // Full-stack server: Node adapter + origin-first ISR + revalidate webhook +
1062
- // poll scheduler. Works on any host no CDN required.
1063
- writeFileSync(resolve(root, 'server.js'), `// Full-stack server. \`node server.js\` → http://localhost:3000.
1064
- // Wires the Node adapter + origin-first ISR engine + on-demand revalidation
1065
- // webhook + poll scheduler. No CDN required.
1108
+ // Client hydration entry loaded by the SSR document (see server.js). Reuses
1109
+ // the server-rendered DOM and attaches interactivity. No bundler involved:
1110
+ // the browser resolves 'what-framework' through the import map server.js
1111
+ // injects, and ./pages/* are served as plain ES modules.
1112
+ writeFileSync(resolve(root, 'src', 'entry-client.js'), `// Client hydration entry.
1113
+ // The SSR document loads this as a native ES module. It matches the current
1114
+ // URL to a page component, re-runs it with the server's loader data (pages
1115
+ // call useLoaderData(), which reads the #__what_data payload the server
1116
+ // embedded), and hydrate() walks the existing server-rendered DOM — attaching
1117
+ // event handlers and reactive bindings instead of recreating elements.
1118
+ //
1119
+ // NOTE: import page modules directly, never ./routes.js — the route table also
1120
+ // imports the server actions, which pull in Node-only modules.
1121
+
1122
+ import { h, hydrate } from 'what-framework';
1123
+ import Home from './pages/home.js';
1124
+ import Post from './pages/post.js';
1125
+ import NewPost from './pages/new.js';
1126
+
1127
+ function matchPage(pathname) {
1128
+ if (pathname === '/') return h(Home, {});
1129
+ const post = pathname.match(/^\\/blog\\/([^/]+)\\/?$/);
1130
+ if (post) return h(Post, { slug: decodeURIComponent(post[1]) });
1131
+ if (pathname === '/new') return h(NewPost, {});
1132
+ return null;
1133
+ }
1134
+
1135
+ const vnode = matchPage(location.pathname);
1136
+ if (vnode) hydrate(vnode, document.body);
1137
+
1138
+ // Progressive enhancement for server-action forms: submit as JSON to
1139
+ // /__what_action with the X-What-Action header (the served-action protocol),
1140
+ // then navigate to the result. The CSRF token comes from the double-submit
1141
+ // cookie the server sets on every HTML response (or the meta tag on
1142
+ // uncached pages) — same lookup the framework's own action() client uses.
1143
+ function csrfToken() {
1144
+ const meta = document.querySelector('meta[name="what-csrf-token"]');
1145
+ if (meta) return meta.getAttribute('content');
1146
+ const match = document.cookie.match(/(?:^|;\\s*)what-csrf=([^;]+)/);
1147
+ return match ? decodeURIComponent(match[1]) : null;
1148
+ }
1149
+
1150
+ for (const form of document.querySelectorAll('form[data-action]')) {
1151
+ form.addEventListener('submit', async (event) => {
1152
+ event.preventDefault();
1153
+ const args = Object.fromEntries(new FormData(form));
1154
+ const headers = { 'content-type': 'application/json', 'x-what-action': form.dataset.action };
1155
+ const token = csrfToken();
1156
+ if (token) headers['x-csrf-token'] = token;
1157
+ const res = await fetch('/__what_action', {
1158
+ method: 'POST',
1159
+ headers,
1160
+ credentials: 'same-origin',
1161
+ body: JSON.stringify({ args: [args] }),
1162
+ });
1163
+ const result = await res.json().catch(() => ({}));
1164
+ if (res.ok) {
1165
+ location.href = result.slug ? \`/blog/\${result.slug}\` : '/';
1166
+ } else {
1167
+ console.error('[what] action failed:', result.message || res.status);
1168
+ }
1169
+ });
1170
+ }
1171
+ `);
1172
+
1173
+ // Stylesheet for the SSR pages (linked from the document head in server.js).
1174
+ writeFileSync(resolve(root, 'src', 'styles.css'), `:root {
1175
+ color-scheme: light;
1176
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
1177
+ }
1178
+
1179
+ * {
1180
+ box-sizing: border-box;
1181
+ }
1182
+
1183
+ body {
1184
+ margin: 0;
1185
+ min-height: 100vh;
1186
+ background: #f4f6fb;
1187
+ color: #0f172a;
1188
+ }
1189
+
1190
+ .container {
1191
+ max-width: 640px;
1192
+ margin: 0 auto;
1193
+ padding: 2.5rem 1.25rem;
1194
+ }
1195
+
1196
+ a {
1197
+ color: #2563eb;
1198
+ }
1199
+
1200
+ button {
1201
+ border: 1px solid #9aa7bb;
1202
+ background: #ffffff;
1203
+ color: #0f172a;
1204
+ padding: 0.4rem 0.9rem;
1205
+ border-radius: 10px;
1206
+ cursor: pointer;
1207
+ }
1208
+
1209
+ button:hover {
1210
+ border-color: #2563eb;
1211
+ }
1212
+
1213
+ .like-demo {
1214
+ display: inline-flex;
1215
+ align-items: center;
1216
+ gap: 0.75rem;
1217
+ margin: 0.5rem 0 1rem;
1218
+ }
1219
+
1220
+ .like-demo output {
1221
+ min-width: 2ch;
1222
+ text-align: center;
1223
+ font-weight: 700;
1224
+ }
1225
+
1226
+ .like-demo .hint {
1227
+ color: #64748b;
1228
+ font-size: 0.875rem;
1229
+ }
1230
+
1231
+ form {
1232
+ display: grid;
1233
+ gap: 0.75rem;
1234
+ max-width: 420px;
1235
+ }
1066
1236
 
1067
- import { createServer, createRequestHandler } from 'what-framework/server';
1237
+ input,
1238
+ textarea {
1239
+ padding: 0.5rem 0.75rem;
1240
+ border: 1px solid #9aa7bb;
1241
+ border-radius: 10px;
1242
+ font: inherit;
1243
+ }
1244
+ `);
1245
+
1246
+ // Full-stack server: Node adapter + origin-first ISR + revalidate webhook +
1247
+ // poll scheduler + static serving for the buildless client (native ESM).
1248
+ writeFileSync(resolve(root, 'server.js'), `// Full-stack server. \`npm run dev\` (auto-restart) or \`node server.js\`
1249
+ // → http://localhost:3000. Wires the Node adapter + origin-first ISR engine +
1250
+ // on-demand revalidation webhook + poll scheduler. No CDN — and no bundler —
1251
+ // required: the client entry and the framework are served as native ES modules
1252
+ // (see the import map below).
1253
+
1254
+ import http from 'node:http';
1255
+ import { randomBytes } from 'node:crypto';
1256
+ import { existsSync, statSync, createReadStream } from 'node:fs';
1257
+ import { extname, join, resolve } from 'node:path';
1258
+ import { fileURLToPath } from 'node:url';
1259
+ import { createRequestHandler, toNodeListener } from 'what-framework/server';
1068
1260
  import {
1069
1261
  createCacheEngine,
1070
1262
  createMemoryStore,
1071
1263
  createRevalidateWebhook,
1072
1264
  createScheduler,
1073
- } from 'what-cache';
1265
+ } from 'what-isr';
1074
1266
  import { routes } from './src/routes.js';
1075
1267
 
1076
- const REVALIDATE_SECRET = process.env.WHAT_REVALIDATE_SECRET || 'dev-secret';
1268
+ const ROOT = fileURLToPath(new URL('.', import.meta.url));
1269
+ const isProd = process.env.NODE_ENV === 'production';
1270
+
1271
+ // Secret for the on-demand revalidation webhook (POST /__what_revalidate).
1272
+ // Required in production; auto-generated per run in dev so there is never a
1273
+ // shared default secret.
1274
+ let REVALIDATE_SECRET = process.env.WHAT_REVALIDATE_SECRET;
1275
+ if (!REVALIDATE_SECRET) {
1276
+ if (isProd) {
1277
+ console.error(
1278
+ '[${packageName}] WHAT_REVALIDATE_SECRET must be set in production.\\n' +
1279
+ \" Generate one: node -e \\\"console.log(require('node:crypto').randomBytes(24).toString('hex'))\\\"\"
1280
+ );
1281
+ process.exit(1);
1282
+ }
1283
+ REVALIDATE_SECRET = randomBytes(24).toString('hex');
1284
+ console.log(\`[dev] WHAT_REVALIDATE_SECRET not set — generated for this run: \${REVALIDATE_SECRET}\`);
1285
+ }
1077
1286
 
1078
1287
  // Origin ISR cache. Swap createMemoryStore() for createFilesystemStore({dir})
1079
1288
  // to survive restarts, or createRedisStore({client}) for multi-instance.
@@ -1083,24 +1292,84 @@ const cache = createCacheEngine({ store: createMemoryStore() });
1083
1292
  const scheduler = createScheduler(cache);
1084
1293
  scheduler.register({ path: '/', query: {}, config: routes[0].page }, { intervalMs: 5 * 60 * 1000 });
1085
1294
 
1295
+ // The browser loads /src/entry-client.js as a native ES module; this import
1296
+ // map resolves its bare imports. Sources are served from node_modules below.
1297
+ const importMap = {
1298
+ imports: {
1299
+ 'what-framework': '/node_modules/what-framework/src/index.js',
1300
+ 'what-core': '/node_modules/what-core/src/index.js',
1301
+ },
1302
+ };
1303
+
1304
+ const documentOptions = {
1305
+ clientEntry: '/src/entry-client.js',
1306
+ head:
1307
+ \`<script type="importmap">\${JSON.stringify(importMap)}</script>\` +
1308
+ '<link rel="stylesheet" href="/src/styles.css">' +
1309
+ '<link rel="icon" type="image/svg+xml" href="/favicon.svg">',
1310
+ };
1311
+
1086
1312
  export function createHandler() {
1087
1313
  return createRequestHandler({
1088
1314
  routes,
1089
1315
  cache,
1090
1316
  revalidateWebhook: createRevalidateWebhook(cache, { secret: REVALIDATE_SECRET }),
1091
- document: { clientEntry: '/src/entry-client.js' },
1317
+ document: documentOptions,
1092
1318
  });
1093
1319
  }
1094
1320
 
1095
- // Started directly (node server.js), not when imported by tests.
1321
+ // --- Static files (client entry, page modules, framework sources, public/) ---
1322
+
1323
+ const MIME = {
1324
+ '.js': 'text/javascript; charset=utf-8',
1325
+ '.mjs': 'text/javascript; charset=utf-8',
1326
+ '.css': 'text/css; charset=utf-8',
1327
+ '.json': 'application/json; charset=utf-8',
1328
+ '.svg': 'image/svg+xml',
1329
+ '.png': 'image/png',
1330
+ '.jpg': 'image/jpeg',
1331
+ '.ico': 'image/x-icon',
1332
+ '.txt': 'text/plain; charset=utf-8',
1333
+ '.woff2': 'font/woff2',
1334
+ };
1335
+
1336
+ // Only these prefixes are served from the project root; anything else falls
1337
+ // back to public/ (favicon etc.) and then to the app handler.
1338
+ const SERVED_PREFIXES = ['/src/', '/node_modules/what-framework/', '/node_modules/what-core/'];
1339
+
1340
+ function resolveStaticFile(pathname) {
1341
+ if (pathname.includes('..') || pathname.includes('\\0')) return null;
1342
+ const file = SERVED_PREFIXES.some((p) => pathname.startsWith(p))
1343
+ ? resolve(join(ROOT, pathname))
1344
+ : resolve(join(ROOT, 'public', pathname));
1345
+ if (!file.startsWith(resolve(ROOT))) return null;
1346
+ if (!existsSync(file) || !statSync(file).isFile()) return null;
1347
+ return file;
1348
+ }
1349
+
1350
+ // --- Start (node server.js / npm run dev), not when imported by tests ---
1351
+
1096
1352
  if (import.meta.url === \`file://\${process.argv[1]}\`) {
1097
- const server = createServer({
1098
- routes,
1099
- cache,
1100
- scheduler,
1101
- revalidateWebhook: createRevalidateWebhook(cache, { secret: REVALIDATE_SECRET }),
1102
- document: { clientEntry: '/src/entry-client.js' },
1353
+ const app = toNodeListener(createHandler());
1354
+
1355
+ const server = http.createServer((req, res) => {
1356
+ if (req.method === 'GET' || req.method === 'HEAD') {
1357
+ const pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
1358
+ const file = resolveStaticFile(pathname);
1359
+ if (file) {
1360
+ res.writeHead(200, { 'content-type': MIME[extname(file)] || 'application/octet-stream' });
1361
+ if (req.method === 'HEAD') return res.end();
1362
+ return createReadStream(file).pipe(res);
1363
+ }
1364
+ }
1365
+ app(req, res);
1103
1366
  });
1367
+
1368
+ scheduler.start();
1369
+ const stop = () => { try { scheduler.stop(); } catch {} server.close(); };
1370
+ process.once('SIGTERM', stop);
1371
+ process.once('SIGINT', stop);
1372
+
1104
1373
  const port = Number(process.env.PORT) || 3000;
1105
1374
  server.listen(port, () => console.log(\`${packageName} → http://localhost:\${port}\`));
1106
1375
  }
@@ -1357,8 +1626,10 @@ count(c => c + 1) // update
1357
1626
  // package.json
1358
1627
  writeFileSync(resolve(root, 'package.json'), generatePackageJson(packageName, options));
1359
1628
 
1360
- // index.html
1361
- writeFileSync(resolve(root, 'index.html'), generateIndexHtml(packageName));
1629
+ // index.html (SPA only — the full-stack server renders its own document)
1630
+ if (options.template !== 'fullstack') {
1631
+ writeFileSync(resolve(root, 'index.html'), generateIndexHtml(packageName));
1632
+ }
1362
1633
 
1363
1634
  // favicon
1364
1635
  writeFileSync(resolve(root, 'public', 'favicon.svg'), `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
@@ -1373,8 +1644,35 @@ count(c => c + 1) // update
1373
1644
  </svg>
1374
1645
  `);
1375
1646
 
1376
- // vite.config.js
1377
- writeFileSync(resolve(root, 'vite.config.js'), generateViteConfig(options));
1647
+ // vite.config.js (SPA only — the full-stack template is buildless)
1648
+ if (options.template !== 'fullstack') {
1649
+ writeFileSync(resolve(root, 'vite.config.js'), generateViteConfig(options));
1650
+ }
1651
+
1652
+ // eslint.config.js — eslint-plugin-what flat config.
1653
+ // SPA: the What compiler handles event casing + reactive wrapping, so use
1654
+ // the `compiler` preset (the compiler-covered rules are off).
1655
+ // Fullstack: buildless h() authoring is the template's design, so use
1656
+ // `recommended` with the JSX-only h() rule disabled.
1657
+ writeFileSync(resolve(root, 'eslint.config.js'), options.template === 'fullstack'
1658
+ ? `import what from 'eslint-plugin-what';
1659
+
1660
+ export default [
1661
+ { ignores: ['node_modules'] },
1662
+ what.configs.recommended,
1663
+ // This template authors pages with h() on purpose (buildless, no compiler).
1664
+ { rules: { 'what/no-h-in-user-code': 'off' } },
1665
+ ];
1666
+ `
1667
+ : `import what from 'eslint-plugin-what';
1668
+
1669
+ export default [
1670
+ { ignores: ['dist', 'node_modules'] },
1671
+ // 'compiler' preset: this project uses the What compiler (via Vite), which
1672
+ // handles event casing and reactive JSX wrapping automatically.
1673
+ what.configs.compiler,
1674
+ ];
1675
+ `);
1378
1676
 
1379
1677
  // tsconfig.json
1380
1678
  writeFileSync(resolve(root, 'tsconfig.json'), JSON.stringify({
@@ -1408,19 +1706,20 @@ count(c => c + 1) // update
1408
1706
  recommendations: [],
1409
1707
  }, null, 2) + '\n');
1410
1708
 
1411
- // src/main.jsx
1412
- writeFileSync(resolve(root, 'src', 'main.jsx'), generateMainJsx(options));
1413
-
1414
- // Full-stack scaffold: file-routed SSR pages + server + ISR config.
1709
+ // Full-stack scaffold: file-routed SSR pages + server + client entry + ISR
1710
+ // config (writes its own src/styles.css). SPA scaffold: Vite entry + styles.
1415
1711
  if (options.template === 'fullstack') {
1416
1712
  generateFullstackFiles(root, packageName);
1417
- }
1418
-
1419
- // src/styles.css
1420
- if (reactCompat && cssApproach === 'none') {
1421
- writeFileSync(resolve(root, 'src', 'styles.css'), generateStylesWithReactCompat());
1422
1713
  } else {
1423
- writeFileSync(resolve(root, 'src', 'styles.css'), generateStyles(options));
1714
+ // src/main.jsx
1715
+ writeFileSync(resolve(root, 'src', 'main.jsx'), generateMainJsx(options));
1716
+
1717
+ // src/styles.css
1718
+ if (reactCompat && cssApproach === 'none') {
1719
+ writeFileSync(resolve(root, 'src', 'styles.css'), generateStylesWithReactCompat());
1720
+ } else {
1721
+ writeFileSync(resolve(root, 'src', 'styles.css'), generateStyles(options));
1722
+ }
1424
1723
  }
1425
1724
 
1426
1725
  // README.md
@@ -1440,7 +1739,11 @@ count(c => c + 1) // update
1440
1739
  console.log('\nNext steps:');
1441
1740
  console.log(` cd ${root}`);
1442
1741
  console.log(' npm install');
1443
- console.log(' npm run dev\n');
1742
+ if (options.template === 'fullstack') {
1743
+ console.log(' npm run dev # SSR + ISR server → http://localhost:3000\n');
1744
+ } else {
1745
+ console.log(' npm run dev\n');
1746
+ }
1444
1747
  }
1445
1748
 
1446
1749
  main().catch(err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-what",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Scaffold a new What Framework project",
5
5
  "type": "module",
6
6
  "bin": {