@opentf/web 0.3.0 → 0.5.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.
Files changed (179) hide show
  1. package/components/Link.jsx +29 -0
  2. package/core/errors.js +48 -0
  3. package/core/signals.js +188 -0
  4. package/index.js +12 -0
  5. package/package.json +27 -18
  6. package/runtime/context.js +89 -0
  7. package/runtime/dom.js +280 -0
  8. package/runtime/error-boundary.js +109 -0
  9. package/runtime/events.js +17 -0
  10. package/runtime/index.js +16 -0
  11. package/runtime/lifecycle.js +9 -0
  12. package/runtime/mount.js +49 -0
  13. package/runtime/portal.js +59 -0
  14. package/runtime/router.js +246 -0
  15. package/server/builtins.js +11 -0
  16. package/server/index.js +7 -0
  17. package/server/render.js +76 -0
  18. package/server/ssg-runtime.js +109 -0
  19. package/dist/CHANGELOG.md +0 -24
  20. package/dist/babel.config.cjs +0 -6
  21. package/dist/compiler/babel-plugin.cjs +0 -731
  22. package/dist/core/mount.js +0 -32
  23. package/dist/index.js +0 -7
  24. package/dist/node_modules/@babel/core/LICENSE +0 -22
  25. package/dist/node_modules/@babel/core/README.md +0 -19
  26. package/dist/node_modules/@babel/core/lib/config/cache-contexts.js +0 -5
  27. package/dist/node_modules/@babel/core/lib/config/cache-contexts.js.map +0 -1
  28. package/dist/node_modules/@babel/core/lib/config/caching.js +0 -261
  29. package/dist/node_modules/@babel/core/lib/config/caching.js.map +0 -1
  30. package/dist/node_modules/@babel/core/lib/config/config-chain.js +0 -469
  31. package/dist/node_modules/@babel/core/lib/config/config-chain.js.map +0 -1
  32. package/dist/node_modules/@babel/core/lib/config/config-descriptors.js +0 -190
  33. package/dist/node_modules/@babel/core/lib/config/config-descriptors.js.map +0 -1
  34. package/dist/node_modules/@babel/core/lib/config/files/configuration.js +0 -290
  35. package/dist/node_modules/@babel/core/lib/config/files/configuration.js.map +0 -1
  36. package/dist/node_modules/@babel/core/lib/config/files/import.cjs +0 -6
  37. package/dist/node_modules/@babel/core/lib/config/files/import.cjs.map +0 -1
  38. package/dist/node_modules/@babel/core/lib/config/files/index-browser.js +0 -58
  39. package/dist/node_modules/@babel/core/lib/config/files/index-browser.js.map +0 -1
  40. package/dist/node_modules/@babel/core/lib/config/files/index.js +0 -78
  41. package/dist/node_modules/@babel/core/lib/config/files/index.js.map +0 -1
  42. package/dist/node_modules/@babel/core/lib/config/files/module-types.js +0 -203
  43. package/dist/node_modules/@babel/core/lib/config/files/module-types.js.map +0 -1
  44. package/dist/node_modules/@babel/core/lib/config/files/package.js +0 -61
  45. package/dist/node_modules/@babel/core/lib/config/files/package.js.map +0 -1
  46. package/dist/node_modules/@babel/core/lib/config/files/plugins.js +0 -220
  47. package/dist/node_modules/@babel/core/lib/config/files/plugins.js.map +0 -1
  48. package/dist/node_modules/@babel/core/lib/config/files/types.js +0 -5
  49. package/dist/node_modules/@babel/core/lib/config/files/types.js.map +0 -1
  50. package/dist/node_modules/@babel/core/lib/config/files/utils.js +0 -36
  51. package/dist/node_modules/@babel/core/lib/config/files/utils.js.map +0 -1
  52. package/dist/node_modules/@babel/core/lib/config/full.js +0 -312
  53. package/dist/node_modules/@babel/core/lib/config/full.js.map +0 -1
  54. package/dist/node_modules/@babel/core/lib/config/helpers/config-api.js +0 -85
  55. package/dist/node_modules/@babel/core/lib/config/helpers/config-api.js.map +0 -1
  56. package/dist/node_modules/@babel/core/lib/config/helpers/deep-array.js +0 -23
  57. package/dist/node_modules/@babel/core/lib/config/helpers/deep-array.js.map +0 -1
  58. package/dist/node_modules/@babel/core/lib/config/helpers/environment.js +0 -12
  59. package/dist/node_modules/@babel/core/lib/config/helpers/environment.js.map +0 -1
  60. package/dist/node_modules/@babel/core/lib/config/index.js +0 -87
  61. package/dist/node_modules/@babel/core/lib/config/index.js.map +0 -1
  62. package/dist/node_modules/@babel/core/lib/config/item.js +0 -67
  63. package/dist/node_modules/@babel/core/lib/config/item.js.map +0 -1
  64. package/dist/node_modules/@babel/core/lib/config/partial.js +0 -158
  65. package/dist/node_modules/@babel/core/lib/config/partial.js.map +0 -1
  66. package/dist/node_modules/@babel/core/lib/config/pattern-to-regex.js +0 -38
  67. package/dist/node_modules/@babel/core/lib/config/pattern-to-regex.js.map +0 -1
  68. package/dist/node_modules/@babel/core/lib/config/plugin.js +0 -33
  69. package/dist/node_modules/@babel/core/lib/config/plugin.js.map +0 -1
  70. package/dist/node_modules/@babel/core/lib/config/printer.js +0 -113
  71. package/dist/node_modules/@babel/core/lib/config/printer.js.map +0 -1
  72. package/dist/node_modules/@babel/core/lib/config/resolve-targets-browser.js +0 -41
  73. package/dist/node_modules/@babel/core/lib/config/resolve-targets-browser.js.map +0 -1
  74. package/dist/node_modules/@babel/core/lib/config/resolve-targets.js +0 -61
  75. package/dist/node_modules/@babel/core/lib/config/resolve-targets.js.map +0 -1
  76. package/dist/node_modules/@babel/core/lib/config/util.js +0 -31
  77. package/dist/node_modules/@babel/core/lib/config/util.js.map +0 -1
  78. package/dist/node_modules/@babel/core/lib/config/validation/option-assertions.js +0 -277
  79. package/dist/node_modules/@babel/core/lib/config/validation/option-assertions.js.map +0 -1
  80. package/dist/node_modules/@babel/core/lib/config/validation/options.js +0 -187
  81. package/dist/node_modules/@babel/core/lib/config/validation/options.js.map +0 -1
  82. package/dist/node_modules/@babel/core/lib/config/validation/plugins.js +0 -67
  83. package/dist/node_modules/@babel/core/lib/config/validation/plugins.js.map +0 -1
  84. package/dist/node_modules/@babel/core/lib/config/validation/removed.js +0 -68
  85. package/dist/node_modules/@babel/core/lib/config/validation/removed.js.map +0 -1
  86. package/dist/node_modules/@babel/core/lib/errors/config-error.js +0 -18
  87. package/dist/node_modules/@babel/core/lib/errors/config-error.js.map +0 -1
  88. package/dist/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js +0 -98
  89. package/dist/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js.map +0 -1
  90. package/dist/node_modules/@babel/core/lib/gensync-utils/async.js +0 -90
  91. package/dist/node_modules/@babel/core/lib/gensync-utils/async.js.map +0 -1
  92. package/dist/node_modules/@babel/core/lib/gensync-utils/fs.js +0 -31
  93. package/dist/node_modules/@babel/core/lib/gensync-utils/fs.js.map +0 -1
  94. package/dist/node_modules/@babel/core/lib/gensync-utils/functional.js +0 -58
  95. package/dist/node_modules/@babel/core/lib/gensync-utils/functional.js.map +0 -1
  96. package/dist/node_modules/@babel/core/lib/index.js +0 -230
  97. package/dist/node_modules/@babel/core/lib/index.js.map +0 -1
  98. package/dist/node_modules/@babel/core/lib/parse.js +0 -45
  99. package/dist/node_modules/@babel/core/lib/parse.js.map +0 -1
  100. package/dist/node_modules/@babel/core/lib/parser/index.js +0 -85
  101. package/dist/node_modules/@babel/core/lib/parser/index.js.map +0 -1
  102. package/dist/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js +0 -337
  103. package/dist/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js.map +0 -1
  104. package/dist/node_modules/@babel/core/lib/tools/build-external-helpers.js +0 -144
  105. package/dist/node_modules/@babel/core/lib/tools/build-external-helpers.js.map +0 -1
  106. package/dist/node_modules/@babel/core/lib/transform-ast.js +0 -48
  107. package/dist/node_modules/@babel/core/lib/transform-ast.js.map +0 -1
  108. package/dist/node_modules/@babel/core/lib/transform-file-browser.js +0 -23
  109. package/dist/node_modules/@babel/core/lib/transform-file-browser.js.map +0 -1
  110. package/dist/node_modules/@babel/core/lib/transform-file.js +0 -40
  111. package/dist/node_modules/@babel/core/lib/transform-file.js.map +0 -1
  112. package/dist/node_modules/@babel/core/lib/transform.js +0 -47
  113. package/dist/node_modules/@babel/core/lib/transform.js.map +0 -1
  114. package/dist/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js +0 -84
  115. package/dist/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js.map +0 -1
  116. package/dist/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs +0 -4
  117. package/dist/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs.map +0 -1
  118. package/dist/node_modules/@babel/core/lib/transformation/file/file.js +0 -204
  119. package/dist/node_modules/@babel/core/lib/transformation/file/file.js.map +0 -1
  120. package/dist/node_modules/@babel/core/lib/transformation/file/generate.js +0 -84
  121. package/dist/node_modules/@babel/core/lib/transformation/file/generate.js.map +0 -1
  122. package/dist/node_modules/@babel/core/lib/transformation/file/merge-map.js +0 -37
  123. package/dist/node_modules/@babel/core/lib/transformation/file/merge-map.js.map +0 -1
  124. package/dist/node_modules/@babel/core/lib/transformation/index.js +0 -90
  125. package/dist/node_modules/@babel/core/lib/transformation/index.js.map +0 -1
  126. package/dist/node_modules/@babel/core/lib/transformation/normalize-file.js +0 -127
  127. package/dist/node_modules/@babel/core/lib/transformation/normalize-file.js.map +0 -1
  128. package/dist/node_modules/@babel/core/lib/transformation/normalize-opts.js +0 -59
  129. package/dist/node_modules/@babel/core/lib/transformation/normalize-opts.js.map +0 -1
  130. package/dist/node_modules/@babel/core/lib/transformation/plugin-pass.js +0 -48
  131. package/dist/node_modules/@babel/core/lib/transformation/plugin-pass.js.map +0 -1
  132. package/dist/node_modules/@babel/core/lib/transformation/util/clone-deep.js +0 -54
  133. package/dist/node_modules/@babel/core/lib/transformation/util/clone-deep.js.map +0 -1
  134. package/dist/node_modules/@babel/core/lib/vendor/import-meta-resolve.js +0 -1042
  135. package/dist/node_modules/@babel/core/lib/vendor/import-meta-resolve.js.map +0 -1
  136. package/dist/node_modules/@babel/core/package.json +0 -82
  137. package/dist/node_modules/@babel/core/src/config/files/index-browser.ts +0 -115
  138. package/dist/node_modules/@babel/core/src/config/files/index.ts +0 -30
  139. package/dist/node_modules/@babel/core/src/config/resolve-targets-browser.ts +0 -42
  140. package/dist/node_modules/@babel/core/src/config/resolve-targets.ts +0 -54
  141. package/dist/node_modules/@babel/core/src/transform-file-browser.ts +0 -33
  142. package/dist/node_modules/@babel/core/src/transform-file.ts +0 -56
  143. package/dist/node_modules/@babel/helper-module-imports/LICENSE +0 -22
  144. package/dist/node_modules/@babel/helper-module-imports/README.md +0 -19
  145. package/dist/node_modules/@babel/helper-module-imports/lib/import-builder.js +0 -122
  146. package/dist/node_modules/@babel/helper-module-imports/lib/import-builder.js.map +0 -1
  147. package/dist/node_modules/@babel/helper-module-imports/lib/import-injector.js +0 -304
  148. package/dist/node_modules/@babel/helper-module-imports/lib/import-injector.js.map +0 -1
  149. package/dist/node_modules/@babel/helper-module-imports/lib/index.js +0 -37
  150. package/dist/node_modules/@babel/helper-module-imports/lib/index.js.map +0 -1
  151. package/dist/node_modules/@babel/helper-module-imports/lib/is-module.js +0 -11
  152. package/dist/node_modules/@babel/helper-module-imports/lib/is-module.js.map +0 -1
  153. package/dist/node_modules/@babel/helper-module-imports/package.json +0 -28
  154. package/dist/node_modules/@babel/plugin-syntax-jsx/LICENSE +0 -22
  155. package/dist/node_modules/@babel/plugin-syntax-jsx/README.md +0 -19
  156. package/dist/node_modules/@babel/plugin-syntax-jsx/lib/index.js +0 -21
  157. package/dist/node_modules/@babel/plugin-syntax-jsx/lib/index.js.map +0 -1
  158. package/dist/node_modules/@babel/plugin-syntax-jsx/package.json +0 -33
  159. package/dist/node_modules/@preact/signals-core/CHANGELOG.md +0 -331
  160. package/dist/node_modules/@preact/signals-core/LICENSE +0 -21
  161. package/dist/node_modules/@preact/signals-core/README.md +0 -258
  162. package/dist/node_modules/@preact/signals-core/dist/signals-core.d.ts +0 -149
  163. package/dist/node_modules/@preact/signals-core/dist/signals-core.js +0 -1
  164. package/dist/node_modules/@preact/signals-core/dist/signals-core.js.map +0 -1
  165. package/dist/node_modules/@preact/signals-core/dist/signals-core.min.js +0 -1
  166. package/dist/node_modules/@preact/signals-core/dist/signals-core.min.js.map +0 -1
  167. package/dist/node_modules/@preact/signals-core/dist/signals-core.mjs +0 -1
  168. package/dist/node_modules/@preact/signals-core/dist/signals-core.mjs.map +0 -1
  169. package/dist/node_modules/@preact/signals-core/dist/signals-core.module.js +0 -1
  170. package/dist/node_modules/@preact/signals-core/dist/signals-core.module.js.map +0 -1
  171. package/dist/node_modules/@preact/signals-core/package.json +0 -48
  172. package/dist/node_modules/@preact/signals-core/src/index.ts +0 -1121
  173. package/dist/package.json +0 -30
  174. package/dist/router/Link.js +0 -73
  175. package/dist/router/index.js +0 -216
  176. package/dist/runtime/For.js +0 -16
  177. package/dist/runtime/dom.js +0 -73
  178. package/dist/runtime/lifecycle.js +0 -25
  179. package/dist/runtime/props.js +0 -103
