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.
Files changed (2) hide show
  1. package/index.js +313 -36
  2. 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
- -y, --yes Skip prompts and use defaults
33
- -h, --help Show this help message
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, useSignal } from 'what-framework';
346
+ return `import { mount, signal } from 'what-framework';
333
347
 
334
348
  function App() {
335
- const count = useSignal(0);
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, React-familiar authoring.</p>
354
+ <p>Compiler-first JSX, fine-grained signals.</p>
341
355
 
342
356
  <section className="counter">
343
- <button onClick={() => count.set(c => c - 1)}>-</button>
357
+ <button onClick={() => count(c => c - 1)}>-</button>
344
358
  <output>{count()}</output>
345
- <button onClick={() => count.set(c => c + 1)}>+</button>
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, useSignal } from 'what-framework';
370
+ return `import { mount, signal } from 'what-framework';
357
371
 
358
372
  function App() {
359
- const count = useSignal(0);
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.set(c => c - 1)}
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.set(c => c + 1)}
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, useSignal } from 'what-framework';
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 = useSignal(0);
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.set(c => c - 1)}>-</button>
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.set(c => c + 1)}>+</button>
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, useSignal } from 'what-framework';
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 = useSignal(0);
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.set(c => c - 1)}
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.set(c => c + 1)}
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, useSignal } from 'what-framework';
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 = useSignal(0);
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.set(c => c - 1)}>-</button>
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.set(c => c + 1)}>+</button>
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, useSignal } from 'what-framework';
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 = useSignal(0);
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.set(c => c - 1)}>-</button>
708
+ <button onClick={() => count(c => c - 1)}>-</button>
695
709
  <output>{count()}</output>
696
- <button onClick={() => count.set(c => c + 1)}>+</button>
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());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-what",
3
- "version": "0.8.4",
3
+ "version": "0.10.0",
4
4
  "description": "Scaffold a new What Framework project",
5
5
  "type": "module",
6
6
  "bin": {