@lukoweb/apitogo 0.2.31 → 0.2.34

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 (41) hide show
  1. package/dist/cli/cli.js +436 -153
  2. package/dist/declarations/app/entry.server.d.ts +2 -2
  3. package/dist/declarations/app/main.d.ts +3 -1
  4. package/dist/declarations/cli/platform/client/platform-api.d.ts +0 -4
  5. package/dist/declarations/cli/platform/helpers/deploy-artifact-hash.d.ts +1 -1
  6. package/dist/declarations/cli/platform/helpers/package-files.d.ts +1 -1
  7. package/dist/declarations/cli/platform/helpers/zip.d.ts +1 -1
  8. package/dist/declarations/cli/platform/tools/infrastructure.d.ts +0 -4
  9. package/dist/declarations/config/effective-redirects.d.ts +6 -0
  10. package/dist/declarations/config/header-navigation-guard.d.ts +2 -0
  11. package/dist/declarations/config/resolve-modules.d.ts +1 -4
  12. package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +1 -2
  13. package/docs/cli/commands.md +19 -0
  14. package/docs/configuration/gateway.md +52 -0
  15. package/docs/guides/customer-account.md +11 -1
  16. package/docs/mcp/tools.md +101 -102
  17. package/package.json +2 -1
  18. package/src/app/demo.tsx +78 -77
  19. package/src/app/entry.client.tsx +115 -107
  20. package/src/app/entry.server.tsx +164 -160
  21. package/src/app/main.tsx +175 -167
  22. package/src/app/standalone.tsx +61 -60
  23. package/src/config/effective-redirects.ts +31 -0
  24. package/src/config/header-navigation-guard.ts +13 -0
  25. package/src/config/resolve-modules.ts +331 -354
  26. package/src/config/v2/home-redirect.ts +104 -98
  27. package/src/config/v2/index.ts +127 -126
  28. package/src/config/v2/ledger.ts +242 -235
  29. package/src/config/v2/resolve.ts +12 -4
  30. package/src/config/v2/rules.ts +716 -714
  31. package/src/config/v2/services.ts +799 -663
  32. package/src/config/v2/workspace.ts +17 -1
  33. package/src/config/validators/HeaderNavigationSchema.ts +42 -44
  34. package/src/lib/assets/powered-by-icon.png +0 -0
  35. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +667 -602
  36. package/src/lib/authentication/providers/dev-portal.tsx +476 -448
  37. package/src/lib/components/HeaderNavigation.tsx +176 -176
  38. package/src/lib/components/MobileTopNavigation.tsx +283 -283
  39. package/src/lib/components/navigation/PoweredByZudoku.tsx +42 -39
  40. package/src/vite/plugin-offroot-resolve.ts +68 -67
  41. package/src/vite/prerender/prerender.ts +242 -242
