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.
- package/README.md +52 -3
- package/index.js +363 -60
- 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. **
|
|
32
|
-
3. **
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
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: '
|
|
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-
|
|
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 =
|
|
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 +
|
|
852
|
-
poll regeneration) — works on any host, no CDN
|
|
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
|
|
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
|
-
|
|
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
|
-
- \`
|
|
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
|
|
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.
|
|
1022
|
-
// createPost server action
|
|
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
|
-
//
|
|
1062
|
-
//
|
|
1063
|
-
|
|
1064
|
-
//
|
|
1065
|
-
|
|
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
|
-
|
|
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-
|
|
1265
|
+
} from 'what-isr';
|
|
1074
1266
|
import { routes } from './src/routes.js';
|
|
1075
1267
|
|
|
1076
|
-
const
|
|
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:
|
|
1317
|
+
document: documentOptions,
|
|
1092
1318
|
});
|
|
1093
1319
|
}
|
|
1094
1320
|
|
|
1095
|
-
//
|
|
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
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1412
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 => {
|