extrojs 0.1.0 → 0.3.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 +3 -3
- package/client.d.ts +8 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +27 -0
- package/dist/commands/build.d.ts +5 -0
- package/dist/commands/build.js +26 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.js +156 -0
- package/dist/config.d.ts +2 -2
- package/dist/core/asset.d.ts +10 -0
- package/dist/core/asset.js +10 -0
- package/dist/dev-assets.d.ts +3 -3
- package/dist/dev-assets.js +33 -12
- package/dist/env.d.ts +10 -0
- package/dist/env.js +18 -0
- package/dist/exports/asset.d.ts +1 -0
- package/dist/exports/asset.js +1 -0
- package/dist/exports/link.d.ts +1 -0
- package/dist/exports/link.js +1 -0
- package/dist/exports/navigation.d.ts +2 -0
- package/dist/exports/navigation.js +1 -0
- package/dist/exports/runtime.d.ts +2 -0
- package/dist/exports/runtime.js +3 -0
- package/dist/index.js +5 -136
- package/dist/load-config.d.ts +1 -1
- package/dist/logger.d.ts +34 -0
- package/dist/logger.js +65 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.js +8 -0
- package/dist/pkg.d.ts +6 -0
- package/dist/pkg.js +5 -0
- package/dist/plugin/app-tree.d.ts +59 -0
- package/dist/plugin/app-tree.js +214 -0
- package/dist/plugin/asset-inventory.d.ts +24 -0
- package/dist/plugin/asset-inventory.js +9 -0
- package/dist/plugin/dev-reactions.d.ts +59 -0
- package/dist/plugin/dev-reactions.js +62 -0
- package/dist/plugin/emit-assets.d.ts +50 -0
- package/dist/plugin/emit-assets.js +40 -0
- package/dist/plugin/generators/html.d.ts +39 -0
- package/dist/plugin/generators/html.js +127 -0
- package/dist/plugin/generators/icons.d.ts +15 -0
- package/dist/plugin/generators/icons.js +16 -0
- package/dist/plugin/generators/public.d.ts +17 -0
- package/dist/plugin/generators/public.js +20 -0
- package/dist/plugin/icons.d.ts +5 -0
- package/dist/plugin/icons.js +20 -0
- package/dist/plugin/index.d.ts +31 -0
- package/dist/plugin/index.js +246 -0
- package/dist/plugin/internal.d.ts +14 -0
- package/dist/plugin/internal.js +6 -0
- package/dist/plugin/manifest.d.ts +29 -0
- package/dist/plugin/manifest.js +68 -0
- package/dist/plugin/public.d.ts +21 -0
- package/dist/plugin/public.js +63 -0
- package/dist/plugin/runtimes/clients/csui-mount.js +90 -0
- package/dist/plugin/runtimes/clients/dev-bridge.js +194 -0
- package/dist/plugin/runtimes/csui-mount.d.ts +18 -0
- package/dist/plugin/runtimes/csui-mount.js +21 -0
- package/dist/plugin/runtimes/dev-bridge.d.ts +22 -0
- package/dist/plugin/runtimes/dev-bridge.js +19 -0
- package/dist/plugin/runtimes/routes-module.d.ts +20 -0
- package/dist/plugin/runtimes/routes-module.js +51 -0
- package/dist/plugin/runtimes/runtime-module.d.ts +16 -0
- package/dist/plugin/runtimes/runtime-module.js +40 -0
- package/dist/plugin/surfaces.d.ts +37 -0
- package/dist/plugin/surfaces.js +67 -0
- package/dist/plugin/types/index.d.ts +9 -0
- package/dist/plugin/types/index.js +1 -0
- package/dist/plugin/utils/read-json.d.ts +1 -0
- package/dist/plugin/utils/read-json.js +8 -0
- package/dist/react/env.d.ts +13 -0
- package/dist/react/env.js +1 -0
- package/dist/router/build-tree.d.ts +46 -0
- package/dist/router/build-tree.js +56 -0
- package/dist/router/context.d.ts +13 -0
- package/dist/router/context.js +2 -0
- package/dist/router/create-router.d.ts +10 -0
- package/dist/router/create-router.js +126 -0
- package/dist/router/defaults.d.ts +24 -0
- package/dist/router/defaults.js +25 -0
- package/dist/router/error-boundary.d.ts +23 -0
- package/dist/router/error-boundary.js +21 -0
- package/dist/router/hooks.d.ts +18 -0
- package/dist/router/hooks.js +34 -0
- package/dist/router/index.d.ts +8 -0
- package/dist/router/index.js +7 -0
- package/dist/router/link.d.ts +305 -0
- package/dist/router/link.js +30 -0
- package/dist/router/match.d.ts +14 -0
- package/dist/router/match.js +27 -0
- package/dist/router/types.d.ts +55 -0
- package/dist/router/types.js +1 -0
- package/dist/types/index.d.ts +152 -0
- package/dist/types/index.js +1 -0
- package/package.json +47 -9
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readJson<T = unknown>(file: string, root: string): T | null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {};
|
|
2
|
+
declare global {
|
|
3
|
+
interface ImportMetaEnv {
|
|
4
|
+
readonly MODE: string;
|
|
5
|
+
readonly DEV: boolean;
|
|
6
|
+
readonly PROD: boolean;
|
|
7
|
+
readonly BASE_URL: string;
|
|
8
|
+
readonly [key: `EXTRO_PUBLIC_${string}`]: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
interface ImportMeta {
|
|
11
|
+
readonly env: ImportMetaEnv;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ComponentType, ReactElement } from "react";
|
|
2
|
+
import type { BoundaryKind } from "../types/index.js";
|
|
3
|
+
import type { Router } from "./context.js";
|
|
4
|
+
import type { ErrorProps, LayoutProps, PageProps } from "./types.js";
|
|
5
|
+
/** A boundary already paired with its loaded component (orchestration zips this). */
|
|
6
|
+
export type ResolvedBoundary = {
|
|
7
|
+
kind: BoundaryKind;
|
|
8
|
+
component: ComponentType<LayoutProps> | ComponentType<ErrorProps>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Every renderable outcome of a navigation. `buildTree` is total over this:
|
|
12
|
+
* `render()` resolves one of these (with the navToken/load orchestration) and
|
|
13
|
+
* hands it here; structure lives nowhere else.
|
|
14
|
+
*/
|
|
15
|
+
export type RenderOutcome = {
|
|
16
|
+
type: "match";
|
|
17
|
+
page: ComponentType<PageProps>;
|
|
18
|
+
params: Record<string, string>;
|
|
19
|
+
boundaries: ResolvedBoundary[];
|
|
20
|
+
} | {
|
|
21
|
+
type: "not-found";
|
|
22
|
+
notFound: ComponentType;
|
|
23
|
+
rootLayout: ComponentType<LayoutProps> | null;
|
|
24
|
+
} | {
|
|
25
|
+
type: "load-error";
|
|
26
|
+
error: Error;
|
|
27
|
+
reset: () => void;
|
|
28
|
+
};
|
|
29
|
+
export type RenderContext = {
|
|
30
|
+
pathname: string;
|
|
31
|
+
search: string;
|
|
32
|
+
router: Router;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* @describe The single, pure home of ADR 0003 §3/§4/§5 structure. Given a
|
|
36
|
+
* resolved navigation outcome, returns the React element tree to mount:
|
|
37
|
+
*
|
|
38
|
+
* - match: <Provider><BuiltInEB> L0 <EB user> ... <Page/> ... </Provider>
|
|
39
|
+
* (each segment's error nested inside its sibling layout, §3)
|
|
40
|
+
* - not-found: <Provider><BuiltInEB> rootLayout? <NotFound/> </Provider> (§4)
|
|
41
|
+
* - load-error: bare <DefaultError/> (load failed outside React render, §5)
|
|
42
|
+
*
|
|
43
|
+
* Pure and total: same inputs, same tree; no DOM, no effects. The always-on
|
|
44
|
+
* built-in error boundary (§5) means a match/not-found surface never blanks.
|
|
45
|
+
*/
|
|
46
|
+
export declare function buildTree(outcome: RenderOutcome, ctx: RenderContext): ReactElement;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { RouterContext } from "./context.js";
|
|
3
|
+
import { ErrorBoundary } from "./error-boundary.js";
|
|
4
|
+
import { DefaultError } from "./defaults.js";
|
|
5
|
+
/**
|
|
6
|
+
* @describe The single, pure home of ADR 0003 §3/§4/§5 structure. Given a
|
|
7
|
+
* resolved navigation outcome, returns the React element tree to mount:
|
|
8
|
+
*
|
|
9
|
+
* - match: <Provider><BuiltInEB> L0 <EB user> ... <Page/> ... </Provider>
|
|
10
|
+
* (each segment's error nested inside its sibling layout, §3)
|
|
11
|
+
* - not-found: <Provider><BuiltInEB> rootLayout? <NotFound/> </Provider> (§4)
|
|
12
|
+
* - load-error: bare <DefaultError/> (load failed outside React render, §5)
|
|
13
|
+
*
|
|
14
|
+
* Pure and total: same inputs, same tree; no DOM, no effects. The always-on
|
|
15
|
+
* built-in error boundary (§5) means a match/not-found surface never blanks.
|
|
16
|
+
*/
|
|
17
|
+
export function buildTree(outcome, ctx) {
|
|
18
|
+
if (outcome.type === "load-error") {
|
|
19
|
+
return createElement(DefaultError, {
|
|
20
|
+
error: outcome.error,
|
|
21
|
+
reset: outcome.reset,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// Router context + the always-on outermost built-in error boundary (§5),
|
|
25
|
+
// shared by the match and not-found paths so they stay consistent.
|
|
26
|
+
const provide = (params, inner) => createElement(RouterContext.Provider, {
|
|
27
|
+
value: {
|
|
28
|
+
pathname: ctx.pathname,
|
|
29
|
+
search: ctx.search,
|
|
30
|
+
params,
|
|
31
|
+
router: ctx.router,
|
|
32
|
+
},
|
|
33
|
+
}, createElement(ErrorBoundary, { fallback: DefaultError, children: inner }));
|
|
34
|
+
if (outcome.type === "not-found") {
|
|
35
|
+
let inner = createElement(outcome.notFound);
|
|
36
|
+
if (outcome.rootLayout) {
|
|
37
|
+
inner = createElement(outcome.rootLayout, { children: inner });
|
|
38
|
+
}
|
|
39
|
+
return provide({}, inner);
|
|
40
|
+
}
|
|
41
|
+
// Fold innermost-first so the outermost boundary wraps everything; each
|
|
42
|
+
// segment's error sits inside its sibling layout (the chain is ordered
|
|
43
|
+
// layout-before-error per segment). Empty chain = just the page.
|
|
44
|
+
const composed = outcome.boundaries.reduceRight((child, boundary) => {
|
|
45
|
+
if (boundary.kind === "error") {
|
|
46
|
+
return createElement(ErrorBoundary, {
|
|
47
|
+
fallback: boundary.component,
|
|
48
|
+
children: child,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return createElement(boundary.component, {
|
|
52
|
+
children: child,
|
|
53
|
+
});
|
|
54
|
+
}, createElement(outcome.page, { params: outcome.params }));
|
|
55
|
+
return provide(outcome.params, composed);
|
|
56
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Router {
|
|
2
|
+
push: (to: string) => void;
|
|
3
|
+
replace: (to: string) => void;
|
|
4
|
+
back: () => void;
|
|
5
|
+
forward: () => void;
|
|
6
|
+
}
|
|
7
|
+
export interface RouterContextValue {
|
|
8
|
+
pathname: string;
|
|
9
|
+
search: string;
|
|
10
|
+
params: Record<string, string>;
|
|
11
|
+
router: Router;
|
|
12
|
+
}
|
|
13
|
+
export declare const RouterContext: import("react").Context<RouterContextValue | null>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CreateRouterOptions, Route, RouterSurfaceOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* @describe Mounts a surface (popup | options | sidepanel) and wires hash-based
|
|
4
|
+
* client-side routing into the given routes array. Called once per surface by
|
|
5
|
+
* the virtual runtime module emitted by @extrojs/vite-plugin.
|
|
6
|
+
*/
|
|
7
|
+
export interface ExtroRouterHandle {
|
|
8
|
+
update: (newRoutes: Route[], opts?: RouterSurfaceOptions) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare const createExtroRouter: (routes: Route[], options?: CreateRouterOptions) => ExtroRouterHandle;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createRoot } from "react-dom/client";
|
|
2
|
+
import { matchRoutes } from "./match.js";
|
|
3
|
+
import { DefaultNotFound } from "./defaults.js";
|
|
4
|
+
import { buildTree } from "./build-tree.js";
|
|
5
|
+
const toError = (err) => err instanceof Error ? err : new Error(String(err));
|
|
6
|
+
export const createExtroRouter = (routes, options = {}) => {
|
|
7
|
+
const { rootId = "root" } = options;
|
|
8
|
+
const el = document.getElementById(rootId);
|
|
9
|
+
if (!el) {
|
|
10
|
+
throw new Error(`Extro: #${rootId} element not found`);
|
|
11
|
+
}
|
|
12
|
+
const root = createRoot(el);
|
|
13
|
+
const router = createRouter();
|
|
14
|
+
let currentRoutes = routes;
|
|
15
|
+
let notFound = options.notFound ?? null;
|
|
16
|
+
let rootLayout = options.rootLayout ?? null;
|
|
17
|
+
let navToken = 0;
|
|
18
|
+
// Built-in fallback when the surface has no not-found.tsx (ADR 0003 §5).
|
|
19
|
+
const loadNotFound = () => notFound ? notFound() : Promise.resolve({ default: DefaultNotFound });
|
|
20
|
+
// Pure orchestration: resolve the navigation outcome (with the navToken
|
|
21
|
+
// guard + load failure handling) and hand it to `buildTree`. No structure
|
|
22
|
+
// lives here — every renderable shape is in build-tree.ts (ADR 0006).
|
|
23
|
+
const render = async () => {
|
|
24
|
+
const token = ++navToken;
|
|
25
|
+
const { pathname, search } = parseLocation();
|
|
26
|
+
const ctx = { pathname, search, router };
|
|
27
|
+
const matches = matchRoutes(pathname, currentRoutes);
|
|
28
|
+
if (!matches) {
|
|
29
|
+
// No Route matched: not-found inside the surface-root layout only
|
|
30
|
+
// (ADR 0003 §4). Nothing matched, so no deeper layout is in scope.
|
|
31
|
+
let nf;
|
|
32
|
+
let rl;
|
|
33
|
+
try {
|
|
34
|
+
;
|
|
35
|
+
[nf, rl] = await Promise.all([
|
|
36
|
+
loadNotFound(),
|
|
37
|
+
rootLayout ? rootLayout() : Promise.resolve(null),
|
|
38
|
+
]);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (token !== navToken)
|
|
42
|
+
return;
|
|
43
|
+
root.render(buildTree({ type: "load-error", error: toError(err), reset: () => void render() }, ctx));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (token !== navToken)
|
|
47
|
+
return;
|
|
48
|
+
root.render(buildTree({
|
|
49
|
+
type: "not-found",
|
|
50
|
+
notFound: nf.default,
|
|
51
|
+
rootLayout: rl ? rl.default : null,
|
|
52
|
+
}, ctx));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const leaf = matches[matches.length - 1];
|
|
56
|
+
// Page + ancestor boundaries load in parallel, in route order
|
|
57
|
+
// (outermost first). A missing/broken module rejects here — outside
|
|
58
|
+
// React render, so no boundary can catch it; surface the built-in
|
|
59
|
+
// error instead of blanking the surface (ADR 0003 §5).
|
|
60
|
+
let mod;
|
|
61
|
+
let boundaryMods;
|
|
62
|
+
try {
|
|
63
|
+
;
|
|
64
|
+
[mod, ...boundaryMods] = await Promise.all([
|
|
65
|
+
leaf.route.load(),
|
|
66
|
+
...leaf.route.boundaries.map((b) => b.load()),
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (token !== navToken)
|
|
71
|
+
return;
|
|
72
|
+
root.render(buildTree({ type: "load-error", error: toError(err), reset: () => void render() }, ctx));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (token !== navToken)
|
|
76
|
+
return;
|
|
77
|
+
// Zip each boundary's kind with its loaded component; structure (the
|
|
78
|
+
// §3 nesting) is buildTree's job, not this orchestrator's.
|
|
79
|
+
const boundaries = leaf.route.boundaries.map((b, i) => ({
|
|
80
|
+
kind: b.kind,
|
|
81
|
+
component: boundaryMods[i].default,
|
|
82
|
+
}));
|
|
83
|
+
root.render(buildTree({
|
|
84
|
+
type: "match",
|
|
85
|
+
page: mod.default,
|
|
86
|
+
params: leaf.params,
|
|
87
|
+
boundaries,
|
|
88
|
+
}, ctx));
|
|
89
|
+
};
|
|
90
|
+
window.addEventListener("hashchange", render);
|
|
91
|
+
render();
|
|
92
|
+
return {
|
|
93
|
+
update: (newRoutes, opts) => {
|
|
94
|
+
currentRoutes = newRoutes;
|
|
95
|
+
if (opts) {
|
|
96
|
+
notFound = opts.notFound ?? null;
|
|
97
|
+
rootLayout = opts.rootLayout ?? null;
|
|
98
|
+
}
|
|
99
|
+
render();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* @describe Normalizes `window.location.hash` into a pathname + search string.
|
|
105
|
+
*/
|
|
106
|
+
const parseLocation = () => {
|
|
107
|
+
const [rawPath = "", search = ""] = window.location.hash.replace(/^#/, "").split("?");
|
|
108
|
+
return { pathname: rawPath || "/", search };
|
|
109
|
+
};
|
|
110
|
+
const stripHash = (to) => (to.startsWith("#") ? to.slice(1) : to);
|
|
111
|
+
/**
|
|
112
|
+
* @describe Builds the stable router object passed through context. `replace`
|
|
113
|
+
* uses history.replaceState + a manual hashchange dispatch because
|
|
114
|
+
* replaceState alone doesn't fire the event.
|
|
115
|
+
*/
|
|
116
|
+
const createRouter = () => ({
|
|
117
|
+
push: (to) => {
|
|
118
|
+
window.location.hash = stripHash(to);
|
|
119
|
+
},
|
|
120
|
+
replace: (to) => {
|
|
121
|
+
window.history.replaceState(null, "", `#${stripHash(to)}`);
|
|
122
|
+
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
|
123
|
+
},
|
|
124
|
+
back: () => window.history.back(),
|
|
125
|
+
forward: () => window.history.forward(),
|
|
126
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ErrorProps } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* @describe Built-in error fallback (ADR 0003 §5). Always the outermost
|
|
4
|
+
* boundary so a thrown render never blanks the surface. Deliberately
|
|
5
|
+
* unstyled and minimal; shows `error.message` always (the extension error
|
|
6
|
+
* surface is seen by the developer far more than end users in v0.x).
|
|
7
|
+
*/
|
|
8
|
+
export declare const DefaultError: ({ error, reset }: ErrorProps) => import("react").DetailedReactHTMLElement<{
|
|
9
|
+
style: {
|
|
10
|
+
padding: number;
|
|
11
|
+
fontFamily: "system-ui, sans-serif";
|
|
12
|
+
};
|
|
13
|
+
}, HTMLElement>;
|
|
14
|
+
/**
|
|
15
|
+
* @describe Built-in not-found fallback (ADR 0003 §4/§5). Rendered when a
|
|
16
|
+
* hash matches no Route. Takes no props per the user contract; reads the
|
|
17
|
+
* unmatched path from the router context it is mounted within.
|
|
18
|
+
*/
|
|
19
|
+
export declare const DefaultNotFound: () => import("react").DetailedReactHTMLElement<{
|
|
20
|
+
style: {
|
|
21
|
+
padding: number;
|
|
22
|
+
fontFamily: "system-ui, sans-serif";
|
|
23
|
+
};
|
|
24
|
+
}, HTMLElement>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { useLocation } from "./hooks.js";
|
|
3
|
+
/**
|
|
4
|
+
* @describe Built-in error fallback (ADR 0003 §5). Always the outermost
|
|
5
|
+
* boundary so a thrown render never blanks the surface. Deliberately
|
|
6
|
+
* unstyled and minimal; shows `error.message` always (the extension error
|
|
7
|
+
* surface is seen by the developer far more than end users in v0.x).
|
|
8
|
+
*/
|
|
9
|
+
export const DefaultError = ({ error, reset }) => createElement("div", { style: { padding: 16, fontFamily: "system-ui, sans-serif" } }, createElement("p", { style: { margin: "0 0 8px", fontWeight: 600 } }, "Something went wrong"), createElement("pre", {
|
|
10
|
+
style: {
|
|
11
|
+
margin: "0 0 12px",
|
|
12
|
+
whiteSpace: "pre-wrap",
|
|
13
|
+
fontSize: 12,
|
|
14
|
+
color: "#b00",
|
|
15
|
+
},
|
|
16
|
+
}, error.message), createElement("button", { onClick: reset }, "Try again"));
|
|
17
|
+
/**
|
|
18
|
+
* @describe Built-in not-found fallback (ADR 0003 §4/§5). Rendered when a
|
|
19
|
+
* hash matches no Route. Takes no props per the user contract; reads the
|
|
20
|
+
* unmatched path from the router context it is mounted within.
|
|
21
|
+
*/
|
|
22
|
+
export const DefaultNotFound = () => {
|
|
23
|
+
const { pathname } = useLocation();
|
|
24
|
+
return createElement("div", { style: { padding: 16, fontFamily: "system-ui, sans-serif" } }, createElement("p", { style: { margin: "0 0 4px", fontWeight: 600 } }, "404"), createElement("p", { style: { margin: 0, fontSize: 13, color: "#666" } }, `No route for ${pathname}`));
|
|
25
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ComponentType, ReactNode } from "react";
|
|
2
|
+
import type { ErrorProps } from "./types.js";
|
|
3
|
+
import { Component } from "react";
|
|
4
|
+
interface ErrorBoundaryProps {
|
|
5
|
+
fallback: ComponentType<ErrorProps>;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
interface ErrorBoundaryState {
|
|
9
|
+
error: Error | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* @describe Catches render errors in its subtree and shows `fallback` with
|
|
13
|
+
* `{ error, reset }`. Composed inside its sibling layout (ADR 0003 §3), so a
|
|
14
|
+
* caught error never tears the layout down, and `reset()` only re-renders the
|
|
15
|
+
* boundary's own contents.
|
|
16
|
+
*/
|
|
17
|
+
export declare class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
18
|
+
state: ErrorBoundaryState;
|
|
19
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
20
|
+
reset: () => void;
|
|
21
|
+
render(): ReactNode;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Component, createElement } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* @describe Catches render errors in its subtree and shows `fallback` with
|
|
4
|
+
* `{ error, reset }`. Composed inside its sibling layout (ADR 0003 §3), so a
|
|
5
|
+
* caught error never tears the layout down, and `reset()` only re-renders the
|
|
6
|
+
* boundary's own contents.
|
|
7
|
+
*/
|
|
8
|
+
export class ErrorBoundary extends Component {
|
|
9
|
+
state = { error: null };
|
|
10
|
+
static getDerivedStateFromError(error) {
|
|
11
|
+
return { error };
|
|
12
|
+
}
|
|
13
|
+
reset = () => this.setState({ error: null });
|
|
14
|
+
render() {
|
|
15
|
+
const { error } = this.state;
|
|
16
|
+
if (error) {
|
|
17
|
+
return createElement(this.props.fallback, { error, reset: this.reset });
|
|
18
|
+
}
|
|
19
|
+
return this.props.children;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Router } from "./context.js";
|
|
2
|
+
export declare const useLocation: () => {
|
|
3
|
+
pathname: string;
|
|
4
|
+
search: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const useParams: <T extends Record<string, string> = Record<string, string>>() => T;
|
|
7
|
+
export declare const useRouter: () => Router;
|
|
8
|
+
type SearchInit = URLSearchParams | string | Record<string, string>;
|
|
9
|
+
interface UseSearchParamsResult {
|
|
10
|
+
params: URLSearchParams;
|
|
11
|
+
setParams: (next: SearchInit) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @describe Reads + writes the URL search string. Updates use `router.replace`
|
|
15
|
+
* so query edits don't pile up history entries.
|
|
16
|
+
*/
|
|
17
|
+
export declare const useSearchParams: () => UseSearchParamsResult;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useContext, useMemo } from "react";
|
|
2
|
+
import { RouterContext } from "./context.js";
|
|
3
|
+
const useRouterContext = () => {
|
|
4
|
+
const ctx = useContext(RouterContext);
|
|
5
|
+
if (!ctx) {
|
|
6
|
+
throw new Error("Extro: router hooks must be used inside a page rendered by createExtroRouter.");
|
|
7
|
+
}
|
|
8
|
+
return ctx;
|
|
9
|
+
};
|
|
10
|
+
export const useLocation = () => {
|
|
11
|
+
const { pathname, search } = useRouterContext();
|
|
12
|
+
return { pathname, search };
|
|
13
|
+
};
|
|
14
|
+
export const useParams = () => {
|
|
15
|
+
return useRouterContext().params;
|
|
16
|
+
};
|
|
17
|
+
export const useRouter = () => useRouterContext().router;
|
|
18
|
+
/**
|
|
19
|
+
* @describe Reads + writes the URL search string. Updates use `router.replace`
|
|
20
|
+
* so query edits don't pile up history entries.
|
|
21
|
+
*/
|
|
22
|
+
export const useSearchParams = () => {
|
|
23
|
+
const { search, pathname, router } = useRouterContext();
|
|
24
|
+
const params = useMemo(() => new URLSearchParams(search), [search]);
|
|
25
|
+
const setParams = (next) => {
|
|
26
|
+
const nextSearch = next instanceof URLSearchParams
|
|
27
|
+
? next.toString()
|
|
28
|
+
: typeof next === "string"
|
|
29
|
+
? next
|
|
30
|
+
: new URLSearchParams(next).toString();
|
|
31
|
+
router.replace(nextSearch ? `${pathname}?${nextSearch}` : pathname);
|
|
32
|
+
};
|
|
33
|
+
return { params, setParams };
|
|
34
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "../react/env.js";
|
|
2
|
+
export { createExtroRouter } from "./create-router.js";
|
|
3
|
+
export type { ExtroRouterHandle } from "./create-router.js";
|
|
4
|
+
export { matchRoutes } from "./match.js";
|
|
5
|
+
export { Link } from "./link.js";
|
|
6
|
+
export { useLocation, useParams, useRouter, useSearchParams, } from "./hooks.js";
|
|
7
|
+
export type { Router, RouterContextValue } from "./context.js";
|
|
8
|
+
export type { CreateRouterOptions, DynamicRoute, ErrorProps, LayoutProps, PageProps, Route, RouteMatch, StaticRoute, } from "./types.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Pulls in the ambient env typing so importing a routing subpath is enough to
|
|
2
|
+
// type `import.meta.env` with no extra setup.
|
|
3
|
+
import "../react/env.js";
|
|
4
|
+
export { createExtroRouter } from "./create-router.js";
|
|
5
|
+
export { matchRoutes } from "./match.js";
|
|
6
|
+
export { Link } from "./link.js";
|
|
7
|
+
export { useLocation, useParams, useRouter, useSearchParams, } from "./hooks.js";
|