create-zoijs 0.1.4 → 0.2.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/CHANGELOG.md +127 -88
- package/LICENSE +21 -21
- package/README.md +98 -97
- package/bin/create-zoijs.js +197 -171
- package/core-cdn.json +36 -0
- package/package.json +44 -43
- package/templates/app/README.md +68 -48
- package/templates/app/_gitignore +1 -1
- package/templates/app/dev-server.mjs +124 -74
- package/templates/app/index.html +24 -22
- package/templates/app/package.json +12 -12
- package/templates/app/src/app.js +98 -98
- package/templates/app/src/components/Header.js +16 -16
- package/templates/app/src/components/StatCard.js +13 -13
- package/templates/app/src/components/TaskItem.js +18 -18
- package/templates/app/src/style.css +241 -241
- package/templates/basic/README.md +50 -30
- package/templates/basic/_gitignore +1 -1
- package/templates/basic/dev-server.mjs +124 -74
- package/templates/basic/index.html +24 -22
- package/templates/basic/package.json +12 -12
- package/templates/basic/src/app.js +18 -18
- package/templates/basic/src/style.css +33 -33
- package/templates/library/package.json +2 -2
- package/templates/minimal/README.md +29 -8
- package/templates/minimal/dev-server.mjs +124 -0
- package/templates/minimal/index.html +5 -3
- package/templates/minimal/package.json +9 -0
- package/templates/typescript/README.md +20 -0
- package/templates/typescript/_gitignore +1 -1
- package/templates/typescript/dev-server.mjs +124 -74
- package/templates/typescript/index.html +24 -22
- package/templates/typescript/package.json +1 -1
- package/templates/typescript/src/style.css +33 -33
|
@@ -1,74 +1,124 @@
|
|
|
1
|
-
// dev-server.mjs — a tiny static file server for this Zoijs app.
|
|
2
|
-
//
|
|
3
|
-
// Zero dependencies (Node built-ins only), no build step, no bundler. It serves
|
|
4
|
-
// the project files
|
|
5
|
-
// node_modules. Tries port 7310, then
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
1
|
+
// dev-server.mjs — a tiny static file server for this Zoijs app. DEVELOPMENT ONLY.
|
|
2
|
+
//
|
|
3
|
+
// Zero dependencies (Node built-ins only), no build step, no bundler. It serves
|
|
4
|
+
// the project files — index.html, your modules, and whatever its import map points
|
|
5
|
+
// at inside this folder (node_modules/, if you installed). Tries port 7310, then
|
|
6
|
+
// 7311 / 7312 / 7313 if one is busy. The same file ships in every Zoijs template.
|
|
7
|
+
//
|
|
8
|
+
// node dev-server.mjs (this is what `npm run dev` runs)
|
|
9
|
+
//
|
|
10
|
+
// Safe defaults for a dev server:
|
|
11
|
+
// - listens on 127.0.0.1 only — other devices on your network can't reach it.
|
|
12
|
+
// To test from a phone on your LAN, opt in explicitly: ZOIJS_HOST=0.0.0.0 npm run dev
|
|
13
|
+
// (anyone on that network can then read your project files while it runs).
|
|
14
|
+
// - never serves dotfiles or dot-directories (.env, .git/, .npmrc, …);
|
|
15
|
+
// - never serves anything outside this folder (no `..`, no symlinks pointing out —
|
|
16
|
+
// except package-manager links inside node_modules/);
|
|
17
|
+
// - answers malformed URLs with 400 instead of crashing.
|
|
18
|
+
// It is not a production server: deploy the static files to a real host.
|
|
19
|
+
|
|
20
|
+
import { createServer } from "node:http";
|
|
21
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
22
|
+
import { extname, resolve, relative, isAbsolute } from "node:path";
|
|
23
|
+
|
|
24
|
+
const PORTS = [7310, 7311, 7312, 7313];
|
|
25
|
+
const HOST = process.env.ZOIJS_HOST || "127.0.0.1";
|
|
26
|
+
const ROOT = resolve(process.cwd());
|
|
27
|
+
|
|
28
|
+
const TYPES = {
|
|
29
|
+
".html": "text/html; charset=utf-8",
|
|
30
|
+
".js": "text/javascript; charset=utf-8",
|
|
31
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
32
|
+
".css": "text/css; charset=utf-8",
|
|
33
|
+
".json": "application/json; charset=utf-8",
|
|
34
|
+
".svg": "image/svg+xml",
|
|
35
|
+
".ico": "image/x-icon",
|
|
36
|
+
".png": "image/png",
|
|
37
|
+
".woff2": "font/woff2",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const HEADERS = { "X-Content-Type-Options": "nosniff", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" };
|
|
41
|
+
|
|
42
|
+
// Is `path` ROOT itself or inside it? (relative() handles separators, case and
|
|
43
|
+
// sibling folders like `app-secrets` correctly — unlike a string-prefix check.)
|
|
44
|
+
const inside = (base, path) => {
|
|
45
|
+
const rel = relative(base, path);
|
|
46
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function send(res, status, body, type = "text/plain; charset=utf-8") {
|
|
50
|
+
res.writeHead(status, { ...HEADERS, "Content-Type": type });
|
|
51
|
+
res.end(body);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const server = createServer(async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
let pathname;
|
|
57
|
+
try {
|
|
58
|
+
// Only the path (never the query string) maps to a file.
|
|
59
|
+
pathname = decodeURIComponent(new URL(req.url, "http://localhost").pathname);
|
|
60
|
+
} catch {
|
|
61
|
+
return send(res, 400, "Bad request");
|
|
62
|
+
}
|
|
63
|
+
if (pathname.includes("\0")) return send(res, 400, "Bad request");
|
|
64
|
+
if (pathname === "/") pathname = "/index.html";
|
|
65
|
+
|
|
66
|
+
const segments = pathname.split(/[\\/]+/).filter(Boolean);
|
|
67
|
+
// Hidden files and folders (.env, .git/, .npmrc, node_modules/.bin, …) are never
|
|
68
|
+
// served. 404, not 403, so it doesn't reveal whether they exist.
|
|
69
|
+
if (segments.some((s) => s.startsWith("."))) return send(res, 404, "Not found");
|
|
70
|
+
|
|
71
|
+
const filePath = resolve(ROOT, ...segments);
|
|
72
|
+
if (!inside(ROOT, filePath)) return send(res, 404, "Not found");
|
|
73
|
+
|
|
74
|
+
// Don't follow symlinks out of the project. Package managers (npm link, pnpm)
|
|
75
|
+
// legitimately link packages into node_modules/, so links there are allowed.
|
|
76
|
+
let real;
|
|
77
|
+
try {
|
|
78
|
+
real = await realpath(filePath);
|
|
79
|
+
} catch {
|
|
80
|
+
return send(res, 404, "Not found");
|
|
81
|
+
}
|
|
82
|
+
if (!inside(ROOT, real) && segments[0] !== "node_modules") return send(res, 404, "Not found");
|
|
83
|
+
|
|
84
|
+
const body = await readFile(real);
|
|
85
|
+
send(res, 200, body, TYPES[extname(filePath)] || "application/octet-stream");
|
|
86
|
+
} catch {
|
|
87
|
+
// Directories, permission errors, files deleted mid-request: a generic answer
|
|
88
|
+
// that never echoes a filesystem path.
|
|
89
|
+
if (!res.headersSent) send(res, 404, "Not found");
|
|
90
|
+
else res.destroy();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function start(i = 0) {
|
|
95
|
+
if (i >= PORTS.length) {
|
|
96
|
+
console.error(`\nAll dev ports are busy (${PORTS.join(", ")}). Free one and try again.\n`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
// Paired once-listeners: each attempt removes its sibling so a failed bind
|
|
100
|
+
// can't leave a stale "listening" handler that prints the wrong port later.
|
|
101
|
+
const onError = (err) => {
|
|
102
|
+
server.removeListener("listening", onListening);
|
|
103
|
+
if (err.code === "EADDRINUSE") start(i + 1);
|
|
104
|
+
else {
|
|
105
|
+
console.error(err);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const onListening = () => {
|
|
110
|
+
server.removeListener("error", onError);
|
|
111
|
+
const lines = ["", " Zoijs dev server", "", ` - Local: http://localhost:${PORTS[i]}`];
|
|
112
|
+
if (i > 0) lines.push(` (ports ${PORTS.slice(0, i).join(", ")} were busy)`);
|
|
113
|
+
if (HOST !== "127.0.0.1" && HOST !== "localhost" && HOST !== "::1") {
|
|
114
|
+
lines.push(` ⚠ Listening on ${HOST} (ZOIJS_HOST): other devices on your network can read`, ` this project's files while the server runs. Development only.`);
|
|
115
|
+
}
|
|
116
|
+
lines.push("");
|
|
117
|
+
console.log(lines.join("\n"));
|
|
118
|
+
};
|
|
119
|
+
server.once("error", onError);
|
|
120
|
+
server.once("listening", onListening);
|
|
121
|
+
server.listen(PORTS[i], HOST);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
start();
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>{{APP_TITLE}}</title>
|
|
7
|
-
<link rel="stylesheet" href="./src/style.css" />
|
|
8
|
-
<!-- No build step: an import map points the @zoijs/core name at the package
|
|
9
|
-
installed in node_modules. `npm install` puts it there. -->
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>{{APP_TITLE}}</title>
|
|
7
|
+
<link rel="stylesheet" href="./src/style.css" />
|
|
8
|
+
<!-- No build step: an import map points the @zoijs/core name at the package
|
|
9
|
+
installed in node_modules. `npm install` puts it there. -->
|
|
10
|
+
<!-- Development entry (warnings + devtools). When you deploy, point "@zoijs/core"
|
|
11
|
+
at ./node_modules/@zoijs/core/src/prod.js — the production entry. -->
|
|
12
|
+
<script type="importmap">
|
|
13
|
+
{
|
|
14
|
+
"imports": {
|
|
15
|
+
"@zoijs/core": "./node_modules/@zoijs/core/src/index.js"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<div id="app"></div>
|
|
22
|
+
<script type="module" src="./src/app.js"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "{{APP_NAME}}",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "node dev-server.mjs"
|
|
8
|
-
},
|
|
9
|
-
"dependencies": {
|
|
10
|
-
"@zoijs/core": "^
|
|
11
|
-
}
|
|
12
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "{{APP_NAME}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "node dev-server.mjs"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@zoijs/core": "^{{ZOIJS_CORE_VERSION}}"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { html, mount, createState } from "@zoijs/core";
|
|
2
|
-
|
|
3
|
-
// A complete Zoijs app: state, markup, and one mount call. No build step.
|
|
4
|
-
function App() {
|
|
5
|
-
const count = createState(0);
|
|
6
|
-
|
|
7
|
-
return html`
|
|
8
|
-
<main>
|
|
9
|
-
<h1>{{APP_TITLE}}</h1>
|
|
10
|
-
<p>Edit <code>src/app.js</code> and reload.</p>
|
|
11
|
-
<button onclick=${() => count.set(count.get() + 1)}>
|
|
12
|
-
Clicked ${() => count.get()} times
|
|
13
|
-
</button>
|
|
14
|
-
</main>
|
|
15
|
-
`;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
mount(App, "#app");
|
|
1
|
+
import { html, mount, createState } from "@zoijs/core";
|
|
2
|
+
|
|
3
|
+
// A complete Zoijs app: state, markup, and one mount call. No build step.
|
|
4
|
+
function App() {
|
|
5
|
+
const count = createState(0);
|
|
6
|
+
|
|
7
|
+
return html`
|
|
8
|
+
<main>
|
|
9
|
+
<h1>{{APP_TITLE}}</h1>
|
|
10
|
+
<p>Edit <code>src/app.js</code> and reload.</p>
|
|
11
|
+
<button onclick=${() => count.set(count.get() + 1)}>
|
|
12
|
+
Clicked ${() => count.get()} times
|
|
13
|
+
</button>
|
|
14
|
+
</main>
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
mount(App, "#app");
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
color-scheme: light dark;
|
|
3
|
-
font-family: system-ui, sans-serif;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
body {
|
|
7
|
-
margin: 0;
|
|
8
|
-
display: grid;
|
|
9
|
-
place-items: center;
|
|
10
|
-
min-height: 100vh;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
main {
|
|
14
|
-
text-align: center;
|
|
15
|
-
padding: 2rem;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
h1 {
|
|
19
|
-
margin: 0 0 0.5rem;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
button {
|
|
23
|
-
font: inherit;
|
|
24
|
-
padding: 0.6rem 1.2rem;
|
|
25
|
-
border: 1px solid currentColor;
|
|
26
|
-
border-radius: 8px;
|
|
27
|
-
background: transparent;
|
|
28
|
-
cursor: pointer;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
button:hover {
|
|
32
|
-
background: rgba(127, 127, 127, 0.12);
|
|
33
|
-
}
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
font-family: system-ui, sans-serif;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
body {
|
|
7
|
+
margin: 0;
|
|
8
|
+
display: grid;
|
|
9
|
+
place-items: center;
|
|
10
|
+
min-height: 100vh;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
main {
|
|
14
|
+
text-align: center;
|
|
15
|
+
padding: 2rem;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
h1 {
|
|
19
|
+
margin: 0 0 0.5rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
button {
|
|
23
|
+
font: inherit;
|
|
24
|
+
padding: 0.6rem 1.2rem;
|
|
25
|
+
border: 1px solid currentColor;
|
|
26
|
+
border-radius: 8px;
|
|
27
|
+
background: transparent;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
button:hover {
|
|
32
|
+
background: rgba(127, 127, 127, 0.12);
|
|
33
|
+
}
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"peerDependencies": {
|
|
32
|
-
"@zoijs/core": "^
|
|
32
|
+
"@zoijs/core": "^{{ZOIJS_CORE_VERSION}}"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@zoijs/core": "^
|
|
35
|
+
"@zoijs/core": "^{{ZOIJS_CORE_VERSION}}",
|
|
36
36
|
"typescript": "^5.9.3"
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
# {{APP_TITLE}}
|
|
2
2
|
|
|
3
|
-
The smallest possible [Zoijs](https://zoijs.dev) app — **two files, no install,
|
|
4
|
-
build step.** A blank canvas.
|
|
3
|
+
The smallest possible [Zoijs](https://zoijs.dev) app — **two source files, no install,
|
|
4
|
+
no build step.** A blank canvas.
|
|
5
5
|
|
|
6
6
|
## Run it
|
|
7
7
|
|
|
8
|
-
Any static file server works. The simplest:
|
|
9
|
-
|
|
10
8
|
```bash
|
|
11
|
-
|
|
9
|
+
npm run dev
|
|
12
10
|
```
|
|
13
11
|
|
|
14
|
-
Then open <http://localhost:7310>.
|
|
12
|
+
Then open <http://localhost:7310>. No `npm install` needed: `@zoijs/core` comes from the
|
|
13
|
+
CDN, and `npm run dev` just runs `node dev-server.mjs` — a tiny static server that
|
|
14
|
+
ships in this folder (Node built-ins only, nothing is downloaded). It is for
|
|
15
|
+
**development only**: it listens on `127.0.0.1`, never serves dotfiles (`.env`, `.git/`)
|
|
16
|
+
or anything outside this folder, and falls back to 7311–7313 if 7310 is busy. To try the
|
|
17
|
+
app from a phone on your network, opt in with `ZOIJS_HOST=0.0.0.0 npm run dev` (anyone
|
|
18
|
+
on that network can then read these files while it runs).
|
|
19
|
+
|
|
20
|
+
Any other static file server works too, but `file://` doesn't — browsers won't load
|
|
21
|
+
ES modules from disk.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Deploy
|
|
25
|
+
|
|
26
|
+
In `index.html`, point `"@zoijs/core"` at the production entry —
|
|
27
|
+
`{{ZOIJS_CORE_CDN}}prod.js` (its integrity hash is already in the map) — so
|
|
28
|
+
development warnings and the devtools hook are off. Keep the exact version; to upgrade,
|
|
29
|
+
change it deliberately and update every URL and integrity hash together.
|
|
30
|
+
|
|
31
|
+
Before going live, go through the
|
|
32
|
+
[production security checklist](https://zoijs.dev/production-security) — your CSP must
|
|
33
|
+
allow `https://cdn.jsdelivr.net` and your import map's hash.
|
|
15
34
|
|
|
16
35
|
## What's here
|
|
17
36
|
|
|
18
|
-
- `index.html` — loads `@zoijs/core` from
|
|
19
|
-
mounts `app.js`.
|
|
37
|
+
- `index.html` — loads `@zoijs/core` {{ZOIJS_CORE_VERSION}} from jsDelivr (that exact version,
|
|
38
|
+
integrity-checked) and mounts `app.js`.
|
|
20
39
|
- `app.js` — a counter: state, markup, and one `mount` call.
|
|
40
|
+
- `dev-server.mjs` + `package.json` — the local development server and its `dev` script
|
|
41
|
+
(no dependencies). Don't deploy them; deploy `index.html` and `app.js`.
|
|
21
42
|
|
|
22
43
|
That's the whole project. Edit `app.js` and reload — there is nothing to build.
|
|
23
44
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// dev-server.mjs — a tiny static file server for this Zoijs app. DEVELOPMENT ONLY.
|
|
2
|
+
//
|
|
3
|
+
// Zero dependencies (Node built-ins only), no build step, no bundler. It serves
|
|
4
|
+
// the project files — index.html, your modules, and whatever its import map points
|
|
5
|
+
// at inside this folder (node_modules/, if you installed). Tries port 7310, then
|
|
6
|
+
// 7311 / 7312 / 7313 if one is busy. The same file ships in every Zoijs template.
|
|
7
|
+
//
|
|
8
|
+
// node dev-server.mjs (this is what `npm run dev` runs)
|
|
9
|
+
//
|
|
10
|
+
// Safe defaults for a dev server:
|
|
11
|
+
// - listens on 127.0.0.1 only — other devices on your network can't reach it.
|
|
12
|
+
// To test from a phone on your LAN, opt in explicitly: ZOIJS_HOST=0.0.0.0 npm run dev
|
|
13
|
+
// (anyone on that network can then read your project files while it runs).
|
|
14
|
+
// - never serves dotfiles or dot-directories (.env, .git/, .npmrc, …);
|
|
15
|
+
// - never serves anything outside this folder (no `..`, no symlinks pointing out —
|
|
16
|
+
// except package-manager links inside node_modules/);
|
|
17
|
+
// - answers malformed URLs with 400 instead of crashing.
|
|
18
|
+
// It is not a production server: deploy the static files to a real host.
|
|
19
|
+
|
|
20
|
+
import { createServer } from "node:http";
|
|
21
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
22
|
+
import { extname, resolve, relative, isAbsolute } from "node:path";
|
|
23
|
+
|
|
24
|
+
const PORTS = [7310, 7311, 7312, 7313];
|
|
25
|
+
const HOST = process.env.ZOIJS_HOST || "127.0.0.1";
|
|
26
|
+
const ROOT = resolve(process.cwd());
|
|
27
|
+
|
|
28
|
+
const TYPES = {
|
|
29
|
+
".html": "text/html; charset=utf-8",
|
|
30
|
+
".js": "text/javascript; charset=utf-8",
|
|
31
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
32
|
+
".css": "text/css; charset=utf-8",
|
|
33
|
+
".json": "application/json; charset=utf-8",
|
|
34
|
+
".svg": "image/svg+xml",
|
|
35
|
+
".ico": "image/x-icon",
|
|
36
|
+
".png": "image/png",
|
|
37
|
+
".woff2": "font/woff2",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const HEADERS = { "X-Content-Type-Options": "nosniff", "Cache-Control": "no-store", "Referrer-Policy": "no-referrer" };
|
|
41
|
+
|
|
42
|
+
// Is `path` ROOT itself or inside it? (relative() handles separators, case and
|
|
43
|
+
// sibling folders like `app-secrets` correctly — unlike a string-prefix check.)
|
|
44
|
+
const inside = (base, path) => {
|
|
45
|
+
const rel = relative(base, path);
|
|
46
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function send(res, status, body, type = "text/plain; charset=utf-8") {
|
|
50
|
+
res.writeHead(status, { ...HEADERS, "Content-Type": type });
|
|
51
|
+
res.end(body);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const server = createServer(async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
let pathname;
|
|
57
|
+
try {
|
|
58
|
+
// Only the path (never the query string) maps to a file.
|
|
59
|
+
pathname = decodeURIComponent(new URL(req.url, "http://localhost").pathname);
|
|
60
|
+
} catch {
|
|
61
|
+
return send(res, 400, "Bad request");
|
|
62
|
+
}
|
|
63
|
+
if (pathname.includes("\0")) return send(res, 400, "Bad request");
|
|
64
|
+
if (pathname === "/") pathname = "/index.html";
|
|
65
|
+
|
|
66
|
+
const segments = pathname.split(/[\\/]+/).filter(Boolean);
|
|
67
|
+
// Hidden files and folders (.env, .git/, .npmrc, node_modules/.bin, …) are never
|
|
68
|
+
// served. 404, not 403, so it doesn't reveal whether they exist.
|
|
69
|
+
if (segments.some((s) => s.startsWith("."))) return send(res, 404, "Not found");
|
|
70
|
+
|
|
71
|
+
const filePath = resolve(ROOT, ...segments);
|
|
72
|
+
if (!inside(ROOT, filePath)) return send(res, 404, "Not found");
|
|
73
|
+
|
|
74
|
+
// Don't follow symlinks out of the project. Package managers (npm link, pnpm)
|
|
75
|
+
// legitimately link packages into node_modules/, so links there are allowed.
|
|
76
|
+
let real;
|
|
77
|
+
try {
|
|
78
|
+
real = await realpath(filePath);
|
|
79
|
+
} catch {
|
|
80
|
+
return send(res, 404, "Not found");
|
|
81
|
+
}
|
|
82
|
+
if (!inside(ROOT, real) && segments[0] !== "node_modules") return send(res, 404, "Not found");
|
|
83
|
+
|
|
84
|
+
const body = await readFile(real);
|
|
85
|
+
send(res, 200, body, TYPES[extname(filePath)] || "application/octet-stream");
|
|
86
|
+
} catch {
|
|
87
|
+
// Directories, permission errors, files deleted mid-request: a generic answer
|
|
88
|
+
// that never echoes a filesystem path.
|
|
89
|
+
if (!res.headersSent) send(res, 404, "Not found");
|
|
90
|
+
else res.destroy();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
function start(i = 0) {
|
|
95
|
+
if (i >= PORTS.length) {
|
|
96
|
+
console.error(`\nAll dev ports are busy (${PORTS.join(", ")}). Free one and try again.\n`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
// Paired once-listeners: each attempt removes its sibling so a failed bind
|
|
100
|
+
// can't leave a stale "listening" handler that prints the wrong port later.
|
|
101
|
+
const onError = (err) => {
|
|
102
|
+
server.removeListener("listening", onListening);
|
|
103
|
+
if (err.code === "EADDRINUSE") start(i + 1);
|
|
104
|
+
else {
|
|
105
|
+
console.error(err);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const onListening = () => {
|
|
110
|
+
server.removeListener("error", onError);
|
|
111
|
+
const lines = ["", " Zoijs dev server", "", ` - Local: http://localhost:${PORTS[i]}`];
|
|
112
|
+
if (i > 0) lines.push(` (ports ${PORTS.slice(0, i).join(", ")} were busy)`);
|
|
113
|
+
if (HOST !== "127.0.0.1" && HOST !== "localhost" && HOST !== "::1") {
|
|
114
|
+
lines.push(` ⚠ Listening on ${HOST} (ZOIJS_HOST): other devices on your network can read`, ` this project's files while the server runs. Development only.`);
|
|
115
|
+
}
|
|
116
|
+
lines.push("");
|
|
117
|
+
console.log(lines.join("\n"));
|
|
118
|
+
};
|
|
119
|
+
server.once("error", onError);
|
|
120
|
+
server.once("listening", onListening);
|
|
121
|
+
server.listen(PORTS[i], HOST);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
start();
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>{{APP_TITLE}}</title>
|
|
7
|
-
<!-- Zero install, zero build:
|
|
8
|
-
|
|
7
|
+
<!-- Zero install, zero build: @zoijs/core {{ZOIJS_CORE_VERSION}} from jsDelivr, pinned to that
|
|
8
|
+
exact version, with an integrity hash for every module file (the browser refuses
|
|
9
|
+
a file whose bytes don't match). If you use a strict CSP, allow
|
|
10
|
+
https://cdn.jsdelivr.net in script-src — or vendor the files instead. -->
|
|
9
11
|
<script type="importmap">
|
|
10
|
-
{
|
|
12
|
+
{{ZOIJS_CORE_IMPORTMAP}}
|
|
11
13
|
</script>
|
|
12
14
|
</head>
|
|
13
15
|
<body>
|
|
@@ -22,6 +22,26 @@ npm run dev
|
|
|
22
22
|
|
|
23
23
|
Open the printed URL (it falls back to 7311–7313 if the port is busy).
|
|
24
24
|
|
|
25
|
+
The dev server is for **development only**: it listens on `127.0.0.1` (this machine),
|
|
26
|
+
never serves dotfiles (`.env`, `.git/`, …) or anything outside the project folder, and is
|
|
27
|
+
not meant for hosting. To try the app from a phone on your network, opt in explicitly with
|
|
28
|
+
`ZOIJS_HOST=0.0.0.0 npm run dev` — while it runs, anyone on that network can read the
|
|
29
|
+
project's files.
|
|
30
|
+
|
|
31
|
+
## Deploy
|
|
32
|
+
|
|
33
|
+
This page loads the **development** entry of `@zoijs/core` (helpful warnings, devtools).
|
|
34
|
+
When you deploy, change the import map in `index.html` to the **production** entry —
|
|
35
|
+
same API, warnings and the devtools hook off:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
"@zoijs/core": "./node_modules/@zoijs/core/src/prod.js"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then deploy the files to a static host (not the dev server) and go through the
|
|
42
|
+
[production security checklist](https://zoijs.dev/production-security) — security headers,
|
|
43
|
+
a Content-Security-Policy with your import map's hash, CSRF, and more.
|
|
44
|
+
|
|
25
45
|
## Type-check
|
|
26
46
|
|
|
27
47
|
```bash
|
|
@@ -1 +1 @@
|
|
|
1
|
-
node_modules
|
|
1
|
+
node_modules
|