create-what 0.8.4 → 0.10.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 +313 -36
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -18,6 +18,8 @@ const positional = args.filter(a => !a.startsWith('-'));
|
|
|
18
18
|
const flags = new Set(args.filter(a => a.startsWith('-')));
|
|
19
19
|
const skipPrompts = flags.has('--yes') || flags.has('-y');
|
|
20
20
|
const showHelp = flags.has('--help') || flags.has('-h');
|
|
21
|
+
let templateFlag = flags.has('--fullstack') ? 'fullstack' : null;
|
|
22
|
+
for (const f of flags) { const m = f.match(/^--template=(.+)$/); if (m) templateFlag = m[1]; }
|
|
21
23
|
const packageVersion = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version;
|
|
22
24
|
const whatVersionRange = `^${packageVersion}`;
|
|
23
25
|
|
|
@@ -29,8 +31,11 @@ if (showHelp) {
|
|
|
29
31
|
create-what [project-name] [options]
|
|
30
32
|
|
|
31
33
|
Options:
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
+
--fullstack Scaffold a full-stack SSR app (file routes, loaders,
|
|
35
|
+
actions, origin-first ISR) instead of an SPA
|
|
36
|
+
--template=<name> 'spa' (default) or 'fullstack'
|
|
37
|
+
-y, --yes Skip prompts and use defaults
|
|
38
|
+
-h, --help Show this help message
|
|
34
39
|
`);
|
|
35
40
|
process.exit(0);
|
|
36
41
|
}
|
|
@@ -110,7 +115,7 @@ async function gatherOptions() {
|
|
|
110
115
|
|
|
111
116
|
if (skipPrompts) {
|
|
112
117
|
projectName = projectName || 'my-what-app';
|
|
113
|
-
return { projectName, reactCompat, cssApproach };
|
|
118
|
+
return { projectName, reactCompat, cssApproach, template: templateFlag || 'spa' };
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
const prompter = createPrompter();
|
|
@@ -121,6 +126,11 @@ async function gatherOptions() {
|
|
|
121
126
|
projectName = (await prompter.ask(' Project name: ')).trim() || 'my-what-app';
|
|
122
127
|
}
|
|
123
128
|
|
|
129
|
+
const template = templateFlag || await prompter.select('Template:', [
|
|
130
|
+
{ label: 'SPA (client-side single-page app)', value: 'spa' },
|
|
131
|
+
{ label: 'Full-stack (SSR + file routes + loaders + actions + ISR)', value: 'fullstack' },
|
|
132
|
+
]);
|
|
133
|
+
|
|
124
134
|
reactCompat = await prompter.confirm('Add React library support? (what-react)');
|
|
125
135
|
|
|
126
136
|
cssApproach = await prompter.select('CSS approach:', [
|
|
@@ -131,14 +141,14 @@ async function gatherOptions() {
|
|
|
131
141
|
|
|
132
142
|
prompter.close();
|
|
133
143
|
|
|
134
|
-
return { projectName, reactCompat, cssApproach };
|
|
144
|
+
return { projectName, reactCompat, cssApproach, template };
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
// ---------------------------------------------------------------------------
|
|
138
148
|
// File generators
|
|
139
149
|
// ---------------------------------------------------------------------------
|
|
140
150
|
|
|
141
|
-
function generatePackageJson(packageName, { reactCompat, cssApproach }) {
|
|
151
|
+
function generatePackageJson(packageName, { reactCompat, cssApproach, template }) {
|
|
142
152
|
const deps = {
|
|
143
153
|
'what-framework': whatVersionRange,
|
|
144
154
|
};
|
|
@@ -149,6 +159,14 @@ function generatePackageJson(packageName, { reactCompat, cssApproach }) {
|
|
|
149
159
|
'@babel/core': '^7.23.0',
|
|
150
160
|
};
|
|
151
161
|
|
|
162
|
+
// Full-stack apps add the origin-first ISR engine + a Node server entry.
|
|
163
|
+
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' };
|
|
166
|
+
if (template === 'fullstack') {
|
|
167
|
+
deps['what-cache'] = whatVersionRange;
|
|
168
|
+
}
|
|
169
|
+
|
|
152
170
|
if (reactCompat) {
|
|
153
171
|
deps['what-react'] = whatVersionRange;
|
|
154
172
|
deps['what-core'] = whatVersionRange;
|
|
@@ -171,11 +189,7 @@ function generatePackageJson(packageName, { reactCompat, cssApproach }) {
|
|
|
171
189
|
private: true,
|
|
172
190
|
version: '0.1.0',
|
|
173
191
|
type: 'module',
|
|
174
|
-
scripts
|
|
175
|
-
dev: 'vite',
|
|
176
|
-
build: 'vite build',
|
|
177
|
-
preview: 'vite preview',
|
|
178
|
-
},
|
|
192
|
+
scripts,
|
|
179
193
|
dependencies: deps,
|
|
180
194
|
devDependencies: devDeps,
|
|
181
195
|
}, null, 2) + '\n';
|
|
@@ -329,20 +343,20 @@ function generateMainJsx({ reactCompat, cssApproach }) {
|
|
|
329
343
|
}
|
|
330
344
|
|
|
331
345
|
function generateMainDefault() {
|
|
332
|
-
return `import { mount,
|
|
346
|
+
return `import { mount, signal } from 'what-framework';
|
|
333
347
|
|
|
334
348
|
function App() {
|
|
335
|
-
const count =
|
|
349
|
+
const count = signal(0);
|
|
336
350
|
|
|
337
351
|
return (
|
|
338
352
|
<main className="app-shell">
|
|
339
353
|
<h1>What Framework</h1>
|
|
340
|
-
<p>Compiler-first JSX,
|
|
354
|
+
<p>Compiler-first JSX, fine-grained signals.</p>
|
|
341
355
|
|
|
342
356
|
<section className="counter">
|
|
343
|
-
<button onClick={() => count
|
|
357
|
+
<button onClick={() => count(c => c - 1)}>-</button>
|
|
344
358
|
<output>{count()}</output>
|
|
345
|
-
<button onClick={() => count
|
|
359
|
+
<button onClick={() => count(c => c + 1)}>+</button>
|
|
346
360
|
</section>
|
|
347
361
|
</main>
|
|
348
362
|
);
|
|
@@ -353,10 +367,10 @@ mount(<App />, '#app');
|
|
|
353
367
|
}
|
|
354
368
|
|
|
355
369
|
function generateMainWithTailwind() {
|
|
356
|
-
return `import { mount,
|
|
370
|
+
return `import { mount, signal } from 'what-framework';
|
|
357
371
|
|
|
358
372
|
function App() {
|
|
359
|
-
const count =
|
|
373
|
+
const count = signal(0);
|
|
360
374
|
|
|
361
375
|
return (
|
|
362
376
|
<main className="min-h-screen flex items-center justify-center bg-slate-50">
|
|
@@ -367,14 +381,14 @@ function App() {
|
|
|
367
381
|
<section className="mt-6 inline-flex items-center gap-3">
|
|
368
382
|
<button
|
|
369
383
|
className="w-9 h-9 rounded-lg border border-slate-300 bg-white text-slate-900 hover:border-blue-600 cursor-pointer"
|
|
370
|
-
onClick={() => count
|
|
384
|
+
onClick={() => count(c => c - 1)}
|
|
371
385
|
>
|
|
372
386
|
-
|
|
373
387
|
</button>
|
|
374
388
|
<output className="min-w-[2ch] text-center font-bold">{count()}</output>
|
|
375
389
|
<button
|
|
376
390
|
className="w-9 h-9 rounded-lg border border-slate-300 bg-white text-slate-900 hover:border-blue-600 cursor-pointer"
|
|
377
|
-
onClick={() => count
|
|
391
|
+
onClick={() => count(c => c + 1)}
|
|
378
392
|
>
|
|
379
393
|
+
|
|
380
394
|
</button>
|
|
@@ -389,7 +403,7 @@ mount(<App />, '#app');
|
|
|
389
403
|
}
|
|
390
404
|
|
|
391
405
|
function generateMainWithStyleX() {
|
|
392
|
-
return `import { mount,
|
|
406
|
+
return `import { mount, signal } from 'what-framework';
|
|
393
407
|
import * as stylex from '@stylexjs/stylex';
|
|
394
408
|
|
|
395
409
|
const styles = stylex.create({
|
|
@@ -443,7 +457,7 @@ const styles = stylex.create({
|
|
|
443
457
|
});
|
|
444
458
|
|
|
445
459
|
function App() {
|
|
446
|
-
const count =
|
|
460
|
+
const count = signal(0);
|
|
447
461
|
|
|
448
462
|
return (
|
|
449
463
|
<main {...stylex.props(styles.page)}>
|
|
@@ -452,9 +466,9 @@ function App() {
|
|
|
452
466
|
<p {...stylex.props(styles.subtitle)}>Compiler-first JSX + StyleX.</p>
|
|
453
467
|
|
|
454
468
|
<section {...stylex.props(styles.counter)}>
|
|
455
|
-
<button {...stylex.props(styles.button)} onClick={() => count
|
|
469
|
+
<button {...stylex.props(styles.button)} onClick={() => count(c => c - 1)}>-</button>
|
|
456
470
|
<output {...stylex.props(styles.output)}>{count()}</output>
|
|
457
|
-
<button {...stylex.props(styles.button)} onClick={() => count
|
|
471
|
+
<button {...stylex.props(styles.button)} onClick={() => count(c => c + 1)}>+</button>
|
|
458
472
|
</section>
|
|
459
473
|
</div>
|
|
460
474
|
</main>
|
|
@@ -469,7 +483,7 @@ function generateMainWithReactCompat({ cssApproach }) {
|
|
|
469
483
|
// React compat demo: uses zustand (a real React library) to show it works
|
|
470
484
|
// with What Framework under the hood.
|
|
471
485
|
if (cssApproach === 'tailwind') {
|
|
472
|
-
return `import { mount,
|
|
486
|
+
return `import { mount, signal } from 'what-framework';
|
|
473
487
|
import { create } from 'zustand';
|
|
474
488
|
|
|
475
489
|
// A real React state library — works with What Framework via what-react!
|
|
@@ -508,7 +522,7 @@ function BearCounter() {
|
|
|
508
522
|
}
|
|
509
523
|
|
|
510
524
|
function App() {
|
|
511
|
-
const count =
|
|
525
|
+
const count = signal(0);
|
|
512
526
|
|
|
513
527
|
return (
|
|
514
528
|
<main className="min-h-screen flex items-center justify-center bg-slate-50">
|
|
@@ -519,14 +533,14 @@ function App() {
|
|
|
519
533
|
<section className="mt-6 inline-flex items-center gap-3">
|
|
520
534
|
<button
|
|
521
535
|
className="w-9 h-9 rounded-lg border border-slate-300 bg-white text-slate-900 hover:border-blue-600 cursor-pointer"
|
|
522
|
-
onClick={() => count
|
|
536
|
+
onClick={() => count(c => c - 1)}
|
|
523
537
|
>
|
|
524
538
|
-
|
|
525
539
|
</button>
|
|
526
540
|
<output className="min-w-[2ch] text-center font-bold">{count()}</output>
|
|
527
541
|
<button
|
|
528
542
|
className="w-9 h-9 rounded-lg border border-slate-300 bg-white text-slate-900 hover:border-blue-600 cursor-pointer"
|
|
529
|
-
onClick={() => count
|
|
543
|
+
onClick={() => count(c => c + 1)}
|
|
530
544
|
>
|
|
531
545
|
+
|
|
532
546
|
</button>
|
|
@@ -543,7 +557,7 @@ mount(<App />, '#app');
|
|
|
543
557
|
}
|
|
544
558
|
|
|
545
559
|
if (cssApproach === 'stylex') {
|
|
546
|
-
return `import { mount,
|
|
560
|
+
return `import { mount, signal } from 'what-framework';
|
|
547
561
|
import { create } from 'zustand';
|
|
548
562
|
import * as stylex from '@stylexjs/stylex';
|
|
549
563
|
|
|
@@ -629,7 +643,7 @@ function BearCounter() {
|
|
|
629
643
|
}
|
|
630
644
|
|
|
631
645
|
function App() {
|
|
632
|
-
const count =
|
|
646
|
+
const count = signal(0);
|
|
633
647
|
|
|
634
648
|
return (
|
|
635
649
|
<main {...stylex.props(styles.page)}>
|
|
@@ -638,9 +652,9 @@ function App() {
|
|
|
638
652
|
<p {...stylex.props(styles.subtitle)}>React compat + StyleX.</p>
|
|
639
653
|
|
|
640
654
|
<section {...stylex.props(styles.counter)}>
|
|
641
|
-
<button {...stylex.props(styles.button)} onClick={() => count
|
|
655
|
+
<button {...stylex.props(styles.button)} onClick={() => count(c => c - 1)}>-</button>
|
|
642
656
|
<output {...stylex.props(styles.output)}>{count()}</output>
|
|
643
|
-
<button {...stylex.props(styles.button)} onClick={() => count
|
|
657
|
+
<button {...stylex.props(styles.button)} onClick={() => count(c => c + 1)}>+</button>
|
|
644
658
|
</section>
|
|
645
659
|
|
|
646
660
|
<BearCounter />
|
|
@@ -654,7 +668,7 @@ mount(<App />, '#app');
|
|
|
654
668
|
}
|
|
655
669
|
|
|
656
670
|
// React compat with vanilla CSS
|
|
657
|
-
return `import { mount,
|
|
671
|
+
return `import { mount, signal } from 'what-framework';
|
|
658
672
|
import { create } from 'zustand';
|
|
659
673
|
|
|
660
674
|
// A real React state library — works with What Framework via what-react!
|
|
@@ -683,7 +697,7 @@ function BearCounter() {
|
|
|
683
697
|
}
|
|
684
698
|
|
|
685
699
|
function App() {
|
|
686
|
-
const count =
|
|
700
|
+
const count = signal(0);
|
|
687
701
|
|
|
688
702
|
return (
|
|
689
703
|
<main className="app-shell">
|
|
@@ -691,9 +705,9 @@ function App() {
|
|
|
691
705
|
<p>React compat enabled — use React libraries with signals.</p>
|
|
692
706
|
|
|
693
707
|
<section className="counter">
|
|
694
|
-
<button onClick={() => count
|
|
708
|
+
<button onClick={() => count(c => c - 1)}>-</button>
|
|
695
709
|
<output>{count()}</output>
|
|
696
|
-
<button onClick={() => count
|
|
710
|
+
<button onClick={() => count(c => c + 1)}>+</button>
|
|
697
711
|
</section>
|
|
698
712
|
|
|
699
713
|
<BearCounter />
|
|
@@ -807,7 +821,7 @@ output {
|
|
|
807
821
|
`;
|
|
808
822
|
}
|
|
809
823
|
|
|
810
|
-
function generateReadme(packageName, { reactCompat, cssApproach }) {
|
|
824
|
+
function generateReadme(packageName, { reactCompat, cssApproach, template }) {
|
|
811
825
|
let notes = `- Canonical package name is \`what-framework\`.
|
|
812
826
|
- Uses the What compiler for JSX transforms and automatic reactivity.
|
|
813
827
|
- Vite is preconfigured; use \`npm run dev/build/preview\`.
|
|
@@ -830,6 +844,46 @@ function generateReadme(packageName, { reactCompat, cssApproach }) {
|
|
|
830
844
|
- StyleX is configured via \`vite-plugin-stylex\`. Define styles with \`stylex.create()\` and apply with \`{...stylex.props()}\`.`;
|
|
831
845
|
}
|
|
832
846
|
|
|
847
|
+
if (template === 'fullstack') {
|
|
848
|
+
return `# ${packageName}
|
|
849
|
+
|
|
850
|
+
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.
|
|
853
|
+
|
|
854
|
+
## Run
|
|
855
|
+
|
|
856
|
+
\`\`\`bash
|
|
857
|
+
npm install
|
|
858
|
+
npm start # full-stack SSR server → http://localhost:3000
|
|
859
|
+
\`\`\`
|
|
860
|
+
|
|
861
|
+
Or develop the client in isolation with \`npm run dev\` (Vite, http://localhost:5173).
|
|
862
|
+
|
|
863
|
+
## Structure
|
|
864
|
+
|
|
865
|
+
- \`src/pages/*\` — pages. Each may export \`page\` (route config), \`loader\` (server
|
|
866
|
+
data), \`getStaticPaths\` (pre-render), and a default component.
|
|
867
|
+
- \`src/actions/*\` — server actions (mutations), served at \`/__what_action\`. Call
|
|
868
|
+
\`revalidatePath\`/\`revalidateTag\` after a mutation to purge the ISR cache.
|
|
869
|
+
- \`src/routes.js\` — the route table (loaders/getStaticPaths/page bound per route).
|
|
870
|
+
- \`server.js\` — Node adapter + ISR engine + revalidate webhook + poll scheduler.
|
|
871
|
+
- \`what.config.js\` — deploy adapter + ISR defaults.
|
|
872
|
+
|
|
873
|
+
### ISR cheat-sheet
|
|
874
|
+
|
|
875
|
+
- \`page = { mode: 'static', revalidate: 60 }\` → cached, regenerated in the
|
|
876
|
+
background after 60s (stale-while-revalidate).
|
|
877
|
+
- \`page = { mode: 'server' }\` → always fresh, never cached.
|
|
878
|
+
- \`revalidatePath('/')\` / \`revalidateTag('posts')\` → on-demand purge.
|
|
879
|
+
- \`POST /__what_revalidate\` with \`WHAT_REVALIDATE_SECRET\` → CMS-triggered purge.
|
|
880
|
+
|
|
881
|
+
## Notes
|
|
882
|
+
|
|
883
|
+
${notes}
|
|
884
|
+
`;
|
|
885
|
+
}
|
|
886
|
+
|
|
833
887
|
return `# ${packageName}
|
|
834
888
|
|
|
835
889
|
## Run
|
|
@@ -847,6 +901,224 @@ ${notes}
|
|
|
847
901
|
`;
|
|
848
902
|
}
|
|
849
903
|
|
|
904
|
+
// ---------------------------------------------------------------------------
|
|
905
|
+
// Full-stack template — file-routed SSR pages, server loaders, getStaticPaths,
|
|
906
|
+
// origin-first ISR, and a server action that revalidates the cache. Authored
|
|
907
|
+
// with h() in plain .js so it runs with `node server.js` (no build step) while
|
|
908
|
+
// `npm run dev` still serves the client. Mirrors examples/blog (proven by its
|
|
909
|
+
// e2e suite). The full loop: SSR → loader → ISR (MISS→HIT) → action → revalidate.
|
|
910
|
+
// ---------------------------------------------------------------------------
|
|
911
|
+
function generateFullstackFiles(root, packageName) {
|
|
912
|
+
mkdirSync(resolve(root, 'src', 'pages'), { recursive: true });
|
|
913
|
+
mkdirSync(resolve(root, 'src', 'actions'), { recursive: true });
|
|
914
|
+
|
|
915
|
+
// Tiny in-memory "database". Swap for SQLite/Postgres — the loader/action
|
|
916
|
+
// contract is identical.
|
|
917
|
+
writeFileSync(resolve(root, 'src', 'db.js'), `// In-memory demo data. A real app would use SQLite/Postgres; the loader and
|
|
918
|
+
// action contracts are identical either way.
|
|
919
|
+
|
|
920
|
+
const posts = [
|
|
921
|
+
{ slug: 'hello-world', title: 'Hello, World', body: 'The first post.', createdAt: 1 },
|
|
922
|
+
{ slug: 'why-signals', title: 'Why Signals', body: 'Fine-grained reactivity, no virtual DOM, components run once.', createdAt: 2 },
|
|
923
|
+
];
|
|
924
|
+
|
|
925
|
+
export function listPosts() {
|
|
926
|
+
return [...posts].sort((a, b) => b.createdAt - a.createdAt);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export function getPost(slug) {
|
|
930
|
+
return posts.find((p) => p.slug === slug) || null;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export function createPost({ title, body }) {
|
|
934
|
+
const slug = String(title).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
935
|
+
const post = { slug, title, body, createdAt: Date.now() };
|
|
936
|
+
posts.push(post);
|
|
937
|
+
return post;
|
|
938
|
+
}
|
|
939
|
+
`);
|
|
940
|
+
|
|
941
|
+
// Server action: mutate, then revalidate the cached pages it affects.
|
|
942
|
+
writeFileSync(resolve(root, 'src', 'actions', 'posts.js'), `// Server action: create a post, then revalidate the cached home listing.
|
|
943
|
+
// revalidatePath purges the origin ISR cache (and any CDN) so the new post
|
|
944
|
+
// appears immediately on the next request.
|
|
945
|
+
|
|
946
|
+
import { action, revalidatePath } from 'what-framework/server';
|
|
947
|
+
import { createPost } from '../db.js';
|
|
948
|
+
|
|
949
|
+
export const createPostAction = action(
|
|
950
|
+
async ({ title, body }) => {
|
|
951
|
+
const post = createPost({ title, body });
|
|
952
|
+
return { ok: true, slug: post.slug };
|
|
953
|
+
},
|
|
954
|
+
{ id: 'createPost', revalidate: ['/'] } // purge the home listing on success
|
|
955
|
+
);
|
|
956
|
+
`);
|
|
957
|
+
|
|
958
|
+
// Home page — static + ISR, lists posts via a loader.
|
|
959
|
+
writeFileSync(resolve(root, 'src', 'pages', 'home.js'), `// Home page.
|
|
960
|
+
// export const page -> route config (JSON-safe: mode, revalidate, tags)
|
|
961
|
+
// export const loader -> runs on the server before render; result -> loaderData
|
|
962
|
+
// export default -> the page component (runs once, no re-renders)
|
|
963
|
+
|
|
964
|
+
import { h, Head, useLoaderData } from 'what-framework';
|
|
965
|
+
import { listPosts } from '../db.js';
|
|
966
|
+
|
|
967
|
+
export const page = { mode: 'static', revalidate: 60, tags: ['posts'] };
|
|
968
|
+
|
|
969
|
+
export const loader = () => ({ posts: listPosts() });
|
|
970
|
+
|
|
971
|
+
export default function Home() {
|
|
972
|
+
const { posts } = useLoaderData();
|
|
973
|
+
return h('main', { class: 'container' },
|
|
974
|
+
h(Head, {
|
|
975
|
+
title: '${packageName}',
|
|
976
|
+
meta: [{ name: 'description', content: 'A full-stack app built with What Framework' }],
|
|
977
|
+
}),
|
|
978
|
+
h('h1', {}, '${packageName}'),
|
|
979
|
+
h('p', {}, 'Server-rendered, ISR-cached, hydrated islands.'),
|
|
980
|
+
h('ul', {}, posts.map((p) =>
|
|
981
|
+
h('li', { key: p.slug }, h('a', { href: \`/blog/\${p.slug}\` }, p.title))
|
|
982
|
+
)),
|
|
983
|
+
h('p', {}, h('a', { href: '/new' }, '+ New post'))
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
`);
|
|
987
|
+
|
|
988
|
+
// Dynamic /blog/[slug] — loader + getStaticPaths + ISR + per-post <Head>.
|
|
989
|
+
writeFileSync(resolve(root, 'src', 'pages', 'post.js'), `// Dynamic post page. getStaticPaths pre-renders known slugs at build; unknown
|
|
990
|
+
// slugs render on first hit (fallback: 'blocking') and are then cached.
|
|
991
|
+
|
|
992
|
+
import { h, Head, useLoaderData } from 'what-framework';
|
|
993
|
+
import { getPost, listPosts } from '../db.js';
|
|
994
|
+
|
|
995
|
+
export const page = { mode: 'static', revalidate: 60, tags: ['posts'] };
|
|
996
|
+
|
|
997
|
+
export const loader = ({ params }) => ({ post: getPost(params.slug) });
|
|
998
|
+
|
|
999
|
+
export async function getStaticPaths() {
|
|
1000
|
+
return {
|
|
1001
|
+
paths: listPosts().map((p) => ({ params: { slug: p.slug } })),
|
|
1002
|
+
fallback: 'blocking',
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
export default function Post() {
|
|
1007
|
+
const { post } = useLoaderData();
|
|
1008
|
+
if (!post) return h('main', { class: 'container' }, h('h1', {}, 'Not found'));
|
|
1009
|
+
return h('main', { class: 'container' },
|
|
1010
|
+
h(Head, {
|
|
1011
|
+
title: \`\${post.title} — ${packageName}\`,
|
|
1012
|
+
meta: [{ property: 'og:title', content: post.title }],
|
|
1013
|
+
}),
|
|
1014
|
+
h('article', {}, h('h1', {}, post.title), h('p', {}, post.body)),
|
|
1015
|
+
h('p', {}, h('a', { href: '/' }, '← Back'))
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
`);
|
|
1019
|
+
|
|
1020
|
+
// /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).
|
|
1023
|
+
|
|
1024
|
+
import { h, Head } from 'what-framework';
|
|
1025
|
+
|
|
1026
|
+
export const page = { mode: 'server' };
|
|
1027
|
+
|
|
1028
|
+
export default function NewPost() {
|
|
1029
|
+
return h('main', { class: 'container' },
|
|
1030
|
+
h(Head, { title: 'New post — ${packageName}' }),
|
|
1031
|
+
h('h1', {}, 'New post'),
|
|
1032
|
+
h('form', { method: 'post', action: '/__what_action', 'data-action': 'createPost' },
|
|
1033
|
+
h('input', { name: 'title', placeholder: 'Title', required: true }),
|
|
1034
|
+
h('textarea', { name: 'body', placeholder: 'Write something…', required: true }),
|
|
1035
|
+
h('button', { type: 'submit' }, 'Publish')
|
|
1036
|
+
),
|
|
1037
|
+
h('p', {}, h('a', { href: '/' }, '← Back'))
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
`);
|
|
1041
|
+
|
|
1042
|
+
// Route table. In a JSX app the vite plugin generates this from src/pages/**;
|
|
1043
|
+
// here it's hand-written so the app runs with plain \`node server.js\`.
|
|
1044
|
+
writeFileSync(resolve(root, 'src', 'routes.js'), `// Each entry carries the page's default component plus its live
|
|
1045
|
+
// loader/getStaticPaths/page bindings.
|
|
1046
|
+
|
|
1047
|
+
import Home, { loader as homeLoader, page as homePage } from './pages/home.js';
|
|
1048
|
+
import Post, { loader as postLoader, getStaticPaths as postPaths, page as postPage } from './pages/post.js';
|
|
1049
|
+
import NewPost, { page as newPage } from './pages/new.js';
|
|
1050
|
+
|
|
1051
|
+
// Importing the action registers it so /__what_action can dispatch it.
|
|
1052
|
+
import './actions/posts.js';
|
|
1053
|
+
|
|
1054
|
+
export const routes = [
|
|
1055
|
+
{ path: '/', component: Home, loader: homeLoader, page: homePage, mode: homePage.mode },
|
|
1056
|
+
{ path: '/blog/:slug', component: Post, loader: postLoader, getStaticPaths: postPaths, page: postPage, mode: postPage.mode },
|
|
1057
|
+
{ path: '/new', component: NewPost, page: newPage, mode: newPage.mode },
|
|
1058
|
+
];
|
|
1059
|
+
`);
|
|
1060
|
+
|
|
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.
|
|
1066
|
+
|
|
1067
|
+
import { createServer, createRequestHandler } from 'what-framework/server';
|
|
1068
|
+
import {
|
|
1069
|
+
createCacheEngine,
|
|
1070
|
+
createMemoryStore,
|
|
1071
|
+
createRevalidateWebhook,
|
|
1072
|
+
createScheduler,
|
|
1073
|
+
} from 'what-cache';
|
|
1074
|
+
import { routes } from './src/routes.js';
|
|
1075
|
+
|
|
1076
|
+
const REVALIDATE_SECRET = process.env.WHAT_REVALIDATE_SECRET || 'dev-secret';
|
|
1077
|
+
|
|
1078
|
+
// Origin ISR cache. Swap createMemoryStore() for createFilesystemStore({dir})
|
|
1079
|
+
// to survive restarts, or createRedisStore({client}) for multi-instance.
|
|
1080
|
+
const cache = createCacheEngine({ store: createMemoryStore() });
|
|
1081
|
+
|
|
1082
|
+
// Keep the home listing warm every 5 minutes regardless of traffic.
|
|
1083
|
+
const scheduler = createScheduler(cache);
|
|
1084
|
+
scheduler.register({ path: '/', query: {}, config: routes[0].page }, { intervalMs: 5 * 60 * 1000 });
|
|
1085
|
+
|
|
1086
|
+
export function createHandler() {
|
|
1087
|
+
return createRequestHandler({
|
|
1088
|
+
routes,
|
|
1089
|
+
cache,
|
|
1090
|
+
revalidateWebhook: createRevalidateWebhook(cache, { secret: REVALIDATE_SECRET }),
|
|
1091
|
+
document: { clientEntry: '/src/entry-client.js' },
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Started directly (node server.js), not when imported by tests.
|
|
1096
|
+
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' },
|
|
1103
|
+
});
|
|
1104
|
+
const port = Number(process.env.PORT) || 3000;
|
|
1105
|
+
server.listen(port, () => console.log(\`${packageName} → http://localhost:\${port}\`));
|
|
1106
|
+
}
|
|
1107
|
+
`);
|
|
1108
|
+
|
|
1109
|
+
// App config — the CLI reads this to pick a deploy adapter + ISR defaults.
|
|
1110
|
+
writeFileSync(resolve(root, 'what.config.js'), `// What Framework app config. \`what build\` / \`what start\` read this to pick a
|
|
1111
|
+
// deploy adapter and ISR defaults.
|
|
1112
|
+
export default {
|
|
1113
|
+
adapter: 'node', // 'node' | 'vercel' | 'cloudflare' | 'static'
|
|
1114
|
+
isr: {
|
|
1115
|
+
store: 'memory', // 'memory' | 'filesystem' | 'redis'
|
|
1116
|
+
defaultRevalidate: 60,
|
|
1117
|
+
},
|
|
1118
|
+
};
|
|
1119
|
+
`);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
850
1122
|
// ---------------------------------------------------------------------------
|
|
851
1123
|
// Main
|
|
852
1124
|
// ---------------------------------------------------------------------------
|
|
@@ -1139,6 +1411,11 @@ count(c => c + 1) // update
|
|
|
1139
1411
|
// src/main.jsx
|
|
1140
1412
|
writeFileSync(resolve(root, 'src', 'main.jsx'), generateMainJsx(options));
|
|
1141
1413
|
|
|
1414
|
+
// Full-stack scaffold: file-routed SSR pages + server + ISR config.
|
|
1415
|
+
if (options.template === 'fullstack') {
|
|
1416
|
+
generateFullstackFiles(root, packageName);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1142
1419
|
// src/styles.css
|
|
1143
1420
|
if (reactCompat && cssApproach === 'none') {
|
|
1144
1421
|
writeFileSync(resolve(root, 'src', 'styles.css'), generateStylesWithReactCompat());
|