@moku-labs/web 0.3.0 → 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/README.md +52 -12
- package/dist/convention-Dr8jxG70.cjs +81 -0
- package/dist/convention-X3zLTlJ8.mjs +33 -0
- package/dist/index.cjs +1503 -295
- package/dist/index.d.cts +1145 -632
- package/dist/index.d.mts +1144 -631
- package/dist/index.mjs +1478 -249
- package/dist/render-BL9Fv6G6.mjs +20 -0
- package/dist/render-BSTM0Akv.cjs +20 -0
- package/dist/writer-BcWqa_7I.mjs +90 -0
- package/dist/writer-DAF0pM25.cjs +92 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -24,7 +24,17 @@ The consumer surface is one `createApp` call plus a typed routing DSL — you ne
|
|
|
24
24
|
## Quick start
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
|
-
|
|
27
|
+
// A Node SSG build — compose the node-only plugins explicitly:
|
|
28
|
+
import {
|
|
29
|
+
createApp,
|
|
30
|
+
defineRoutes,
|
|
31
|
+
route,
|
|
32
|
+
contentPlugin,
|
|
33
|
+
buildPlugin,
|
|
34
|
+
deployPlugin,
|
|
35
|
+
dotenv,
|
|
36
|
+
processEnv
|
|
37
|
+
} from "@moku-labs/web";
|
|
28
38
|
|
|
29
39
|
const routes = defineRoutes({
|
|
30
40
|
home: route("/")
|
|
@@ -38,8 +48,10 @@ const routes = defineRoutes({
|
|
|
38
48
|
});
|
|
39
49
|
|
|
40
50
|
const app = createApp({
|
|
51
|
+
plugins: [contentPlugin, buildPlugin, deployPlugin], // node-only — added per target
|
|
41
52
|
config: { mode: "production" },
|
|
42
53
|
pluginConfigs: {
|
|
54
|
+
env: { providers: [dotenv(), processEnv()] },
|
|
43
55
|
site: { name: "My Blog", url: "https://blog.dev", author: "Me", description: "A personal blog." },
|
|
44
56
|
i18n: { locales: ["en", "uk"], defaultLocale: "en" },
|
|
45
57
|
content: { contentDir: "./content" },
|
|
@@ -56,19 +68,47 @@ Content lives on disk as `content/{slug}/{locale}.md` with YAML frontmatter
|
|
|
56
68
|
(`title`, `date`, `description`, `tags`, `language`, optional `draft`/`author`). Drafts are excluded
|
|
57
69
|
from production builds.
|
|
58
70
|
|
|
71
|
+
## Composition model
|
|
72
|
+
|
|
73
|
+
`createApp`'s **defaults are the isomorphic plugins** — the ones that run unchanged on
|
|
74
|
+
both Node and the browser: `site`, `i18n`, `router`, `head`, `spa` (plus the `log`/`env`
|
|
75
|
+
core). The **node-only** plugins (`content`, `build`, `deploy`) are exported but not
|
|
76
|
+
defaults — add them with `createApp({ plugins: [...] })` for a Node build, and omit them
|
|
77
|
+
in a browser app (with `"sideEffects": false`, your bundler tree-shakes them out). You
|
|
78
|
+
also choose the `env` provider per target: `[dotenv(), processEnv()]` on Node,
|
|
79
|
+
`[browserEnv()]` in the browser. The framework never hard-blocks either runtime.
|
|
80
|
+
|
|
81
|
+
`data` is a special case — an **optional, domain-agnostic data provider**: composed on
|
|
82
|
+
Node, `build` calls `data.write(...)` to persist each page's real `load()` output as JSON
|
|
83
|
+
(one file per page URL); composed in the browser, `data.at(path)` fetches that JSON and
|
|
84
|
+
`spa` re-runs the route's own `render` from it (validated through the route's `parse`
|
|
85
|
+
gate) instead of fetching full HTML. The route owns rendering — the SAME `render` runs at
|
|
86
|
+
build (SSG) and on the client, so parity is structural. Add `data` on both sides for
|
|
87
|
+
client DATA navigation; omit it for a plain static site (HTML-over-fetch).
|
|
88
|
+
|
|
89
|
+
The single switch is **`router.mode`** (`"ssg" | "spa" | "hybrid"`): `build` writes data +
|
|
90
|
+
`spa` data-renders only when it is not `"ssg"`. A route that opts into data nav MUST
|
|
91
|
+
declare `.parse(raw => data)` (validated at the client trust boundary) — `build` errors
|
|
92
|
+
otherwise.
|
|
93
|
+
|
|
94
|
+
A browser entry is just your own `createApp(...).start()` over the defaults (plus
|
|
95
|
+
`dataPlugin` for DATA nav) — `spa`'s `onStart` mounts islands onto the SSR'd DOM and
|
|
96
|
+
intercepts navigation.
|
|
97
|
+
|
|
59
98
|
## Plugins
|
|
60
99
|
|
|
61
|
-
| Plugin | Responsibility |
|
|
62
|
-
|
|
63
|
-
| `site` | Site identity (name, URL, author) + canonical URL helper |
|
|
64
|
-
| `i18n` | Locales, default-locale fallback, translations, hreflang/ogLocale maps |
|
|
65
|
-
| `router` | Type-safe route DSL (`route`/`defineRoutes`)
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `deploy` | Cloudflare Pages: `wrangler.jsonc` scaffolding + deploy |
|
|
71
|
-
| `
|
|
100
|
+
| Plugin | Default? | Responsibility |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `site` | ✅ isomorphic | Site identity (name, URL, author) + canonical URL helper |
|
|
103
|
+
| `i18n` | ✅ isomorphic | Locales, default-locale fallback, translations, hreflang/ogLocale maps |
|
|
104
|
+
| `router` | ✅ isomorphic | Type-safe route DSL (`route`/`defineRoutes`) with `.load`/`.render`/`.parse`, matching, `mode()`, URL/file derivation |
|
|
105
|
+
| `head` | ✅ isomorphic | SEO `<head>` composition: title template, canonical, OG/Twitter, JSON-LD, hreflang |
|
|
106
|
+
| `spa` | ✅ isomorphic | Client runtime: island hydration + intercepted navigation (inert on Node) |
|
|
107
|
+
| `content` | ➕ node-only | Markdown pipeline → sanitized HTML, frontmatter, reading time, locale model |
|
|
108
|
+
| `build` | ➕ node-only | SSG orchestrator: pages, feeds (RSS/Atom/JSON), sitemap, OG images |
|
|
109
|
+
| `deploy` | ➕ node-only | Cloudflare Pages: `wrangler.jsonc` scaffolding + deploy |
|
|
110
|
+
| `data` | ➕ optional provider | Agnostic: Node `write()` persists per-page JSON (keyed by URL); browser `at()` fetches it for `spa` DATA nav (validated via `route.parse`) |
|
|
111
|
+
| `log`, `env` | ✅ core | Structured logging + validated environment access |
|
|
72
112
|
|
|
73
113
|
SEO primitives are exported for route `.head()` handlers: `meta`, `og`, `twitter`, `jsonLd`,
|
|
74
114
|
`canonical`, `hreflang`, `feedLink`, `buildArticleHead`.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __exportAll = (all, no_symbols) => {
|
|
9
|
+
let target = {};
|
|
10
|
+
for (var name in all) __defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true
|
|
13
|
+
});
|
|
14
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
15
|
+
return target;
|
|
16
|
+
};
|
|
17
|
+
var __copyProps = (to, from, except, desc) => {
|
|
18
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
19
|
+
key = keys[i];
|
|
20
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
21
|
+
get: ((k) => from[k]).bind(null, key),
|
|
22
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return to;
|
|
26
|
+
};
|
|
27
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
28
|
+
value: mod,
|
|
29
|
+
enumerable: true
|
|
30
|
+
}) : target, mod));
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/plugins/data/convention.ts
|
|
33
|
+
/**
|
|
34
|
+
* @file data plugin — the pure URL/file convention (no `node:*`, no DOM).
|
|
35
|
+
*
|
|
36
|
+
* ONE function maps a page path to its data suffix, so the browser fetch URL
|
|
37
|
+
* (`baseUrl + suffix`) and the on-disk file (`outputDir + "/" + suffix`) are
|
|
38
|
+
* derived from the same source and cannot drift. The data file mirrors the page
|
|
39
|
+
* URL exactly, mirroring how `build` writes `…/index.html` per page:
|
|
40
|
+
* `/` → `index.json`
|
|
41
|
+
* `/en/hello/` → `en/hello/index.json`
|
|
42
|
+
* `/en/hello` → `en/hello/index.json` (trailing slash normalized)
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Compute the data-file suffix for a page path: strip the leading slash, ensure a
|
|
46
|
+
* single trailing slash, then append `index.json`. The root path collapses to
|
|
47
|
+
* `index.json`.
|
|
48
|
+
*
|
|
49
|
+
* @param path - The page URL path (e.g. `/en/hello/`).
|
|
50
|
+
* @returns The suffix shared by the fetch URL and the on-disk file.
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* dataSuffix("/en/hello/"); // "en/hello/index.json"
|
|
54
|
+
* dataSuffix("/"); // "index.json"
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
function dataSuffix(path) {
|
|
58
|
+
let trimmed = path.split("?")[0] ?? path;
|
|
59
|
+
while (trimmed.startsWith("/")) trimmed = trimmed.slice(1);
|
|
60
|
+
while (trimmed.endsWith("/")) trimmed = trimmed.slice(0, -1);
|
|
61
|
+
return trimmed.length > 0 ? `${trimmed}/index.json` : "index.json";
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
Object.defineProperty(exports, "__exportAll", {
|
|
65
|
+
enumerable: true,
|
|
66
|
+
get: function() {
|
|
67
|
+
return __exportAll;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
Object.defineProperty(exports, "__toESM", {
|
|
71
|
+
enumerable: true,
|
|
72
|
+
get: function() {
|
|
73
|
+
return __toESM;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
Object.defineProperty(exports, "dataSuffix", {
|
|
77
|
+
enumerable: true,
|
|
78
|
+
get: function() {
|
|
79
|
+
return dataSuffix;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/plugins/data/convention.ts
|
|
2
|
+
/**
|
|
3
|
+
* @file data plugin — the pure URL/file convention (no `node:*`, no DOM).
|
|
4
|
+
*
|
|
5
|
+
* ONE function maps a page path to its data suffix, so the browser fetch URL
|
|
6
|
+
* (`baseUrl + suffix`) and the on-disk file (`outputDir + "/" + suffix`) are
|
|
7
|
+
* derived from the same source and cannot drift. The data file mirrors the page
|
|
8
|
+
* URL exactly, mirroring how `build` writes `…/index.html` per page:
|
|
9
|
+
* `/` → `index.json`
|
|
10
|
+
* `/en/hello/` → `en/hello/index.json`
|
|
11
|
+
* `/en/hello` → `en/hello/index.json` (trailing slash normalized)
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Compute the data-file suffix for a page path: strip the leading slash, ensure a
|
|
15
|
+
* single trailing slash, then append `index.json`. The root path collapses to
|
|
16
|
+
* `index.json`.
|
|
17
|
+
*
|
|
18
|
+
* @param path - The page URL path (e.g. `/en/hello/`).
|
|
19
|
+
* @returns The suffix shared by the fetch URL and the on-disk file.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* dataSuffix("/en/hello/"); // "en/hello/index.json"
|
|
23
|
+
* dataSuffix("/"); // "index.json"
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function dataSuffix(path) {
|
|
27
|
+
let trimmed = path.split("?")[0] ?? path;
|
|
28
|
+
while (trimmed.startsWith("/")) trimmed = trimmed.slice(1);
|
|
29
|
+
while (trimmed.endsWith("/")) trimmed = trimmed.slice(0, -1);
|
|
30
|
+
return trimmed.length > 0 ? `${trimmed}/index.json` : "index.json";
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { dataSuffix as t };
|