create-what 0.5.1 → 0.5.4

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 +77 -0
  2. package/index.js +824 -84
  3. package/package.json +7 -3
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # create-what
2
+
3
+ Scaffold a new [What Framework](https://whatfw.com) project with one command.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx create-what my-app
9
+ cd my-app
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ Or with Bun:
15
+
16
+ ```bash
17
+ bun create what@latest my-app
18
+ ```
19
+
20
+ ### Skip prompts
21
+
22
+ ```bash
23
+ npx create-what my-app --yes
24
+ ```
25
+
26
+ ## Options
27
+
28
+ The scaffolder prompts you for:
29
+
30
+ 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
33
+
34
+ ## What You Get
35
+
36
+ ```
37
+ my-app/
38
+ src/
39
+ main.jsx # App entry point with counter example
40
+ styles.css # Styles (vanilla, Tailwind, or StyleX)
41
+ public/
42
+ favicon.svg # What Framework logo
43
+ index.html # HTML entry
44
+ vite.config.js # Pre-configured Vite (What compiler or React compat)
45
+ tsconfig.json # TypeScript config
46
+ package.json
47
+ .gitignore
48
+ ```
49
+
50
+ ### With React compat enabled
51
+
52
+ The scaffold includes a working zustand demo showing a React state library running on What's signal engine.
53
+
54
+ ### With Tailwind CSS
55
+
56
+ Tailwind v4 is configured via `@tailwindcss/vite`. The counter example uses utility classes.
57
+
58
+ ### With StyleX
59
+
60
+ StyleX is configured via `vite-plugin-stylex`. The counter example uses `stylex.create()` and `stylex.props()`.
61
+
62
+ ## Scripts
63
+
64
+ | Script | Command |
65
+ |---|---|
66
+ | `npm run dev` | Start Vite dev server |
67
+ | `npm run build` | Production build |
68
+ | `npm run preview` | Preview production build |
69
+
70
+ ## Links
71
+
72
+ - [Documentation](https://whatfw.com)
73
+ - [GitHub](https://github.com/zvndev/what-fw)
74
+
75
+ ## License
76
+
77
+ MIT
package/index.js CHANGED
@@ -4,53 +4,219 @@
4
4
  // Canonical scaffold for What Framework projects.
5
5
  // Usage:
6
6
  // npx create-what my-app
7
- // npx create-what my-app --vanilla
7
+ // npx create-what my-app --yes (skip prompts, use defaults)
8
8
 
9
9
  import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
+ import { createInterface } from 'node:readline';
11
12
 
13
+ // ---------------------------------------------------------------------------
14
+ // CLI arguments
15
+ // ---------------------------------------------------------------------------
12
16
  const args = process.argv.slice(2);
13
- const flags = args.filter(a => a.startsWith('-'));
14
17
  const positional = args.filter(a => !a.startsWith('-'));
18
+ const flags = new Set(args.filter(a => a.startsWith('-')));
19
+ const skipPrompts = flags.has('--yes') || flags.has('-y');
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Prompt helpers (zero-dependency, uses Node readline)
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Creates an async prompt interface that works with both TTY and piped stdin.
27
+ * When stdin is piped, readline can close before all questions are asked.
28
+ * We buffer all incoming lines to handle this gracefully.
29
+ */
30
+ function createPrompter() {
31
+ const lines = [];
32
+ let lineResolve = null;
33
+ let closed = false;
34
+
35
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: process.stdin.isTTY ?? false });
36
+
37
+ rl.on('line', (line) => {
38
+ if (lineResolve) {
39
+ const resolve = lineResolve;
40
+ lineResolve = null;
41
+ resolve(line);
42
+ } else {
43
+ lines.push(line);
44
+ }
45
+ });
46
+
47
+ rl.on('close', () => {
48
+ closed = true;
49
+ if (lineResolve) {
50
+ const resolve = lineResolve;
51
+ lineResolve = null;
52
+ resolve('');
53
+ }
54
+ });
55
+
56
+ function ask(question) {
57
+ process.stdout.write(question);
58
+ if (lines.length > 0) return Promise.resolve(lines.shift());
59
+ if (closed) return Promise.resolve('');
60
+ return new Promise(resolve => { lineResolve = resolve; });
61
+ }
62
+
63
+ async function confirm(message, defaultYes = false) {
64
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
65
+ const answer = (await ask(` ${message} ${hint} `)).trim().toLowerCase();
66
+ if (answer === '') return defaultYes;
67
+ return answer === 'y' || answer === 'yes';
68
+ }
69
+
70
+ async function select(message, choices) {
71
+ console.log(` ${message}`);
72
+ choices.forEach((c, i) => console.log(` ${i + 1}) ${c.label}`));
73
+ const answer = (await ask(` Choice [1]: `)).trim();
74
+ const idx = answer === '' ? 0 : parseInt(answer, 10) - 1;
75
+ if (idx >= 0 && idx < choices.length) return choices[idx].value;
76
+ return choices[0].value;
77
+ }
78
+
79
+ function close() {
80
+ rl.close();
81
+ }
82
+
83
+ return { ask, confirm, select, close };
84
+ }
15
85
 
16
- const projectName = positional[0] || 'my-what-app';
17
- const useJSX = !flags.includes('--no-jsx') && !flags.includes('--vanilla');
86
+ // ---------------------------------------------------------------------------
87
+ // Gather options
88
+ // ---------------------------------------------------------------------------
89
+ async function gatherOptions() {
90
+ let projectName = positional[0];
91
+ let reactCompat = false;
92
+ let cssApproach = 'none'; // 'none' | 'tailwind' | 'stylex'
18
93
 
19
- const root = join(process.cwd(), projectName);
94
+ if (skipPrompts) {
95
+ projectName = projectName || 'my-what-app';
96
+ return { projectName, reactCompat, cssApproach };
97
+ }
20
98
 
21
- if (existsSync(root)) {
22
- console.error(`\nError: "${projectName}" already exists.`);
23
- process.exit(1);
99
+ const prompter = createPrompter();
100
+
101
+ console.log('\n create-what - scaffold a What Framework project\n');
102
+
103
+ if (!projectName) {
104
+ projectName = (await prompter.ask(' Project name: ')).trim() || 'my-what-app';
105
+ }
106
+
107
+ reactCompat = await prompter.confirm('Add React library support? (what-react)');
108
+
109
+ cssApproach = await prompter.select('CSS approach:', [
110
+ { label: 'None (vanilla CSS)', value: 'none' },
111
+ { label: 'Tailwind CSS v4', value: 'tailwind' },
112
+ { label: 'StyleX', value: 'stylex' },
113
+ ]);
114
+
115
+ prompter.close();
116
+
117
+ return { projectName, reactCompat, cssApproach };
24
118
  }
25
119
 
26
- mkdirSync(join(root, 'src'), { recursive: true });
27
- mkdirSync(join(root, 'public'), { recursive: true });
120
+ // ---------------------------------------------------------------------------
121
+ // File generators
122
+ // ---------------------------------------------------------------------------
123
+
124
+ function generatePackageJson(projectName, { reactCompat, cssApproach }) {
125
+ const deps = {
126
+ 'what-framework': '^0.5.3',
127
+ };
128
+ const devDeps = {
129
+ vite: '^6.0.0',
130
+ 'what-compiler': '^0.5.3',
131
+ 'what-devtools': '^0.5.3',
132
+ 'what-devtools-mcp': '^0.1.0',
133
+ };
134
+
135
+ if (reactCompat) {
136
+ deps['what-react'] = '^0.1.0';
137
+ deps['what-core'] = '^0.5.3';
138
+ // Include zustand as a demo React library
139
+ deps['zustand'] = '^5.0.0';
140
+ }
141
+
142
+ if (cssApproach === 'tailwind') {
143
+ devDeps['tailwindcss'] = '^4.0.0';
144
+ devDeps['@tailwindcss/vite'] = '^4.0.0';
145
+ }
146
+
147
+ if (cssApproach === 'stylex') {
148
+ devDeps['@stylexjs/stylex'] = '^0.10.0';
149
+ devDeps['vite-plugin-stylex'] = '^0.12.0';
150
+ }
151
+
152
+ return JSON.stringify({
153
+ name: projectName,
154
+ private: true,
155
+ version: '0.1.0',
156
+ type: 'module',
157
+ scripts: {
158
+ dev: 'vite',
159
+ build: 'vite build',
160
+ preview: 'vite preview',
161
+ },
162
+ dependencies: deps,
163
+ devDependencies: devDeps,
164
+ }, null, 2) + '\n';
165
+ }
28
166
 
29
- const ext = useJSX ? 'jsx' : 'js';
30
- const entry = `src/main.${ext}`;
167
+ function generateViteConfig({ reactCompat, cssApproach }) {
168
+ const imports = [];
169
+ const plugins = [];
31
170
 
32
- writeFileSync(join(root, '.gitignore'), `node_modules\ndist\n.DS_Store\n`);
171
+ imports.push(`import { defineConfig } from 'vite';`);
33
172
 
34
- writeFileSync(join(root, 'package.json'), JSON.stringify({
35
- name: projectName,
36
- private: true,
37
- version: '0.1.0',
38
- type: 'module',
39
- scripts: {
40
- dev: 'vite',
41
- build: 'vite build',
42
- preview: 'vite preview',
43
- },
44
- dependencies: {
45
- 'what-framework': '^0.5.1',
46
- },
47
- devDependencies: {
48
- vite: '^5.4.0',
49
- ...(useJSX ? { 'what-compiler': '^0.5.1' } : {}),
50
- },
51
- }, null, 2) + '\n');
173
+ // AI-powered debugging — auto-injects devtools in dev mode
174
+ imports.push(`import whatDevToolsMCP from 'what-devtools-mcp/vite-plugin';`);
175
+
176
+ if (reactCompat) {
177
+ // React compat projects use the what-react Vite plugin instead of the
178
+ // What compiler. The what-react plugin aliases react/react-dom imports
179
+ // to what-react, which runs React code on What Framework's signal engine.
180
+ imports.push(`import { reactCompat } from 'what-react/vite';`);
181
+ plugins.push('reactCompat()');
182
+ } else {
183
+ // Standard What Framework projects use the What compiler for JSX
184
+ imports.push(`import what from 'what-compiler/vite';`);
185
+ plugins.push('what()');
186
+ }
187
+
188
+ plugins.push('whatDevToolsMCP()');
189
+
190
+ if (cssApproach === 'tailwind') {
191
+ imports.push(`import tailwindcss from '@tailwindcss/vite';`);
192
+ plugins.push('tailwindcss()');
193
+ }
52
194
 
53
- writeFileSync(join(root, 'index.html'), `<!doctype html>
195
+ if (cssApproach === 'stylex') {
196
+ imports.push(`import stylexPlugin from 'vite-plugin-stylex';`);
197
+ plugins.push('stylexPlugin()');
198
+ }
199
+
200
+ let config = `${imports.join('\n')}
201
+
202
+ export default defineConfig({
203
+ plugins: [${plugins.join(', ')}],`;
204
+
205
+ if (reactCompat) {
206
+ // Note: the what-react/vite plugin handles optimizeDeps.exclude and
207
+ // resolve.alias automatically. No manual configuration needed.
208
+ config += `
209
+ // what-react/vite handles all React aliasing and optimizeDeps automatically.
210
+ // Any React library you install (zustand, @tanstack/react-query, etc.)
211
+ // will be auto-detected and excluded from pre-bundling.`;
212
+ }
213
+
214
+ config += '\n});\n';
215
+ return config;
216
+ }
217
+
218
+ function generateIndexHtml(projectName) {
219
+ return `<!doctype html>
54
220
  <html lang="en">
55
221
  <head>
56
222
  <meta charset="UTF-8" />
@@ -61,33 +227,93 @@ writeFileSync(join(root, 'index.html'), `<!doctype html>
61
227
  </head>
62
228
  <body>
63
229
  <div id="app"></div>
64
- <script type="module" src="/${entry}"></script>
230
+ <script type="module" src="/src/main.jsx"></script>
65
231
  </body>
66
232
  </html>
67
- `);
233
+ `;
234
+ }
68
235
 
69
- writeFileSync(join(root, 'public', 'favicon.svg'), `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
70
- <defs>
71
- <linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
72
- <stop offset="0%" stop-color="#2563eb" />
73
- <stop offset="100%" stop-color="#1d4ed8" />
74
- </linearGradient>
75
- </defs>
76
- <rect width="64" height="64" rx="14" fill="url(#g)" />
77
- <path d="M17 20h10l5 20 5-20h10L36 49h-8z" fill="#fff" />
78
- </svg>
79
- `);
236
+ function generateStyles({ cssApproach }) {
237
+ if (cssApproach === 'tailwind') {
238
+ // Tailwind v4: just one import, utility classes handle the rest
239
+ return `@import "tailwindcss";
80
240
 
81
- if (useJSX) {
82
- writeFileSync(join(root, 'vite.config.js'), `import { defineConfig } from 'vite';
83
- import what from 'what-compiler/vite';
241
+ /* Custom styles — use Tailwind utility classes in your JSX instead */
242
+ `;
243
+ }
84
244
 
85
- export default defineConfig({
86
- plugins: [what()],
87
- });
88
- `);
245
+ // Vanilla CSS and StyleX both get a baseline stylesheet
246
+ return `:root {
247
+ color-scheme: light;
248
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
249
+ }
89
250
 
90
- writeFileSync(join(root, 'src', 'main.jsx'), `import { mount, useSignal } from 'what-framework';
251
+ * {
252
+ box-sizing: border-box;
253
+ }
254
+
255
+ body {
256
+ margin: 0;
257
+ min-height: 100vh;
258
+ display: grid;
259
+ place-items: center;
260
+ background: #f4f6fb;
261
+ color: #0f172a;
262
+ }
263
+
264
+ .app-shell {
265
+ width: min(560px, calc(100vw - 2rem));
266
+ background: #ffffff;
267
+ border: 1px solid #dbe2ee;
268
+ border-radius: 16px;
269
+ padding: 2rem;
270
+ box-shadow: 0 16px 40px rgba(15, 23, 42, 0.06);
271
+ }
272
+
273
+ .counter {
274
+ margin-top: 1rem;
275
+ display: inline-flex;
276
+ align-items: center;
277
+ gap: 0.75rem;
278
+ }
279
+
280
+ button {
281
+ border: 1px solid #9aa7bb;
282
+ background: #ffffff;
283
+ color: #0f172a;
284
+ width: 2.25rem;
285
+ height: 2.25rem;
286
+ border-radius: 10px;
287
+ cursor: pointer;
288
+ }
289
+
290
+ button:hover {
291
+ border-color: #2563eb;
292
+ }
293
+
294
+ output {
295
+ min-width: 2ch;
296
+ text-align: center;
297
+ font-weight: 700;
298
+ }
299
+ `;
300
+ }
301
+
302
+ function generateMainJsx({ reactCompat, cssApproach }) {
303
+ if (reactCompat) {
304
+ return generateMainWithReactCompat({ cssApproach });
305
+ }
306
+ if (cssApproach === 'stylex') {
307
+ return generateMainWithStyleX();
308
+ }
309
+ if (cssApproach === 'tailwind') {
310
+ return generateMainWithTailwind();
311
+ }
312
+ return generateMainDefault();
313
+ }
314
+
315
+ function generateMainDefault() {
316
+ return `import { mount, useSignal } from 'what-framework';
91
317
 
92
318
  function App() {
93
319
  const count = useSignal(0);
@@ -107,34 +333,364 @@ function App() {
107
333
  }
108
334
 
109
335
  mount(<App />, '#app');
110
- `);
111
- } else {
112
- writeFileSync(join(root, 'vite.config.js'), `import { defineConfig } from 'vite';
336
+ `;
337
+ }
113
338
 
114
- export default defineConfig({});
115
- `);
339
+ function generateMainWithTailwind() {
340
+ return `import { mount, useSignal } from 'what-framework';
341
+
342
+ function App() {
343
+ const count = useSignal(0);
344
+
345
+ return (
346
+ <main className="min-h-screen flex items-center justify-center bg-slate-50">
347
+ <div className="w-full max-w-md bg-white border border-slate-200 rounded-2xl p-8 shadow-lg">
348
+ <h1 className="text-2xl font-bold text-slate-900">What Framework</h1>
349
+ <p className="mt-1 text-slate-500">Compiler-first JSX + Tailwind CSS.</p>
350
+
351
+ <section className="mt-6 inline-flex items-center gap-3">
352
+ <button
353
+ className="w-9 h-9 rounded-lg border border-slate-300 bg-white text-slate-900 hover:border-blue-600 cursor-pointer"
354
+ onClick={() => count.set(c => c - 1)}
355
+ >
356
+ -
357
+ </button>
358
+ <output className="min-w-[2ch] text-center font-bold">{count()}</output>
359
+ <button
360
+ className="w-9 h-9 rounded-lg border border-slate-300 bg-white text-slate-900 hover:border-blue-600 cursor-pointer"
361
+ onClick={() => count.set(c => c + 1)}
362
+ >
363
+ +
364
+ </button>
365
+ </section>
366
+ </div>
367
+ </main>
368
+ );
369
+ }
370
+
371
+ mount(<App />, '#app');
372
+ `;
373
+ }
374
+
375
+ function generateMainWithStyleX() {
376
+ return `import { mount, useSignal } from 'what-framework';
377
+ import * as stylex from '@stylexjs/stylex';
378
+
379
+ const styles = stylex.create({
380
+ page: {
381
+ minHeight: '100vh',
382
+ display: 'grid',
383
+ placeItems: 'center',
384
+ background: '#f4f6fb',
385
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, sans-serif',
386
+ },
387
+ shell: {
388
+ width: 'min(560px, calc(100vw - 2rem))',
389
+ background: '#ffffff',
390
+ border: '1px solid #dbe2ee',
391
+ borderRadius: 16,
392
+ padding: '2rem',
393
+ boxShadow: '0 16px 40px rgba(15, 23, 42, 0.06)',
394
+ },
395
+ heading: {
396
+ fontSize: '1.5rem',
397
+ fontWeight: 700,
398
+ color: '#0f172a',
399
+ },
400
+ subtitle: {
401
+ marginTop: '0.25rem',
402
+ color: '#64748b',
403
+ },
404
+ counter: {
405
+ marginTop: '1rem',
406
+ display: 'inline-flex',
407
+ alignItems: 'center',
408
+ gap: '0.75rem',
409
+ },
410
+ button: {
411
+ width: '2.25rem',
412
+ height: '2.25rem',
413
+ borderRadius: 10,
414
+ border: '1px solid #9aa7bb',
415
+ background: '#ffffff',
416
+ color: '#0f172a',
417
+ cursor: 'pointer',
418
+ ':hover': {
419
+ borderColor: '#2563eb',
420
+ },
421
+ },
422
+ output: {
423
+ minWidth: '2ch',
424
+ textAlign: 'center',
425
+ fontWeight: 700,
426
+ },
427
+ });
116
428
 
117
- writeFileSync(join(root, 'src', 'main.js'), `import { h, mount, signal } from 'what-framework';
429
+ function App() {
430
+ const count = useSignal(0);
431
+
432
+ return (
433
+ <main {...stylex.props(styles.page)}>
434
+ <div {...stylex.props(styles.shell)}>
435
+ <h1 {...stylex.props(styles.heading)}>What Framework</h1>
436
+ <p {...stylex.props(styles.subtitle)}>Compiler-first JSX + StyleX.</p>
437
+
438
+ <section {...stylex.props(styles.counter)}>
439
+ <button {...stylex.props(styles.button)} onClick={() => count.set(c => c - 1)}>-</button>
440
+ <output {...stylex.props(styles.output)}>{count()}</output>
441
+ <button {...stylex.props(styles.button)} onClick={() => count.set(c => c + 1)}>+</button>
442
+ </section>
443
+ </div>
444
+ </main>
445
+ );
446
+ }
447
+
448
+ mount(<App />, '#app');
449
+ `;
450
+ }
451
+
452
+ function generateMainWithReactCompat({ cssApproach }) {
453
+ // React compat demo: uses zustand (a real React library) to show it works
454
+ // with What Framework under the hood.
455
+ if (cssApproach === 'tailwind') {
456
+ return `import { mount, useSignal } from 'what-framework';
457
+ import { create } from 'zustand';
458
+
459
+ // A real React state library — works with What Framework via what-react!
460
+ const useStore = create((set) => ({
461
+ bears: 0,
462
+ increase: () => set((state) => ({ bears: state.bears + 1 })),
463
+ reset: () => set({ bears: 0 }),
464
+ }));
465
+
466
+ function BearCounter() {
467
+ // useStore is a React hook from zustand — what-react makes it work seamlessly
468
+ const bears = useStore((state) => state.bears);
469
+ const increase = useStore((state) => state.increase);
470
+ const reset = useStore((state) => state.reset);
471
+
472
+ return (
473
+ <div className="mt-6 p-4 bg-amber-50 border border-amber-200 rounded-xl">
474
+ <p className="text-sm text-amber-700 font-medium">Zustand Store (React library)</p>
475
+ <p className="mt-1 text-2xl font-bold text-amber-900">{bears} bears</p>
476
+ <div className="mt-2 flex gap-2">
477
+ <button
478
+ className="px-3 py-1 rounded-lg border border-amber-300 bg-white text-amber-800 hover:border-amber-500 cursor-pointer text-sm"
479
+ onClick={increase}
480
+ >
481
+ Add bear
482
+ </button>
483
+ <button
484
+ className="px-3 py-1 rounded-lg border border-amber-300 bg-white text-amber-800 hover:border-amber-500 cursor-pointer text-sm"
485
+ onClick={reset}
486
+ >
487
+ Reset
488
+ </button>
489
+ </div>
490
+ </div>
491
+ );
492
+ }
118
493
 
119
494
  function App() {
120
- const count = signal(0);
121
-
122
- return h('main', { class: 'app-shell' },
123
- h('h1', null, 'What Framework'),
124
- h('p', null, 'Runtime h() path (advanced).'),
125
- h('section', { class: 'counter' },
126
- h('button', { onClick: () => count.set(c => c - 1) }, '-'),
127
- h('output', null, () => count()),
128
- h('button', { onClick: () => count.set(c => c + 1) }, '+'),
129
- ),
495
+ const count = useSignal(0);
496
+
497
+ return (
498
+ <main className="min-h-screen flex items-center justify-center bg-slate-50">
499
+ <div className="w-full max-w-md bg-white border border-slate-200 rounded-2xl p-8 shadow-lg">
500
+ <h1 className="text-2xl font-bold text-slate-900">What Framework</h1>
501
+ <p className="mt-1 text-slate-500">React compat + Tailwind CSS.</p>
502
+
503
+ <section className="mt-6 inline-flex items-center gap-3">
504
+ <button
505
+ className="w-9 h-9 rounded-lg border border-slate-300 bg-white text-slate-900 hover:border-blue-600 cursor-pointer"
506
+ onClick={() => count.set(c => c - 1)}
507
+ >
508
+ -
509
+ </button>
510
+ <output className="min-w-[2ch] text-center font-bold">{count()}</output>
511
+ <button
512
+ className="w-9 h-9 rounded-lg border border-slate-300 bg-white text-slate-900 hover:border-blue-600 cursor-pointer"
513
+ onClick={() => count.set(c => c + 1)}
514
+ >
515
+ +
516
+ </button>
517
+ </section>
518
+
519
+ <BearCounter />
520
+ </div>
521
+ </main>
130
522
  );
131
523
  }
132
524
 
133
- mount(h(App), '#app');
134
- `);
525
+ mount(<App />, '#app');
526
+ `;
527
+ }
528
+
529
+ if (cssApproach === 'stylex') {
530
+ return `import { mount, useSignal } from 'what-framework';
531
+ import { create } from 'zustand';
532
+ import * as stylex from '@stylexjs/stylex';
533
+
534
+ const styles = stylex.create({
535
+ page: {
536
+ minHeight: '100vh',
537
+ display: 'grid',
538
+ placeItems: 'center',
539
+ background: '#f4f6fb',
540
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, sans-serif',
541
+ },
542
+ shell: {
543
+ width: 'min(560px, calc(100vw - 2rem))',
544
+ background: '#ffffff',
545
+ border: '1px solid #dbe2ee',
546
+ borderRadius: 16,
547
+ padding: '2rem',
548
+ boxShadow: '0 16px 40px rgba(15, 23, 42, 0.06)',
549
+ },
550
+ heading: { fontSize: '1.5rem', fontWeight: 700, color: '#0f172a' },
551
+ subtitle: { marginTop: '0.25rem', color: '#64748b' },
552
+ counter: {
553
+ marginTop: '1rem',
554
+ display: 'inline-flex',
555
+ alignItems: 'center',
556
+ gap: '0.75rem',
557
+ },
558
+ button: {
559
+ width: '2.25rem',
560
+ height: '2.25rem',
561
+ borderRadius: 10,
562
+ border: '1px solid #9aa7bb',
563
+ background: '#ffffff',
564
+ color: '#0f172a',
565
+ cursor: 'pointer',
566
+ ':hover': { borderColor: '#2563eb' },
567
+ },
568
+ output: { minWidth: '2ch', textAlign: 'center', fontWeight: 700 },
569
+ zustandBox: {
570
+ marginTop: '1.5rem',
571
+ padding: '1rem',
572
+ background: '#fffbeb',
573
+ border: '1px solid #fde68a',
574
+ borderRadius: 12,
575
+ },
576
+ zustandLabel: { fontSize: '0.875rem', color: '#92400e', fontWeight: 500 },
577
+ zustandCount: { marginTop: '0.25rem', fontSize: '1.5rem', fontWeight: 700, color: '#78350f' },
578
+ zustandButtons: { marginTop: '0.5rem', display: 'flex', gap: '0.5rem' },
579
+ zustandBtn: {
580
+ padding: '0.25rem 0.75rem',
581
+ borderRadius: 8,
582
+ border: '1px solid #fbbf24',
583
+ background: '#ffffff',
584
+ color: '#78350f',
585
+ cursor: 'pointer',
586
+ fontSize: '0.875rem',
587
+ ':hover': { borderColor: '#d97706' },
588
+ },
589
+ });
590
+
591
+ // A real React state library — works with What Framework via what-react!
592
+ const useStore = create((set) => ({
593
+ bears: 0,
594
+ increase: () => set((state) => ({ bears: state.bears + 1 })),
595
+ reset: () => set({ bears: 0 }),
596
+ }));
597
+
598
+ function BearCounter() {
599
+ const bears = useStore((state) => state.bears);
600
+ const increase = useStore((state) => state.increase);
601
+ const reset = useStore((state) => state.reset);
602
+
603
+ return (
604
+ <div {...stylex.props(styles.zustandBox)}>
605
+ <p {...stylex.props(styles.zustandLabel)}>Zustand Store (React library)</p>
606
+ <p {...stylex.props(styles.zustandCount)}>{bears} bears</p>
607
+ <div {...stylex.props(styles.zustandButtons)}>
608
+ <button {...stylex.props(styles.zustandBtn)} onClick={increase}>Add bear</button>
609
+ <button {...stylex.props(styles.zustandBtn)} onClick={reset}>Reset</button>
610
+ </div>
611
+ </div>
612
+ );
613
+ }
614
+
615
+ function App() {
616
+ const count = useSignal(0);
617
+
618
+ return (
619
+ <main {...stylex.props(styles.page)}>
620
+ <div {...stylex.props(styles.shell)}>
621
+ <h1 {...stylex.props(styles.heading)}>What Framework</h1>
622
+ <p {...stylex.props(styles.subtitle)}>React compat + StyleX.</p>
623
+
624
+ <section {...stylex.props(styles.counter)}>
625
+ <button {...stylex.props(styles.button)} onClick={() => count.set(c => c - 1)}>-</button>
626
+ <output {...stylex.props(styles.output)}>{count()}</output>
627
+ <button {...stylex.props(styles.button)} onClick={() => count.set(c => c + 1)}>+</button>
628
+ </section>
629
+
630
+ <BearCounter />
631
+ </div>
632
+ </main>
633
+ );
634
+ }
635
+
636
+ mount(<App />, '#app');
637
+ `;
638
+ }
639
+
640
+ // React compat with vanilla CSS
641
+ return `import { mount, useSignal } from 'what-framework';
642
+ import { create } from 'zustand';
643
+
644
+ // A real React state library — works with What Framework via what-react!
645
+ const useStore = create((set) => ({
646
+ bears: 0,
647
+ increase: () => set((state) => ({ bears: state.bears + 1 })),
648
+ reset: () => set({ bears: 0 }),
649
+ }));
650
+
651
+ function BearCounter() {
652
+ // useStore is a React hook from zustand — what-react makes it work seamlessly
653
+ const bears = useStore((state) => state.bears);
654
+ const increase = useStore((state) => state.increase);
655
+ const reset = useStore((state) => state.reset);
656
+
657
+ return (
658
+ <div className="zustand-demo">
659
+ <p className="zustand-label">Zustand Store (React library)</p>
660
+ <p className="zustand-count">{bears} bears</p>
661
+ <div className="zustand-buttons">
662
+ <button className="zustand-btn" onClick={increase}>Add bear</button>
663
+ <button className="zustand-btn" onClick={reset}>Reset</button>
664
+ </div>
665
+ </div>
666
+ );
135
667
  }
136
668
 
137
- writeFileSync(join(root, 'src', 'styles.css'), `:root {
669
+ function App() {
670
+ const count = useSignal(0);
671
+
672
+ return (
673
+ <main className="app-shell">
674
+ <h1>What Framework</h1>
675
+ <p>React compat enabled — use React libraries with signals.</p>
676
+
677
+ <section className="counter">
678
+ <button onClick={() => count.set(c => c - 1)}>-</button>
679
+ <output>{count()}</output>
680
+ <button onClick={() => count.set(c => c + 1)}>+</button>
681
+ </section>
682
+
683
+ <BearCounter />
684
+ </main>
685
+ );
686
+ }
687
+
688
+ mount(<App />, '#app');
689
+ `;
690
+ }
691
+
692
+ function generateStylesWithReactCompat() {
693
+ return `:root {
138
694
  color-scheme: light;
139
695
  font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
140
696
  }
@@ -187,9 +743,78 @@ output {
187
743
  text-align: center;
188
744
  font-weight: 700;
189
745
  }
190
- `);
191
746
 
192
- writeFileSync(join(root, 'README.md'), `# ${projectName}
747
+ /* Zustand demo */
748
+ .zustand-demo {
749
+ margin-top: 1.5rem;
750
+ padding: 1rem;
751
+ background: #fffbeb;
752
+ border: 1px solid #fde68a;
753
+ border-radius: 12px;
754
+ }
755
+
756
+ .zustand-label {
757
+ font-size: 0.875rem;
758
+ color: #92400e;
759
+ font-weight: 500;
760
+ margin: 0;
761
+ }
762
+
763
+ .zustand-count {
764
+ margin: 0.25rem 0 0;
765
+ font-size: 1.5rem;
766
+ font-weight: 700;
767
+ color: #78350f;
768
+ }
769
+
770
+ .zustand-buttons {
771
+ margin-top: 0.5rem;
772
+ display: flex;
773
+ gap: 0.5rem;
774
+ }
775
+
776
+ .zustand-btn {
777
+ width: auto;
778
+ height: auto;
779
+ padding: 0.25rem 0.75rem;
780
+ border-radius: 8px;
781
+ border: 1px solid #fbbf24;
782
+ background: #ffffff;
783
+ color: #78350f;
784
+ cursor: pointer;
785
+ font-size: 0.875rem;
786
+ }
787
+
788
+ .zustand-btn:hover {
789
+ border-color: #d97706;
790
+ }
791
+ `;
792
+ }
793
+
794
+ function generateReadme(projectName, { reactCompat, cssApproach }) {
795
+ let notes = `- Canonical package name is \`what-framework\`.
796
+ - Uses the What compiler for JSX transforms and automatic reactivity.
797
+ - Vite is preconfigured; use \`npm run dev/build/preview\`.
798
+ - Event handlers accept both \`onClick\` and \`onclick\`; docs and templates use \`onClick\`.
799
+ - Bun is also supported: \`bun create what@latest\`, \`bun run dev\`.`;
800
+
801
+ if (reactCompat) {
802
+ notes += `
803
+ - React compat is enabled via \`what-react\`. Any React library (zustand, @tanstack/react-query, framer-motion, etc.) works out of the box.
804
+ - The \`what-react/vite\` plugin handles all aliasing automatically.`;
805
+ }
806
+
807
+ if (cssApproach === 'tailwind') {
808
+ notes += `
809
+ - Tailwind CSS v4 is configured via the \`@tailwindcss/vite\` plugin.`;
810
+ }
811
+
812
+ if (cssApproach === 'stylex') {
813
+ notes += `
814
+ - StyleX is configured via \`vite-plugin-stylex\`. Define styles with \`stylex.create()\` and apply with \`{...stylex.props()}\`.`;
815
+ }
816
+
817
+ return `# ${projectName}
193
818
 
194
819
  ## Run
195
820
 
@@ -202,15 +827,130 @@ Open [http://localhost:5173](http://localhost:5173).
202
827
 
203
828
  ## Notes
204
829
 
205
- - Canonical package name is \`what-framework\`.
206
- - JSX path is compiler-first and recommended.
207
- - Runtime \`h()\` path is available with \`--vanilla\`.
208
- - Vite is preconfigured under the hood; use \`npm run dev/build/preview\`.
209
- - Event handlers accept both \`onClick\` and \`onclick\`; docs and templates use \`onClick\`.
830
+ ${notes}
831
+ `;
832
+ }
833
+
834
+ // ---------------------------------------------------------------------------
835
+ // Main
836
+ // ---------------------------------------------------------------------------
837
+ async function main() {
838
+ const options = await gatherOptions();
839
+ const { projectName, reactCompat, cssApproach } = options;
840
+
841
+ const root = join(process.cwd(), projectName);
842
+
843
+ if (existsSync(root)) {
844
+ console.error(`\nError: "${projectName}" already exists.`);
845
+ process.exit(1);
846
+ }
847
+
848
+ mkdirSync(join(root, 'src'), { recursive: true });
849
+ mkdirSync(join(root, 'public'), { recursive: true });
850
+ mkdirSync(join(root, '.claude'), { recursive: true });
851
+ mkdirSync(join(root, '.cursor'), { recursive: true });
852
+
853
+ // .gitignore
854
+ writeFileSync(join(root, '.gitignore'), `node_modules\ndist\n.DS_Store\n`);
855
+
856
+ // package.json
857
+ writeFileSync(join(root, 'package.json'), generatePackageJson(projectName, options));
858
+
859
+ // index.html
860
+ writeFileSync(join(root, 'index.html'), generateIndexHtml(projectName));
861
+
862
+ // favicon
863
+ writeFileSync(join(root, 'public', 'favicon.svg'), `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
864
+ <defs>
865
+ <linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
866
+ <stop offset="0%" stop-color="#2563eb" />
867
+ <stop offset="100%" stop-color="#1d4ed8" />
868
+ </linearGradient>
869
+ </defs>
870
+ <rect width="64" height="64" rx="14" fill="url(#g)" />
871
+ <path d="M17 20h10l5 20 5-20h10L36 49h-8z" fill="#fff" />
872
+ </svg>
210
873
  `);
211
874
 
212
- console.log(`\nCreated ${projectName} (${useJSX ? 'jsx' : 'vanilla'} mode).`);
213
- console.log('Next steps:');
214
- console.log(` cd ${projectName}`);
215
- console.log(' npm install');
216
- console.log(' npm run dev\n');
875
+ // vite.config.js
876
+ writeFileSync(join(root, 'vite.config.js'), generateViteConfig(options));
877
+
878
+ // tsconfig.json
879
+ writeFileSync(join(root, 'tsconfig.json'), JSON.stringify({
880
+ compilerOptions: {
881
+ target: 'ES2022',
882
+ module: 'ESNext',
883
+ moduleResolution: 'bundler',
884
+ jsx: 'preserve',
885
+ jsxImportSource: 'what-core',
886
+ strict: true,
887
+ noEmit: true,
888
+ skipLibCheck: true,
889
+ esModuleInterop: true,
890
+ resolveJsonModule: true,
891
+ isolatedModules: true,
892
+ types: ['vite/client'],
893
+ },
894
+ include: ['src'],
895
+ }, null, 2) + '\n');
896
+
897
+ // .vscode
898
+ mkdirSync(join(root, '.vscode'), { recursive: true });
899
+
900
+ writeFileSync(join(root, '.vscode', 'settings.json'), JSON.stringify({
901
+ 'typescript.tsdk': 'node_modules/typescript/lib',
902
+ 'editor.formatOnSave': true,
903
+ }, null, 2) + '\n');
904
+
905
+ writeFileSync(join(root, '.vscode', 'extensions.json'), JSON.stringify({
906
+ recommendations: [
907
+ 'zvndev.thenjs',
908
+ ],
909
+ }, null, 2) + '\n');
910
+
911
+ // MCP configs for AI-powered debugging (what-devtools-mcp)
912
+ const mcpConfig = JSON.stringify({
913
+ mcpServers: {
914
+ 'what-devtools': {
915
+ command: 'npx',
916
+ args: ['what-devtools-mcp'],
917
+ },
918
+ },
919
+ }, null, 2) + '\n';
920
+ writeFileSync(join(root, '.claude', 'mcp.json'), mcpConfig);
921
+ writeFileSync(join(root, '.cursor', 'mcp.json'), mcpConfig);
922
+
923
+ // src/main.jsx
924
+ writeFileSync(join(root, 'src', 'main.jsx'), generateMainJsx(options));
925
+
926
+ // src/styles.css
927
+ if (reactCompat && cssApproach === 'none') {
928
+ writeFileSync(join(root, 'src', 'styles.css'), generateStylesWithReactCompat());
929
+ } else {
930
+ writeFileSync(join(root, 'src', 'styles.css'), generateStyles(options));
931
+ }
932
+
933
+ // README.md
934
+ writeFileSync(join(root, 'README.md'), generateReadme(projectName, options));
935
+
936
+ // Summary
937
+ console.log(`\nCreated ${projectName}.`);
938
+
939
+ const features = [];
940
+ if (reactCompat) features.push('React compat (what-react + zustand demo)');
941
+ if (cssApproach === 'tailwind') features.push('Tailwind CSS v4');
942
+ if (cssApproach === 'stylex') features.push('StyleX');
943
+ if (features.length > 0) {
944
+ console.log(`Features: ${features.join(', ')}`);
945
+ }
946
+
947
+ console.log('\nNext steps:');
948
+ console.log(` cd ${projectName}`);
949
+ console.log(' npm install');
950
+ console.log(' npm run dev\n');
951
+ }
952
+
953
+ main().catch(err => {
954
+ console.error(err);
955
+ process.exit(1);
956
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-what",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
4
4
  "description": "Scaffold a new What Framework project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,10 @@
21
21
  "license": "MIT",
22
22
  "repository": {
23
23
  "type": "git",
24
- "url": "git+https://github.com/aspect/what-fw.git"
25
- }
24
+ "url": "https://github.com/zvndev/what-fw"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/zvndev/what-fw/issues"
28
+ },
29
+ "homepage": "https://whatfw.com"
26
30
  }