@lukoweb/apitogo 0.2.49 → 0.2.51

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.
@@ -1,5 +1,7 @@
1
1
  import { HTTPSnippet } from "@zudoku/httpsnippet";
2
2
  import type { OperationsFragmentFragment } from "../graphql/graphql.js";
3
+ export declare const KEY_HEADER_EXTENSION = "x-apitogo-key-header";
4
+ export declare const KEY_PLACEHOLDER = "YOUR_API_KEY";
3
5
  export declare const createHttpSnippet: ({ operation, selectedServer, exampleBody, }: {
4
6
  operation: OperationsFragmentFragment;
5
7
  selectedServer: string;
package/docs/cli/shape.md CHANGED
@@ -41,12 +41,12 @@ ready to go live.
41
41
  On a TTY, bare `npx apitogo config` opens the wizard. Each choice is written immediately; the loop
42
42
  continues until you type `done`:
43
43
 
44
- | Choice | What it does |
45
- | --------- | ------------------------------------------------------------------------ |
46
- | `backend` | Set `gateway.origin`: API code folder, runtime and runtime version |
47
- | `openapi` | Add an OpenAPI file to `gateway.specs` |
48
- | `page` | Create `apitogo/portal/pages/<slug>.mdx` (the sidebar follows the files) |
49
- | `plan` | Add a plan to `apitogo/plans.json` (title, price, rate limit, quota) |
44
+ | Choice | What it does |
45
+ | --------- | --------------------------------------------------------------------------------------- |
46
+ | `backend` | Set `gateway.origin`: API code folder, runtime and runtime version |
47
+ | `openapi` | Add an OpenAPI file to `gateway.specs` |
48
+ | `page` | Create the React page `apitogo/portal/pages/<slug>.tsx` (the sidebar follows the files) |
49
+ | `plan` | Add a plan to `apitogo/plans.json` (title, price, rate limit, quota) |
50
50
 
51
51
  Fonts and colors are **not** prompted. See [Colors & theme](../customization/colors-theme.mdx).
52
52
 
@@ -88,7 +88,7 @@ After `npx apitogo config`, edit the manifest and site files directly:
88
88
 
89
89
  - **Services, backend, OpenAPI files** — `apitogo.config.json`
90
90
  - **Plans** — `apitogo/plans.json`
91
- - **Docs pages** — `apitogo/portal/pages/**/*.mdx`
91
+ - **Docs pages** — `apitogo/portal/pages/**/*.{md,mdx,tsx}` (`.tsx` files are React pages)
92
92
  - **Home page** — the file in `portal.landing` (for example
93
93
  `apitogo/portal/landing/LandingPage.tsx`)
94
94
 
@@ -19,16 +19,18 @@ version 1 config, it stops and points to `apitogo create`.
19
19
 
20
20
  ## What goes where
21
21
 
22
- | You change | Where it is written |
23
- | ------------------- | ---------------------------------------------------------------- |
24
- | Portal display name | `portal.name` in `apitogo.config.json` |
25
- | API code (backend) | `gateway.origin` (`mode: managed`, `source`, `runtime`, version) |
26
- | OpenAPI file | `gateway.specs` in `apitogo.config.json` |
27
- | Docs page | `apitogo/portal/pages/<slug>.mdx` (the config does not change) |
28
- | Plan | `apitogo/plans.json` (or the file in `plans.file`) |
22
+ | You change | Where it is written |
23
+ | ------------------- | ------------------------------------------------------------------ |
24
+ | Portal display name | `portal.name` in `apitogo.config.json` |
25
+ | API code (backend) | `gateway.origin` (`mode: managed`, `source`, `runtime`, version) |
26
+ | OpenAPI file | `gateway.specs` in `apitogo.config.json` |
27
+ | Docs page | `apitogo/portal/pages/<slug>.tsx`, a React page (config unchanged) |
28
+ | Plan | `apitogo/plans.json` (or the file in `plans.file`) |
29
29
 
30
30
  The portal builds its sidebar from the pages folder, so adding a page needs no navigation entry. A
31
- page in `guides/quickstart.mdx` shows under a **Guides** section at `/guides/quickstart`.
31
+ page in `guides/quickstart.tsx` shows under a **Guides** section at `/guides/quickstart`. New pages
32
+ are React components by default; pass `--page-format mdx` for a Markdown page. See
33
+ [Custom pages](../guides/custom-pages.md).
32
34
 
33
35
  Changing a gateway or plan needs that service in the project. Add it first with
34
36
  `apitogo add gateway` or `apitogo add plans`.
@@ -40,7 +42,7 @@ On a TTY, bare `npx apitogo config` opens the wizard. Each answer is applied at
40
42
 
41
43
  - **`backend`**: folder of your API code, runtime (`nodejs` / `python` / `dotnet`) and version
42
44
  - **`openapi`**: an OpenAPI file to add to `gateway.specs`
43
- - **`page`**: page title and URL path; writes `apitogo/portal/pages/<slug>.mdx`
45
+ - **`page`**: page title and URL path; writes the React page `apitogo/portal/pages/<slug>.tsx`
44
46
  - **`plan`**: title, fixed or custom price, rate limit per minute, quota per 30 days; added to
45
47
  `plans.json`
46
48
 
@@ -58,7 +60,8 @@ plans also need Starter or Professional platform hosting.
58
60
  | `--runtime <name>` | Runtime of `--source`: `nodejs` (default), `python` or `dotnet` |
59
61
  | `--runtime-version <v>` | Runtime version, for example `NODE_22_LTS` |
60
62
  | `--openapi <file>` | Add an OpenAPI file to `gateway.specs` |
61
- | `--page-slug <path>` | Add a page at `apitogo/portal/pages/<path>.mdx` |
63
+ | `--page-slug <path>` | Add a page at `apitogo/portal/pages/<path>.tsx` |
64
+ | `--page-format <tsx\|mdx>` | Format of a new page: `tsx` (React, the default) or `mdx` |
62
65
  | `--page-title <title>` | Title of the page (required when adding) |
63
66
  | `--page-content <markdown>` | Markdown body of the page |
64
67
  | `--page-update` | Change an existing page instead of adding one |
@@ -61,7 +61,7 @@ one of several types. At the simplest level you may only have links and categori
61
61
  In a config version 2 project, the portal builds the sidebar from the pages under
62
62
  `apitogo/portal/pages/`: the index page first, then the other pages by name, then one section per
63
63
  folder. There is no `navigation` key to maintain. Add a page with **`npx apitogo config`** (see
64
- **[Make config (CLI)](./make-config.mdx)**) or by creating the `.md` / `.mdx` file.
64
+ **[Make config (CLI)](./make-config.mdx)**) or by creating the `.md`, `.mdx` or `.tsx` (React) file.
65
65
 
66
66
  ## Navigation Items
67
67
 
@@ -11,16 +11,45 @@ components that APIToGo already has such as buttons, links and headers.
11
11
 
12
12
  Start by creating the page you want to add.
13
13
 
14
+ ## React pages in a project made by `apitogo create`
15
+
16
+ Put a `.tsx` file next to your Markdown pages, in `apitogo/portal/pages/` (or any folder in
17
+ `portal.docs.content`). Its default export is the page, and `export const title` is its label in the
18
+ sidebar. The page's address follows its path, like a Markdown page: `guides/quickstart.tsx` is
19
+ `/guides/quickstart`, under a **Guides** section.
20
+
21
+ ```tsx title="apitogo/portal/pages/guides/quickstart.tsx"
22
+ import { useState } from "react";
23
+ import { Button, Heading, Typography } from "@lukoweb/apitogo/components";
24
+
25
+ export const title = "Quickstart";
26
+
27
+ export default function QuickstartPage() {
28
+ const [count, setCount] = useState(0);
29
+ return (
30
+ <Typography>
31
+ <Heading level={1}>{title}</Heading>
32
+ <Button onClick={() => setCount(count + 1)}>Clicked {count} times</Button>
33
+ </Typography>
34
+ );
35
+ }
36
+ ```
37
+
38
+ - A file or folder whose name starts with `_` is not a page, so keep shared components there, for
39
+ example `apitogo/portal/pages/_components/Chart.tsx`. Test and story files are not pages either.
40
+ - When a Markdown page and a React page have the same address, the Markdown page wins.
41
+ - Nothing in `apitogo.config.json` changes: the sidebar is built from the files.
42
+
14
43
  ## CLI scaffold
15
44
 
16
- `npx apitogo config` can create a starter Markdown page at `apitogo/portal/pages/<slug>.mdx`. Choose
17
- `page` in the wizard loop, or pass `--page-slug` and `--page-title`. The sidebar is built from the
18
- pages folder, so no navigation entry is needed.
45
+ `npx apitogo config` creates a starter React page at `apitogo/portal/pages/<slug>.tsx`. Choose
46
+ `page` in the wizard loop, or pass `--page-slug` and `--page-title`. Add `--page-format mdx` for a
47
+ Markdown page instead.
19
48
 
20
49
  Predefined UI components (Callout, Card, Button, Heading, and more) are documented under
21
50
  [/docs/components](/docs/components).
22
51
 
23
- ## Setup a custom page
52
+ ## Setup a custom page (version 1 projects)
24
53
 
25
54
  Each custom page is a page component of its own and lives in a `src` directory at the root of your
26
55
  project. Let's create the `<MyCustomPage />` component as an example.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.49",
3
+ "version": "0.2.51",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -3,9 +3,13 @@ import type { InputNavigationItem } from "../validators/InputNavigationSchema.js
3
3
  /** Label of the one header tab that holds the sidebar made from the docs pages. */
4
4
  export const DOCS_NAVIGATION_LABEL = "Documentation";
5
5
 
6
- type Branch = { pages: Set<string>; folders: Map<string, Branch> };
6
+ /** A React page in the sidebar. `element` is the placeholder the generated wrapper replaces with the component. */
7
+ export type CustomDocsPage = { route: string; label: string; element: string };
7
8
 
8
- const emptyBranch = (): Branch => ({ pages: new Set(), folders: new Map() });
9
+ type Page = { kind: "doc" } | ({ kind: "custom" } & CustomDocsPage);
10
+ type Branch = { pages: Map<string, Page>; folders: Map<string, Branch> };
11
+
12
+ const emptyBranch = (): Branch => ({ pages: new Map(), folders: new Map() });
9
13
 
10
14
  /** "getting-started" becomes "Getting started". */
11
15
  export function folderLabel(name: string): string {
@@ -16,22 +20,28 @@ export function folderLabel(name: string): string {
16
20
  const byName = (a: string, b: string) => a.localeCompare(b);
17
21
 
18
22
  const toItems = (branch: Branch, prefix: string): InputNavigationItem[] => {
19
- const pages = [...branch.pages].sort((a, b) =>
23
+ const names = [...branch.pages.keys()].sort((a, b) =>
20
24
  a === "index" ? -1 : b === "index" ? 1 : byName(a, b),
21
25
  );
22
26
  const folders = [...branch.folders.keys()].sort(byName);
27
+ const folderPath = prefix === "" ? "/" : `/${prefix.slice(0, -1)}`;
23
28
  return [
24
- // The index page of a folder is the folder's own address, so the sidebar also shows on `/`.
25
- ...pages.map(
26
- (page): InputNavigationItem =>
27
- page === "index"
28
- ? {
29
- type: "doc",
30
- file: `${prefix}${page}`,
31
- path: prefix === "" ? "/" : `/${prefix.slice(0, -1)}`,
32
- }
33
- : `${prefix}${page}`,
34
- ),
29
+ ...names.map((name): InputNavigationItem => {
30
+ const page = branch.pages.get(name) as Page;
31
+ if (page.kind === "custom") {
32
+ return {
33
+ type: "custom-page",
34
+ // The index page of a folder is the folder's own address.
35
+ path: name === "index" ? folderPath : page.route,
36
+ label: page.label,
37
+ element: page.element,
38
+ };
39
+ }
40
+ // The index page of a folder is the folder's own address, so the sidebar also shows on `/`.
41
+ return name === "index"
42
+ ? { type: "doc", file: `${prefix}${name}`, path: folderPath }
43
+ : `${prefix}${name}`;
44
+ }),
35
45
  ...folders.map(
36
46
  (folder): InputNavigationItem => ({
37
47
  type: "category",
@@ -47,23 +57,36 @@ const toItems = (branch: Branch, prefix: string): InputNavigationItem[] => {
47
57
  ];
48
58
  };
49
59
 
60
+ const branchOf = (root: Branch, route: string) => {
61
+ const parts = route.split("/").filter(Boolean);
62
+ const page = parts.pop();
63
+ const branch = parts.reduce((current, folder) => {
64
+ const next = current.folders.get(folder) ?? emptyBranch();
65
+ current.folders.set(folder, next);
66
+ return next;
67
+ }, root);
68
+ return { branch, page };
69
+ };
70
+
50
71
  /**
51
72
  * The sidebar for hosted docs, made from the pages themselves: the index page first, then the other pages by
52
- * name, then one section per folder. `routes` are the docs plugin's route paths (`/overview`, `/guides/style`).
73
+ * name, then one section per folder. `routes` are the docs plugin's route paths (`/overview`, `/guides/style`);
74
+ * `customPages` are React pages placed the same way. A Markdown page wins over a React page at the same address.
53
75
  * Without pages there is nothing to navigate, so the result is empty.
54
76
  */
55
- export function docsNavigation(routes: string[]): InputNavigationItem[] {
77
+ export function docsNavigation(
78
+ routes: string[],
79
+ customPages: readonly CustomDocsPage[] = [],
80
+ ): InputNavigationItem[] {
56
81
  const root = emptyBranch();
57
82
  for (const route of routes) {
58
- const parts = route.split("/").filter(Boolean);
59
- const page = parts.pop();
60
- if (!page) continue;
61
- const branch = parts.reduce((current, folder) => {
62
- const next = current.folders.get(folder) ?? emptyBranch();
63
- current.folders.set(folder, next);
64
- return next;
65
- }, root);
66
- branch.pages.add(page);
83
+ const { branch, page } = branchOf(root, route);
84
+ if (page) branch.pages.set(page, { kind: "doc" });
85
+ }
86
+ for (const custom of customPages) {
87
+ const { branch, page } = branchOf(root, custom.route);
88
+ if (page && !branch.pages.has(page))
89
+ branch.pages.set(page, { kind: "custom", ...custom });
67
90
  }
68
91
  const items = toItems(root, "");
69
92
  return items.length === 0
@@ -105,7 +105,7 @@ export const FULL_SPEC = (name: string) => `openapi: 3.1.0
105
105
  info:
106
106
  title: ${JSON.stringify(name)}
107
107
  version: 1.0.0
108
- description: A small sample API. Edit server/index.js and this file together.
108
+ description: A sample API with a greeting and a list of items.
109
109
  paths:
110
110
  /health:
111
111
  get:
@@ -18,7 +18,13 @@ export type ReferenceOptions = {
18
18
  };
19
19
 
20
20
  /** Header of the API key when the gateway does not say otherwise (same as the platform's OpenAPI export). */
21
- export const DEFAULT_KEY_HEADER = "Ocp-Apim-Subscription-Key";
21
+ export const DEFAULT_KEY_HEADER = "X-API-Key";
22
+
23
+ /**
24
+ * Operation extension naming the header a protected operation's key goes in. The API page's code samples read it
25
+ * (`createHttpSnippet`), so they show the header with a placeholder key.
26
+ */
27
+ export const KEY_HEADER_EXTENSION = "x-apitogo-key-header";
22
28
 
23
29
  const METHODS = [
24
30
  "get",
@@ -175,10 +181,17 @@ export function buildReferenceSpec(
175
181
  else delete doc.components;
176
182
  // One requirement with both schemes means both are needed together.
177
183
  doc.security = Object.keys(requirement).length > 0 ? [requirement] : [];
178
- for (const [, item, method] of operationsOf(paths)) {
184
+ for (const [path, item, method] of operationsOf(paths)) {
179
185
  const operation = item[method] as Json;
180
186
  if (Array.isArray(operation.security) && operation.security.length > 0)
181
187
  delete operation.security;
188
+ // The page's code samples add this header, so a copied sample works once the key is filled in (API-120).
189
+ if (
190
+ options.apiKeyRequired &&
191
+ !options.publicOperations.has(actionKey(method, path))
192
+ )
193
+ operation[KEY_HEADER_EXTENSION] = options.apiKeyHeader;
194
+ else delete operation[KEY_HEADER_EXTENSION];
182
195
  }
183
196
 
184
197
  return JSON.parse(hideOrigin(JSON.stringify(doc), options)) as Json;
@@ -101,6 +101,7 @@ export {
101
101
  requiredPackages,
102
102
  WRAPPER_FILE,
103
103
  } from "./wrapper.js";
104
+ export { findTsxPages, type TsxPage, tsxPagePatterns } from "./tsx-pages.js";
104
105
  export { materializeWorkspace, portalArtifactHash } from "./workspace.js";
105
106
  export {
106
107
  type AddOptions,
@@ -59,7 +59,7 @@ title: Welcome
59
59
 
60
60
  # ${name}
61
61
 
62
- This page was created by \`apitogo init\`. Edit it, and add more \`.md\` or \`.mdx\` files next to it: each one becomes a page of your portal.
62
+ Welcome to the ${name} developer portal. Here you will find guides, the API reference, and everything you need to get an API key and make your first call.
63
63
  `;
64
64
  }
65
65
 
@@ -6,6 +6,7 @@ import { billingNeeded, stripeKeySecretName } from "./billing.js";
6
6
  import type { ConfigIssue } from "./errors.js";
7
7
  import { type ResolvedConfigV2, toGlobPath } from "./resolve.js";
8
8
  import type { ConfigV2 } from "./schema.js";
9
+ import { findTsxPages } from "./tsx-pages.js";
9
10
 
10
11
  /**
11
12
  * The rules of `apitogo check` that need no network (Reference, section 2). Online rules (provider capabilities,
@@ -277,6 +278,8 @@ async function docsContentRule(
277
278
  ? await glob(include, { nodir: true, ignore, absolute: true })
278
279
  : [];
279
280
  if (matches.length > 0) return;
281
+ // React pages (.tsx) next to the docs count too.
282
+ if ((await findTsxPages(patterns)).length > 0) return;
280
283
 
281
284
  const shown = config.portal?.docs?.content ?? [
282
285
  path.relative(resolved.configDir, patterns[0] ?? ""),
@@ -285,7 +288,7 @@ async function docsContentRule(
285
288
  code: "PORTAL_DOCS_EMPTY",
286
289
  path: "portal.docs",
287
290
  message: `portal.docs matched 0 files (${shown.join(", ")}). The site will only contain the API reference.`,
288
- hint: "Add .md or .mdx files, or point docs.content at your existing docs.",
291
+ hint: "Add .md, .mdx or .tsx pages, or point docs.content at your existing docs.",
289
292
  });
290
293
  }
291
294
 
@@ -9,15 +9,22 @@ export function starterLandingPage(options: {
9
9
  }): string {
10
10
  const name = JSON.stringify(options.name);
11
11
  const plansImport = options.withPlans
12
- ? 'import { usePlansPreview } from "@lukoweb/apitogo-plugin-dev-portal-billing";\n'
12
+ ? 'import {\n useGatewayCall,\n usePlansPreview,\n} from "@lukoweb/apitogo-plugin-dev-portal-billing";\n'
13
13
  : "";
14
14
  const plansHook = options.withPlans
15
- ? " // The plans of the project's catalog: the same ones the pricing page shows.\n const plans = usePlansPreview();\n\n"
15
+ ? " // The plans of the project's catalog: the same ones the pricing page shows.\n const plans = usePlansPreview();\n // The real gateway address and key header, so the sample works when it is pasted with a key.\n const gateway = useGatewayCall();\n\n"
16
16
  : "";
17
+ // Without the plans service the page cannot learn the gateway, so the sample names the platform's default header.
18
+ const sampleUrl = options.withPlans
19
+ ? '${gateway.baseUrl ?? "https://YOUR-GATEWAY"}'
20
+ : "https://YOUR-GATEWAY";
21
+ const sampleHeader = options.withPlans ? "${gateway.keyHeader}" : "X-API-Key";
17
22
  const plansSection = options.withPlans
18
23
  ? " <PlansPreviewSection plans={plans} />\n"
19
24
  : "";
20
- return `import {
25
+ return `// Your portal's home page. Edit this file (apitogo/portal/landing/LandingPage.tsx) to make it yours: everything
26
+ // on it is public, so keep notes for yourself in comments like this one.
27
+ import {
21
28
  ApiShowcaseSection,
22
29
  FeaturesSection,
23
30
  HeroSection,
@@ -47,8 +54,8 @@ ${plansHook} return (
47
54
  {
48
55
  filename: "Call the API",
49
56
  language: "bash",
50
- code: \`$ curl https://your-api.example.com/health \\\\
51
- -H "x-api-key: YOUR_API_KEY"
57
+ code: \`$ curl ${sampleUrl}/health \\\\
58
+ -H "${sampleHeader}: YOUR_API_KEY"
52
59
  # { "status": "ok" }\`,
53
60
  },
54
61
  ]}
@@ -58,7 +65,7 @@ ${plansHook} return (
58
65
  label: "What you get",
59
66
  title: "A developer portal that is ready on day one",
60
67
  description:
61
- "Edit this page in apitogo/portal/landing/LandingPage.tsx to make it yours.",
68
+ "Docs, a live API reference, self-service API keys and clear plans, all in one place.",
62
69
  }}
63
70
  features={[
64
71
  {
@@ -0,0 +1,100 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { glob } from "glob";
4
+ import globParent from "glob-parent";
5
+ import { folderLabel } from "./docs-navigation.js";
6
+ import { toGlobPath } from "./resolve.js";
7
+
8
+ /**
9
+ * React pages next to the Markdown docs: a `.tsx` file in a docs folder is a page whose default export is the page
10
+ * component. Its address follows the same rule as a Markdown page (the path inside the folder, without the extension),
11
+ * so `pages/guides/quickstart.tsx` is `/guides/quickstart`. A file or folder whose name starts with `_` is a helper
12
+ * (shared components, hooks), not a page.
13
+ */
14
+ export type TsxPage = {
15
+ /** `/guides/quickstart`. */
16
+ route: string;
17
+ /** Absolute path of the component file. */
18
+ file: string;
19
+ /** Sidebar label: `export const title = "..."` in the file, or the file name. */
20
+ label: string;
21
+ };
22
+
23
+ const IGNORED = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
24
+
25
+ /**
26
+ * A helper next to the pages, not a page: anything under a name that starts with `_`, tests and stories. Checked on
27
+ * the path inside the docs folder (the glob library's `ignore` does not match absolute Windows paths).
28
+ */
29
+ export function isTsxHelper(relative: string): boolean {
30
+ return (
31
+ relative.split("/").some((part) => part.startsWith("_")) ||
32
+ /\.(test|spec|stories)\.tsx$/.test(relative) ||
33
+ relative.split("/").includes("node_modules")
34
+ );
35
+ }
36
+
37
+ /** The `.tsx` patterns for the docs content globs: every file below each glob's folder. Negated globs stay as they are. */
38
+ export function tsxPagePatterns(contentGlobs: readonly string[]): string[] {
39
+ const globs = contentGlobs.map(toGlobPath);
40
+ const included = globs.filter((pattern) => !pattern.startsWith("!"));
41
+ const excluded = globs.filter((pattern) => pattern.startsWith("!"));
42
+ return [
43
+ ...new Set(
44
+ included.map((pattern) =>
45
+ path.posix.join(globParent(pattern), "**", "*.tsx"),
46
+ ),
47
+ ),
48
+ ...excluded,
49
+ ];
50
+ }
51
+
52
+ const TITLE =
53
+ /export\s+const\s+title\s*(?::\s*string\s*)?=\s*(["'`])((?:\\.|(?!\1).)*)\1/;
54
+
55
+ /** The label a page shows in the sidebar. */
56
+ export function tsxPageLabel(source: string, file: string): string {
57
+ const match = TITLE.exec(source);
58
+ if (match?.[2]) return match[2].replace(/\\(.)/g, "$1");
59
+ const name = path.basename(file, ".tsx");
60
+ return folderLabel(
61
+ name === "index" ? path.basename(path.dirname(file)) : name,
62
+ );
63
+ }
64
+
65
+ /** The React pages the docs content globs reach, sorted by address. */
66
+ export async function findTsxPages(
67
+ contentGlobs: readonly string[],
68
+ ): Promise<TsxPage[]> {
69
+ const patterns = tsxPagePatterns(contentGlobs);
70
+ const ignore = [
71
+ ...IGNORED,
72
+ ...patterns
73
+ .filter((pattern) => pattern.startsWith("!"))
74
+ .map((pattern) => pattern.slice(1)),
75
+ ];
76
+ const found = new Map<string, TsxPage>();
77
+ for (const pattern of patterns.filter((p) => !p.startsWith("!"))) {
78
+ const parent = globParent(pattern);
79
+ const files = await glob(pattern, {
80
+ absolute: true,
81
+ posix: true,
82
+ nodir: true,
83
+ ignore,
84
+ });
85
+ for (const raw of files.sort()) {
86
+ // On Windows the glob library answers with the extended form `//?/C:/...`, which would not relate to `parent`.
87
+ const file = raw.replace(/^\/\/\?\//, "");
88
+ const relative = path.posix.relative(parent, file);
89
+ if (isTsxHelper(relative)) continue;
90
+ const route = `/${relative.replace(/\.tsx$/, "")}`;
91
+ if (found.has(route)) continue;
92
+ const source = await readFile(file, "utf8").catch(() => "");
93
+ found.set(route, { route, file, label: tsxPageLabel(source, file) });
94
+ }
95
+ }
96
+ return [...found.values()].sort((a, b) => a.route.localeCompare(b.route));
97
+ }
98
+
99
+ /** The placeholder a sidebar item carries until the generated wrapper puts the component in its place. */
100
+ export const TSX_PAGE_ELEMENT_PREFIX = "apitogo:tsx-page:";
@@ -5,6 +5,7 @@ import { glob } from "glob";
5
5
  import { ensureStateDir } from "./layout.js";
6
6
  import { linkDirectory } from "./link-directory.js";
7
7
  import type { ResolvedConfigV2 } from "./resolve.js";
8
+ import { tsxPagePatterns } from "./tsx-pages.js";
8
9
  import { CONTENT_LINK, type PreparedPortal } from "./wrapper.js";
9
10
 
10
11
  const exists = (file: string) =>
@@ -92,6 +93,10 @@ export async function portalArtifactHash(
92
93
  const portal = resolved.portal;
93
94
  const patterns = [
94
95
  ...(portal?.docs.content ?? []),
96
+ // React docs pages and the helpers next to them are imported by the wrapper, so their contents decide the build too.
97
+ ...(portal?.docs.provider === "apitogo"
98
+ ? tsxPagePatterns(portal.docs.content)
99
+ : []),
95
100
  ...(resolved.gateway?.specs ?? []),
96
101
  ...(portal
97
102
  ? [portal.theme, path.join(portal.dir, "public", "**", "*")]