what-framework-cli 0.11.7 → 0.11.8
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 +20 -4
- package/package.json +2 -2
- package/src/cli.js +364 -132
package/README.md
CHANGED
|
@@ -27,8 +27,8 @@ what dev --host 0.0.0.0
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Features:
|
|
30
|
-
- WebSocket-based HMR with automatic reconnection
|
|
31
|
-
- Bare import transforms (`what` ->
|
|
30
|
+
- WebSocket-based HMR with automatic reconnection (same-origin only)
|
|
31
|
+
- Bare import transforms (`what-framework`, `what-framework/router`, `what-framework/server` -> `/@what/*.js`)
|
|
32
32
|
- File-based routing from `src/pages/`
|
|
33
33
|
- SPA fallback for client-side routing
|
|
34
34
|
- Server action endpoint
|
|
@@ -46,7 +46,11 @@ Output:
|
|
|
46
46
|
- Content-hashed filenames for cache busting
|
|
47
47
|
- Gzipped copies of all JS files
|
|
48
48
|
- `manifest.json` mapping original filenames to hashed versions
|
|
49
|
-
-
|
|
49
|
+
- The framework runtime under `dist/@what/` (unhashed, so app imports keep resolving)
|
|
50
|
+
|
|
51
|
+
The build fails with a non-zero exit code if there is no app to build, if the
|
|
52
|
+
framework runtime cannot be resolved (`npm install what-framework`), or if any
|
|
53
|
+
bare import specifier survives into the output.
|
|
50
54
|
|
|
51
55
|
### `what preview`
|
|
52
56
|
|
|
@@ -59,12 +63,23 @@ what preview --port 4000
|
|
|
59
63
|
|
|
60
64
|
### `what generate`
|
|
61
65
|
|
|
62
|
-
Static site generation. Runs a build, then pre-renders
|
|
66
|
+
Static site generation. Runs a build, then pre-renders every page module in
|
|
67
|
+
`pagesDir` to `dist/<route>/index.html` (running each page's `loader` first and
|
|
68
|
+
collecting its `<Head>` tags). Dynamic routes (`[id].js`) are skipped.
|
|
63
69
|
|
|
64
70
|
```bash
|
|
65
71
|
what generate
|
|
66
72
|
```
|
|
67
73
|
|
|
74
|
+
### `what start`
|
|
75
|
+
|
|
76
|
+
Run the project's full-stack server (`server.js`, Node adapter + ISR). Scaffold
|
|
77
|
+
one with `npm create what@latest -- --fullstack`.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
what start
|
|
81
|
+
```
|
|
82
|
+
|
|
68
83
|
### `what init`
|
|
69
84
|
|
|
70
85
|
Create a new project (prefer `npx create-what` for the full scaffolding experience).
|
|
@@ -91,6 +106,7 @@ export default {
|
|
|
91
106
|
|---|---|---|
|
|
92
107
|
| `--port` | Server port | `3000` (dev), `4000` (preview) |
|
|
93
108
|
| `--host` | Server host | `localhost` |
|
|
109
|
+
| `--version` | Print the CLI version | |
|
|
94
110
|
|
|
95
111
|
## Links
|
|
96
112
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "what-framework-cli",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.8",
|
|
4
4
|
"description": "What Framework CLI - Dev server, build, and deployment tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"author": "ZVN DEV (https://zvndev.com)",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"what-framework": "^0.11.
|
|
25
|
+
"what-framework": "^0.11.8"
|
|
26
26
|
},
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
package/src/cli.js
CHANGED
|
@@ -4,48 +4,88 @@
|
|
|
4
4
|
// Commands: dev, build, preview, generate
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync, statSync, copyFileSync, realpathSync } from 'fs';
|
|
7
|
-
import { join, resolve, relative, extname, basename, normalize } from 'path';
|
|
7
|
+
import { join, resolve, relative, extname, basename, normalize, sep } from 'path';
|
|
8
8
|
import { createServer } from 'http';
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
10
|
import { createRequire } from 'module';
|
|
11
|
-
import { fileURLToPath } from 'url';
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
12
12
|
import { createHash } from 'crypto';
|
|
13
13
|
import { gzipSync } from 'zlib';
|
|
14
14
|
|
|
15
|
-
// Security: Prevent path traversal attacks
|
|
15
|
+
// Security: Prevent path traversal attacks. `userPath` is a URL pathname, so it
|
|
16
|
+
// starts with '/', so it must be joined onto the base as a RELATIVE path, otherwise
|
|
17
|
+
// resolve() would discard the base entirely.
|
|
16
18
|
function safePath(base, userPath) {
|
|
17
19
|
try {
|
|
20
|
+
let decoded;
|
|
21
|
+
try {
|
|
22
|
+
decoded = decodeURIComponent(userPath);
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (decoded.includes('\0')) return null;
|
|
27
|
+
|
|
18
28
|
// Reject paths that contain .. segments (path traversal attempt)
|
|
19
|
-
const normalized = normalize(
|
|
29
|
+
const normalized = normalize(decoded);
|
|
20
30
|
if (normalized.startsWith('..') || normalized.includes('/..') || normalized.includes('\\..')) {
|
|
21
31
|
return null;
|
|
22
32
|
}
|
|
23
33
|
|
|
34
|
+
// Never serve dotfiles (.env, .git, ...)
|
|
35
|
+
if (normalized.split(/[\\/]/).some((s) => s.length > 1 && s.startsWith('.'))) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
// Get the real base path (resolve symlinks)
|
|
25
40
|
const realBase = realpathSync(base);
|
|
26
41
|
|
|
27
|
-
// Resolve the user path against the base
|
|
28
|
-
const
|
|
42
|
+
// Resolve the user path against the base, always relatively
|
|
43
|
+
const rel = normalized.startsWith('/') || normalized.startsWith('\\') ? '.' + normalized : './' + normalized;
|
|
44
|
+
const resolved = resolve(realBase, rel);
|
|
45
|
+
if (!isInside(resolved, realBase)) return null;
|
|
29
46
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
47
|
+
// readFileSync follows symlinks, so the RESOLVED target must be contained too:
|
|
48
|
+
// `public/leak.txt -> ../../.env` passes the textual check but escapes the root.
|
|
49
|
+
const real = realpathSync(resolved);
|
|
50
|
+
if (!isInside(real, realBase)) return null;
|
|
34
51
|
|
|
35
|
-
return
|
|
52
|
+
return real;
|
|
36
53
|
} catch {
|
|
37
54
|
return null;
|
|
38
55
|
}
|
|
39
56
|
}
|
|
40
57
|
|
|
58
|
+
function isInside(target, base) {
|
|
59
|
+
return target === base || target.startsWith(base + sep);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// WS handshakes are exempt from the same-origin policy, so any page the developer
|
|
63
|
+
// browses could otherwise subscribe to the HMR stream (a live feed of edited file
|
|
64
|
+
// paths). Non-browser clients send no Origin at all; browsers always do.
|
|
65
|
+
function isAllowedOrigin(origin, allowedHosts) {
|
|
66
|
+
if (!origin) return true;
|
|
67
|
+
try {
|
|
68
|
+
const { hostname, port } = new URL(origin);
|
|
69
|
+
return allowedHosts.has(`${hostname}:${port}`);
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
41
75
|
// Simple WebSocket implementation using native Node.js APIs (no external deps)
|
|
42
76
|
class SimpleWebSocketServer {
|
|
43
|
-
constructor({ server }) {
|
|
77
|
+
constructor({ server, allowedHosts = new Set() }) {
|
|
44
78
|
this.clients = new Set();
|
|
45
79
|
server.on('upgrade', (req, socket, head) => {
|
|
46
80
|
if (req.headers.upgrade?.toLowerCase() !== 'websocket') return;
|
|
47
81
|
|
|
48
82
|
const key = req.headers['sec-websocket-key'];
|
|
83
|
+
if (!key || !isAllowedOrigin(req.headers.origin, allowedHosts)) {
|
|
84
|
+
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
85
|
+
socket.destroy();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
49
89
|
const accept = createHash('sha1')
|
|
50
90
|
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
|
51
91
|
.digest('base64');
|
|
@@ -153,6 +193,7 @@ class SimpleWebSocket {
|
|
|
153
193
|
|
|
154
194
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
155
195
|
const cwd = process.cwd();
|
|
196
|
+
const MAX_ACTION_BODY = 1024 * 1024;
|
|
156
197
|
const packageVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
157
198
|
|
|
158
199
|
const args = process.argv.slice(2);
|
|
@@ -160,8 +201,7 @@ const command = args[0];
|
|
|
160
201
|
|
|
161
202
|
const commands = { dev, build, preview, generate, start, init };
|
|
162
203
|
|
|
163
|
-
|
|
164
|
-
console.log(`
|
|
204
|
+
const help = `
|
|
165
205
|
what - The closest framework to vanilla JS
|
|
166
206
|
|
|
167
207
|
Usage: what <command>
|
|
@@ -170,18 +210,47 @@ if (!command || !commands[command]) {
|
|
|
170
210
|
dev Start dev server with HMR
|
|
171
211
|
build Production build
|
|
172
212
|
preview Preview production build
|
|
173
|
-
generate Static site generation
|
|
213
|
+
generate Static site generation (pre-render src/pages to HTML)
|
|
174
214
|
start Run the full-stack server (Node adapter + ISR)
|
|
175
215
|
init Create a new project (same scaffold as npm create what@latest)
|
|
176
216
|
|
|
177
217
|
Options:
|
|
178
|
-
--port
|
|
179
|
-
--host
|
|
180
|
-
|
|
181
|
-
|
|
218
|
+
--port Dev server port (default: 3000)
|
|
219
|
+
--host Dev server host (default: localhost)
|
|
220
|
+
--version Print the CLI version
|
|
221
|
+
`;
|
|
222
|
+
|
|
223
|
+
function main() {
|
|
224
|
+
if (command === '--version' || command === '-v') {
|
|
225
|
+
console.log(packageVersion);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!command || command === '--help' || command === '-h') {
|
|
229
|
+
console.log(help);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!commands[command]) {
|
|
233
|
+
console.error(`\n Unknown command: ${command}`);
|
|
234
|
+
console.error(help);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
commands[command]();
|
|
182
238
|
}
|
|
183
239
|
|
|
184
|
-
|
|
240
|
+
// Guarded so the module can be imported (by tests) without running a command.
|
|
241
|
+
// argv[1] is the bin symlink under node_modules/.bin, hence the realpath compare.
|
|
242
|
+
function isMainModule() {
|
|
243
|
+
if (!process.argv[1]) return false;
|
|
244
|
+
try {
|
|
245
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isMainModule()) main();
|
|
252
|
+
|
|
253
|
+
export { safePath, isAllowedOrigin, transformImports, fileToRoute };
|
|
185
254
|
|
|
186
255
|
// --- Dev Server ---
|
|
187
256
|
|
|
@@ -189,6 +258,7 @@ async function dev() {
|
|
|
189
258
|
const port = getFlag('--port', 3000);
|
|
190
259
|
const host = getFlag('--host', 'localhost');
|
|
191
260
|
const config = await loadConfigAsync();
|
|
261
|
+
const runtimeDirs = requireRuntimeDirs('what dev');
|
|
192
262
|
|
|
193
263
|
const server = createServer(async (req, res) => {
|
|
194
264
|
const url = new URL(req.url, `http://${host}:${port}`);
|
|
@@ -198,8 +268,22 @@ async function dev() {
|
|
|
198
268
|
if (pathname === '/__what_action' && req.method === 'POST') {
|
|
199
269
|
const actionId = req.headers['x-what-action'];
|
|
200
270
|
let body = '';
|
|
201
|
-
|
|
271
|
+
let size = 0;
|
|
272
|
+
let tooLarge = false;
|
|
273
|
+
req.on('data', chunk => {
|
|
274
|
+
if (tooLarge) return;
|
|
275
|
+
size += chunk.length;
|
|
276
|
+
if (size > MAX_ACTION_BODY) {
|
|
277
|
+
tooLarge = true;
|
|
278
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
279
|
+
res.end(JSON.stringify({ message: 'Request body too large' }));
|
|
280
|
+
req.destroy();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
body += chunk;
|
|
284
|
+
});
|
|
202
285
|
req.on('end', async () => {
|
|
286
|
+
if (tooLarge) return;
|
|
203
287
|
try {
|
|
204
288
|
const { args } = JSON.parse(body);
|
|
205
289
|
// In production, this would call the registered action
|
|
@@ -220,11 +304,10 @@ async function dev() {
|
|
|
220
304
|
|
|
221
305
|
// Serve framework modules
|
|
222
306
|
if (pathname.startsWith('/@what/')) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
if (modPath) {
|
|
307
|
+
const mod = resolveFrameworkModule(pathname.slice(7), runtimeDirs);
|
|
308
|
+
if (mod) {
|
|
226
309
|
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
227
|
-
res.end(
|
|
310
|
+
res.end(mod);
|
|
228
311
|
return;
|
|
229
312
|
}
|
|
230
313
|
}
|
|
@@ -321,7 +404,10 @@ async function dev() {
|
|
|
321
404
|
});
|
|
322
405
|
|
|
323
406
|
// Initialize WebSocket server
|
|
324
|
-
const
|
|
407
|
+
const allowedHosts = new Set(
|
|
408
|
+
['localhost', '127.0.0.1', '::1', host].map((h) => `${h}:${port}`)
|
|
409
|
+
);
|
|
410
|
+
const wss = new SimpleWebSocketServer({ server, allowedHosts });
|
|
325
411
|
wss.on('connection', (ws) => {
|
|
326
412
|
wsClients.add(ws);
|
|
327
413
|
ws.onclose = () => wsClients.delete(ws);
|
|
@@ -359,12 +445,25 @@ async function build() {
|
|
|
359
445
|
console.log('\n what build\n');
|
|
360
446
|
if (useHash) console.log(' Hash: Enabled (cache busting)\n');
|
|
361
447
|
|
|
362
|
-
mkdirSync(outDir, { recursive: true });
|
|
363
|
-
|
|
364
448
|
// Collect all source files
|
|
365
449
|
const srcDir = join(cwd, 'src');
|
|
366
450
|
const files = collectFiles(srcDir);
|
|
367
451
|
|
|
452
|
+
if (files.length === 0) {
|
|
453
|
+
console.error(`
|
|
454
|
+
what build: no app found in ${cwd}
|
|
455
|
+
|
|
456
|
+
Expected a src/ directory containing your entry point (src/main.js and/or
|
|
457
|
+
src/index.html). Scaffold one with \`npm create what@latest\`, or run this
|
|
458
|
+
from your project root.
|
|
459
|
+
`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const runtimeDirs = requireRuntimeDirs('what build');
|
|
465
|
+
mkdirSync(outDir, { recursive: true });
|
|
466
|
+
|
|
368
467
|
let totalSize = 0;
|
|
369
468
|
let gzipSize = 0;
|
|
370
469
|
|
|
@@ -426,7 +525,24 @@ async function build() {
|
|
|
426
525
|
}
|
|
427
526
|
|
|
428
527
|
// Bundle the framework runtime
|
|
429
|
-
bundleRuntime(outDir,
|
|
528
|
+
bundleRuntime(outDir, runtimeDirs);
|
|
529
|
+
|
|
530
|
+
// A bare specifier left in the output is a module no browser can load, so the
|
|
531
|
+
// build must fail rather than hand back an artifact that 404s on first paint.
|
|
532
|
+
const unresolved = findBareSpecifiers(outDir);
|
|
533
|
+
if (unresolved.length > 0) {
|
|
534
|
+
console.error(`\n what build: the output contains imports no browser can resolve:\n`);
|
|
535
|
+
for (const { file, spec } of unresolved.slice(0, 10)) {
|
|
536
|
+
console.error(` ${file}: '${spec}'`);
|
|
537
|
+
}
|
|
538
|
+
if (unresolved.length > 10) console.error(` ...and ${unresolved.length - 10} more`);
|
|
539
|
+
console.error(`
|
|
540
|
+
Import the framework as 'what-framework' (or 'what-framework/router',
|
|
541
|
+
'what-framework/server') so the build can rewrite it to /@what/*.js.
|
|
542
|
+
`);
|
|
543
|
+
process.exit(1);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
430
546
|
|
|
431
547
|
// Write manifest for production use
|
|
432
548
|
if (useHash && Object.keys(hashManifest).length > 0) {
|
|
@@ -494,18 +610,77 @@ async function generate() {
|
|
|
494
610
|
// First do a normal build
|
|
495
611
|
await build();
|
|
496
612
|
|
|
497
|
-
// Then pre-render all pages
|
|
498
613
|
const pagesDir = join(cwd, config.pagesDir || 'src/pages');
|
|
499
|
-
if (existsSync(pagesDir)) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
614
|
+
if (!existsSync(pagesDir)) {
|
|
615
|
+
console.error(`
|
|
616
|
+
what generate: no pages directory at ${relative(cwd, pagesDir)}/
|
|
617
|
+
|
|
618
|
+
Static generation pre-renders every page module in that directory (each one
|
|
619
|
+
exporting a default component, optionally a loader). Create it, or use
|
|
620
|
+
\`what build\` for a client-rendered app.
|
|
621
|
+
`);
|
|
622
|
+
process.exit(1);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const { renderPage } = await import(pathToFileURL(join(requireRuntimeDirs('what generate').server, 'index.js')).href);
|
|
627
|
+
const pages = collectFiles(pagesDir).filter(f => extname(f) === '.js');
|
|
628
|
+
let count = 0;
|
|
629
|
+
|
|
630
|
+
for (const page of pages) {
|
|
631
|
+
const route = fileToRoute(relative(pagesDir, page));
|
|
632
|
+
if (route.includes(':') || route.includes('*')) {
|
|
633
|
+
console.log(` Skipped: ${route} (dynamic route, no params to pre-render)`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let html;
|
|
638
|
+
try {
|
|
639
|
+
const mod = await import(pathToFileURL(page).href);
|
|
640
|
+
if (typeof (mod.default || mod) !== 'function') {
|
|
641
|
+
throw new Error('no default-exported component');
|
|
642
|
+
}
|
|
643
|
+
const { body, head } = await renderPage(mod, { params: {}, query: {}, path: route });
|
|
644
|
+
html = staticDocument(body, head);
|
|
645
|
+
} catch (e) {
|
|
646
|
+
console.error(`\n what generate: failed to pre-render ${relative(cwd, page)}\n\n ${e.message}\n`);
|
|
647
|
+
process.exit(1);
|
|
648
|
+
return;
|
|
505
649
|
}
|
|
650
|
+
|
|
651
|
+
const outPath = route === '/' ? join(outDir, 'index.html') : join(outDir, route.slice(1), 'index.html');
|
|
652
|
+
mkdirSync(join(outPath, '..'), { recursive: true });
|
|
653
|
+
writeFileSync(outPath, html);
|
|
654
|
+
console.log(` Pre-rendered: ${route} -> ${relative(cwd, outPath)}`);
|
|
655
|
+
count++;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (count === 0) {
|
|
659
|
+
console.error(`
|
|
660
|
+
what generate: no static pages found in ${relative(cwd, pagesDir)}/
|
|
661
|
+
|
|
662
|
+
Add a page module (e.g. src/pages/index.js exporting a default component).
|
|
663
|
+
`);
|
|
664
|
+
process.exit(1);
|
|
665
|
+
return;
|
|
506
666
|
}
|
|
507
667
|
|
|
508
|
-
console.log(
|
|
668
|
+
console.log(`\n Static generation complete (${count} page${count === 1 ? '' : 's'}).\n`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function staticDocument(body, head) {
|
|
672
|
+
return `<!DOCTYPE html>
|
|
673
|
+
<html lang="en">
|
|
674
|
+
<head>
|
|
675
|
+
<meta charset="UTF-8">
|
|
676
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
677
|
+
${head || '<title>What App</title>'}
|
|
678
|
+
</head>
|
|
679
|
+
<body>
|
|
680
|
+
<div id="app">${body}</div>
|
|
681
|
+
</body>
|
|
682
|
+
</html>
|
|
683
|
+
`;
|
|
509
684
|
}
|
|
510
685
|
|
|
511
686
|
// --- Start (full-stack server) ---
|
|
@@ -521,7 +696,7 @@ async function start() {
|
|
|
521
696
|
|
|
522
697
|
if (!existsSync(serverEntry)) {
|
|
523
698
|
console.error(`
|
|
524
|
-
what start
|
|
699
|
+
what start: no server.js found in ${cwd}
|
|
525
700
|
|
|
526
701
|
Full-stack apps run from a server.js (Node adapter + ISR engine). Scaffold one
|
|
527
702
|
with \`npm create what@latest -- --fullstack\`, or create server.js wiring
|
|
@@ -683,11 +858,13 @@ function resolvePageFile(pathname, config) {
|
|
|
683
858
|
}
|
|
684
859
|
|
|
685
860
|
function fileToRoute(filepath) {
|
|
686
|
-
|
|
861
|
+
const route = '/' + filepath
|
|
862
|
+
.split(sep).join('/')
|
|
687
863
|
.replace(/\.js$/, '')
|
|
688
|
-
.replace(
|
|
864
|
+
.replace(/\[\.\.\.(\w+)\]/g, '*')
|
|
689
865
|
.replace(/\[(\w+)\]/g, ':$1')
|
|
690
|
-
.replace(
|
|
866
|
+
.replace(/(^|\/)index$/, '');
|
|
867
|
+
return route.length > 1 && route.endsWith('/') ? route.slice(0, -1) : route;
|
|
691
868
|
}
|
|
692
869
|
|
|
693
870
|
async function renderDevPage(pagePath, pathname, config) {
|
|
@@ -791,36 +968,144 @@ function injectDevClient(html) {
|
|
|
791
968
|
return html.replace('</body>', devScript + '\n</body>');
|
|
792
969
|
}
|
|
793
970
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
971
|
+
// The runtime lives in the installed packages, NOT at a path relative to this
|
|
972
|
+
// file: from node_modules/what-framework-cli/src, `../../what/src` points at a
|
|
973
|
+
// package name that was never published. Resolve by package name instead, from
|
|
974
|
+
// the CLI, from what-framework itself (pnpm-style trees), then from the project.
|
|
975
|
+
var _runtimeDirs = null;
|
|
976
|
+
function resolveRuntimeDirs() {
|
|
977
|
+
if (_runtimeDirs) return _runtimeDirs;
|
|
978
|
+
|
|
979
|
+
const cliRequire = createRequire(import.meta.url);
|
|
980
|
+
const requires = [cliRequire];
|
|
981
|
+
try { requires.push(createRequire(cliRequire.resolve('what-framework'))); } catch { /* not resolvable here */ }
|
|
982
|
+
try { requires.push(createRequire(join(cwd, 'package.json'))); } catch { /* no project package.json */ }
|
|
983
|
+
|
|
984
|
+
const dirs = {};
|
|
985
|
+
const missing = [];
|
|
986
|
+
for (const [key, name] of [['core', 'what-core'], ['router', 'what-router'], ['server', 'what-server']]) {
|
|
987
|
+
for (const req of requires) {
|
|
988
|
+
try { dirs[key] = join(req.resolve(name), '..'); break; } catch { /* try the next root */ }
|
|
989
|
+
}
|
|
990
|
+
if (!dirs[key]) missing.push(name);
|
|
991
|
+
}
|
|
992
|
+
if (missing.length > 0) return { missing };
|
|
799
993
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
'router.js': join(whatDir, 'router.js'),
|
|
804
|
-
'server.js': join(whatDir, 'server.js'),
|
|
805
|
-
'islands.js': join(serverDir, 'islands.js'),
|
|
806
|
-
};
|
|
994
|
+
_runtimeDirs = dirs;
|
|
995
|
+
return dirs;
|
|
996
|
+
}
|
|
807
997
|
|
|
808
|
-
|
|
998
|
+
// Same resolution, but a missing runtime is fatal: every command that needs it
|
|
999
|
+
// produces a broken artifact or a 404ing dev server without it.
|
|
1000
|
+
function requireRuntimeDirs(commandName) {
|
|
1001
|
+
const dirs = resolveRuntimeDirs();
|
|
1002
|
+
if (dirs.missing) {
|
|
1003
|
+
console.error(`
|
|
1004
|
+
${commandName}: could not locate the What Framework runtime (${dirs.missing.join(', ')})
|
|
1005
|
+
|
|
1006
|
+
Install the framework alongside the CLI: npm install what-framework
|
|
1007
|
+
`);
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
return dirs;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Entry modules served/emitted under /@what/. Each package's sources are copied
|
|
1014
|
+
// into /@what/<pkg>/, so the entries are one-line re-export shims.
|
|
1015
|
+
const RUNTIME_ENTRIES = {
|
|
1016
|
+
'core.js': "export * from './core/index.js';",
|
|
1017
|
+
'reactive.js': "export * from './core/reactive.js';",
|
|
1018
|
+
'router.js': "export * from './router/index.js';",
|
|
1019
|
+
'server.js': "export * from './server/index.js';\nexport * from './server/islands.js';",
|
|
1020
|
+
'islands.js': "export * from './server/islands.js';",
|
|
1021
|
+
'jsx-runtime.js': "export * from './core/jsx-runtime.js';",
|
|
1022
|
+
'jsx-dev-runtime.js': "export * from './core/jsx-dev-runtime.js';",
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// App-facing specifier -> served URL. 'what' is the pre-0.11 package name, kept
|
|
1026
|
+
// so older apps keep building.
|
|
1027
|
+
const IMPORT_MAP = {
|
|
1028
|
+
'what-framework': '/@what/core.js',
|
|
1029
|
+
'what-framework/router': '/@what/router.js',
|
|
1030
|
+
'what-framework/server': '/@what/server.js',
|
|
1031
|
+
'what-framework/jsx-runtime': '/@what/jsx-runtime.js',
|
|
1032
|
+
'what-framework/jsx-dev-runtime': '/@what/jsx-dev-runtime.js',
|
|
1033
|
+
'what-core': '/@what/core.js',
|
|
1034
|
+
'what-router': '/@what/router.js',
|
|
1035
|
+
'what-server': '/@what/server.js',
|
|
1036
|
+
'what-server/islands': '/@what/islands.js',
|
|
1037
|
+
'what': '/@what/core.js',
|
|
1038
|
+
'what/router': '/@what/router.js',
|
|
1039
|
+
'what/server': '/@what/server.js',
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const SPECIFIER_RE = /(from\s*|import\s*\(\s*)(['"])([^'"]+)\2/g;
|
|
1043
|
+
|
|
1044
|
+
function resolveFrameworkModule(name, runtimeDirs) {
|
|
1045
|
+
if (RUNTIME_ENTRIES[name]) return RUNTIME_ENTRIES[name];
|
|
1046
|
+
|
|
1047
|
+
const slash = name.indexOf('/');
|
|
1048
|
+
if (slash === -1) return null;
|
|
1049
|
+
const dir = runtimeDirs[name.slice(0, slash)];
|
|
1050
|
+
if (!dir) return null;
|
|
1051
|
+
const file = safePath(dir, '/' + name.slice(slash + 1));
|
|
1052
|
+
if (!file || !existsSync(file) || extname(file) !== '.js') return null;
|
|
1053
|
+
|
|
1054
|
+
return rewriteRuntimeImports(readFileSync(file, 'utf-8'), name.split('/').length - 1);
|
|
809
1055
|
}
|
|
810
1056
|
|
|
811
1057
|
function transformImports(code) {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1058
|
+
return code.replace(SPECIFIER_RE, (match, prefix, quote, spec) => {
|
|
1059
|
+
const mapped = IMPORT_MAP[spec];
|
|
1060
|
+
return mapped ? `${prefix}${quote}${mapped}${quote}` : match;
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Rewrites the runtime's own cross-package imports ('what-core',
|
|
1065
|
+
// 'what-server/islands', ...) to relative paths inside /@what/. `depth` is how
|
|
1066
|
+
// many directories below /@what/ the importing file sits.
|
|
1067
|
+
function rewriteRuntimeImports(code, depth) {
|
|
1068
|
+
const prefix = depth > 0 ? '../'.repeat(depth) : './';
|
|
1069
|
+
return code.replace(SPECIFIER_RE, (match, p, quote, spec) => {
|
|
1070
|
+
const target = runtimeTarget(spec);
|
|
1071
|
+
return target ? `${p}${quote}${prefix}${target}${quote}` : match;
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function runtimeTarget(spec) {
|
|
1076
|
+
if (spec === 'what-framework') return 'core/index.js';
|
|
1077
|
+
if (spec.startsWith('what-framework/')) {
|
|
1078
|
+
const sub = spec.slice('what-framework/'.length);
|
|
1079
|
+
if (sub === 'router' || sub === 'server') return `${sub}/index.js`;
|
|
1080
|
+
return `core/${sub}.js`;
|
|
1081
|
+
}
|
|
1082
|
+
const m = /^what-(core|router|server)(?:\/(.+))?$/.exec(spec);
|
|
1083
|
+
return m ? `${m[1]}/${m[2] || 'index'}.js` : null;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function findBareSpecifiers(dir) {
|
|
1087
|
+
const found = [];
|
|
1088
|
+
for (const file of collectFiles(dir)) {
|
|
1089
|
+
if (extname(file) !== '.js') continue;
|
|
1090
|
+
const code = readFileSync(file, 'utf-8');
|
|
1091
|
+
const re = /(?:^|[;{}\n])\s*(?:import|export)\b[^;'"\n]*from\s*(['"])([^'"]+)\1/g;
|
|
1092
|
+
let m;
|
|
1093
|
+
while ((m = re.exec(code)) !== null) {
|
|
1094
|
+
const spec = m[2];
|
|
1095
|
+
if (/^[./]/.test(spec) || /^(https?:|node:|data:)/.test(spec)) continue;
|
|
1096
|
+
found.push({ file: relative(dir, file), spec });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return found;
|
|
817
1100
|
}
|
|
818
1101
|
|
|
819
1102
|
function minifyJS(code) {
|
|
820
|
-
// Lightweight minification: strip comments, collapse whitespace
|
|
1103
|
+
// Lightweight minification: strip comments, collapse whitespace.
|
|
1104
|
+
// Line comments are only stripped at line start, and an inline `//` is usually a
|
|
1105
|
+
// URL inside a string ('http://www.w3.org/2000/svg').
|
|
821
1106
|
return code
|
|
822
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
823
|
-
.replace(
|
|
1107
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
|
|
1108
|
+
.replace(/^[ \t]*\/\/[^\n]*$/gm, '') // line comments
|
|
824
1109
|
.replace(/^\s+/gm, '') // leading whitespace
|
|
825
1110
|
.replace(/\n\s*\n/g, '\n') // empty lines
|
|
826
1111
|
.trim();
|
|
@@ -844,83 +1129,30 @@ function addHash(filename, hash) {
|
|
|
844
1129
|
return `${base}.${hash}${ext}`;
|
|
845
1130
|
}
|
|
846
1131
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
const routerDir = resolve(__dirname, '../../router/src');
|
|
852
|
-
const serverDir = resolve(__dirname, '../../server/src');
|
|
1132
|
+
// Copies the framework runtime into the output. Runtime files are NOT
|
|
1133
|
+
// content-hashed: app code imports them by their stable /@what/*.js URL, so a
|
|
1134
|
+
// hashed name would leave that import pointing at a file that does not exist.
|
|
1135
|
+
function bundleRuntime(outDir, runtimeDirs) {
|
|
853
1136
|
const runtimeDir = join(outDir, '@what');
|
|
854
1137
|
mkdirSync(runtimeDir, { recursive: true });
|
|
855
1138
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
// Bundle main entry point
|
|
864
|
-
const whatFiles = [
|
|
865
|
-
{ src: join(whatDir, 'index.js'), out: 'core.js' },
|
|
866
|
-
{ src: join(whatDir, 'router.js'), out: 'router.js' },
|
|
867
|
-
{ src: join(whatDir, 'server.js'), out: 'server.js' },
|
|
868
|
-
];
|
|
869
|
-
|
|
870
|
-
for (const { src, out } of whatFiles) {
|
|
871
|
-
if (existsSync(src)) {
|
|
872
|
-
let code = readFileSync(src, 'utf-8');
|
|
873
|
-
code = minifyJS(code);
|
|
874
|
-
let outName = out;
|
|
875
|
-
|
|
876
|
-
if (useHash) {
|
|
877
|
-
const hash = contentHash(code);
|
|
878
|
-
const hashedName = addHash(outName, hash);
|
|
879
|
-
hashManifest[`@what/${outName}`] = `@what/${hashedName}`;
|
|
880
|
-
outName = hashedName;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
writeFileSync(join(runtimeDir, outName), code);
|
|
884
|
-
const gzipped = gzipSync(code);
|
|
885
|
-
writeFileSync(join(runtimeDir, outName + '.gz'), gzipped);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Bundle core modules
|
|
890
|
-
for (const mod of coreModules) {
|
|
891
|
-
const src = join(coreDir, mod);
|
|
892
|
-
if (existsSync(src)) {
|
|
893
|
-
let code = readFileSync(src, 'utf-8');
|
|
894
|
-
code = minifyJS(code);
|
|
895
|
-
let outName = mod;
|
|
1139
|
+
for (const [pkg, srcDir] of Object.entries(runtimeDirs)) {
|
|
1140
|
+
for (const src of collectFiles(srcDir)) {
|
|
1141
|
+
if (extname(src) !== '.js') continue;
|
|
1142
|
+
const rel = relative(srcDir, src);
|
|
1143
|
+
const outPath = join(runtimeDir, pkg, rel);
|
|
1144
|
+
mkdirSync(join(outPath, '..'), { recursive: true });
|
|
896
1145
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
outName = hashedName;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
writeFileSync(join(runtimeDir, outName), code);
|
|
905
|
-
const gzipped = gzipSync(code);
|
|
906
|
-
writeFileSync(join(runtimeDir, outName + '.gz'), gzipped);
|
|
1146
|
+
const depth = rel.split(sep).length;
|
|
1147
|
+
const code = minifyJS(rewriteRuntimeImports(readFileSync(src, 'utf-8'), depth));
|
|
1148
|
+
writeFileSync(outPath, code);
|
|
1149
|
+
writeFileSync(outPath + '.gz', gzipSync(code));
|
|
907
1150
|
}
|
|
908
1151
|
}
|
|
909
1152
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
let code = readFileSync(routerSrc, 'utf-8');
|
|
914
|
-
code = minifyJS(code);
|
|
915
|
-
writeFileSync(join(runtimeDir, 'router-impl.js'), code);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
// Bundle islands
|
|
919
|
-
const islandsSrc = join(serverDir, 'islands.js');
|
|
920
|
-
if (existsSync(islandsSrc)) {
|
|
921
|
-
let code = readFileSync(islandsSrc, 'utf-8');
|
|
922
|
-
code = minifyJS(code);
|
|
923
|
-
writeFileSync(join(runtimeDir, 'islands.js'), code);
|
|
1153
|
+
for (const [name, code] of Object.entries(RUNTIME_ENTRIES)) {
|
|
1154
|
+
writeFileSync(join(runtimeDir, name), code + '\n');
|
|
1155
|
+
writeFileSync(join(runtimeDir, name + '.gz'), gzipSync(code + '\n'));
|
|
924
1156
|
}
|
|
925
1157
|
}
|
|
926
1158
|
|