@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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -2
  3. package/dist/actions/index.d.ts +1 -0
  4. package/dist/actions/index.js +1 -0
  5. package/dist/actions/plugin.actions.d.ts +6 -0
  6. package/dist/actions/plugin.actions.js +60 -0
  7. package/dist/actions.d.ts +1 -0
  8. package/dist/actions.js +1 -0
  9. package/dist/cli/add.d.ts +4 -0
  10. package/dist/cli/add.js +191 -0
  11. package/dist/cli/eject.d.ts +2 -0
  12. package/dist/cli/eject.js +136 -0
  13. package/dist/cli/generate-plugin.d.ts +3 -0
  14. package/dist/cli/generate-plugin.js +48 -0
  15. package/dist/cli/index.d.ts +6 -0
  16. package/dist/cli/index.js +6 -0
  17. package/dist/cli/package-manager.d.ts +14 -0
  18. package/dist/cli/package-manager.js +62 -0
  19. package/dist/cli/stubs.d.ts +16 -0
  20. package/dist/cli/stubs.js +42 -0
  21. package/dist/cli/utils.d.ts +3 -0
  22. package/dist/cli/utils.js +164 -0
  23. package/dist/client.d.ts +6 -0
  24. package/dist/client.js +8 -0
  25. package/dist/common/context.d.ts +26 -0
  26. package/dist/common/context.js +26 -0
  27. package/dist/common/events.d.ts +19 -0
  28. package/dist/common/events.js +26 -0
  29. package/dist/common/index.d.ts +3 -0
  30. package/dist/common/index.js +3 -0
  31. package/dist/common/types.d.ts +195 -0
  32. package/dist/common/types.js +40 -0
  33. package/dist/index.d.ts +5 -0
  34. package/dist/index.js +5 -0
  35. package/dist/migrations/0001_create_plugins_tables.d.ts +4 -0
  36. package/dist/migrations/0001_create_plugins_tables.js +29 -0
  37. package/dist/migrations/index.d.ts +4 -0
  38. package/dist/migrations/index.js +7 -0
  39. package/dist/models/SystemPlugin.d.ts +19 -0
  40. package/dist/models/SystemPlugin.js +14 -0
  41. package/dist/models/SystemUserWidget.d.ts +19 -0
  42. package/dist/models/SystemUserWidget.js +17 -0
  43. package/dist/models/index.d.ts +2 -0
  44. package/dist/models/index.js +2 -0
  45. package/dist/models.d.ts +1 -0
  46. package/dist/models.js +1 -0
  47. package/dist/navigation/breadcrumbs.d.ts +2 -0
  48. package/dist/navigation/breadcrumbs.js +84 -0
  49. package/dist/navigation/index.d.ts +2 -0
  50. package/dist/navigation/index.js +2 -0
  51. package/dist/navigation/navigation.service.d.ts +27 -0
  52. package/dist/navigation/navigation.service.js +183 -0
  53. package/dist/providers/index.d.ts +1 -0
  54. package/dist/providers/index.js +1 -0
  55. package/dist/providers/plugins.provider.d.ts +20 -0
  56. package/dist/providers/plugins.provider.js +142 -0
  57. package/dist/registry/contracts.d.ts +72 -0
  58. package/dist/registry/contracts.js +4 -0
  59. package/dist/registry/facade.d.ts +45 -0
  60. package/dist/registry/facade.js +49 -0
  61. package/dist/registry/index.d.ts +5 -0
  62. package/dist/registry/index.js +5 -0
  63. package/dist/registry/migration-runner.d.ts +8 -0
  64. package/dist/registry/migration-runner.js +12 -0
  65. package/dist/registry/plugin.repository.d.ts +19 -0
  66. package/dist/registry/plugin.repository.js +74 -0
  67. package/dist/registry/registry.service.d.ts +69 -0
  68. package/dist/registry/registry.service.js +535 -0
  69. package/dist/router/discovery.d.ts +27 -0
  70. package/dist/router/discovery.js +37 -0
  71. package/dist/router/handler.d.ts +6 -0
  72. package/dist/router/handler.js +40 -0
  73. package/dist/router/index.d.ts +13 -0
  74. package/dist/router/index.js +13 -0
  75. package/dist/router/matcher.d.ts +5 -0
  76. package/dist/router/matcher.js +32 -0
  77. package/dist/router/middlewares.d.ts +25 -0
  78. package/dist/router/middlewares.js +119 -0
  79. package/dist/router/react/error-boundary.d.ts +30 -0
  80. package/dist/router/react/error-boundary.js +68 -0
  81. package/dist/router/react/index.d.ts +4 -0
  82. package/dist/router/react/index.js +4 -0
  83. package/dist/router/react/renderer.d.ts +13 -0
  84. package/dist/router/react/renderer.js +159 -0
  85. package/dist/router/react/soft-navigation.d.ts +4 -0
  86. package/dist/router/react/soft-navigation.js +34 -0
  87. package/dist/router/react/with-router.d.ts +22 -0
  88. package/dist/router/react/with-router.js +19 -0
  89. package/dist/router/route-tree.d.ts +177 -0
  90. package/dist/router/route-tree.js +633 -0
  91. package/dist/router/router.service.d.ts +25 -0
  92. package/dist/router/router.service.js +127 -0
  93. package/dist/router/scanner.d.ts +4 -0
  94. package/dist/router/scanner.js +337 -0
  95. package/dist/router/utils.d.ts +10 -0
  96. package/dist/router/utils.js +48 -0
  97. package/dist/router.d.ts +6 -0
  98. package/dist/router.js +6 -0
  99. package/dist/server.d.ts +11 -0
  100. package/dist/server.js +17 -0
  101. package/dist/widgets/index.d.ts +8 -0
  102. package/dist/widgets/index.js +8 -0
  103. package/dist/widgets/path-prefix-context.d.ts +5 -0
  104. package/dist/widgets/path-prefix-context.js +9 -0
  105. package/dist/widgets/plugin-extension-point-client.d.ts +15 -0
  106. package/dist/widgets/plugin-extension-point-client.js +44 -0
  107. package/dist/widgets/plugin-extension-point.d.ts +14 -0
  108. package/dist/widgets/plugin-extension-point.js +51 -0
  109. package/dist/widgets/plugin-widget-area-client.d.ts +11 -0
  110. package/dist/widgets/plugin-widget-area-client.js +31 -0
  111. package/dist/widgets/plugin-widget-area.d.ts +10 -0
  112. package/dist/widgets/plugin-widget-area.js +19 -0
  113. package/dist/widgets/use-confirm-action.d.ts +19 -0
  114. package/dist/widgets/use-confirm-action.js +68 -0
  115. package/dist/widgets/widget-composer-client.d.ts +21 -0
  116. package/dist/widgets/widget-composer-client.js +281 -0
  117. package/dist/widgets/widget-composer.d.ts +8 -0
  118. package/dist/widgets/widget-composer.js +28 -0
  119. package/package.json +115 -4
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Unified route matcher for Veap.
3
+ * Supports standard parameters (:id) and catch-all patterns (:path(.*)).
4
+ */
5
+ export declare function matchRoute(pattern: string, path: string): Record<string, string> | null;
@@ -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,4 @@
1
+ export { RouterErrorBoundary } from "./error-boundary";
2
+ export { VeapRouter } from "./renderer";
3
+ export { SoftNavigationInterceptor } from "./soft-navigation";
4
+ export { type WithRouterConfig, withRouter } from "./with-router";
@@ -0,0 +1,4 @@
1
+ export { RouterErrorBoundary } from "./error-boundary";
2
+ export { VeapRouter } from "./renderer";
3
+ export { SoftNavigationInterceptor } from "./soft-navigation";
4
+ export { withRouter } from "./with-router";
@@ -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,4 @@
1
+ import * as React from "react";
2
+ export declare function SoftNavigationInterceptor({ children, }: {
3
+ children: React.ReactNode;
4
+ }): React.JSX.Element;
@@ -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
+ }