@veap/plugins 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +131 -2
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/plugin.actions.d.ts +6 -0
- package/dist/actions/plugin.actions.js +60 -0
- package/dist/actions.d.ts +1 -0
- package/dist/actions.js +1 -0
- package/dist/cli/add.d.ts +4 -0
- package/dist/cli/add.js +191 -0
- package/dist/cli/eject.d.ts +2 -0
- package/dist/cli/eject.js +136 -0
- package/dist/cli/generate-plugin.d.ts +3 -0
- package/dist/cli/generate-plugin.js +48 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +6 -0
- package/dist/cli/package-manager.d.ts +14 -0
- package/dist/cli/package-manager.js +62 -0
- package/dist/cli/stubs.d.ts +16 -0
- package/dist/cli/stubs.js +42 -0
- package/dist/cli/utils.d.ts +3 -0
- package/dist/cli/utils.js +164 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +8 -0
- package/dist/common/context.d.ts +26 -0
- package/dist/common/context.js +26 -0
- package/dist/common/events.d.ts +19 -0
- package/dist/common/events.js +26 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.js +3 -0
- package/dist/common/types.d.ts +195 -0
- package/dist/common/types.js +40 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/migrations/0001_create_plugins_tables.d.ts +4 -0
- package/dist/migrations/0001_create_plugins_tables.js +29 -0
- package/dist/migrations/index.d.ts +4 -0
- package/dist/migrations/index.js +7 -0
- package/dist/models/SystemPlugin.d.ts +19 -0
- package/dist/models/SystemPlugin.js +14 -0
- package/dist/models/SystemUserWidget.d.ts +19 -0
- package/dist/models/SystemUserWidget.js +17 -0
- package/dist/models/index.d.ts +2 -0
- package/dist/models/index.js +2 -0
- package/dist/models.d.ts +1 -0
- package/dist/models.js +1 -0
- package/dist/navigation/breadcrumbs.d.ts +2 -0
- package/dist/navigation/breadcrumbs.js +84 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/navigation.service.d.ts +27 -0
- package/dist/navigation/navigation.service.js +183 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/plugins.provider.d.ts +20 -0
- package/dist/providers/plugins.provider.js +142 -0
- package/dist/registry/contracts.d.ts +72 -0
- package/dist/registry/contracts.js +4 -0
- package/dist/registry/facade.d.ts +45 -0
- package/dist/registry/facade.js +49 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +5 -0
- package/dist/registry/migration-runner.d.ts +8 -0
- package/dist/registry/migration-runner.js +12 -0
- package/dist/registry/plugin.repository.d.ts +19 -0
- package/dist/registry/plugin.repository.js +74 -0
- package/dist/registry/registry.service.d.ts +69 -0
- package/dist/registry/registry.service.js +535 -0
- package/dist/router/discovery.d.ts +27 -0
- package/dist/router/discovery.js +37 -0
- package/dist/router/handler.d.ts +6 -0
- package/dist/router/handler.js +40 -0
- package/dist/router/index.d.ts +13 -0
- package/dist/router/index.js +13 -0
- package/dist/router/matcher.d.ts +5 -0
- package/dist/router/matcher.js +32 -0
- package/dist/router/middlewares.d.ts +25 -0
- package/dist/router/middlewares.js +119 -0
- package/dist/router/react/error-boundary.d.ts +30 -0
- package/dist/router/react/error-boundary.js +68 -0
- package/dist/router/react/index.d.ts +4 -0
- package/dist/router/react/index.js +4 -0
- package/dist/router/react/renderer.d.ts +13 -0
- package/dist/router/react/renderer.js +159 -0
- package/dist/router/react/soft-navigation.d.ts +4 -0
- package/dist/router/react/soft-navigation.js +34 -0
- package/dist/router/react/with-router.d.ts +22 -0
- package/dist/router/react/with-router.js +19 -0
- package/dist/router/route-tree.d.ts +177 -0
- package/dist/router/route-tree.js +633 -0
- package/dist/router/router.service.d.ts +25 -0
- package/dist/router/router.service.js +127 -0
- package/dist/router/scanner.d.ts +4 -0
- package/dist/router/scanner.js +337 -0
- package/dist/router/utils.d.ts +10 -0
- package/dist/router/utils.js +48 -0
- package/dist/router.d.ts +6 -0
- package/dist/router.js +6 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +17 -0
- package/dist/widgets/index.d.ts +8 -0
- package/dist/widgets/index.js +8 -0
- package/dist/widgets/path-prefix-context.d.ts +5 -0
- package/dist/widgets/path-prefix-context.js +9 -0
- package/dist/widgets/plugin-extension-point-client.d.ts +15 -0
- package/dist/widgets/plugin-extension-point-client.js +44 -0
- package/dist/widgets/plugin-extension-point.d.ts +14 -0
- package/dist/widgets/plugin-extension-point.js +51 -0
- package/dist/widgets/plugin-widget-area-client.d.ts +11 -0
- package/dist/widgets/plugin-widget-area-client.js +31 -0
- package/dist/widgets/plugin-widget-area.d.ts +10 -0
- package/dist/widgets/plugin-widget-area.js +19 -0
- package/dist/widgets/use-confirm-action.d.ts +19 -0
- package/dist/widgets/use-confirm-action.js +68 -0
- package/dist/widgets/widget-composer-client.d.ts +21 -0
- package/dist/widgets/widget-composer-client.js +281 -0
- package/dist/widgets/widget-composer.d.ts +8 -0
- package/dist/widgets/widget-composer.js +28 -0
- package/package.json +115 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified route matcher for Veap.
|
|
3
|
+
* Supports standard parameters (:id) and catch-all patterns (:path(.*)).
|
|
4
|
+
*/
|
|
5
|
+
export function matchRoute(pattern, path) {
|
|
6
|
+
const paramNames = [];
|
|
7
|
+
let regexStr = pattern;
|
|
8
|
+
// Replace :name(.*) (catch-all) and :name (dynamic) in a single pass so that
|
|
9
|
+
// capture-group order always matches the order of placeholders in the
|
|
10
|
+
// pattern. Doing it in two passes would collect catch-all names first even
|
|
11
|
+
// when they appear after dynamic params in the pattern.
|
|
12
|
+
regexStr = regexStr.replace(/:([a-zA-Z0-9_]+)\(\.\*\)|:([a-zA-Z0-9_]+)/g, (_, catchAllName, dynamicName) => {
|
|
13
|
+
if (catchAllName) {
|
|
14
|
+
paramNames.push(catchAllName);
|
|
15
|
+
return "(.*)";
|
|
16
|
+
}
|
|
17
|
+
paramNames.push(dynamicName);
|
|
18
|
+
return "([^\\/]+)";
|
|
19
|
+
});
|
|
20
|
+
// Escape slashes
|
|
21
|
+
regexStr = regexStr.replace(/\//g, "\\/");
|
|
22
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
23
|
+
const match = path.match(regex);
|
|
24
|
+
if (!match)
|
|
25
|
+
return null;
|
|
26
|
+
const params = {};
|
|
27
|
+
paramNames.forEach((name, index) => {
|
|
28
|
+
// biome-ignore lint/style/noNonNullAssertion: We know the match exists and the index is valid
|
|
29
|
+
params[name] = match[index + 1];
|
|
30
|
+
});
|
|
31
|
+
return params;
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ApiMiddleware, VeapMiddleware, VeapMiddlewareContext } from "../common/types";
|
|
2
|
+
export declare const EnsuredGuest: VeapMiddleware;
|
|
3
|
+
export declare const EnsuredUser: VeapMiddleware;
|
|
4
|
+
/**
|
|
5
|
+
* Page-level opt-out from modular security requirements (2FA challenges,
|
|
6
|
+
* onboarding gates, ...). Append it AFTER EnsuredAuth in a page's
|
|
7
|
+
* `middlewares` export. Its marker propagates to the renderer, which skips
|
|
8
|
+
* EnsuredAuth entirely for this route, while explicit auth/roles/permissions
|
|
9
|
+
* (from the page itself) are still enforced by the pipeline below.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SkipSecurity: VeapMiddleware;
|
|
12
|
+
export declare const EnsuredAuth: VeapMiddleware;
|
|
13
|
+
export declare const ApiEnsuredAuth: ApiMiddleware;
|
|
14
|
+
/**
|
|
15
|
+
* Route middleware enforcing same-origin requests for state-changing operations.
|
|
16
|
+
* Throws AppError.Forbidden if request originates from an untrusted origin.
|
|
17
|
+
*/
|
|
18
|
+
export declare const SameOrigin: VeapMiddleware;
|
|
19
|
+
/**
|
|
20
|
+
* API route middleware enforcing same-origin requests for state-changing operations.
|
|
21
|
+
* Returns a 403 JSON Response if request originates from an untrusted origin.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ApiSameOrigin: ApiMiddleware;
|
|
24
|
+
export declare function runPipeline(middlewares: VeapMiddleware[], context: VeapMiddlewareContext, final: (context: VeapMiddlewareContext) => Promise<React.ReactNode>): Promise<React.ReactNode>;
|
|
25
|
+
export declare function runApiPipeline(middlewares: ApiMiddleware[], request: Request, context: any, final: (request: Request, context: any) => Promise<Response>): Promise<Response>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { checkSecurity, getAuthRoutes, getCurrentSession } from "@veap/auth";
|
|
2
|
+
import { AppError, verifySameOrigin } from "@veap/kernel";
|
|
3
|
+
import { headers } from "next/headers";
|
|
4
|
+
import { redirect } from "next/navigation";
|
|
5
|
+
export const EnsuredGuest = async (ctx, next) => {
|
|
6
|
+
const { user, session } = await getCurrentSession();
|
|
7
|
+
if (user && session) {
|
|
8
|
+
const headersList = await headers();
|
|
9
|
+
const referer = headersList.get("referer");
|
|
10
|
+
if (referer && !referer.includes(ctx.path)) {
|
|
11
|
+
return redirect(referer);
|
|
12
|
+
}
|
|
13
|
+
return redirect("/");
|
|
14
|
+
}
|
|
15
|
+
return await next();
|
|
16
|
+
};
|
|
17
|
+
export const EnsuredUser = async (_ctx, next) => {
|
|
18
|
+
const { user, session } = await getCurrentSession();
|
|
19
|
+
if (!user || !session) {
|
|
20
|
+
const authRoutes = getAuthRoutes();
|
|
21
|
+
return redirect(authRoutes.signIn);
|
|
22
|
+
}
|
|
23
|
+
return await next();
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Page-level opt-out from modular security requirements (2FA challenges,
|
|
27
|
+
* onboarding gates, ...). Append it AFTER EnsuredAuth in a page's
|
|
28
|
+
* `middlewares` export. Its marker propagates to the renderer, which skips
|
|
29
|
+
* EnsuredAuth entirely for this route, while explicit auth/roles/permissions
|
|
30
|
+
* (from the page itself) are still enforced by the pipeline below.
|
|
31
|
+
*/
|
|
32
|
+
export const SkipSecurity = async (ctx, next) => {
|
|
33
|
+
ctx.__skipSecurity =
|
|
34
|
+
true;
|
|
35
|
+
return await next();
|
|
36
|
+
};
|
|
37
|
+
export const EnsuredAuth = async (ctx, next) => {
|
|
38
|
+
const { user, session } = await getCurrentSession();
|
|
39
|
+
const authRoutes = getAuthRoutes();
|
|
40
|
+
if (!user || !session) {
|
|
41
|
+
return redirect(authRoutes.signIn);
|
|
42
|
+
}
|
|
43
|
+
const security = await checkSecurity(session, user, ctx.roles, ctx.permissions, undefined, ctx.path);
|
|
44
|
+
if (!security.satisfied) {
|
|
45
|
+
return redirect(security.redirect || authRoutes.signIn);
|
|
46
|
+
}
|
|
47
|
+
return await next();
|
|
48
|
+
};
|
|
49
|
+
export const ApiEnsuredAuth = async (_request, context, next) => {
|
|
50
|
+
const { user, session } = await getCurrentSession();
|
|
51
|
+
if (!user || !session) {
|
|
52
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
53
|
+
status: 401,
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const security = await checkSecurity(session, user, context.roles, context.permissions, undefined, context.path);
|
|
58
|
+
if (!security.satisfied) {
|
|
59
|
+
return new Response(JSON.stringify({
|
|
60
|
+
error: "Security requirement not met",
|
|
61
|
+
redirect: security.redirect,
|
|
62
|
+
}), {
|
|
63
|
+
status: 401,
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return await next();
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Route middleware enforcing same-origin requests for state-changing operations.
|
|
71
|
+
* Throws AppError.Forbidden if request originates from an untrusted origin.
|
|
72
|
+
*/
|
|
73
|
+
export const SameOrigin = async (_ctx, next) => {
|
|
74
|
+
const headersList = await headers();
|
|
75
|
+
const origin = headersList.get("origin");
|
|
76
|
+
const secFetchSite = headersList.get("sec-fetch-site");
|
|
77
|
+
const secFetchMode = headersList.get("sec-fetch-mode");
|
|
78
|
+
if (origin || (secFetchSite && secFetchMode !== "navigate")) {
|
|
79
|
+
if (!verifySameOrigin({ headers: headersList, method: "POST" })) {
|
|
80
|
+
throw AppError.Forbidden("Cross-origin request rejected");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return await next();
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* API route middleware enforcing same-origin requests for state-changing operations.
|
|
87
|
+
* Returns a 403 JSON Response if request originates from an untrusted origin.
|
|
88
|
+
*/
|
|
89
|
+
export const ApiSameOrigin = async (request, _context, next) => {
|
|
90
|
+
if (!verifySameOrigin(request)) {
|
|
91
|
+
return new Response(JSON.stringify({ error: "Forbidden: Cross-origin request rejected" }), {
|
|
92
|
+
status: 403,
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return await next();
|
|
97
|
+
};
|
|
98
|
+
export async function runPipeline(middlewares, context, final) {
|
|
99
|
+
let index = 0;
|
|
100
|
+
async function next() {
|
|
101
|
+
if (index < middlewares.length) {
|
|
102
|
+
const middleware = middlewares[index++];
|
|
103
|
+
return await middleware(context, next);
|
|
104
|
+
}
|
|
105
|
+
return await final(context);
|
|
106
|
+
}
|
|
107
|
+
return await next();
|
|
108
|
+
}
|
|
109
|
+
export async function runApiPipeline(middlewares, request, context, final) {
|
|
110
|
+
let index = 0;
|
|
111
|
+
async function next() {
|
|
112
|
+
if (index < middlewares.length) {
|
|
113
|
+
const middleware = middlewares[index++];
|
|
114
|
+
return await middleware(request, context, next);
|
|
115
|
+
}
|
|
116
|
+
return await final(request, context);
|
|
117
|
+
}
|
|
118
|
+
return await next();
|
|
119
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
interface ErrorBoundaryProps {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
fallback?: React.ComponentType<{
|
|
5
|
+
error: Error;
|
|
6
|
+
reset: () => void;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
interface ErrorBoundaryState {
|
|
10
|
+
error: Error | null;
|
|
11
|
+
resetKey: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Client-side error boundary for the Veap unstable router.
|
|
15
|
+
*
|
|
16
|
+
* Wraps route segments to catch rendering errors and display
|
|
17
|
+
* a recoverable fallback UI. Supports custom fallback components
|
|
18
|
+
* or renders a default error panel.
|
|
19
|
+
*
|
|
20
|
+
* Reset is implemented via internal key increment, which forces
|
|
21
|
+
* React to remount the children subtree.
|
|
22
|
+
*/
|
|
23
|
+
export declare class RouterErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
24
|
+
constructor(props: ErrorBoundaryProps);
|
|
25
|
+
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState>;
|
|
26
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
|
|
27
|
+
private reset;
|
|
28
|
+
render(): React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
/**
|
|
5
|
+
* Client-side error boundary for the Veap unstable router.
|
|
6
|
+
*
|
|
7
|
+
* Wraps route segments to catch rendering errors and display
|
|
8
|
+
* a recoverable fallback UI. Supports custom fallback components
|
|
9
|
+
* or renders a default error panel.
|
|
10
|
+
*
|
|
11
|
+
* Reset is implemented via internal key increment, which forces
|
|
12
|
+
* React to remount the children subtree.
|
|
13
|
+
*/
|
|
14
|
+
export class RouterErrorBoundary extends React.Component {
|
|
15
|
+
constructor(props) {
|
|
16
|
+
super(props);
|
|
17
|
+
this.state = { error: null, resetKey: 0 };
|
|
18
|
+
}
|
|
19
|
+
static getDerivedStateFromError(error) {
|
|
20
|
+
return { error };
|
|
21
|
+
}
|
|
22
|
+
componentDidCatch(error, errorInfo) {
|
|
23
|
+
if (process.env.NODE_ENV === "development") {
|
|
24
|
+
console.warn("[veap:Router] Caught error during rendering:", error, errorInfo);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
reset = () => {
|
|
28
|
+
this.setState((prev) => ({
|
|
29
|
+
error: null,
|
|
30
|
+
resetKey: prev.resetKey + 1,
|
|
31
|
+
}));
|
|
32
|
+
};
|
|
33
|
+
render() {
|
|
34
|
+
const { error, resetKey } = this.state;
|
|
35
|
+
const { children, fallback: Fallback } = this.props;
|
|
36
|
+
if (error !== null) {
|
|
37
|
+
if (Fallback) {
|
|
38
|
+
return _jsx(Fallback, { error: error, reset: this.reset });
|
|
39
|
+
}
|
|
40
|
+
return (_jsxs("div", { role: "alert", style: {
|
|
41
|
+
padding: "2rem",
|
|
42
|
+
borderRadius: "0.5rem",
|
|
43
|
+
border: "1px solid #fca5a5",
|
|
44
|
+
backgroundColor: "#fef2f2",
|
|
45
|
+
color: "#991b1b",
|
|
46
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
47
|
+
}, children: [_jsx("h2", { style: {
|
|
48
|
+
margin: "0 0 0.5rem",
|
|
49
|
+
fontSize: "1.125rem",
|
|
50
|
+
fontWeight: 600,
|
|
51
|
+
}, children: "Something went wrong" }), _jsx("p", { style: {
|
|
52
|
+
margin: "0 0 1rem",
|
|
53
|
+
fontSize: "0.875rem",
|
|
54
|
+
color: "#b91c1c",
|
|
55
|
+
}, children: error.message }), _jsx("button", { type: "button", onClick: this.reset, style: {
|
|
56
|
+
padding: "0.5rem 1rem",
|
|
57
|
+
fontSize: "0.875rem",
|
|
58
|
+
fontWeight: 500,
|
|
59
|
+
color: "#ffffff",
|
|
60
|
+
backgroundColor: "#dc2626",
|
|
61
|
+
border: "none",
|
|
62
|
+
borderRadius: "0.375rem",
|
|
63
|
+
cursor: "pointer",
|
|
64
|
+
}, children: "Try again" })] }));
|
|
65
|
+
}
|
|
66
|
+
return _jsx(React.Fragment, { children: children }, resetKey);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { RouteTree } from "../route-tree";
|
|
3
|
+
export declare function VeapRouter({ tree, path, searchParams, CustomPageComponent, customConfig, }: {
|
|
4
|
+
tree: RouteTree;
|
|
5
|
+
path: string;
|
|
6
|
+
searchParams: Record<string, any>;
|
|
7
|
+
CustomPageComponent?: React.ComponentType<any>;
|
|
8
|
+
customConfig?: {
|
|
9
|
+
roles?: string[];
|
|
10
|
+
permissions?: string[];
|
|
11
|
+
middlewares?: any[];
|
|
12
|
+
};
|
|
13
|
+
}): Promise<React.ReactNode>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { eventBus } from "@veap/kernel/events";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { EnsuredAuth, runPipeline, SkipSecurity } from "../middlewares";
|
|
5
|
+
import { collectAuthRequirements, collectMiddlewares } from "../utils";
|
|
6
|
+
import { RouterErrorBoundary } from "./error-boundary";
|
|
7
|
+
import { SoftNavigationInterceptor } from "./soft-navigation";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Parallel slot resolution
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
async function resolveParallelSlot(slotTree, path, searchParams, params, _slotName, context) {
|
|
12
|
+
const { RouteTree: RouteTreeClass } = await import("../route-tree");
|
|
13
|
+
const tempTree = new RouteTreeClass(slotTree);
|
|
14
|
+
const slotMatch = tempTree.match(path);
|
|
15
|
+
if (slotMatch?.node.page) {
|
|
16
|
+
const SlotPage = slotMatch.node.page;
|
|
17
|
+
const mergedParams = { ...params, ...slotMatch.params };
|
|
18
|
+
return (_jsx(SlotPage, { params: mergedParams, searchParams: searchParams, context: context }));
|
|
19
|
+
}
|
|
20
|
+
if (slotTree.default) {
|
|
21
|
+
const DefaultComponent = slotTree.default;
|
|
22
|
+
return (_jsx(DefaultComponent, { params: params, searchParams: searchParams, context: context }));
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Parallel slots resolver (all slots for a layout entry)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
async function resolveAllParallelSlots(parallelSlots, path, searchParams, params, context) {
|
|
30
|
+
if (!parallelSlots || Object.keys(parallelSlots).length === 0) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
const entries = await Promise.all(Object.entries(parallelSlots).map(async ([slotName, slotNode]) => {
|
|
34
|
+
const rendered = await resolveParallelSlot(slotNode, path, searchParams, params, slotName, context);
|
|
35
|
+
return [slotName, rendered];
|
|
36
|
+
}));
|
|
37
|
+
const result = {};
|
|
38
|
+
for (const [name, node] of entries) {
|
|
39
|
+
result[name] = node;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Main renderer
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
export async function VeapRouter({ tree, path, searchParams, CustomPageComponent, customConfig, }) {
|
|
47
|
+
const match = tree.match(path);
|
|
48
|
+
if (process.env.NODE_ENV !== "production") {
|
|
49
|
+
eventBus
|
|
50
|
+
.publish("router:request", {
|
|
51
|
+
path,
|
|
52
|
+
searchParams,
|
|
53
|
+
params: match?.params || {},
|
|
54
|
+
matchedNode: match?.node?.segment || "/",
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
})
|
|
57
|
+
.catch(() => { });
|
|
58
|
+
}
|
|
59
|
+
if (!match) {
|
|
60
|
+
console.warn("[veap:Router] [renderer] match should never be null");
|
|
61
|
+
throw new Error(`[VeapRouter] Invariant failed: match should never be null`);
|
|
62
|
+
}
|
|
63
|
+
const allMiddlewares = collectMiddlewares(match.layoutChain, match.node);
|
|
64
|
+
const auth = collectAuthRequirements(match.layoutChain, match.node);
|
|
65
|
+
if (customConfig) {
|
|
66
|
+
if (customConfig.roles?.length) {
|
|
67
|
+
auth.roles.push(...customConfig.roles);
|
|
68
|
+
auth.needsAuth = true;
|
|
69
|
+
}
|
|
70
|
+
if (customConfig.permissions?.length) {
|
|
71
|
+
auth.permissions.push(...customConfig.permissions);
|
|
72
|
+
auth.needsAuth = true;
|
|
73
|
+
}
|
|
74
|
+
if (customConfig.middlewares?.length) {
|
|
75
|
+
allMiddlewares.push(...customConfig.middlewares);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (auth.needsAuth && !allMiddlewares.includes(SkipSecurity)) {
|
|
79
|
+
allMiddlewares.unshift(EnsuredAuth);
|
|
80
|
+
}
|
|
81
|
+
const context = {
|
|
82
|
+
params: match.params,
|
|
83
|
+
searchParams,
|
|
84
|
+
path,
|
|
85
|
+
roles: auth.roles.length > 0 ? auth.roles : undefined,
|
|
86
|
+
permissions: auth.permissions.length > 0 ? auth.permissions : undefined,
|
|
87
|
+
};
|
|
88
|
+
return runPipeline(allMiddlewares, context, async (ctx) => {
|
|
89
|
+
return buildLayoutTree(match, searchParams, path, ctx, CustomPageComponent);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Layout tree builder
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
async function buildLayoutTree(match, searchParams, path, context, CustomPageComponent) {
|
|
96
|
+
const { node, params, layoutChain } = match;
|
|
97
|
+
const { getPluginBreadcrumbs: getBreadcrumbs } = await import("../../navigation/breadcrumbs");
|
|
98
|
+
const breadcrumbs = await getBreadcrumbs(path, searchParams);
|
|
99
|
+
let content;
|
|
100
|
+
if (CustomPageComponent) {
|
|
101
|
+
content = (_jsx(CustomPageComponent, { params: params, searchParams: searchParams, breadcrumbs: breadcrumbs, context: context }));
|
|
102
|
+
}
|
|
103
|
+
else if (match.isExact && node.page) {
|
|
104
|
+
const Page = node.page;
|
|
105
|
+
content = (_jsx(Page, { params: params, searchParams: searchParams, breadcrumbs: breadcrumbs, context: context }));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
let NotFound = node.notFound;
|
|
109
|
+
if (!NotFound) {
|
|
110
|
+
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
111
|
+
if (layoutChain[i].notFound) {
|
|
112
|
+
NotFound = layoutChain[i].notFound;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (NotFound) {
|
|
118
|
+
content = _jsx(NotFound, {});
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
content = (_jsx("div", { role: "alert", style: {
|
|
122
|
+
display: "flex",
|
|
123
|
+
alignItems: "center",
|
|
124
|
+
justifyContent: "center",
|
|
125
|
+
minHeight: "50vh",
|
|
126
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
127
|
+
}, children: _jsxs("div", { style: { textAlign: "center" }, children: [_jsx("h1", { style: { fontSize: "3rem", fontWeight: 700, margin: "0 0 1rem" }, children: "404" }), _jsxs("p", { style: { fontSize: "1.125rem", color: "#6b7280" }, children: ["Page not found:", " ", _jsx("code", { style: {
|
|
128
|
+
padding: "0.125rem 0.375rem",
|
|
129
|
+
borderRadius: "0.25rem",
|
|
130
|
+
backgroundColor: "#f3f4f6",
|
|
131
|
+
fontSize: "0.875rem",
|
|
132
|
+
}, children: path })] })] }) }));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (node.error) {
|
|
136
|
+
content = _jsx(RouterErrorBoundary, { children: content });
|
|
137
|
+
}
|
|
138
|
+
const reversedChain = [...layoutChain].reverse();
|
|
139
|
+
for (const entry of reversedChain) {
|
|
140
|
+
if (entry.error) {
|
|
141
|
+
content = (_jsx(RouterErrorBoundary, { children: content }, `error-${entry.consumedPath || entry.id || "root"}`));
|
|
142
|
+
}
|
|
143
|
+
if (entry.loading) {
|
|
144
|
+
const Loading = entry.loading;
|
|
145
|
+
content = (_jsx(React.Suspense, { fallback: _jsx(Loading, {}), children: content }, `suspense-${entry.consumedPath || entry.id || "root"}`));
|
|
146
|
+
}
|
|
147
|
+
if (entry.layout) {
|
|
148
|
+
const Layout = entry.layout;
|
|
149
|
+
const consumedCount = entry.consumedPath
|
|
150
|
+
? entry.consumedPath.split("/").filter(Boolean).length
|
|
151
|
+
: 0;
|
|
152
|
+
const remainingSegments = match.matchedSegments.slice(consumedCount);
|
|
153
|
+
const remainingPath = remainingSegments.join("/") || "/";
|
|
154
|
+
const resolvedSlots = await resolveAllParallelSlots(entry.parallelSlots, remainingPath, searchParams, params, context);
|
|
155
|
+
content = (_jsx(Layout, { params: params, context: context, ...resolvedSlots, children: content }));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return _jsx(SoftNavigationInterceptor, { children: content });
|
|
159
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
export function SoftNavigationInterceptor({ children, }) {
|
|
6
|
+
const router = useRouter();
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
const handleClick = (e) => {
|
|
9
|
+
// Ignore clicks with modifiers
|
|
10
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || e.button !== 0) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// Find the closest anchor tag
|
|
14
|
+
let target = e.target;
|
|
15
|
+
while (target && target.tagName !== "A") {
|
|
16
|
+
target = target.parentElement;
|
|
17
|
+
}
|
|
18
|
+
if (!target)
|
|
19
|
+
return;
|
|
20
|
+
const anchor = target;
|
|
21
|
+
const href = anchor.getAttribute("href");
|
|
22
|
+
// Only intercept internal links
|
|
23
|
+
if (href?.startsWith("/") &&
|
|
24
|
+
!href.startsWith("//") &&
|
|
25
|
+
anchor.target !== "_blank") {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
router.push(href);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
document.addEventListener("click", handleClick);
|
|
31
|
+
return () => document.removeEventListener("click", handleClick);
|
|
32
|
+
}, [router]);
|
|
33
|
+
return _jsx(_Fragment, { children: children });
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A Higher-Order Component (HOC) that wraps a physical Next.js page component
|
|
4
|
+
* with the Veap Virtual Router. This allows physical pages to inherit virtual layouts,
|
|
5
|
+
* parallel slots (like @sidebar), and middleware protection from plugins.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { withRouter } from "@veap/plugins/router";
|
|
10
|
+
*
|
|
11
|
+
* export default withRouter(async function CustomPage() {
|
|
12
|
+
* return <div>My Custom Physical Page</div>;
|
|
13
|
+
* }, "/app/custom");
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export interface WithRouterConfig {
|
|
17
|
+
path?: string;
|
|
18
|
+
roles?: string[];
|
|
19
|
+
permissions?: string[];
|
|
20
|
+
middlewares?: any[];
|
|
21
|
+
}
|
|
22
|
+
export declare function withRouter(Component: React.ComponentType<any>, configOrPath?: WithRouterConfig | string): (props: any) => Promise<React.JSX.Element>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { headers } from "next/headers";
|
|
3
|
+
import { buildRouteTree } from "../discovery";
|
|
4
|
+
import { VeapRouter } from "./renderer";
|
|
5
|
+
export function withRouter(Component, configOrPath) {
|
|
6
|
+
return async function WrappedPage(props) {
|
|
7
|
+
const searchParams = (await props.searchParams) || {};
|
|
8
|
+
const config = typeof configOrPath === "string"
|
|
9
|
+
? { path: configOrPath }
|
|
10
|
+
: configOrPath || {};
|
|
11
|
+
let routePath = config.path;
|
|
12
|
+
if (!routePath) {
|
|
13
|
+
const headersList = await headers();
|
|
14
|
+
routePath = headersList.get("x-invoke-path") || "/";
|
|
15
|
+
}
|
|
16
|
+
const tree = await buildRouteTree(true);
|
|
17
|
+
return (_jsx(VeapRouter, { tree: tree, path: routePath || "/", searchParams: searchParams, CustomPageComponent: Component, customConfig: config }));
|
|
18
|
+
};
|
|
19
|
+
}
|