@@ -1,107 +1,115 @@
1
- import { createRoot, hydrateRoot } from "react-dom/client";
2
- import {
3
- createBrowserRouter,
4
- matchRoutes,
5
- type RouteObject,
6
- } from "react-router";
7
- import config from "virtual:zudoku-config";
8
- import "vite/modulepreload-polyfill";
9
- import { Bootstrap } from "../lib/components/Bootstrap.js";
10
- import { getRoutesByConfig, shikiReady } from "./main.js";
11
-
12
- const routes = getRoutesByConfig(config);
13
- // biome-ignore lint/style/noNonNullAssertion: We know the root element exists
14
- const root = document.getElementById("root")!;
15
-
16
- declare global {
17
- interface Window {
18
- ZUDOKU_VERSION: string;
19
- }
20
- }
21
-
22
- window.ZUDOKU_VERSION = process.env.ZUDOKU_VERSION ?? "unknown";
23
-
24
- if (process.env.SENTRY_DSN) {
25
- void import("./sentry.js").then((mod) => {
26
- mod.initSentry({ dsn: process.env.SENTRY_DSN as string });
27
- });
28
- }
29
-
30
- if (root.childElementCount > 0) {
31
- void hydrate(routes);
32
- } else {
33
- void render(routes);
34
- }
35
-
36
- if (import.meta.env.IS_ZUPLO) {
37
- // biome-ignore lint/suspicious/noConsole: Logging allowed here
38
- console.log(
39
- `%cPowered by Zuplo v${window.ZUDOKU_VERSION}`,
40
- [
41
- "color: #Df0097",
42
- "line-height: 50px",
43
- "font-weight: bolder",
44
- "font-size: 15px",
45
- "text-align: center",
46
- "letter-spacing: 5px",
47
- ].join(" ;"),
48
- );
49
- } else {
50
- // biome-ignore lint/suspicious/noConsole: Logging allowed here
51
- console.log(
52
- `%cAPITOGO v${window.ZUDOKU_VERSION}`,
53
- [
54
- "color: #0d9488",
55
- "line-height: 50px",
56
- "font-weight: bolder",
57
- "font-size: 15px",
58
- "text-align: center",
59
- "letter-spacing: 5px",
60
- ].join(" ;"),
61
- );
62
- // biome-ignore lint/suspicious/noConsole: Logging allowed here
63
- console.log("» Learn more at https://apitogo.com");
64
- }
65
-
66
- async function hydrateLazyRoutes(routes: RouteObject[]) {
67
- const path = window.location.pathname;
68
- const lazyMatches = matchRoutes(routes, path, config.basePath)?.filter(
69
- (m) => m.route.lazy,
70
- );
71
-
72
- if (lazyMatches?.length) {
73
- await Promise.all(
74
- lazyMatches.map(async (m) => {
75
- if (typeof m.route.lazy !== "function") return;
76
-
77
- const routeModule = await m.route.lazy();
78
- Object.assign(m.route, { ...routeModule, lazy: undefined });
79
- }),
80
- );
81
- }
82
- }
83
-
84
- function render(routes: RouteObject[]) {
85
- const router = createBrowserRouter(routes, {
86
- basename: config.basePath,
87
- });
88
- createRoot(root).render(<Bootstrap router={router} />);
89
- }
90
-
91
- async function hydrate(routes: RouteObject[]) {
92
- await Promise.all([hydrateLazyRoutes(routes), shikiReady]);
93
-
94
- const router = createBrowserRouter(routes, {
95
- basename: config.basePath,
96
- });
97
-
98
- hydrateRoot(root, <Bootstrap hydrate router={router} />);
99
- }
100
-
101
- // This is a workaround to avoid version skewing
102
- // See https://vite.dev/guide/build.html#load-error-handling
103
- // TODO: Implement a more advanced solution if there are CDN urls or e.g. Vercel Skew Protection
104
- window.addEventListener("vite:preloadError", (e) => {
105
- e.preventDefault();
106
- window.location.reload();
107
- });
1
+ import { createRoot, hydrateRoot } from "react-dom/client";
2
+ import {
3
+ createBrowserRouter,
4
+ matchRoutes,
5
+ type RouteObject,
6
+ } from "react-router";
7
+ import config from "virtual:zudoku-config";
8
+ import "vite/modulepreload-polyfill";
9
+ import type { ResolvedModulesConfig } from "../config/validators/ModulesSchema.js";
10
+ import { Bootstrap } from "../lib/components/Bootstrap.js";
11
+ import { getRoutesByConfig } from "./main.js";
12
+
13
+ // The build resolved the modules and put them in the config; parsing them again would put zod in this bundle.
14
+ const routes = getRoutesByConfig(
15
+ config,
16
+ (c) => (c as { __resolvedModules: ResolvedModulesConfig }).__resolvedModules,
17
+ );
18
+ // biome-ignore lint/style/noNonNullAssertion: We know the root element exists
19
+ const root = document.getElementById("root")!;
20
+
21
+ declare global {
22
+ interface Window {
23
+ ZUDOKU_VERSION: string;
24
+ }
25
+ }
26
+
27
+ window.ZUDOKU_VERSION = process.env.ZUDOKU_VERSION ?? "unknown";
28
+
29
+ if (process.env.SENTRY_DSN) {
30
+ void import("./sentry.js").then((mod) => {
31
+ mod.initSentry({ dsn: process.env.SENTRY_DSN as string });
32
+ });
33
+ }
34
+
35
+ if (root.childElementCount > 0) {
36
+ void hydrate(routes);
37
+ } else {
38
+ void render(routes);
39
+ }
40
+
41
+ if (import.meta.env.IS_ZUPLO) {
42
+ // biome-ignore lint/suspicious/noConsole: Logging allowed here
43
+ console.log(
44
+ `%cPowered by Zuplo v${window.ZUDOKU_VERSION}`,
45
+ [
46
+ "color: #Df0097",
47
+ "line-height: 50px",
48
+ "font-weight: bolder",
49
+ "font-size: 15px",
50
+ "text-align: center",
51
+ "letter-spacing: 5px",
52
+ ].join(" ;"),
53
+ );
54
+ } else {
55
+ // biome-ignore lint/suspicious/noConsole: Logging allowed here
56
+ console.log(
57
+ `%cAPITOGO v${window.ZUDOKU_VERSION}`,
58
+ [
59
+ "color: #0d9488",
60
+ "line-height: 50px",
61
+ "font-weight: bolder",
62
+ "font-size: 15px",
63
+ "text-align: center",
64
+ "letter-spacing: 5px",
65
+ ].join(" ;"),
66
+ );
67
+ // biome-ignore lint/suspicious/noConsole: Logging allowed here
68
+ console.log("» Learn more at https://apitogo.com");
69
+ }
70
+
71
+ async function hydrateLazyRoutes(routes: RouteObject[]) {
72
+ const path = window.location.pathname;
73
+ const lazyMatches = matchRoutes(routes, path, config.basePath)?.filter(
74
+ (m) => m.route.lazy,
75
+ );
76
+
77
+ if (lazyMatches?.length) {
78
+ await Promise.all(
79
+ lazyMatches.map(async (m) => {
80
+ if (typeof m.route.lazy !== "function") return;
81
+
82
+ const routeModule = await m.route.lazy();
83
+ Object.assign(m.route, { ...routeModule, lazy: undefined });
84
+ }),
85
+ );
86
+ }
87
+ }
88
+
89
+ function render(routes: RouteObject[]) {
90
+ const router = createBrowserRouter(routes, {
91
+ basename: config.basePath,
92
+ });
93
+ createRoot(root).render(<Bootstrap router={router} />);
94
+ }
95
+
96
+ async function hydrate(routes: RouteObject[]) {
97
+ // Only the route chunks are awaited. The highlighter keeps loading in the background: whatever needs it reads
98
+ // it with use() and waits on its own, with the prerendered HTML staying on screen, so a page without code
99
+ // (pricing, account, checkout) is not held back by ~260 KB of grammars it never uses.
100
+ await hydrateLazyRoutes(routes);
101
+
102
+ const router = createBrowserRouter(routes, {
103
+ basename: config.basePath,
104
+ });
105
+
106
+ hydrateRoot(root, <Bootstrap hydrate router={router} />);
107
+ }
108
+
109
+ // This is a workaround to avoid version skewing
110
+ // See https://vite.dev/guide/build.html#load-error-handling
111
+ // TODO: Implement a more advanced solution if there are CDN urls or e.g. Vercel Skew Protection
112
+ window.addEventListener("vite:preloadError", (e) => {
113
+ e.preventDefault();
114
+ window.location.reload();
115
+ });
@@ -1,160 +1,164 @@
1
- import { dehydrate, QueryClient } from "@tanstack/react-query";
2
- import type { HelmetData } from "@zudoku/react-helmet-async";
3
- import logger from "loglevel";
4
- import { renderToReadableStream, renderToStaticMarkup } from "react-dom/server";
5
- import {
6
- createStaticHandler,
7
- createStaticRouter,
8
- isRouteErrorResponse,
9
- type RouteObject,
10
- } from "react-router";
11
- import "vite/modulepreload-polyfill";
12
- import { BootstrapStatic } from "../lib/components/Bootstrap.js";
13
- import { NO_DEHYDRATE } from "../lib/components/cache.js";
14
- import { ServerError } from "../lib/errors/ServerError.js";
15
- import { highlighterPromise } from "../lib/shiki.js";
16
- import { getRoutesByConfig } from "./main.js";
17
- export { getRoutesByConfig };
18
-
19
- // Statically importing shiki.ts here ensures it's in the SSR bundle.
20
- // main.tsx dynamically imports it instead to enable lazy loading on the client.
21
- await import("virtual:zudoku-shiki-register").then(
22
- async ({ registerShiki }) => {
23
- const highlighter = await highlighterPromise;
24
- await registerShiki(highlighter);
25
- },
26
- );
27
- export const handleRequest = async ({
28
- template,
29
- request,
30
- routes,
31
- basePath,
32
- bypassProtection,
33
- }: {
34
- template: string;
35
- request: Request;
36
- routes: RouteObject[];
37
- basePath?: string;
38
- bypassProtection?: boolean;
39
- }): Promise<Response> => {
40
- const { query, dataRoutes } = createStaticHandler(routes, {
41
- basename: basePath,
42
- });
43
- const queryClient = new QueryClient();
44
-
45
- const context = await query(request);
46
-
47
- if (context instanceof Response) {
48
- if ([301, 302, 303, 307, 308].includes(context.status)) {
49
- return new Response(null, {
50
- status: context.status,
51
- headers: { Location: context.headers.get("Location") ?? "" },
52
- });
53
- }
54
- throw context;
55
- }
56
-
57
- let status = 200;
58
- if (context.errors) {
59
- const firstError = Object.values(context.errors).find(isRouteErrorResponse);
60
- if (firstError?.status) {
61
- status = firstError.status;
62
- }
63
- }
64
-
65
- const router = createStaticRouter(dataRoutes, context);
66
- const helmetContext = {} as HelmetData["context"];
67
- const renderContext = {
68
- status: 200,
69
- bypassProtection: bypassProtection ?? false,
70
- };
71
-
72
- const App = (
73
- <BootstrapStatic
74
- router={router}
75
- context={context}
76
- queryClient={queryClient}
77
- helmetContext={helmetContext}
78
- bypassProtection={bypassProtection}
79
- renderContext={renderContext}
80
- />
81
- );
82
-
83
- try {
84
- const reactStream = await renderToReadableStream(App, {
85
- onError(error) {
86
- status = 500;
87
- if (import.meta.env.PROD) {
88
- logger.error("SSR Error:", error);
89
- }
90
- },
91
- });
92
-
93
- await reactStream.allReady;
94
-
95
- const [htmlStart, htmlEnd] = template.split("<!--app-html-->");
96
- if (!htmlStart) {
97
- throw new Error("No <!--app-html--> found in template");
98
- }
99
-
100
- const encoder = new TextEncoder();
101
- const reader = reactStream.getReader();
102
-
103
- const stream = new ReadableStream<Uint8Array>({
104
- async start(controller) {
105
- const helmetHtml = [
106
- helmetContext.helmet?.title?.toString() ?? "",
107
- helmetContext.helmet?.meta?.toString() ?? "",
108
- helmetContext.helmet?.link?.toString() ?? "",
109
- helmetContext.helmet?.style?.toString() ?? "",
110
- helmetContext.helmet?.script?.toString() ?? "",
111
- ].join("\n");
112
-
113
- controller.enqueue(
114
- encoder.encode(htmlStart.replace("<!--app-helmet-->", helmetHtml)),
115
- );
116
-
117
- while (true) {
118
- const { done, value } = await reader.read();
119
- if (done) break;
120
- controller.enqueue(value);
121
- }
122
-
123
- if (htmlEnd) {
124
- const dehydrated = dehydrate(queryClient, {
125
- shouldDehydrateQuery: (q) => !q.queryKey.includes(NO_DEHYDRATE),
126
- });
127
- const serialized = JSON.stringify(dehydrated)
128
- .replace(/</g, "\\u003c")
129
- .replace(/>/g, "\\u003e");
130
-
131
- const closingTag = "</body>";
132
- const idx = htmlEnd.lastIndexOf(closingTag);
133
-
134
- if (idx === -1) {
135
- controller.enqueue(encoder.encode(htmlEnd));
136
- } else {
137
- controller.enqueue(encoder.encode(htmlEnd.slice(0, idx)));
138
- controller.enqueue(
139
- encoder.encode(`<script>window.DATA=${serialized}</script>`),
140
- );
141
- controller.enqueue(encoder.encode(htmlEnd.slice(idx)));
142
- }
143
- }
144
-
145
- controller.close();
146
- },
147
- });
148
-
149
- return new Response(stream, {
150
- status: renderContext.status !== 200 ? renderContext.status : status,
151
- headers: { "Content-Type": "text/html; charset=utf-8" },
152
- });
153
- } catch (error) {
154
- const html = renderToStaticMarkup(<ServerError error={error} />);
155
- return new Response(html, {
156
- status: 500,
157
- headers: { "Content-Type": "text/html; charset=utf-8" },
158
- });
159
- }
160
- };
1
+ import { dehydrate, QueryClient } from "@tanstack/react-query";
2
+ import type { HelmetData } from "@zudoku/react-helmet-async";
3
+ import logger from "loglevel";
4
+ import { renderToReadableStream, renderToStaticMarkup } from "react-dom/server";
5
+ import {
6
+ createStaticHandler,
7
+ createStaticRouter,
8
+ isRouteErrorResponse,
9
+ type RouteObject,
10
+ } from "react-router";
11
+ import "vite/modulepreload-polyfill";
12
+ import type { ApitogoConfig } from "../config/config.js";
13
+ import { resolveModulesConfig } from "../config/resolve-modules.js";
14
+ import { BootstrapStatic } from "../lib/components/Bootstrap.js";
15
+ import { NO_DEHYDRATE } from "../lib/components/cache.js";
16
+ import { ServerError } from "../lib/errors/ServerError.js";
17
+ import { highlighterPromise } from "../lib/shiki.js";
18
+ import { getRoutesByConfig as routesByConfig } from "./main.js";
19
+
20
+ export const getRoutesByConfig = (config: ApitogoConfig) =>
21
+ routesByConfig(config, resolveModulesConfig);
22
+
23
+ // Statically importing shiki.ts here ensures it's in the SSR bundle.
24
+ // main.tsx dynamically imports it instead to enable lazy loading on the client.
25
+ await import("virtual:zudoku-shiki-register").then(
26
+ async ({ registerShiki }) => {
27
+ const highlighter = await highlighterPromise;
28
+ await registerShiki(highlighter);
29
+ },
30
+ );
31
+ export const handleRequest = async ({
32
+ template,
33
+ request,
34
+ routes,
35
+ basePath,
36
+ bypassProtection,
37
+ }: {
38
+ template: string;
39
+ request: Request;
40
+ routes: RouteObject[];
41
+ basePath?: string;
42
+ bypassProtection?: boolean;
43
+ }): Promise<Response> => {
44
+ const { query, dataRoutes } = createStaticHandler(routes, {
45
+ basename: basePath,
46
+ });
47
+ const queryClient = new QueryClient();
48
+
49
+ const context = await query(request);
50
+
51
+ if (context instanceof Response) {
52
+ if ([301, 302, 303, 307, 308].includes(context.status)) {
53
+ return new Response(null, {
54
+ status: context.status,
55
+ headers: { Location: context.headers.get("Location") ?? "" },
56
+ });
57
+ }
58
+ throw context;
59
+ }
60
+
61
+ let status = 200;
62
+ if (context.errors) {
63
+ const firstError = Object.values(context.errors).find(isRouteErrorResponse);
64
+ if (firstError?.status) {
65
+ status = firstError.status;
66
+ }
67
+ }
68
+
69
+ const router = createStaticRouter(dataRoutes, context);
70
+ const helmetContext = {} as HelmetData["context"];
71
+ const renderContext = {
72
+ status: 200,
73
+ bypassProtection: bypassProtection ?? false,
74
+ };
75
+
76
+ const App = (
77
+ <BootstrapStatic
78
+ router={router}
79
+ context={context}
80
+ queryClient={queryClient}
81
+ helmetContext={helmetContext}
82
+ bypassProtection={bypassProtection}
83
+ renderContext={renderContext}
84
+ />
85
+ );
86
+
87
+ try {
88
+ const reactStream = await renderToReadableStream(App, {
89
+ onError(error) {
90
+ status = 500;
91
+ if (import.meta.env.PROD) {
92
+ logger.error("SSR Error:", error);
93
+ }
94
+ },
95
+ });
96
+
97
+ await reactStream.allReady;
98
+
99
+ const [htmlStart, htmlEnd] = template.split("<!--app-html-->");
100
+ if (!htmlStart) {
101
+ throw new Error("No <!--app-html--> found in template");
102
+ }
103
+
104
+ const encoder = new TextEncoder();
105
+ const reader = reactStream.getReader();
106
+
107
+ const stream = new ReadableStream<Uint8Array>({
108
+ async start(controller) {
109
+ const helmetHtml = [
110
+ helmetContext.helmet?.title?.toString() ?? "",
111
+ helmetContext.helmet?.meta?.toString() ?? "",
112
+ helmetContext.helmet?.link?.toString() ?? "",
113
+ helmetContext.helmet?.style?.toString() ?? "",
114
+ helmetContext.helmet?.script?.toString() ?? "",
115
+ ].join("\n");
116
+
117
+ controller.enqueue(
118
+ encoder.encode(htmlStart.replace("<!--app-helmet-->", helmetHtml)),
119
+ );
120
+
121
+ while (true) {
122
+ const { done, value } = await reader.read();
123
+ if (done) break;
124
+ controller.enqueue(value);
125
+ }
126
+
127
+ if (htmlEnd) {
128
+ const dehydrated = dehydrate(queryClient, {
129
+ shouldDehydrateQuery: (q) => !q.queryKey.includes(NO_DEHYDRATE),
130
+ });
131
+ const serialized = JSON.stringify(dehydrated)
132
+ .replace(/</g, "\\u003c")
133
+ .replace(/>/g, "\\u003e");
134
+
135
+ const closingTag = "</body>";
136
+ const idx = htmlEnd.lastIndexOf(closingTag);
137
+
138
+ if (idx === -1) {
139
+ controller.enqueue(encoder.encode(htmlEnd));
140
+ } else {
141
+ controller.enqueue(encoder.encode(htmlEnd.slice(0, idx)));
142
+ controller.enqueue(
143
+ encoder.encode(`<script>window.DATA=${serialized}</script>`),
144
+ );
145
+ controller.enqueue(encoder.encode(htmlEnd.slice(idx)));
146
+ }
147
+ }
148
+
149
+ controller.close();
150
+ },
151
+ });
152
+
153
+ return new Response(stream, {
154
+ status: renderContext.status !== 200 ? renderContext.status : status,
155
+ headers: { "Content-Type": "text/html; charset=utf-8" },
156
+ });
157
+ } catch (error) {
158
+ const html = renderToStaticMarkup(<ServerError error={error} />);
159
+ return new Response(html, {
160
+ status: 500,
161
+ headers: { "Content-Type": "text/html; charset=utf-8" },
162
+ });
163
+ }
164
+ };