@moku-labs/web 0.3.1 → 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 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
- import { createApp, defineRoutes, route } from "@moku-labs/web";
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`), matching, URL/file derivation |
66
- | `content` | Markdown pipeline sanitized HTML, frontmatter, reading time, locale model |
67
- | `head` | SEO `<head>` composition: title template, canonical, OG/Twitter, JSON-LD, hreflang |
68
- | `build` | SSG orchestrator: pages, feeds (RSS/Atom/JSON), sitemap, OG images |
69
- | `spa` | Client runtime: island hydration + intercepted navigation |
70
- | `deploy` | Cloudflare Pages: `wrangler.jsonc` scaffolding + deploy |
71
- | `log`, `env` | Core plugins: structured logging + validated environment access |
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 };