@opentf/web 0.4.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.
- package/components/Link.jsx +29 -0
- package/core/errors.js +48 -0
- package/core/signals.js +188 -0
- package/index.js +12 -0
- package/package.json +27 -18
- package/runtime/context.js +89 -0
- package/runtime/dom.js +280 -0
- package/runtime/error-boundary.js +109 -0
- package/runtime/events.js +17 -0
- package/runtime/index.js +16 -0
- package/runtime/lifecycle.js +9 -0
- package/runtime/mount.js +49 -0
- package/runtime/portal.js +59 -0
- package/runtime/router.js +246 -0
- package/server/builtins.js +11 -0
- package/server/index.js +7 -0
- package/server/render.js +76 -0
- package/server/ssg-runtime.js +109 -0
- package/README.md +0 -41
- package/dist/CHANGELOG.md +0 -49
- package/dist/README.md +0 -41
- package/dist/babel.config.cjs +0 -6
- package/dist/compiler/babel-plugin.cjs +0 -798
- package/dist/core/mount.js +0 -32
- package/dist/index.js +0 -7
- package/dist/node_modules/@babel/core/LICENSE +0 -22
- package/dist/node_modules/@babel/core/README.md +0 -19
- package/dist/node_modules/@babel/core/lib/config/cache-contexts.js +0 -5
- package/dist/node_modules/@babel/core/lib/config/cache-contexts.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/caching.js +0 -261
- package/dist/node_modules/@babel/core/lib/config/caching.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/config-chain.js +0 -469
- package/dist/node_modules/@babel/core/lib/config/config-chain.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/config-descriptors.js +0 -190
- package/dist/node_modules/@babel/core/lib/config/config-descriptors.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/configuration.js +0 -290
- package/dist/node_modules/@babel/core/lib/config/files/configuration.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/import.cjs +0 -6
- package/dist/node_modules/@babel/core/lib/config/files/import.cjs.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/index-browser.js +0 -58
- package/dist/node_modules/@babel/core/lib/config/files/index-browser.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/index.js +0 -78
- package/dist/node_modules/@babel/core/lib/config/files/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/module-types.js +0 -203
- package/dist/node_modules/@babel/core/lib/config/files/module-types.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/package.js +0 -61
- package/dist/node_modules/@babel/core/lib/config/files/package.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/plugins.js +0 -220
- package/dist/node_modules/@babel/core/lib/config/files/plugins.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/types.js +0 -5
- package/dist/node_modules/@babel/core/lib/config/files/types.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/files/utils.js +0 -36
- package/dist/node_modules/@babel/core/lib/config/files/utils.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/full.js +0 -312
- package/dist/node_modules/@babel/core/lib/config/full.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/helpers/config-api.js +0 -85
- package/dist/node_modules/@babel/core/lib/config/helpers/config-api.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/helpers/deep-array.js +0 -23
- package/dist/node_modules/@babel/core/lib/config/helpers/deep-array.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/helpers/environment.js +0 -12
- package/dist/node_modules/@babel/core/lib/config/helpers/environment.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/index.js +0 -87
- package/dist/node_modules/@babel/core/lib/config/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/item.js +0 -67
- package/dist/node_modules/@babel/core/lib/config/item.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/partial.js +0 -158
- package/dist/node_modules/@babel/core/lib/config/partial.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/pattern-to-regex.js +0 -38
- package/dist/node_modules/@babel/core/lib/config/pattern-to-regex.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/plugin.js +0 -33
- package/dist/node_modules/@babel/core/lib/config/plugin.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/printer.js +0 -113
- package/dist/node_modules/@babel/core/lib/config/printer.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/resolve-targets-browser.js +0 -41
- package/dist/node_modules/@babel/core/lib/config/resolve-targets-browser.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/resolve-targets.js +0 -61
- package/dist/node_modules/@babel/core/lib/config/resolve-targets.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/util.js +0 -31
- package/dist/node_modules/@babel/core/lib/config/util.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/validation/option-assertions.js +0 -277
- package/dist/node_modules/@babel/core/lib/config/validation/option-assertions.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/validation/options.js +0 -187
- package/dist/node_modules/@babel/core/lib/config/validation/options.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/validation/plugins.js +0 -67
- package/dist/node_modules/@babel/core/lib/config/validation/plugins.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/config/validation/removed.js +0 -68
- package/dist/node_modules/@babel/core/lib/config/validation/removed.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/errors/config-error.js +0 -18
- package/dist/node_modules/@babel/core/lib/errors/config-error.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js +0 -98
- package/dist/node_modules/@babel/core/lib/errors/rewrite-stack-trace.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/gensync-utils/async.js +0 -90
- package/dist/node_modules/@babel/core/lib/gensync-utils/async.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/gensync-utils/fs.js +0 -31
- package/dist/node_modules/@babel/core/lib/gensync-utils/fs.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/gensync-utils/functional.js +0 -58
- package/dist/node_modules/@babel/core/lib/gensync-utils/functional.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/index.js +0 -230
- package/dist/node_modules/@babel/core/lib/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/parse.js +0 -45
- package/dist/node_modules/@babel/core/lib/parse.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/parser/index.js +0 -85
- package/dist/node_modules/@babel/core/lib/parser/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js +0 -337
- package/dist/node_modules/@babel/core/lib/parser/util/missing-plugin-helper.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/tools/build-external-helpers.js +0 -144
- package/dist/node_modules/@babel/core/lib/tools/build-external-helpers.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transform-ast.js +0 -48
- package/dist/node_modules/@babel/core/lib/transform-ast.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transform-file-browser.js +0 -23
- package/dist/node_modules/@babel/core/lib/transform-file-browser.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transform-file.js +0 -40
- package/dist/node_modules/@babel/core/lib/transform-file.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transform.js +0 -47
- package/dist/node_modules/@babel/core/lib/transform.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js +0 -84
- package/dist/node_modules/@babel/core/lib/transformation/block-hoist-plugin.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs +0 -4
- package/dist/node_modules/@babel/core/lib/transformation/file/babel-7-helpers.cjs.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/file/file.js +0 -204
- package/dist/node_modules/@babel/core/lib/transformation/file/file.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/file/generate.js +0 -84
- package/dist/node_modules/@babel/core/lib/transformation/file/generate.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/file/merge-map.js +0 -37
- package/dist/node_modules/@babel/core/lib/transformation/file/merge-map.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/index.js +0 -90
- package/dist/node_modules/@babel/core/lib/transformation/index.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/normalize-file.js +0 -127
- package/dist/node_modules/@babel/core/lib/transformation/normalize-file.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/normalize-opts.js +0 -59
- package/dist/node_modules/@babel/core/lib/transformation/normalize-opts.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/plugin-pass.js +0 -48
- package/dist/node_modules/@babel/core/lib/transformation/plugin-pass.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/transformation/util/clone-deep.js +0 -54
- package/dist/node_modules/@babel/core/lib/transformation/util/clone-deep.js.map +0 -1
- package/dist/node_modules/@babel/core/lib/vendor/import-meta-resolve.js +0 -1042
- package/dist/node_modules/@babel/core/lib/vendor/import-meta-resolve.js.map +0 -1
- package/dist/node_modules/@babel/core/package.json +0 -82
- package/dist/node_modules/@babel/core/src/config/files/index-browser.ts +0 -115
- package/dist/node_modules/@babel/core/src/config/files/index.ts +0 -30
- package/dist/node_modules/@babel/core/src/config/resolve-targets-browser.ts +0 -42
- package/dist/node_modules/@babel/core/src/config/resolve-targets.ts +0 -54
- package/dist/node_modules/@babel/core/src/transform-file-browser.ts +0 -33
- package/dist/node_modules/@babel/core/src/transform-file.ts +0 -56
- package/dist/node_modules/@babel/helper-module-imports/LICENSE +0 -22
- package/dist/node_modules/@babel/helper-module-imports/README.md +0 -19
- package/dist/node_modules/@babel/helper-module-imports/lib/import-builder.js +0 -122
- package/dist/node_modules/@babel/helper-module-imports/lib/import-builder.js.map +0 -1
- package/dist/node_modules/@babel/helper-module-imports/lib/import-injector.js +0 -304
- package/dist/node_modules/@babel/helper-module-imports/lib/import-injector.js.map +0 -1
- package/dist/node_modules/@babel/helper-module-imports/lib/index.js +0 -37
- package/dist/node_modules/@babel/helper-module-imports/lib/index.js.map +0 -1
- package/dist/node_modules/@babel/helper-module-imports/lib/is-module.js +0 -11
- package/dist/node_modules/@babel/helper-module-imports/lib/is-module.js.map +0 -1
- package/dist/node_modules/@babel/helper-module-imports/package.json +0 -28
- package/dist/node_modules/@babel/plugin-syntax-jsx/LICENSE +0 -22
- package/dist/node_modules/@babel/plugin-syntax-jsx/README.md +0 -19
- package/dist/node_modules/@babel/plugin-syntax-jsx/lib/index.js +0 -21
- package/dist/node_modules/@babel/plugin-syntax-jsx/lib/index.js.map +0 -1
- package/dist/node_modules/@babel/plugin-syntax-jsx/package.json +0 -33
- package/dist/node_modules/@preact/signals-core/CHANGELOG.md +0 -331
- package/dist/node_modules/@preact/signals-core/LICENSE +0 -21
- package/dist/node_modules/@preact/signals-core/README.md +0 -258
- package/dist/node_modules/@preact/signals-core/dist/signals-core.d.ts +0 -149
- package/dist/node_modules/@preact/signals-core/dist/signals-core.js +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.js.map +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.min.js +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.min.js.map +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.mjs +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.mjs.map +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.module.js +0 -1
- package/dist/node_modules/@preact/signals-core/dist/signals-core.module.js.map +0 -1
- package/dist/node_modules/@preact/signals-core/package.json +0 -48
- package/dist/node_modules/@preact/signals-core/src/index.ts +0 -1121
- package/dist/package.json +0 -30
- package/dist/router/Link.js +0 -73
- package/dist/router/index.js +0 -216
- package/dist/runtime/For.js +0 -16
- package/dist/runtime/dom.js +0 -73
- package/dist/runtime/lifecycle.js +0 -25
- 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);
|
package/server/index.js
ADDED
|
@@ -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";
|
package/server/render.js
ADDED
|
@@ -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 === "&" ? "&" : c === "<" ? "<" : ">"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Escape an attribute value. */
|
|
29
|
+
export function escapeAttr(s) {
|
|
30
|
+
return String(s).replace(/[&"]/g, (c) => (c === "&" ? "&" : """));
|
|
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/README.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# @opentf/web
|
|
2
|
-
|
|
3
|
-
The core engine of the **Web App Framework**, a lightweight, high-performance SPA framework designed for the modern web.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- 🚀 **Zero-VDOM**: Compiles JSX directly to imperative DOM operations for maximum speed.
|
|
8
|
-
- ⚡ **Signal-Based Reactivity**: Powered by `@preact/signals-core` for fine-grained, surgical updates.
|
|
9
|
-
- 📦 **Web Component Native**: Every component is a standard Custom Element, ensuring perfect encapsulation and interoperability.
|
|
10
|
-
- 🛠️ **Convention-over-Configuration**: File-based routing and automatic component registration.
|
|
11
|
-
- 📏 **Ultra-Lightweight**: Minimal runtime footprint.
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install @opentf/web
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Basic Usage
|
|
20
|
-
|
|
21
|
-
```jsx
|
|
22
|
-
export default function Counter() {
|
|
23
|
-
let count = $state(0);
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="counter">
|
|
27
|
-
<button onclick={() => count--}>-</button>
|
|
28
|
-
<span>{count}</span>
|
|
29
|
-
<button onclick={() => count++}>+</button>
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Documentation
|
|
36
|
-
|
|
37
|
-
Visit [web.opentf.workers.dev](https://web.opentf.workers.dev) for full documentation and guides.
|
|
38
|
-
|
|
39
|
-
## License
|
|
40
|
-
|
|
41
|
-
MIT © [Open Tech Foundation](https://github.com/Open-Tech-Foundation)
|
package/dist/CHANGELOG.md
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# @opentf/web
|
|
2
|
-
|
|
3
|
-
## 0.4.0
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- ba984c2: Here is a summary of the changes for your changelog:
|
|
8
|
-
|
|
9
|
-
### 🚀 Features
|
|
10
|
-
|
|
11
|
-
- **Compiler:** Added comprehensive support for **SVG camelCase attributes** (e.g., `viewBox`, `strokeWidth`, `strokeLinecap`). The compiler now preserves the original casing for these attributes, ensuring icons and SVGs render correctly in the browser.
|
|
12
|
-
- **Router:** Improved route matching to support **optional trailing slashes** (e.g., `/about` and `/about/` now match the same route).
|
|
13
|
-
- **Playground:** Added a new **SVG Icons Showcase** page to demonstrate seamless integration with icon libraries like Lucide and Heroicons.
|
|
14
|
-
|
|
15
|
-
### 🐞 Bug Fixes
|
|
16
|
-
|
|
17
|
-
- **Compiler:** Fixed a critical bug where **imported components** were incorrectly compiled as dynamic variables (e.g., `document.createElement(Link)`) instead of using their registered custom element tag names (e.g., `document.createElement("web-link")`).
|
|
18
|
-
- **Playground:** Resolved a build failure in the Icons page by correcting an illegal `const` reassignment of a reactive state variable.
|
|
19
|
-
- **Performance:** Reduced the simulated network delay in `routeGuard.js` from 800ms to 100ms for a snappier development experience.
|
|
20
|
-
|
|
21
|
-
### 🏠 Internal & Maintenance
|
|
22
|
-
|
|
23
|
-
- **Refactoring:** Optimized the Babel plugin by moving large static lists (SVG tags, properties, etc.) to the top-level scope, improving compilation speed.
|
|
24
|
-
- **Testing:** Expanded the test suite with new cases for **icon libraries** and **complex React patterns** (dynamic tags, render props, spread overrides).
|
|
25
|
-
|
|
26
|
-
***
|
|
27
|
-
|
|
28
|
-
## 0.3.0
|
|
29
|
-
|
|
30
|
-
### Minor Changes
|
|
31
|
-
|
|
32
|
-
- 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.
|
|
33
|
-
|
|
34
|
-
## 0.2.0
|
|
35
|
-
|
|
36
|
-
### Minor Changes
|
|
37
|
-
|
|
38
|
-
- a56fdfe: Fix the core components to prebuilt before publishing
|
|
39
|
-
|
|
40
|
-
## 0.1.0
|
|
41
|
-
|
|
42
|
-
### Minor Changes
|
|
43
|
-
|
|
44
|
-
- Global rebranding to Web App Framework and monorepo restructuring.
|
|
45
|
-
- Renamed all packages from `@opentf/mwaf-*` to `@opentf/web-*`.
|
|
46
|
-
- Updated Custom Element prefix from `mwaf-` to `web-`.
|
|
47
|
-
- Restructured the repository into a scoped monorepo.
|
|
48
|
-
- Updated scaffolding tool `@opentf/create-web` with new templates.
|
|
49
|
-
- Aligned all package versions to 0.1.0-alpha.1.
|
package/dist/README.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# @opentf/web
|
|
2
|
-
|
|
3
|
-
The core engine of the **Web App Framework**, a lightweight, high-performance SPA framework designed for the modern web.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- 🚀 **Zero-VDOM**: Compiles JSX directly to imperative DOM operations for maximum speed.
|
|
8
|
-
- ⚡ **Signal-Based Reactivity**: Powered by `@preact/signals-core` for fine-grained, surgical updates.
|
|
9
|
-
- 📦 **Web Component Native**: Every component is a standard Custom Element, ensuring perfect encapsulation and interoperability.
|
|
10
|
-
- 🛠️ **Convention-over-Configuration**: File-based routing and automatic component registration.
|
|
11
|
-
- 📏 **Ultra-Lightweight**: Minimal runtime footprint.
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install @opentf/web
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Basic Usage
|
|
20
|
-
|
|
21
|
-
```jsx
|
|
22
|
-
export default function Counter() {
|
|
23
|
-
let count = $state(0);
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<div className="counter">
|
|
27
|
-
<button onclick={() => count--}>-</button>
|
|
28
|
-
<span>{count}</span>
|
|
29
|
-
<button onclick={() => count++}>+</button>
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
## Documentation
|
|
36
|
-
|
|
37
|
-
Visit [web.opentf.workers.dev](https://web.opentf.workers.dev) for full documentation and guides.
|
|
38
|
-
|
|
39
|
-
## License
|
|
40
|
-
|
|
41
|
-
MIT © [Open Tech Foundation](https://github.com/Open-Tech-Foundation)
|