@@ -0,0 +1,246 @@
1
+ // File-based client router for the OTF Web runtime.
2
+ //
3
+ // Pages/layouts compile to factory functions (default export) that return a DOM
4
+ // node; `mountApp` registers a route table discovered from the filesystem (by the
5
+ // dev server / build) and renders the matching page into the app root, swapping
6
+ // it on navigation. Route state (pathname/params/query) is exposed reactively via
7
+ // the `router` facade so views can read `router.pathname` etc. inside bindings.
8
+ //
9
+ // Dynamic segments use the `[param]` / `[...rest]` folder convention (SPEC §6).
10
+ //
11
+ // NOTE: passing route params to a page via its `props` argument
12
+ // (`function Page(props) { props.params.id }`) and layout `props.children`
13
+ // composition both require signal-free page props, which the compiler does not
14
+ // emit yet — for now pages read params via the reactive `router.params`.
15
+
16
+ import { clearError, reportError } from "../core/errors.js";
17
+ import { signal } from "../core/signals.js";
18
+ import { runCleanup, runMount } from "./mount.js";
19
+
20
+ const isBrowser = typeof window !== "undefined";
21
+
22
+ const state = {
23
+ pathname: signal(isBrowser ? window.location.pathname : "/"),
24
+ searchParams: signal(new URLSearchParams(isBrowser ? window.location.search : "")),
25
+ params: signal({}),
26
+ };
27
+
28
+ export const routes = { pages: {}, layouts: {}, notFound: null };
29
+ let guard = null;
30
+ let rootEl = null;
31
+ let currentNodes = [];
32
+
33
+ /** Reactive router facade — getters read signal values (tracked in effects). */
34
+ export const router = {
35
+ get pathname() {
36
+ return state.pathname.value;
37
+ },
38
+ get searchParams() {
39
+ return state.searchParams.value;
40
+ },
41
+ get query() {
42
+ return Object.fromEntries(state.searchParams.value);
43
+ },
44
+ get params() {
45
+ return state.params.value;
46
+ },
47
+ push: (path) => navigate(path),
48
+ replace: (path) => navigate(path, true),
49
+ };
50
+
51
+ /** Derive the route ("/counter", "/") from a `.../app/<route>/page.jsx` path. */
52
+ function routeFromPath(filePath) {
53
+ const r = filePath
54
+ .replace(/^.*\/app/, "")
55
+ .replace(/\/(page|layout|404)\.(jsx|tsx)$/, "");
56
+ return r === "" ? "/" : r;
57
+ }
58
+
59
+ /**
60
+ * Register routes from a `{ path: entry }` map. Each `entry` is either a module
61
+ * namespace (eager) or a `() => import(...)` loader (lazy, code-split) — both
62
+ * resolve to a default-export factory at navigation time. `layout.jsx` files
63
+ * register as layouts (wrapping nested pages), `404.jsx` as the fallback.
64
+ */
65
+ export function registerRoutes(modules) {
66
+ for (const file in modules) {
67
+ const entry = modules[file];
68
+ if (/\/404\.(jsx|tsx)$/.test(file)) routes.notFound = entry;
69
+ else if (/\/layout\.(jsx|tsx)$/.test(file)) routes.layouts[routeFromPath(file)] = entry;
70
+ else routes.pages[routeFromPath(file)] = entry;
71
+ }
72
+ }
73
+
74
+ /** Layout entries that wrap `route`, outermost (root) first. */
75
+ export function layoutChain(route) {
76
+ const chain = [];
77
+ if (!route) return chain;
78
+ let p = route;
79
+ while (true) {
80
+ if (routes.layouts[p]) chain.unshift(routes.layouts[p]);
81
+ if (p === "/") break;
82
+ p = p.slice(0, p.lastIndexOf("/")) || "/";
83
+ }
84
+ return chain;
85
+ }
86
+
87
+ /** Resolve a route entry (module namespace or lazy loader) to its factory. */
88
+ export async function resolveFactory(entry) {
89
+ const mod = typeof entry === "function" ? await entry() : entry;
90
+ return mod && mod.default ? mod.default : mod;
91
+ }
92
+
93
+ /**
94
+ * Build the DOM node for a matched route: the page factory wrapped by its layout
95
+ * chain (most-specific inward, root outermost). Returns the outermost `node` plus
96
+ * the ordered `nodes` list (page → … → root) so callers can run lifecycle on each.
97
+ * Shared by the client router (`navigate`) and server render (`renderToString`).
98
+ */
99
+ export async function buildRouteNode(match, query = {}) {
100
+ const props = { params: match.params, query };
101
+ const pageFactory = await resolveFactory(match.entry);
102
+ let node = pageFactory(props);
103
+ const nodes = [node];
104
+ const chain = layoutChain(match.route);
105
+ for (let i = chain.length - 1; i >= 0; i--) {
106
+ const layout = await resolveFactory(chain[i]);
107
+ node = layout({ ...props, children: node });
108
+ nodes.push(node);
109
+ }
110
+ return { node, nodes };
111
+ }
112
+
113
+ /**
114
+ * Set the reactive route state directly (no history/render). Used by server render
115
+ * so a page reading `router.pathname`/`params`/`query` resolves to the route being
116
+ * pre-rendered. The client uses `navigate` instead.
117
+ */
118
+ export function setRouteState({ pathname = "/", search = "", params = {} } = {}) {
119
+ state.pathname.value = pathname;
120
+ state.searchParams.value = new URLSearchParams(search);
121
+ state.params.value = params;
122
+ }
123
+
124
+ /** Match `pathname` against the registered routes, resolving `[param]` segments. */
125
+ export function matchRoute(pathname) {
126
+ for (const route in routes.pages) {
127
+ const pattern = route
128
+ .replace(/\[\.\.\.([^\]]+)\]/g, "(?<$1>.+)")
129
+ .replace(/\[([^\]]+)\]/g, "(?<$1>[^/]+)");
130
+ const m = pathname.match(new RegExp(`^${pattern}/?$`));
131
+ if (m) {
132
+ const params = { ...(m.groups || {}) };
133
+ for (const k in params) {
134
+ if (route.includes(`[...${k}]`)) params[k] = params[k].split("/");
135
+ }
136
+ return { entry: routes.pages[route], params, route };
137
+ }
138
+ }
139
+ return null;
140
+ }
141
+
142
+ /**
143
+ * Navigate to `path`. Runs an optional route guard, swaps the rendered page
144
+ * (tearing down the previous one's lifecycle), and updates window.history.
145
+ */
146
+ export async function navigate(path, replace = false, isPop = false) {
147
+ if (!path || !rootEl) return;
148
+ const url = new URL(path, window.location.origin);
149
+
150
+ if (guard) {
151
+ let redirected = false;
152
+ const to = {
153
+ path: url.pathname,
154
+ params: matchRoute(url.pathname)?.params || {},
155
+ query: Object.fromEntries(url.searchParams),
156
+ };
157
+ await new Promise((resolve) => {
158
+ const tools = {
159
+ next: () => resolve(),
160
+ redirect: (p) => {
161
+ redirected = true;
162
+ navigate(p, true);
163
+ resolve();
164
+ },
165
+ replace: (p) => {
166
+ redirected = true;
167
+ navigate(p, true);
168
+ resolve();
169
+ },
170
+ };
171
+ Promise.resolve(guard(to, tools)).catch(() => resolve());
172
+ });
173
+ if (redirected) return;
174
+ }
175
+
176
+ const match =
177
+ matchRoute(url.pathname) ||
178
+ (routes.notFound ? { entry: routes.notFound, params: {}, route: null } : null);
179
+
180
+ state.pathname.value = url.pathname;
181
+ state.searchParams.value = url.searchParams;
182
+ state.params.value = match ? match.params : {};
183
+
184
+ if (!isPop) {
185
+ if (replace) window.history.replaceState({}, "", path);
186
+ else window.history.pushState({}, "", path);
187
+ }
188
+
189
+ // Resolve (and lazily load) the page + its layout chain before tearing down the
190
+ // current view, so a slow/failed import doesn't leave a blank page.
191
+ let nodes = null;
192
+ if (match) {
193
+ try {
194
+ const built = await buildRouteNode(match, Object.fromEntries(url.searchParams));
195
+ nodes = built.nodes;
196
+ } catch (e) {
197
+ reportError(e, { phase: "route", path: url.pathname });
198
+ rootEl.replaceChildren();
199
+ rootEl.innerHTML = `<pre style="color:#f87171;padding:1rem">Failed to load ${url.pathname}\n${e?.message ?? e}</pre>`;
200
+ currentNodes = [];
201
+ return;
202
+ }
203
+ }
204
+
205
+ for (const n of currentNodes) runCleanup(n);
206
+ rootEl.replaceChildren();
207
+
208
+ if (nodes) {
209
+ rootEl.appendChild(nodes[nodes.length - 1]); // outermost node
210
+ for (const n of nodes) runMount(n); // run onMount for page + every layout
211
+ currentNodes = nodes;
212
+ clearError({ phase: "route" }); // a good render dismisses a prior error overlay
213
+ } else {
214
+ rootEl.innerHTML = "<h1>404 — Not Found</h1>";
215
+ currentNodes = [];
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Bootstrap the application: register routes, wire history, and render the page
221
+ * for the current URL.
222
+ *
223
+ * @param {Object} opts
224
+ * @param {Object} opts.pages `{ path: module }` route map (from the dev server).
225
+ * @param {Element} [opts.target] the app root (defaults to `#app`).
226
+ * @param {Function} [opts.guard] optional `(to, tools) => …` route guard.
227
+ */
228
+ export function mountApp({ pages, target, guard: g } = {}) {
229
+ rootEl = target || (isBrowser ? document.getElementById("app") : null);
230
+ if (pages) registerRoutes(pages);
231
+ guard = g || null;
232
+ if (isBrowser) {
233
+ window.addEventListener("popstate", () =>
234
+ navigate(
235
+ window.location.pathname + window.location.search + window.location.hash,
236
+ false,
237
+ true,
238
+ ),
239
+ );
240
+ }
241
+ navigate(window.location.pathname + window.location.search + window.location.hash, true, true);
242
+ }
243
+
244
+ // `<Link>` is a pure JSX component (packages/web/components/Link.jsx), compiled by
245
+ // the app's pipeline like any UI component — it lives there, not as a hand-written
246
+ // Custom Element here. It imports `navigate` from this module.
@@ -0,0 +1,11 @@
1
+ //! SSG renderers for the built-in runtime components (the ones that are
2
+ //! hand-written Custom Elements, not compiled). Registered on import so the SSG
3
+ //! bundle can render `<Link>`, `<ContextProvider>`, `<Portal>`, `<ErrorBoundary>`.
4
+
5
+ import { defineSSG } from "./ssg-runtime.js";
6
+
7
+ // (web-link is provided by the compiled components/Link.jsx, not hand-written.)
8
+ // Passthrough: their effect is structural/client-side; SSG renders children inline.
9
+ defineSSG("web-context-provider", (_props, children) => children);
10
+ defineSSG("web-portal", (_props, children) => children);
11
+ defineSSG("web-error-boundary", (_props, children) => children);
@@ -0,0 +1,7 @@
1
+ // Server entry for SSG: the string-building helpers the compiler emits, the
2
+ // render/route API, and the built-in component SSG renderers (registered on
3
+ // import). Imported as "@opentf/web/server".
4
+
5
+ export * from "./ssg-runtime.js";
6
+ export * from "./render.js";
7
+ import "./builtins.js";
@@ -0,0 +1,76 @@
1
+ //! Server render for SSG (ARCHITECTURE.md §6) — composes a route's HTML by
2
+ //! calling the SSG render functions the compiler emits (page/layout factories
3
+ //! that return strings, components registered in the SSG registry). No DOM: this
4
+ //! runs in plain Bun/Node at build time.
5
+ //
6
+ // The route table (registerRoutes/matchRoute/layoutChain) is shared with the
7
+ // client router; here we resolve a route's page + layout chain to string
8
+ // renderers and concatenate them, passing `children` as an HTML string.
9
+
10
+ import {
11
+ layoutChain,
12
+ matchRoute,
13
+ resolveFactory,
14
+ routes,
15
+ setRouteState,
16
+ } from "../runtime/router.js";
17
+
18
+ /**
19
+ * Render `pathname` to an HTML string (the markup for inside `#app`). Falls back
20
+ * to the registered `404` page for an unmatched path; returns `null` if there's
21
+ * no match and no 404.
22
+ */
23
+ export async function renderToString(pathname, search = "") {
24
+ const match =
25
+ matchRoute(pathname) ||
26
+ (routes.notFound ? { entry: routes.notFound, params: {}, route: null } : null);
27
+ if (!match) return null;
28
+
29
+ // Let a page reading `router.params`/`pathname`/`query` resolve to this route.
30
+ setRouteState({ pathname, search, params: match.params });
31
+
32
+ const props = { params: match.params, query: Object.fromEntries(new URLSearchParams(search)) };
33
+ let html = (await resolveFactory(match.entry))(props);
34
+
35
+ // Wrap with layouts, most-specific inward to root outermost.
36
+ const chain = layoutChain(match.route);
37
+ for (let i = chain.length - 1; i >= 0; i--) {
38
+ const layout = await resolveFactory(chain[i]);
39
+ html = layout({ ...props, children: html });
40
+ }
41
+ return html;
42
+ }
43
+
44
+ /** Substitute `[param]` / `[...rest]` in a route with concrete values. */
45
+ function fillRoute(route, params) {
46
+ return route
47
+ .replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => [].concat(params[k] ?? []).join("/"))
48
+ .replace(/\[([^\]]+)\]/g, (_, k) => String(params[k] ?? ""));
49
+ }
50
+
51
+ /**
52
+ * Enumerate the concrete paths to pre-render. Static routes are taken as-is;
53
+ * dynamic routes (`[param]`) are expanded via the page module's optional
54
+ * `getStaticPaths()` (returning `[{ params }]`), else collected as `skipped`.
55
+ */
56
+ export async function collectRoutePaths() {
57
+ const paths = [];
58
+ const skipped = [];
59
+ for (const route in routes.pages) {
60
+ if (!route.includes("[")) {
61
+ paths.push(route);
62
+ continue;
63
+ }
64
+ const ns = routes.pages[route];
65
+ const getStaticPaths =
66
+ ns && (ns.getStaticPaths || (ns.default && ns.default.getStaticPaths));
67
+ if (typeof getStaticPaths === "function") {
68
+ for (const entry of (await getStaticPaths()) || []) {
69
+ paths.push(fillRoute(route, entry.params || entry));
70
+ }
71
+ } else {
72
+ skipped.push(route);
73
+ }
74
+ }
75
+ return { paths, skipped };
76
+ }
@@ -0,0 +1,109 @@
1
+ //! SSG runtime helpers — the string-building primitives the SSG codegen emits
2
+ //! (ARCHITECTURE.md §6). No DOM: these run in plain Bun/Node at build time and
3
+ //! return HTML strings. Component output is composed via a tag→renderer registry,
4
+ //! mirroring how CSR composes by Custom Element tag.
5
+
6
+ const VOID = new Set([
7
+ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
8
+ "param", "source", "track", "wbr",
9
+ ]);
10
+
11
+ const UNITLESS = new Set([
12
+ "opacity", "zIndex", "fontWeight", "lineHeight", "flex", "flexGrow", "flexShrink",
13
+ "order", "zoom", "tabSize", "columnCount", "fillOpacity", "strokeOpacity",
14
+ ]);
15
+
16
+ const registry = {};
17
+
18
+ /** Register a component's SSG renderer under its Custom Element tag. */
19
+ export function defineSSG(tag, render) {
20
+ registry[tag] = render;
21
+ }
22
+
23
+ /** Escape text content for HTML. */
24
+ export function escapeHtml(s) {
25
+ return String(s).replace(/[&<>]/g, (c) => (c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"));
26
+ }
27
+
28
+ /** Escape an attribute value. */
29
+ export function escapeAttr(s) {
30
+ return String(s).replace(/[&"]/g, (c) => (c === "&" ? "&amp;" : "&quot;"));
31
+ }
32
+
33
+ /** clsx-style class normalization (mirrors runtime/dom.js). */
34
+ export function clsx(value) {
35
+ if (!value) return "";
36
+ if (typeof value === "string") return value;
37
+ if (typeof value === "number") return String(value);
38
+ if (Array.isArray(value)) {
39
+ const parts = [];
40
+ for (const v of value) {
41
+ const s = clsx(v);
42
+ if (s) parts.push(s);
43
+ }
44
+ return parts.join(" ");
45
+ }
46
+ if (typeof value === "object") {
47
+ const parts = [];
48
+ for (const k in value) if (value[k]) parts.push(k);
49
+ return parts.join(" ");
50
+ }
51
+ return "";
52
+ }
53
+
54
+ /** Serialize a style value (string passthrough, or object → `prop:val;`). */
55
+ export function styleString(value) {
56
+ if (!value) return "";
57
+ if (typeof value === "string") return value;
58
+ let s = "";
59
+ for (const k in value) {
60
+ const v = value[k];
61
+ if (v == null) continue;
62
+ const prop = k.startsWith("--") ? k : k.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
63
+ const val = typeof v === "number" && !UNITLESS.has(k) ? `${v}px` : v;
64
+ s += `${prop}:${val};`;
65
+ }
66
+ return s;
67
+ }
68
+
69
+ /** Render one attribute: ` name="value"`, ` name` (true), or "" (null/false). */
70
+ export function attr(name, value) {
71
+ if (name === "class") value = clsx(value);
72
+ else if (name === "style") value = styleString(value);
73
+ if (value == null || value === false) return "";
74
+ if (value === true) return ` ${name}`;
75
+ return ` ${name}="${escapeAttr(String(value))}"`;
76
+ }
77
+
78
+ /** A dynamic hole's value → HTML: trusted `{__html}` raw, else escaped text. */
79
+ export function ssgText(v) {
80
+ if (v == null || v === false || v === true) return "";
81
+ if (typeof v === "object" && v.__html != null) return v.__html;
82
+ return escapeHtml(String(v));
83
+ }
84
+
85
+ /** Render an array.map list to concatenated HTML. */
86
+ export function ssgList(arr, fn) {
87
+ return (Array.isArray(arr) ? arr : []).map(fn).join("");
88
+ }
89
+
90
+ /**
91
+ * Render a child component `<tag>inner</tag>` via the registry. Props are passed to
92
+ * the renderer (which decides where they land — declared props become properties /
93
+ * forward into the view), so the host tag carries no reflected attributes — exactly
94
+ * as CSR composes a component (props go through `setProp`, declared ones as
95
+ * properties, not host attributes). Avoids duplicating e.g. `class` on both the
96
+ * host and the element the component renders.
97
+ */
98
+ export function ssgComponent(tag, props, children) {
99
+ const render = registry[tag];
100
+ let inner;
101
+ try {
102
+ inner = render ? render(props || {}, children ?? "") : (children ?? "");
103
+ } catch {
104
+ inner = ""; // fail soft (client renders/handles it)
105
+ }
106
+ return `<${tag}>${inner}</${tag}>`;
107
+ }
108
+
109
+ export { VOID };
package/dist/CHANGELOG.md DELETED
@@ -1,24 +0,0 @@
1
- # @opentf/web
2
-
3
- ## 0.3.0
4
-
5
- ### Minor Changes
6
-
7
- - 476d65a: Fix reactivity issues by re-exporting all signals from the core package and updating the compiler to use a unified reactivity instance. This prevents state-update failures in complex project structures.
8
-
9
- ## 0.2.0
10
-
11
- ### Minor Changes
12
-
13
- - a56fdfe: Fix the core components to prebuilt before publishing
14
-
15
- ## 0.1.0
16
-
17
- ### Minor Changes
18
-
19
- - Global rebranding to Web App Framework and monorepo restructuring.
20
- - Renamed all packages from `@opentf/mwaf-*` to `@opentf/web-*`.
21
- - Updated Custom Element prefix from `mwaf-` to `web-`.
22
- - Restructured the repository into a scoped monorepo.
23
- - Updated scaffolding tool `@opentf/create-web` with new templates.
24
- - Aligned all package versions to 0.1.0-alpha.1.
@@ -1,6 +0,0 @@
1
- module.exports = {
2
- plugins: [
3
- "@babel/plugin-syntax-jsx",
4
- ["./compiler/babel-plugin.cjs", { "runtimeSource": "../index.js" }]
5
- ]
6
- };