@zvndev/darkmown 0.4.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 +34 -0
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/docs/cli.md +51 -0
- package/docs/spec-alignment.md +60 -0
- package/package.json +50 -0
- package/site/_/feature-card.wd +7 -0
- package/site/_/features.json +14 -0
- package/site/_/logo.svg +1 -0
- package/site/_/nav.skin +84 -0
- package/site/_/nav.wd +8 -0
- package/site/_/quotes.json +6 -0
- package/site/_/sprint-loop.wd +11 -0
- package/site/_/team.json +7 -0
- package/site/pages/-draft.wd +3 -0
- package/site/pages/data.js +8 -0
- package/site/pages/data.skin +98 -0
- package/site/pages/data.wd +110 -0
- package/site/pages/docs/-relative-note.wd +5 -0
- package/site/pages/docs/.secret.wd +3 -0
- package/site/pages/docs/index.skin +39 -0
- package/site/pages/docs/index.wd +125 -0
- package/site/pages/index.js +10 -0
- package/site/pages/index.skin +134 -0
- package/site/pages/index.wd +64 -0
- package/site/pages/markdown.md +36 -0
- package/site/pages/markdown.skin +63 -0
- package/site/pages/reactive.skin +62 -0
- package/site/pages/reactive.wd +68 -0
- package/src/builder.js +87 -0
- package/src/cli.js +156 -0
- package/src/compiler.js +907 -0
- package/src/config.js +19 -0
- package/src/dev.js +30 -0
- package/src/router.js +51 -0
- package/src/runtime.js +222 -0
- package/src/scaffold.js +95 -0
- package/src/skin.js +68 -0
- package/src/statics.js +31 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Reactive Markdown
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
@include /nav.wd
|
|
6
|
+
|
|
7
|
+
<main>
|
|
8
|
+
|
|
9
|
+
# Reactive Markdown
|
|
10
|
+
|
|
11
|
+
This page opts into the runtime because it declares state. Static pages ship zero framework JavaScript.
|
|
12
|
+
|
|
13
|
+
:state count = 0
|
|
14
|
+
|
|
15
|
+
The count is { count }.
|
|
16
|
+
|
|
17
|
+
:button "Increment" -> count++
|
|
18
|
+
|
|
19
|
+
:state open = false
|
|
20
|
+
|
|
21
|
+
:button "Show details" -> open = true
|
|
22
|
+
:button "Hide details" -> open = false
|
|
23
|
+
|
|
24
|
+
:if open
|
|
25
|
+
The details are visible now.
|
|
26
|
+
:else
|
|
27
|
+
The details are hidden.
|
|
28
|
+
:endif
|
|
29
|
+
|
|
30
|
+
## Keyed loops over real objects
|
|
31
|
+
|
|
32
|
+
The same `@loop` that unrolls static data also drives reactive lists. Items are objects, bindings use dotted paths, and the runtime patches the list by key instead of re-rendering it:
|
|
33
|
+
|
|
34
|
+
:state todos = [{"id": 1, "title": "Route pages", "owner": "PM"}, {"id": 2, "title": "Ship tiny runtime", "owner": "Dev"}]
|
|
35
|
+
|
|
36
|
+
@loop todos into todo
|
|
37
|
+
- **{ todo.title }** — { todo.owner }
|
|
38
|
+
@endloop
|
|
39
|
+
|
|
40
|
+
:button "Add todo" -> todos += {"id": 3, "title": "Live compile", "owner": "Testing"}
|
|
41
|
+
|
|
42
|
+
## Section-scoped state
|
|
43
|
+
|
|
44
|
+
Two sections can each own a state value named `tally` without colliding. Buttons resolve to the nearest scope:
|
|
45
|
+
|
|
46
|
+
::: section #left .tally-card
|
|
47
|
+
:state tally = 0
|
|
48
|
+
|
|
49
|
+
### Left section
|
|
50
|
+
|
|
51
|
+
The left tally is { tally }.
|
|
52
|
+
|
|
53
|
+
:button "Bump left" -> tally++
|
|
54
|
+
:::
|
|
55
|
+
|
|
56
|
+
::: section #right .tally-card
|
|
57
|
+
:state tally = 10
|
|
58
|
+
|
|
59
|
+
### Right section
|
|
60
|
+
|
|
61
|
+
The right tally is { tally }.
|
|
62
|
+
|
|
63
|
+
:button "Bump right" -> tally++
|
|
64
|
+
:::
|
|
65
|
+
|
|
66
|
+
Static pages like the docs stay runtime-free.
|
|
67
|
+
|
|
68
|
+
</main>
|
package/src/builder.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { compilePage } from "./compiler.js";
|
|
5
|
+
import { createPaths } from "./config.js";
|
|
6
|
+
import { discoverRoutes, outputPathForRoute } from "./router.js";
|
|
7
|
+
import { compileSkin } from "./skin.js";
|
|
8
|
+
|
|
9
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
export function buildSite(cwd = process.cwd()) {
|
|
12
|
+
const paths = createPaths(cwd);
|
|
13
|
+
fs.rmSync(paths.distRoot, { recursive: true, force: true });
|
|
14
|
+
fs.mkdirSync(paths.distRoot, { recursive: true });
|
|
15
|
+
emitShelfAssets(paths);
|
|
16
|
+
const routes = discoverRoutes(paths.routesRoot);
|
|
17
|
+
const manifest = [];
|
|
18
|
+
|
|
19
|
+
const warned = new Set();
|
|
20
|
+
|
|
21
|
+
for (const route of routes) {
|
|
22
|
+
const page = compilePage(route.file, paths);
|
|
23
|
+
for (const warning of page.warnings || []) {
|
|
24
|
+
if (warned.has(warning)) continue;
|
|
25
|
+
warned.add(warning);
|
|
26
|
+
console.warn(`hint: ${warning}`);
|
|
27
|
+
}
|
|
28
|
+
const outFile = outputPathForRoute(paths.distRoot, route.route);
|
|
29
|
+
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
30
|
+
fs.writeFileSync(outFile, page.html);
|
|
31
|
+
emitAssets(page.assets, paths);
|
|
32
|
+
if (page.assets.runtime) emitRuntime(paths);
|
|
33
|
+
manifest.push({
|
|
34
|
+
route: route.route,
|
|
35
|
+
file: path.relative(cwd, route.file).replaceAll(path.sep, "/"),
|
|
36
|
+
assets: {
|
|
37
|
+
skins: [...page.assets.skins],
|
|
38
|
+
scripts: page.assets.runtime ? ["/__wd/runtime.js", ...page.assets.scripts] : [...page.assets.scripts],
|
|
39
|
+
runtime: page.assets.runtime
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fs.writeFileSync(path.join(paths.distRoot, "routes.json"), JSON.stringify(manifest, null, 2));
|
|
45
|
+
return { routes: manifest, distRoot: paths.distRoot };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function emitRuntime(paths) {
|
|
49
|
+
const out = path.join(paths.distRoot, "__wd/runtime.js");
|
|
50
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
51
|
+
fs.copyFileSync(path.join(moduleDir, "runtime.js"), out);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function emitAssets(assets, paths) {
|
|
55
|
+
for (const [source, href] of assets.files) {
|
|
56
|
+
const out = path.join(paths.distRoot, href);
|
|
57
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
58
|
+
if (source.endsWith(".skin")) {
|
|
59
|
+
fs.writeFileSync(out, compileSkin(fs.readFileSync(source, "utf8")));
|
|
60
|
+
} else {
|
|
61
|
+
fs.copyFileSync(source, out);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function emitShelfAssets(paths) {
|
|
67
|
+
if (!fs.existsSync(paths.shelfRoot)) return;
|
|
68
|
+
for (const file of walk(paths.shelfRoot)) {
|
|
69
|
+
const ext = path.extname(file);
|
|
70
|
+
if ([".md", ".wd"].includes(ext)) continue;
|
|
71
|
+
const rel = path.relative(paths.shelfRoot, file);
|
|
72
|
+
const folder = ext === ".json" ? "__wd/data" : "__wd/media";
|
|
73
|
+
const out = path.join(paths.distRoot, folder, rel);
|
|
74
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
75
|
+
fs.copyFileSync(file, out);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function walk(dir) {
|
|
80
|
+
const files = [];
|
|
81
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
82
|
+
const abs = path.join(dir, entry.name);
|
|
83
|
+
if (entry.isDirectory()) files.push(...walk(abs));
|
|
84
|
+
else files.push(abs);
|
|
85
|
+
}
|
|
86
|
+
return files;
|
|
87
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { buildSite } from "./builder.js";
|
|
8
|
+
import { devClientPath, devClientScript, devEventsPath, injectDevClient } from "./dev.js";
|
|
9
|
+
import { initProject } from "./scaffold.js";
|
|
10
|
+
import { contentType, resolvePublicFile, serve } from "./statics.js";
|
|
11
|
+
|
|
12
|
+
const cliPath = fileURLToPath(import.meta.url);
|
|
13
|
+
const command = process.argv[2] || "build";
|
|
14
|
+
|
|
15
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
16
|
+
printHelp();
|
|
17
|
+
} else if (command === "version" || command === "--version" || command === "-v") {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
19
|
+
console.log(pkg.version);
|
|
20
|
+
} else if (command === "init") {
|
|
21
|
+
const target = process.argv[3] || ".";
|
|
22
|
+
const result = initProject(path.resolve(process.cwd(), target));
|
|
23
|
+
console.log(`Created Darkmown project at ${path.relative(process.cwd(), result.root) || "."}`);
|
|
24
|
+
console.log(`Next: cd ${path.relative(process.cwd(), result.root) || "."} && npm install && npm run dev`);
|
|
25
|
+
} else if (command === "build") {
|
|
26
|
+
const result = buildSite();
|
|
27
|
+
console.log(`Built ${result.routes.length} routes into ${path.relative(process.cwd(), result.distRoot)}`);
|
|
28
|
+
} else if (command === "dev") {
|
|
29
|
+
const port = Number(process.env.PORT || 5173);
|
|
30
|
+
const distRoot = path.join(process.cwd(), "dist");
|
|
31
|
+
buildSite();
|
|
32
|
+
const clients = new Set();
|
|
33
|
+
let timer;
|
|
34
|
+
|
|
35
|
+
// Rebuild in a child process so changes to Darkmown's own src/ always load
|
|
36
|
+
// fresh modules — an in-process rebuild would reuse the stale import cache.
|
|
37
|
+
const rebuild = () => {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
timer = setTimeout(() => {
|
|
40
|
+
const started = performance.now();
|
|
41
|
+
execFile(process.execPath, [cliPath, "build"], { cwd: process.cwd() }, (error, stdout, stderr) => {
|
|
42
|
+
if (error) {
|
|
43
|
+
const message = (stderr || stdout || String(error)).trim();
|
|
44
|
+
console.error(message);
|
|
45
|
+
broadcast(clients, `event: builderror\ndata: ${JSON.stringify({ message: message.slice(0, 4000) })}\n\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const elapsed = Math.round(performance.now() - started);
|
|
49
|
+
if (stderr.trim()) console.warn(stderr.trim());
|
|
50
|
+
broadcast(clients, `event: reload\ndata: ${JSON.stringify({ elapsed })}\n\n`);
|
|
51
|
+
console.log(`${stdout.trim().split("\n").at(-1)} (${elapsed}ms)`);
|
|
52
|
+
});
|
|
53
|
+
}, 30);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
for (const dir of ["site", "src"]) {
|
|
57
|
+
if (fs.existsSync(dir)) {
|
|
58
|
+
fs.watch(dir, { recursive: true }, rebuild);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const server = http.createServer((req, res) => {
|
|
63
|
+
try {
|
|
64
|
+
const url = req.url || "/";
|
|
65
|
+
if (url.split("?")[0] === devEventsPath) {
|
|
66
|
+
res.writeHead(200, {
|
|
67
|
+
"content-type": "text/event-stream",
|
|
68
|
+
"cache-control": "no-cache",
|
|
69
|
+
connection: "keep-alive"
|
|
70
|
+
});
|
|
71
|
+
res.write("\n");
|
|
72
|
+
clients.add(res);
|
|
73
|
+
req.on("close", () => clients.delete(res));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (url.split("?")[0] === devClientPath) {
|
|
77
|
+
res.writeHead(200, { "content-type": "text/javascript" });
|
|
78
|
+
res.end(devClientScript());
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (url.split("?")[0] === "/__wd/echo" && req.method === "POST") {
|
|
82
|
+
let body = "";
|
|
83
|
+
req.on("data", (chunk) => (body += chunk));
|
|
84
|
+
req.on("end", () => {
|
|
85
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
86
|
+
res.end(JSON.stringify({
|
|
87
|
+
ok: true,
|
|
88
|
+
received: Object.fromEntries(new URLSearchParams(body)),
|
|
89
|
+
at: new Date().toISOString()
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
serveDev(distRoot, url, res);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
97
|
+
res.end(error.stack || String(error));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
server.listen(port, () => {
|
|
101
|
+
console.log(`Darkmown dev server ready at http://localhost:${port}`);
|
|
102
|
+
console.log(`Live compiler watching site/ and src/`);
|
|
103
|
+
});
|
|
104
|
+
} else if (command === "serve") {
|
|
105
|
+
const port = Number(process.env.PORT || 4173);
|
|
106
|
+
const distRoot = path.join(process.cwd(), "dist");
|
|
107
|
+
if (!fs.existsSync(distRoot)) {
|
|
108
|
+
console.error("No dist directory found. Run `darkmown build` first.");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
http.createServer((req, res) => serve(distRoot, req.url || "/", res)).listen(port, () => {
|
|
112
|
+
console.log(`Darkmown preview of dist at http://localhost:${port}`);
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
console.error(`Unknown command: ${command}`);
|
|
116
|
+
printHelp();
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function serveDev(distRoot, url, res) {
|
|
121
|
+
const file = resolvePublicFile(distRoot, url);
|
|
122
|
+
if (!file || !fs.existsSync(file)) {
|
|
123
|
+
serve(distRoot, url, res);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (file.endsWith(".html")) {
|
|
127
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
128
|
+
res.end(injectDevClient(fs.readFileSync(file, "utf8")));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
res.writeHead(200, { "content-type": contentType(file) });
|
|
132
|
+
fs.createReadStream(file).pipe(res);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function broadcast(clients, message) {
|
|
136
|
+
for (const client of clients) client.write(message);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function printHelp() {
|
|
140
|
+
console.log(`Darkmown
|
|
141
|
+
|
|
142
|
+
Usage:
|
|
143
|
+
darkmown init [dir] Scaffold a new Darkmown project
|
|
144
|
+
darkmown dev Start the live compiler dev server
|
|
145
|
+
darkmown build Compile site/pages into dist
|
|
146
|
+
darkmown serve Preview the built dist locally
|
|
147
|
+
darkmown help Show this help
|
|
148
|
+
|
|
149
|
+
Authoring:
|
|
150
|
+
site/pages File-based routes: .md stays plain, .wd adds directives
|
|
151
|
+
site/_ Include shelf for @include /name.wd
|
|
152
|
+
@loop x into item Loop over JSON files, in-scope values, or :state lists
|
|
153
|
+
*.skin Colocated indentation-based CSS
|
|
154
|
+
*.js Colocated page behavior
|
|
155
|
+
`);
|
|
156
|
+
}
|