@lukoweb/apitogo 0.2.60 → 0.2.64
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/dist/cli/cli.js +1101 -589
- package/dist/declarations/cli/platform/client/platform-api.d.ts +3 -0
- package/dist/declarations/cli/platform/errors.d.ts +5 -0
- package/docs/cli/commands.md +6 -6
- package/docs/cli/go-live.md +12 -12
- package/docs/cli/overview.md +26 -27
- package/docs/cli/setup.md +38 -46
- package/docs/cli/shape.md +32 -35
- package/docs/cli/walkthrough.md +19 -19
- package/docs/components/slot.mdx +116 -116
- package/docs/configuration/api-reference.md +1 -1
- package/docs/configuration/authentication.md +3 -3
- package/docs/configuration/footer.mdx +89 -89
- package/docs/configuration/gateway.md +8 -8
- package/docs/configuration/make-config.mdx +20 -10
- package/docs/configuration/overview.md +3 -3
- package/docs/configuration/plans.md +5 -5
- package/docs/configuration/portal.md +36 -0
- package/docs/customization/colors-theme.mdx +255 -255
- package/docs/customization/fonts.md +105 -105
- package/docs/customization/landing-page.md +1 -1
- package/docs/deploy/apitogo.md +3 -3
- package/docs/deployment.md +3 -3
- package/docs/guides/custom-pages.md +3 -3
- package/docs/guides/customer-account.md +1 -1
- package/docs/guides/stripe-setup.mdx +9 -9
- package/docs/markdown/frontmatter.md +2 -2
- package/docs/mcp/go-live.md +7 -7
- package/docs/mcp/overview.md +58 -58
- package/docs/mcp/setup.md +20 -15
- package/docs/mcp/shape.md +2 -2
- package/docs/mcp/tools.md +57 -57
- package/docs/quickstart.md +6 -4
- package/docs/writing.mdx +68 -68
- package/package.json +1 -1
- package/src/config/apitogo-config-core.ts +11 -1
- package/src/config/v2/home-redirect.ts +5 -5
- package/src/config/v2/index.ts +5 -1
- package/src/config/v2/portal-profile.ts +17 -0
- package/src/config/v2/resolve.ts +19 -0
- package/src/config/v2/rules.ts +124 -0
- package/src/config/v2/schema.ts +28 -0
- package/src/config/v2/wrapper.ts +76 -13
- package/src/vite/config.ts +278 -273
- package/src/vite/markdown-output-path.ts +48 -0
- package/src/vite/plugin-markdown-export.ts +210 -214
- package/src/vite/prerender/prerender.ts +4 -4
- package/src/vite/watch-ignore.ts +19 -0
package/docs/mcp/tools.md
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: MCP tools
|
|
3
|
-
description: Every tool of the APItoGo MCP server and the CLI command it runs.
|
|
4
|
-
sidebar_icon: wrench
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Each tool runs one `apitogo` command with `--json` in the project folder and answers with its JSON.
|
|
8
|
-
Every tool takes `directory`: the project folder, absolute or relative to where the server runs
|
|
9
|
-
(default: that folder).
|
|
10
|
-
|
|
11
|
-
## Make and shape a project
|
|
12
|
-
|
|
13
|
-
| Tool | Runs | What it does |
|
|
14
|
-
| ---------------- | -------------------------- | ----------------------------------------------------------------------- |
|
|
15
|
-
| `apitogo_create` | `apitogo create` | A new project folder; `template` `full` or `blank`, optional `services` |
|
|
16
|
-
| `apitogo_init` | `apitogo init` | Prepares an existing folder: `apitogo.config.json` and `apitogo/` only |
|
|
17
|
-
| `apitogo_add` | `apitogo add <service>` | Adds `portal`, `gateway`, `identity` or `plans`; settings in `options` |
|
|
18
|
-
| `apitogo_remove` | `apitogo remove <service>` | Takes a service out of the config; `deleteFiles` also deletes its files |
|
|
19
|
-
| `apitogo_config` | `apitogo config` | Portal name, OpenAPI, docs pages, plans, or a `patch` of the config |
|
|
20
|
-
|
|
21
|
-
`options` of `apitogo_add` and `apitogo_config` are the command's flags in camelCase, for example
|
|
22
|
-
`{"planSku":"pro","planPrice":"19"}` for `--plan-sku pro --plan-price 19`. A flag the command does
|
|
23
|
-
not have is refused before anything runs. `true` and `false` are `--flag` and `--no-flag`; a list
|
|
24
|
-
repeats the flag.
|
|
25
|
-
|
|
26
|
-
## Check, preview, deploy
|
|
27
|
-
|
|
28
|
-
| Tool | Runs | What it does |
|
|
29
|
-
| ---------------- | ---------------- | ------------------------------------------------------------ |
|
|
30
|
-
| `apitogo_check` | `apitogo check` | Checks the config and its files; `offline`, `validateBuild` |
|
|
31
|
-
| `apitogo_plan` | `apitogo plan` | What a deploy would do for each service; changes nothing |
|
|
32
|
-
| `apitogo_deploy` | `apitogo deploy` | Deploys what changed; `only`, `force`, `confirmIrreversible` |
|
|
33
|
-
| `apitogo_status` | `apitogo status` | Each service's state and addresses; `skipLive` is faster |
|
|
34
|
-
|
|
35
|
-
## Platform project
|
|
36
|
-
|
|
37
|
-
| Tool | Runs | What it does |
|
|
38
|
-
| ------------------------ | -------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
39
|
-
| `apitogo_project_create` | `apitogo project create` | Creates the project; `plan` free, starter or professional |
|
|
40
|
-
| `apitogo_project_use` | `apitogo project use <id>` | Points the folder at another existing project |
|
|
41
|
-
| `apitogo_secrets_list` | `apitogo secrets list` | The names of the project's secrets, never their values |
|
|
42
|
-
| `apitogo_secrets_remove` | `apitogo secrets rm` | Removes a secret |
|
|
43
|
-
| `apitogo_teardown` | `apitogo teardown` | Deletes the project, or with `only` its gateway, identity or plans; needs `confirmProjectName` |
|
|
44
|
-
|
|
45
|
-
## Not a tool, on purpose
|
|
46
|
-
|
|
47
|
-
| Command | Why |
|
|
48
|
-
| --------------------- | --------------------------------------------------------- |
|
|
49
|
-
| `apitogo login` | Opens a browser to sign you in; run it once in a terminal |
|
|
50
|
-
| `apitogo secrets set` | A secret's value must not pass through the agent |
|
|
51
|
-
| `apitogo run` | The local preview keeps running; start it yourself |
|
|
52
|
-
|
|
53
|
-
## Safety hints
|
|
54
|
-
|
|
55
|
-
Tools carry MCP hints so hosts can ask before running them: `apitogo_check`, `apitogo_plan`,
|
|
56
|
-
`apitogo_status` and `apitogo_secrets_list` are read-only; `apitogo_remove`,
|
|
57
|
-
`apitogo_secrets_remove` and `apitogo_teardown` are marked destructive.
|
|
1
|
+
---
|
|
2
|
+
title: MCP tools
|
|
3
|
+
description: Every tool of the APItoGo MCP server and the CLI command it runs.
|
|
4
|
+
sidebar_icon: wrench
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Each tool runs one `apitogo` command with `--json` in the project folder and answers with its JSON.
|
|
8
|
+
Every tool takes `directory`: the project folder, absolute or relative to where the server runs
|
|
9
|
+
(default: that folder).
|
|
10
|
+
|
|
11
|
+
## Make and shape a project
|
|
12
|
+
|
|
13
|
+
| Tool | Runs | What it does |
|
|
14
|
+
| ---------------- | -------------------------- | ----------------------------------------------------------------------- |
|
|
15
|
+
| `apitogo_create` | `apitogo create` | A new project folder; `template` `full` or `blank`, optional `services` |
|
|
16
|
+
| `apitogo_init` | `apitogo init` | Prepares an existing folder: `apitogo.config.json` and `apitogo/` only |
|
|
17
|
+
| `apitogo_add` | `apitogo add <service>` | Adds `portal`, `gateway`, `identity` or `plans`; settings in `options` |
|
|
18
|
+
| `apitogo_remove` | `apitogo remove <service>` | Takes a service out of the config; `deleteFiles` also deletes its files |
|
|
19
|
+
| `apitogo_config` | `apitogo config` | Portal name, OpenAPI, docs pages, plans, or a `patch` of the config |
|
|
20
|
+
|
|
21
|
+
`options` of `apitogo_add` and `apitogo_config` are the command's flags in camelCase, for example
|
|
22
|
+
`{"planSku":"pro","planPrice":"19"}` for `--plan-sku pro --plan-price 19`. A flag the command does
|
|
23
|
+
not have is refused before anything runs. `true` and `false` are `--flag` and `--no-flag`; a list
|
|
24
|
+
repeats the flag.
|
|
25
|
+
|
|
26
|
+
## Check, preview, deploy
|
|
27
|
+
|
|
28
|
+
| Tool | Runs | What it does |
|
|
29
|
+
| ---------------- | ---------------- | ------------------------------------------------------------ |
|
|
30
|
+
| `apitogo_check` | `apitogo check` | Checks the config and its files; `offline`, `validateBuild` |
|
|
31
|
+
| `apitogo_plan` | `apitogo plan` | What a deploy would do for each service; changes nothing |
|
|
32
|
+
| `apitogo_deploy` | `apitogo deploy` | Deploys what changed; `only`, `force`, `confirmIrreversible` |
|
|
33
|
+
| `apitogo_status` | `apitogo status` | Each service's state and addresses; `skipLive` is faster |
|
|
34
|
+
|
|
35
|
+
## Platform project
|
|
36
|
+
|
|
37
|
+
| Tool | Runs | What it does |
|
|
38
|
+
| ------------------------ | -------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
39
|
+
| `apitogo_project_create` | `apitogo project create` | Creates the project; `plan` free, starter or professional |
|
|
40
|
+
| `apitogo_project_use` | `apitogo project use <id>` | Points the folder at another existing project |
|
|
41
|
+
| `apitogo_secrets_list` | `apitogo secrets list` | The names of the project's secrets, never their values |
|
|
42
|
+
| `apitogo_secrets_remove` | `apitogo secrets rm` | Removes a secret |
|
|
43
|
+
| `apitogo_teardown` | `apitogo teardown` | Deletes the project, or with `only` its gateway, identity or plans; needs `confirmProjectName` |
|
|
44
|
+
|
|
45
|
+
## Not a tool, on purpose
|
|
46
|
+
|
|
47
|
+
| Command | Why |
|
|
48
|
+
| --------------------- | --------------------------------------------------------- |
|
|
49
|
+
| `apitogo login` | Opens a browser to sign you in; run it once in a terminal |
|
|
50
|
+
| `apitogo secrets set` | A secret's value must not pass through the agent |
|
|
51
|
+
| `apitogo run` | The local preview keeps running; start it yourself |
|
|
52
|
+
|
|
53
|
+
## Safety hints
|
|
54
|
+
|
|
55
|
+
Tools carry MCP hints so hosts can ask before running them: `apitogo_check`, `apitogo_plan`,
|
|
56
|
+
`apitogo_status` and `apitogo_secrets_list` are read-only; `apitogo_remove`,
|
|
57
|
+
`apitogo_secrets_remove` and `apitogo_teardown` are marked destructive.
|
package/docs/quickstart.md
CHANGED
|
@@ -13,6 +13,8 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
13
13
|
## Prerequisites
|
|
14
14
|
|
|
15
15
|
- **Node.js 20+**
|
|
16
|
+
- **The `apitogo` CLI, installed globally:** `npm install -g @lukoweb/apitogo` (see
|
|
17
|
+
[Install the CLI](./cli/setup.md))
|
|
16
18
|
- An account on [apitogo.com](https://apitogo.com) (needed to deploy, not to try it locally)
|
|
17
19
|
|
|
18
20
|
## Get started
|
|
@@ -22,7 +24,7 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
22
24
|
1. **Create the project**
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
|
|
27
|
+
apitogo create --template full
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
It asks for a name, makes a folder for it and writes everything a working product needs:
|
|
@@ -37,7 +39,7 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
41
|
cd <your project folder>
|
|
40
|
-
|
|
42
|
+
apitogo run
|
|
41
43
|
```
|
|
42
44
|
|
|
43
45
|
Open `http://localhost:3000`. The console signs in with a demo account (`demo@apitogo.local` /
|
|
@@ -47,7 +49,7 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
47
49
|
1. **Sign in to APItoGo**
|
|
48
50
|
|
|
49
51
|
```bash
|
|
50
|
-
|
|
52
|
+
apitogo login
|
|
51
53
|
```
|
|
52
54
|
|
|
53
55
|
A browser window opens; sign in there.
|
|
@@ -55,7 +57,7 @@ that checks API keys and limits, sign-in for your users, and plans they pick. On
|
|
|
55
57
|
1. **Deploy**
|
|
56
58
|
|
|
57
59
|
```bash
|
|
58
|
-
|
|
60
|
+
apitogo deploy
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
The first deploy creates the project (it asks which hosting plan: Free, Starter or Professional),
|
package/docs/writing.mdx
CHANGED
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Writing
|
|
3
|
-
sidebar_icon: book-open-text
|
|
4
|
-
description: A guide to writing documentation in APIToGo using Markdown and MDX.
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Get started with creating rich documentation in APIToGo using Markdown and MDX. This guide covers
|
|
8
|
-
the essentials to help you begin documenting your project.
|
|
9
|
-
|
|
10
|
-
## Quick Start
|
|
11
|
-
|
|
12
|
-
1. **Create a markdown file** in `apitogo/portal/pages/`
|
|
13
|
-
2. **Add frontmatter** with title and metadata
|
|
14
|
-
3. **Write content** using Markdown or MDX
|
|
15
|
-
|
|
16
|
-
The page shows in the sidebar on its own.
|
|
17
|
-
|
|
18
|
-
### Basic Document Structure
|
|
19
|
-
|
|
20
|
-
```md
|
|
21
|
-
---
|
|
22
|
-
title: My Document
|
|
23
|
-
sidebar_icon: file-text
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
Your content goes here using standard Markdown syntax.
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Adding to Navigation
|
|
30
|
-
|
|
31
|
-
The sidebar is built from the files: every page in `apitogo/portal/pages/` (or the folders
|
|
32
|
-
`portal.docs.content` names) is listed, and each folder becomes a section. A folder's `index` page
|
|
33
|
-
comes first, then pages with an `order`, lowest first, then the rest by name:
|
|
34
|
-
|
|
35
|
-
```md
|
|
36
|
-
---
|
|
37
|
-
title: Authentication
|
|
38
|
-
order: 2
|
|
39
|
-
---
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
A page with `draft: true` is left out of deployed portals.
|
|
43
|
-
|
|
44
|
-
## File Organization
|
|
45
|
-
|
|
46
|
-
Organize your documentation files in logical directories:
|
|
47
|
-
|
|
48
|
-
```
|
|
49
|
-
pages/
|
|
50
|
-
├── getting-started/
|
|
51
|
-
│ ├── installation.md
|
|
52
|
-
│ └── quick-start.md
|
|
53
|
-
├── guides/
|
|
54
|
-
│ ├── authentication.md
|
|
55
|
-
│ └── deployment.md
|
|
56
|
-
└── api/
|
|
57
|
-
└── reference.md
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## What's Next?
|
|
61
|
-
|
|
62
|
-
Explore the detailed guides to enhance your documentation:
|
|
63
|
-
|
|
64
|
-
- **[Markdown Overview](/docs/markdown/overview)** - Complete markdown syntax reference
|
|
65
|
-
- **[Frontmatter](/docs/markdown/frontmatter)** - Document metadata and configuration
|
|
66
|
-
- **[MDX](/docs/markdown/mdx)** - Interactive components in markdown
|
|
67
|
-
- **[Admonitions](/docs/markdown/admonitions)** - Callouts and alerts
|
|
68
|
-
- **[Code Blocks](/docs/markdown/code-blocks)** - Syntax highlighting and features
|
|
1
|
+
---
|
|
2
|
+
title: Writing
|
|
3
|
+
sidebar_icon: book-open-text
|
|
4
|
+
description: A guide to writing documentation in APIToGo using Markdown and MDX.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Get started with creating rich documentation in APIToGo using Markdown and MDX. This guide covers
|
|
8
|
+
the essentials to help you begin documenting your project.
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
1. **Create a markdown file** in `apitogo/portal/pages/`
|
|
13
|
+
2. **Add frontmatter** with title and metadata
|
|
14
|
+
3. **Write content** using Markdown or MDX
|
|
15
|
+
|
|
16
|
+
The page shows in the sidebar on its own.
|
|
17
|
+
|
|
18
|
+
### Basic Document Structure
|
|
19
|
+
|
|
20
|
+
```md
|
|
21
|
+
---
|
|
22
|
+
title: My Document
|
|
23
|
+
sidebar_icon: file-text
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Your content goes here using standard Markdown syntax.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Adding to Navigation
|
|
30
|
+
|
|
31
|
+
The sidebar is built from the files: every page in `apitogo/portal/pages/` (or the folders
|
|
32
|
+
`portal.docs.content` names) is listed, and each folder becomes a section. A folder's `index` page
|
|
33
|
+
comes first, then pages with an `order`, lowest first, then the rest by name:
|
|
34
|
+
|
|
35
|
+
```md
|
|
36
|
+
---
|
|
37
|
+
title: Authentication
|
|
38
|
+
order: 2
|
|
39
|
+
---
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A page with `draft: true` is left out of deployed portals.
|
|
43
|
+
|
|
44
|
+
## File Organization
|
|
45
|
+
|
|
46
|
+
Organize your documentation files in logical directories:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
pages/
|
|
50
|
+
├── getting-started/
|
|
51
|
+
│ ├── installation.md
|
|
52
|
+
│ └── quick-start.md
|
|
53
|
+
├── guides/
|
|
54
|
+
│ ├── authentication.md
|
|
55
|
+
│ └── deployment.md
|
|
56
|
+
└── api/
|
|
57
|
+
└── reference.md
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## What's Next?
|
|
61
|
+
|
|
62
|
+
Explore the detailed guides to enhance your documentation:
|
|
63
|
+
|
|
64
|
+
- **[Markdown Overview](/docs/markdown/overview)** - Complete markdown syntax reference
|
|
65
|
+
- **[Frontmatter](/docs/markdown/frontmatter)** - Document metadata and configuration
|
|
66
|
+
- **[MDX](/docs/markdown/mdx)** - Interactive components in markdown
|
|
67
|
+
- **[Admonitions](/docs/markdown/admonitions)** - Callouts and alerts
|
|
68
|
+
- **[Code Blocks](/docs/markdown/code-blocks)** - Syntax highlighting and features
|
package/package.json
CHANGED
|
@@ -558,8 +558,18 @@ function syncAuthentication(normalized: ApitogoJsonConfig): void {
|
|
|
558
558
|
return;
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
// `authentication.enabled: false` turns sign-in off: no provider at all, so the header shows no "Log in" and
|
|
562
|
+
// "Get started". Without this the block below gave it a dev-portal type, and the auth plugin loads a provider for
|
|
563
|
+
// any type. The user panel needs sign-in, so with the panel on it still wins.
|
|
564
|
+
if (!userPanelEnabled && authentication?.enabled === false) {
|
|
565
|
+
delete normalized.authentication;
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
561
569
|
const auth = authentication ?? {};
|
|
562
|
-
|
|
570
|
+
// Only the panel sets `enabled`: writing `false` here would read as "turned off" when the config is normalized
|
|
571
|
+
// again, and drop an OpenID provider that a portal without the panel still signs in with. Readers check `=== true`.
|
|
572
|
+
if (userPanelEnabled) auth.enabled = true;
|
|
563
573
|
// A portal that signs in with an OpenID provider (the project's identity, with no plans backend) keeps it: the
|
|
564
574
|
// dev-portal defaults below (native sign-up, an empty provider list, the demo sign-in form) belong to the
|
|
565
575
|
// dev-portal provider only.
|
|
@@ -117,7 +117,7 @@ export type HomeRedirectInput = {
|
|
|
117
117
|
* "Page not found".
|
|
118
118
|
*
|
|
119
119
|
* - Hosted docs without an index page: the first page, so the team's own docs never need an extra file.
|
|
120
|
-
* - Hosted docs without any page: the API reference, when there is one.
|
|
120
|
+
* - Hosted docs without any page: the API reference, when there is one, else the console pages as below (API-116).
|
|
121
121
|
* - External docs and no console: the external docs site, as the portal has nothing else to show.
|
|
122
122
|
* - No hosted docs but a console: the pricing page when there is one, else the account pages, which lead a signed-out
|
|
123
123
|
* visitor to sign in. The console has no page of its own at `/`, so the address `deploy` prints showed "Page not
|
|
@@ -131,11 +131,11 @@ export function homeRedirect(
|
|
|
131
131
|
return undefined;
|
|
132
132
|
const landing = pickLandingRoute(input.docRoutes, input.pageOrders);
|
|
133
133
|
if (landing) return { from: "/", to: landing };
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
if (input.apiReferencePath)
|
|
135
|
+
return { from: "/", to: input.apiReferencePath };
|
|
136
|
+
// No pages and no reference yet: the console pages, when there are any, rather than "Page not found" (API-116).
|
|
137
137
|
}
|
|
138
|
-
if (input.externalDocsUrl && !input.userPanel) {
|
|
138
|
+
if (!input.hostsDocs && input.externalDocsUrl && !input.userPanel) {
|
|
139
139
|
return { from: "/", to: input.externalDocsUrl };
|
|
140
140
|
}
|
|
141
141
|
if (input.userPanel) {
|
package/src/config/v2/index.ts
CHANGED
|
@@ -54,7 +54,11 @@ export {
|
|
|
54
54
|
ConfigV2Schema,
|
|
55
55
|
type Layout,
|
|
56
56
|
} from "./schema.js";
|
|
57
|
-
export {
|
|
57
|
+
export {
|
|
58
|
+
type PortalProfile,
|
|
59
|
+
portalOnlyLinksExternalDocs,
|
|
60
|
+
portalProfile,
|
|
61
|
+
} from "./portal-profile.js";
|
|
58
62
|
export {
|
|
59
63
|
hasCapability,
|
|
60
64
|
type ManifestServiceName,
|
|
@@ -49,6 +49,23 @@ const NOT_BUILT: PortalProfile = {
|
|
|
49
49
|
modules: { docs: false, userPanel: false },
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The portal's docs are on an external site and no service (identity or plans) feeds its console, so the portal would be
|
|
54
|
+
* an empty site. `project create` takes no website hosting for it and `deploy` does not build or publish it (API-101: such
|
|
55
|
+
* a portal waited on hosting it never needed, and deploy exited 1).
|
|
56
|
+
*/
|
|
57
|
+
export function portalOnlyLinksExternalDocs(
|
|
58
|
+
resolved: Pick<ResolvedConfigV2, "portal" | "identity" | "plans">,
|
|
59
|
+
): boolean {
|
|
60
|
+
const portal = resolved.portal;
|
|
61
|
+
if (!portal?.enabled) return false;
|
|
62
|
+
if (!portal.docs.enabled || portal.docs.provider !== "external") return false;
|
|
63
|
+
const consoleFed =
|
|
64
|
+
portal.console.enabled &&
|
|
65
|
+
(resolved.identity?.enabled === true || resolved.plans?.enabled === true);
|
|
66
|
+
return !consoleFed;
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
export function portalProfile(resolved: ResolvedConfigV2): PortalProfile {
|
|
53
70
|
const portal = resolved.portal;
|
|
54
71
|
if (!portal?.enabled) return NOT_BUILT;
|
package/src/config/v2/resolve.ts
CHANGED
|
@@ -19,6 +19,16 @@ function fromConfig(configDir: string, value: string): string {
|
|
|
19
19
|
: toGlobPath(path.resolve(configDir, value));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A header link. `to` is where it goes. A page link also has `page`, the absolute path of its file (a React page or a
|
|
24
|
+
* Markdown page), which the portal serves at `/<file name>` (so `to` is that address) and keeps out of the docs sidebar.
|
|
25
|
+
*/
|
|
26
|
+
export type ResolvedHeaderLink = { label: string; to: string; page?: string };
|
|
27
|
+
|
|
28
|
+
/** The address of a header page: its file name without the extension, `about.tsx` is `/about`. */
|
|
29
|
+
export const headerPageRoute = (file: string): string =>
|
|
30
|
+
`/${path.basename(file).replace(/\.(mdx?|tsx)$/i, "")}`;
|
|
31
|
+
|
|
22
32
|
export type ResolvedPortal = {
|
|
23
33
|
enabled: boolean;
|
|
24
34
|
dir: string;
|
|
@@ -36,6 +46,8 @@ export type ResolvedPortal = {
|
|
|
36
46
|
landing?: string;
|
|
37
47
|
/** Absolute path of the footer component, when the portal has one (API-129). */
|
|
38
48
|
footer?: string;
|
|
49
|
+
/** Links of the header's top menu (`portal.header.links`), in their order. Empty when there are none. */
|
|
50
|
+
headerLinks: ResolvedHeaderLink[];
|
|
39
51
|
/** The header logo, when the portal has one: absolute paths (inside the portal's `public/`) or https URLs (API-129). */
|
|
40
52
|
logo?: { light: string; dark: string; alt?: string };
|
|
41
53
|
requireAuth: boolean | { exclude: string[] };
|
|
@@ -159,6 +171,13 @@ export function resolveConfigV2(
|
|
|
159
171
|
...(portal.footer
|
|
160
172
|
? { footer: path.resolve(configDir, portal.footer) }
|
|
161
173
|
: {}),
|
|
174
|
+
headerLinks: (portal.header?.links ?? []).map(
|
|
175
|
+
(link): ResolvedHeaderLink => {
|
|
176
|
+
if ("to" in link) return { label: link.label, to: link.to };
|
|
177
|
+
const page = path.resolve(configDir, link.page);
|
|
178
|
+
return { label: link.label, to: headerPageRoute(page), page };
|
|
179
|
+
},
|
|
180
|
+
),
|
|
162
181
|
...(portal.logo
|
|
163
182
|
? {
|
|
164
183
|
logo:
|
package/src/config/v2/rules.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { glob } from "glob";
|
|
|
4
4
|
import { parse as parseYaml } from "yaml";
|
|
5
5
|
import { billingNeeded, stripeKeySecretName } from "./billing.js";
|
|
6
6
|
import type { ConfigIssue } from "./errors.js";
|
|
7
|
+
import { docRoutes } from "./home-redirect.js";
|
|
8
|
+
import { portalOnlyLinksExternalDocs } from "./portal-profile.js";
|
|
7
9
|
import { isLogoUrl, type ResolvedConfigV2, toGlobPath } from "./resolve.js";
|
|
8
10
|
import type { ConfigV2 } from "./schema.js";
|
|
9
11
|
import { findTsxPages } from "./tsx-pages.js";
|
|
@@ -247,6 +249,17 @@ async function portalRules(
|
|
|
247
249
|
});
|
|
248
250
|
}
|
|
249
251
|
|
|
252
|
+
// Docs on an external site and nothing for the console: the portal would be an empty site (API-101).
|
|
253
|
+
if (portalOnlyLinksExternalDocs(resolved)) {
|
|
254
|
+
out.warn({
|
|
255
|
+
code: "PORTAL_NOTHING_TO_HOST",
|
|
256
|
+
path: "portal",
|
|
257
|
+
message:
|
|
258
|
+
"docs are on an external site and no service feeds the console, so there is no portal to host: `project create` takes no website hosting for it and `deploy` skips it.",
|
|
259
|
+
hint: "Enable identity or plans for a console next to your docs, or remove the portal section.",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
250
263
|
// A missing footer or logo file would fail the build, so check says so first (API-129).
|
|
251
264
|
if (portal.footer && !(await isFile(portal.footer)))
|
|
252
265
|
out.error({
|
|
@@ -255,6 +268,7 @@ async function portalRules(
|
|
|
255
268
|
message: `the file ${path.relative(resolved.configDir, portal.footer)} does not exist.`,
|
|
256
269
|
hint: "Create it, or remove portal.footer.",
|
|
257
270
|
});
|
|
271
|
+
await headerLinksRule(portal, resolved, out);
|
|
258
272
|
const publicDir = path.join(portal.dir, "public");
|
|
259
273
|
for (const file of new Set(
|
|
260
274
|
[portal.logo?.light, portal.logo?.dark].filter(
|
|
@@ -315,6 +329,16 @@ async function docsContentRule(
|
|
|
315
329
|
const shown = config.portal?.docs?.content ?? [
|
|
316
330
|
path.relative(resolved.configDir, patterns[0] ?? ""),
|
|
317
331
|
];
|
|
332
|
+
// No pages and nothing else for `/` either: the deployed home page would say "Page not found" (API-116).
|
|
333
|
+
if (portalHasNothingToShow(resolved)) {
|
|
334
|
+
out.error({
|
|
335
|
+
code: "PORTAL_HOME_EMPTY",
|
|
336
|
+
path: "portal.docs",
|
|
337
|
+
message: `portal.docs matched 0 files (${shown.join(", ")}), and the portal has no API reference, console pages or home page of its own, so its home page would say "Page not found".`,
|
|
338
|
+
hint: "Add a .md or .mdx page there (for example introduction.mdx), point docs.content at your existing docs, or enable the gateway with an OpenAPI file for an API reference.",
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
318
342
|
out.warn({
|
|
319
343
|
code: "PORTAL_DOCS_EMPTY",
|
|
320
344
|
path: "portal.docs",
|
|
@@ -323,6 +347,106 @@ async function docsContentRule(
|
|
|
323
347
|
});
|
|
324
348
|
}
|
|
325
349
|
|
|
350
|
+
/**
|
|
351
|
+
* A portal that hosts docs but has nothing else to show at `/`: no home page of its own, no gateway for an API reference,
|
|
352
|
+
* and no service (identity or plans) for console pages.
|
|
353
|
+
*/
|
|
354
|
+
function portalHasNothingToShow(resolved: ResolvedConfigV2): boolean {
|
|
355
|
+
const portal = resolved.portal;
|
|
356
|
+
if (!portal?.enabled || portal.landing) return false;
|
|
357
|
+
if (resolved.gateway?.enabled === true) return false;
|
|
358
|
+
const consoleFed =
|
|
359
|
+
portal.console.enabled &&
|
|
360
|
+
(resolved.identity?.enabled === true || resolved.plans?.enabled === true);
|
|
361
|
+
return !consoleFed;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Addresses the portal itself uses, so a header page cannot take them. */
|
|
365
|
+
const RESERVED_HEADER_ROUTES = [
|
|
366
|
+
"/",
|
|
367
|
+
"/index",
|
|
368
|
+
"/api",
|
|
369
|
+
"/pricing",
|
|
370
|
+
"/account",
|
|
371
|
+
"/login",
|
|
372
|
+
"/signup",
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Header pages (`portal.header.links[].page`) are React or Markdown files the portal serves at `/<file name>`, outside
|
|
377
|
+
* the sidebar. A wrong one would fail the build or quietly take another page's address, so check says so first.
|
|
378
|
+
*/
|
|
379
|
+
async function headerLinksRule(
|
|
380
|
+
portal: NonNullable<ResolvedConfigV2["portal"]>,
|
|
381
|
+
resolved: ResolvedConfigV2,
|
|
382
|
+
out: Collector,
|
|
383
|
+
) {
|
|
384
|
+
const pages = portal.headerLinks
|
|
385
|
+
.map((link, index) => ({ link, index }))
|
|
386
|
+
.filter(({ link }) => link.page !== undefined);
|
|
387
|
+
if (pages.length === 0) return;
|
|
388
|
+
const hostsDocs = portal.docs.enabled && portal.docs.provider === "apitogo";
|
|
389
|
+
const docs = hostsDocs
|
|
390
|
+
? [
|
|
391
|
+
...(await docRoutes(portal.docs.content, { includeDrafts: true })),
|
|
392
|
+
...(await findTsxPages(portal.docs.content)).map((page) => page.route),
|
|
393
|
+
]
|
|
394
|
+
: [];
|
|
395
|
+
const seen = new Set<string>();
|
|
396
|
+
for (const { link, index } of pages) {
|
|
397
|
+
const file = link.page as string;
|
|
398
|
+
const at = `portal.header.links[${index}].page`;
|
|
399
|
+
const shown = path.relative(resolved.configDir, file);
|
|
400
|
+
if (!hostsDocs) {
|
|
401
|
+
out.error({
|
|
402
|
+
code: "PORTAL_HEADER_PAGE_NO_DOCS",
|
|
403
|
+
path: at,
|
|
404
|
+
message: `the header page ${shown} needs hosted docs, which this portal does not have.`,
|
|
405
|
+
hint: 'Header pages are served with the docs pages: set portal.docs.provider to "apitogo", or link an address with "to" instead.',
|
|
406
|
+
});
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (!/\.(mdx?|tsx)$/i.test(file)) {
|
|
410
|
+
out.error({
|
|
411
|
+
code: "PORTAL_HEADER_PAGE_INVALID",
|
|
412
|
+
path: at,
|
|
413
|
+
message: `the header page ${shown} is neither a React page nor a Markdown page.`,
|
|
414
|
+
hint: "Use a .tsx file (its default export is the page) or a .md / .mdx file.",
|
|
415
|
+
});
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (!(await isFile(file))) {
|
|
419
|
+
out.error({
|
|
420
|
+
code: "PORTAL_FILE_MISSING",
|
|
421
|
+
path: at,
|
|
422
|
+
message: `the file ${shown} does not exist.`,
|
|
423
|
+
hint: "Create it (`apitogo config --page-slug <name> --page-title <title> --page-header`), or remove the link.",
|
|
424
|
+
});
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const route = link.to;
|
|
428
|
+
if (RESERVED_HEADER_ROUTES.includes(route.toLowerCase())) {
|
|
429
|
+
out.error({
|
|
430
|
+
code: "PORTAL_HEADER_PAGE_ROUTE_TAKEN",
|
|
431
|
+
path: at,
|
|
432
|
+
message: `the header page ${shown} would be served at ${route}, which the portal uses itself.`,
|
|
433
|
+
hint: "Rename the file: its name is the page's address.",
|
|
434
|
+
});
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (docs.includes(route) || seen.has(route)) {
|
|
438
|
+
out.error({
|
|
439
|
+
code: "PORTAL_HEADER_PAGE_ROUTE_TAKEN",
|
|
440
|
+
path: at,
|
|
441
|
+
message: `the header page ${shown} would be served at ${route}, which another page already has.`,
|
|
442
|
+
hint: 'Rename the file: its name is the page\'s address. A docs page that should be in the header too can be linked with "to" instead.',
|
|
443
|
+
});
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
seen.add(route);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
326
450
|
// --- gateway (2.4) --------------------------------------------------------------------------------------------------
|
|
327
451
|
|
|
328
452
|
function isIpLiteral(hostname: string): boolean {
|
package/src/config/v2/schema.ts
CHANGED
|
@@ -31,6 +31,32 @@ const pathList = z.array(nonEmpty);
|
|
|
31
31
|
|
|
32
32
|
const enabled = z.boolean().optional();
|
|
33
33
|
|
|
34
|
+
const headerLabel = z
|
|
35
|
+
.string()
|
|
36
|
+
.trim()
|
|
37
|
+
.min(1, { error: "must not be empty" })
|
|
38
|
+
.max(40, { error: "must be 40 characters or fewer" });
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A link in the portal header's top menu: a page of its own (a React `.tsx` or Markdown file, served at `/<file name>`
|
|
42
|
+
* and kept out of the docs sidebar) or an address (`/path` on the portal, or `https://` elsewhere).
|
|
43
|
+
*/
|
|
44
|
+
const HeaderLinkSchema = z.union(
|
|
45
|
+
[
|
|
46
|
+
z.strictObject({ label: headerLabel, page: path }),
|
|
47
|
+
z.strictObject({
|
|
48
|
+
label: headerLabel,
|
|
49
|
+
to: z.string().regex(/^(\/(?!\/)|https:\/\/)/, {
|
|
50
|
+
error: 'must start with "/" or "https://"',
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
],
|
|
54
|
+
{
|
|
55
|
+
error:
|
|
56
|
+
'must be { "label": "...", "page": "path/to/page.tsx" } or { "label": "...", "to": "/path or https://..." }',
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
34
60
|
export const PortalSchema = z.strictObject({
|
|
35
61
|
enabled,
|
|
36
62
|
dir: path.optional(),
|
|
@@ -49,6 +75,8 @@ export const PortalSchema = z.strictObject({
|
|
|
49
75
|
landing: path.optional(),
|
|
50
76
|
/** The footer: a React component file shown under every page except the console's (API-129). */
|
|
51
77
|
footer: path.optional(),
|
|
78
|
+
/** Links of the header's top menu, after the built-in ones (Documentation, API Reference; the plans add Pricing). */
|
|
79
|
+
header: z.strictObject({ links: z.array(HeaderLinkSchema) }).optional(),
|
|
52
80
|
/**
|
|
53
81
|
* The logo in the portal header: one image, or one per color scheme (API-129). `dark` is optional; without it the
|
|
54
82
|
* light image is used in both.
|