create-rapidoc 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +22 -0
- package/README.md +36 -2
- package/package.json +45 -4
- package/src/index.js +96 -0
- package/src/scaffold.js +562 -0
- package/src/wizard.js +44 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Mrinmoy Majumdar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-rapidoc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scaffolds a [RapiDoc Portal](https://portal.rapidocui.com): a developer portal built from OpenAPI specs and Markdown guides.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create rapidoc@latest my-portal
|
|
7
|
+
cd my-portal
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Run it in a folder that already has OpenAPI files and the portal uses them: one API reference and navigation tab per spec. Otherwise it adds a sample spec.
|
|
13
|
+
|
|
14
|
+
Node.js 22.12 or later.
|
|
15
|
+
|
|
16
|
+
## What it writes
|
|
17
|
+
|
|
18
|
+
- `rapidoc.config.yaml`: site, theme, header, navigation tabs and APIs
|
|
19
|
+
- `package.json` with `@rapidoc/cli` and `@rapidoc/portal`, and `dev`, `build`, `validate` and `preview` scripts
|
|
20
|
+
- `docs/`: a home page, a getting-started guide and a Markdoc guide
|
|
21
|
+
- `specs/api.yaml` (when the folder has no specs), `.gitignore` and `README.md`
|
|
22
|
+
|
|
23
|
+
Existing files are kept unless you pass `--force`.
|
|
24
|
+
|
|
25
|
+
## Options
|
|
26
|
+
|
|
27
|
+
| Option | Meaning |
|
|
28
|
+
| :---------------- | :-------------------------------------------------------------------------------- |
|
|
29
|
+
| `--title <title>` | Site title, e.g. `"Acme Developer Portal"` |
|
|
30
|
+
| `--force`, `-f` | Overwrite existing files |
|
|
31
|
+
| `--yes`, `-y` | Don't ask; use the defaults (in a terminal it asks for the title and which specs) |
|
|
32
|
+
|
|
33
|
+
`rapidoc init` in [`@rapidoc/cli`](https://www.npmjs.com/package/@rapidoc/cli) does the same in an existing project.
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
|
|
37
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-rapidoc",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Interactive scaffolding CLI to create new RapiDoc Developer Portals",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"create-rapidoc",
|
|
7
|
+
"developer-portal",
|
|
8
|
+
"documentation",
|
|
9
|
+
"openapi",
|
|
10
|
+
"rapidoc",
|
|
11
|
+
"scaffold"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://portal.rapidocui.com",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/rapi-doc/RapiDoc/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Mrinmoy Majumdar <mrin9@yahoo.com>",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/rapi-doc/RapiDoc.git",
|
|
22
|
+
"directory": "packages/create-rapidoc"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"create-rapidoc": "src/index.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"main": "./src/index.js",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./src/index.js",
|
|
34
|
+
"./scaffold": "./src/scaffold.js",
|
|
35
|
+
"./wizard": "./src/wizard.js"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "node --test tests/**/*.test.js"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@clack/prompts": "^1.8.1",
|
|
42
|
+
"yaml": "^2.9.1"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22.12.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import pkg from "../package.json" with { type: "json" };
|
|
6
|
+
import { findOpenApiSpecs, scaffoldProject } from "./scaffold.js";
|
|
7
|
+
import { askInitQuestions } from "./wizard.js";
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
function printHelp() {
|
|
12
|
+
console.log(`
|
|
13
|
+
create-rapidoc - Scaffold a modern RapiDoc Developer Portal
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
npm create rapidoc@latest [directory] [options]
|
|
17
|
+
npx create-rapidoc [directory] [options]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--title <title> Human-readable site title (e.g. "Acme Developer Portal")
|
|
21
|
+
--force, -f Overwrite existing files in target directory
|
|
22
|
+
--yes, -y Don't ask: use the defaults (in a terminal it asks for the title and which specs)
|
|
23
|
+
--help, -h Show this help message
|
|
24
|
+
--version, -v Show version information
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
npm create rapidoc@latest my-portal
|
|
28
|
+
npx create-rapidoc ./company-docs --title "Stripe Alternative Docs"
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
33
|
+
printHelp();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
38
|
+
console.log(pkg.version);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let targetDir = args.find((a) => !a.startsWith("-")) || "my-developer-portal";
|
|
43
|
+
const titleIdx = args.indexOf("--title");
|
|
44
|
+
const customTitle = titleIdx !== -1 && args[titleIdx + 1] ? args[titleIdx + 1] : undefined;
|
|
45
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
46
|
+
const yes = args.includes("--yes") || args.includes("-y");
|
|
47
|
+
const foundSpecs = fs.existsSync(targetDir) ? findOpenApiSpecs(targetDir) : [];
|
|
48
|
+
|
|
49
|
+
// In a terminal, the same questions as `rapidoc init` (installing is left to the next steps).
|
|
50
|
+
let answers = { title: customTitle };
|
|
51
|
+
if (!yes && process.stdin.isTTY && process.stdout.isTTY && !process.env.CI) {
|
|
52
|
+
const clack = await import("@clack/prompts");
|
|
53
|
+
const answer = (prompt) => async (options) => {
|
|
54
|
+
const value = await prompt(options);
|
|
55
|
+
if (clack.isCancel(value)) {
|
|
56
|
+
clack.cancel("Cancelled.");
|
|
57
|
+
process.exit(130);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
};
|
|
61
|
+
clack.intro("Create a RapiDoc developer portal");
|
|
62
|
+
answers = await askInitQuestions({
|
|
63
|
+
ask: {
|
|
64
|
+
text: answer(clack.text),
|
|
65
|
+
multiselect: answer(clack.multiselect),
|
|
66
|
+
confirm: answer(clack.confirm),
|
|
67
|
+
},
|
|
68
|
+
targetDir,
|
|
69
|
+
foundSpecs,
|
|
70
|
+
known: { title: customTitle },
|
|
71
|
+
askInstall: false,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`\n🚀 Scaffolding new RapiDoc Developer Portal into: ${targetDir}`);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result = await scaffoldProject(targetDir, {
|
|
79
|
+
title: answers.title || undefined,
|
|
80
|
+
overwrite: force,
|
|
81
|
+
specs: answers.specs ?? foundSpecs,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
console.log(`\n✨ Successfully created ${result.projectName}!`);
|
|
85
|
+
console.log(` • Site Title: ${result.siteTitle}`);
|
|
86
|
+
console.log(` • Location: ${result.targetDir}`);
|
|
87
|
+
console.log(` • Files: ${result.filesCreated.length} starter files generated`);
|
|
88
|
+
|
|
89
|
+
console.log(`\nNext steps:`);
|
|
90
|
+
console.log(` 1. cd ${path.relative(process.cwd(), result.targetDir) || "."}`);
|
|
91
|
+
console.log(` 2. npm install`);
|
|
92
|
+
console.log(` 3. npm run dev\n`);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`\n❌ Failed to scaffold portal: ${err.message}\n`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pkg from "../package.json" with { type: "json" };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Range for the @rapidoc packages a new project depends on. They release with create-rapidoc as one
|
|
7
|
+
* fixed group (.changeset/config.json), so its own version is theirs too.
|
|
8
|
+
*/
|
|
9
|
+
const RAPIDOC_PACKAGES_RANGE = `^${pkg.version}`;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Starter template contents for scaffolding a new RapiDoc Developer Portal.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const STARTER_SPEC = `openapi: 3.1.0
|
|
16
|
+
info:
|
|
17
|
+
title: Core Services API
|
|
18
|
+
version: 1.0.0
|
|
19
|
+
description: High-performance microservices and developer APIs.
|
|
20
|
+
servers:
|
|
21
|
+
- url: https://api.example.com/v1
|
|
22
|
+
description: Production Server
|
|
23
|
+
- url: https://sandbox.example.com/v1
|
|
24
|
+
description: Sandbox Environment
|
|
25
|
+
paths:
|
|
26
|
+
/health:
|
|
27
|
+
get:
|
|
28
|
+
summary: Service Health Check
|
|
29
|
+
description: Returns the operational status and uptime of the system.
|
|
30
|
+
operationId: getHealth
|
|
31
|
+
tags:
|
|
32
|
+
- System
|
|
33
|
+
responses:
|
|
34
|
+
'200':
|
|
35
|
+
description: Service operational
|
|
36
|
+
content:
|
|
37
|
+
application/json:
|
|
38
|
+
schema:
|
|
39
|
+
type: object
|
|
40
|
+
properties:
|
|
41
|
+
status:
|
|
42
|
+
type: string
|
|
43
|
+
example: healthy
|
|
44
|
+
timestamp:
|
|
45
|
+
type: string
|
|
46
|
+
format: date-time
|
|
47
|
+
|
|
48
|
+
/items:
|
|
49
|
+
get:
|
|
50
|
+
summary: List items
|
|
51
|
+
description: Retrieve a paginated collection of catalog items.
|
|
52
|
+
operationId: listItems
|
|
53
|
+
tags:
|
|
54
|
+
- Items
|
|
55
|
+
responses:
|
|
56
|
+
'200':
|
|
57
|
+
description: List of items
|
|
58
|
+
content:
|
|
59
|
+
application/json:
|
|
60
|
+
schema:
|
|
61
|
+
type: array
|
|
62
|
+
items:
|
|
63
|
+
$ref: '#/components/schemas/Item'
|
|
64
|
+
post:
|
|
65
|
+
summary: Create item
|
|
66
|
+
description: Creates a new catalog item.
|
|
67
|
+
operationId: createItem
|
|
68
|
+
tags:
|
|
69
|
+
- Items
|
|
70
|
+
requestBody:
|
|
71
|
+
required: true
|
|
72
|
+
content:
|
|
73
|
+
application/json:
|
|
74
|
+
schema:
|
|
75
|
+
$ref: '#/components/schemas/CreateItemRequest'
|
|
76
|
+
responses:
|
|
77
|
+
'201':
|
|
78
|
+
description: Item created successfully
|
|
79
|
+
content:
|
|
80
|
+
application/json:
|
|
81
|
+
schema:
|
|
82
|
+
$ref: '#/components/schemas/Item'
|
|
83
|
+
|
|
84
|
+
components:
|
|
85
|
+
schemas:
|
|
86
|
+
Item:
|
|
87
|
+
type: object
|
|
88
|
+
required:
|
|
89
|
+
- id
|
|
90
|
+
- name
|
|
91
|
+
properties:
|
|
92
|
+
id:
|
|
93
|
+
type: string
|
|
94
|
+
example: item_123
|
|
95
|
+
name:
|
|
96
|
+
type: string
|
|
97
|
+
example: Standard Widget
|
|
98
|
+
status:
|
|
99
|
+
type: string
|
|
100
|
+
enum: [active, archived]
|
|
101
|
+
example: active
|
|
102
|
+
|
|
103
|
+
CreateItemRequest:
|
|
104
|
+
type: object
|
|
105
|
+
required:
|
|
106
|
+
- name
|
|
107
|
+
properties:
|
|
108
|
+
name:
|
|
109
|
+
type: string
|
|
110
|
+
example: Premium Widget
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
const STARTER_GETTING_STARTED = `---
|
|
114
|
+
title: Getting Started
|
|
115
|
+
description: Quickstart guide to integrating with the Core Developer Platform.
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
# Getting Started
|
|
119
|
+
|
|
120
|
+
Welcome to the Developer Portal. This guide walks you through authenticating with our services and making your very first API request.
|
|
121
|
+
|
|
122
|
+
## Prerequisites
|
|
123
|
+
|
|
124
|
+
Before making requests, ensure you have:
|
|
125
|
+
1. Created an account on the Developer Dashboard.
|
|
126
|
+
2. Generated a secure API token with read/write access.
|
|
127
|
+
|
|
128
|
+
## Base URLs
|
|
129
|
+
|
|
130
|
+
| Environment | Base URL |
|
|
131
|
+
| :--- | :--- |
|
|
132
|
+
| **Production** | \`https://api.example.com/v1\` |
|
|
133
|
+
| **Sandbox** | \`https://sandbox.example.com/v1\` |
|
|
134
|
+
|
|
135
|
+
## First Request
|
|
136
|
+
|
|
137
|
+
Send an authenticated \`GET\` request to the health check endpoint:
|
|
138
|
+
|
|
139
|
+
\`\`\`bash
|
|
140
|
+
curl -X GET "https://sandbox.example.com/v1/health" \\
|
|
141
|
+
-H "Authorization: Bearer YOUR_API_TOKEN"
|
|
142
|
+
\`\`\`
|
|
143
|
+
|
|
144
|
+
You will receive a \`200 OK\` response:
|
|
145
|
+
|
|
146
|
+
\`\`\`json
|
|
147
|
+
{
|
|
148
|
+
"status": "healthy",
|
|
149
|
+
"timestamp": "2026-10-18T12:00:00Z"
|
|
150
|
+
}
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
## Next Steps
|
|
154
|
+
|
|
155
|
+
Explore the interactive API console or review our [Architecture Overview](/docs/guides/overview).
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const STARTER_OVERVIEW = (withStarterSpec) => `---
|
|
159
|
+
title: Architecture Overview
|
|
160
|
+
description: Learn how microservices, caching, and rate limiting work across the platform.
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
# Architecture Overview
|
|
164
|
+
|
|
165
|
+
Our platform is engineered for high throughput, predictable latencies, and distributed fault tolerance.
|
|
166
|
+
|
|
167
|
+
{% callout type="info" title="Zero Downtime Deployments" %}
|
|
168
|
+
All endpoints operate across multiple geographically distributed availability zones.
|
|
169
|
+
{% /callout %}
|
|
170
|
+
|
|
171
|
+
## Implementation Steps
|
|
172
|
+
|
|
173
|
+
{% steps %}
|
|
174
|
+
{% step title="Generate Credentials" %}
|
|
175
|
+
Obtain your API Key from the portal settings and configure it in your environment variables.
|
|
176
|
+
{% /step %}
|
|
177
|
+
|
|
178
|
+
{% step title="Configure Webhooks" %}
|
|
179
|
+
Register a secure HTTPS callback URL to receive real-time event notifications.
|
|
180
|
+
{% /step %}
|
|
181
|
+
|
|
182
|
+
{% step title="Test Endpoints in Sandbox" %}
|
|
183
|
+
Validate schema payloads against the sandbox environment before going live.
|
|
184
|
+
{% /step %}
|
|
185
|
+
{% /steps %}
|
|
186
|
+
${
|
|
187
|
+
withStarterSpec
|
|
188
|
+
? `
|
|
189
|
+
## Try It
|
|
190
|
+
|
|
191
|
+
Any operation from your specs can be embedded live in a guide (\`spec\` is the API's \`id\` in \`rapidoc.config.yaml\`):
|
|
192
|
+
|
|
193
|
+
{% api-operation spec="core" path="/items" method="get" /%}
|
|
194
|
+
`
|
|
195
|
+
: ""
|
|
196
|
+
}
|
|
197
|
+
## Code Examples
|
|
198
|
+
|
|
199
|
+
{% tabs %}
|
|
200
|
+
{% tab name="JavaScript" %}
|
|
201
|
+
\`\`\`javascript
|
|
202
|
+
const res = await fetch('https://api.example.com/v1/items', {
|
|
203
|
+
headers: { Authorization: 'Bearer YOUR_TOKEN' }
|
|
204
|
+
});
|
|
205
|
+
const items = await res.json();
|
|
206
|
+
\`\`\`
|
|
207
|
+
{% /tab %}
|
|
208
|
+
|
|
209
|
+
{% tab name="Python" %}
|
|
210
|
+
\`\`\`python
|
|
211
|
+
import requests
|
|
212
|
+
|
|
213
|
+
res = requests.get('https://api.example.com/v1/items', headers={
|
|
214
|
+
'Authorization': 'Bearer YOUR_TOKEN'
|
|
215
|
+
})
|
|
216
|
+
items = res.json()
|
|
217
|
+
\`\`\`
|
|
218
|
+
{% /tab %}
|
|
219
|
+
|
|
220
|
+
{% tab name="cURL" %}
|
|
221
|
+
\`\`\`bash
|
|
222
|
+
curl -H "Authorization: Bearer YOUR_TOKEN" https://api.example.com/v1/items
|
|
223
|
+
\`\`\`
|
|
224
|
+
{% /tab %}
|
|
225
|
+
{% /tabs %}
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
const STARTER_HOME = (title, apiId) => `---
|
|
229
|
+
title: "${title}"
|
|
230
|
+
description: Guides and interactive API reference.
|
|
231
|
+
template: splash
|
|
232
|
+
hero:
|
|
233
|
+
# layout: stacked # split (default): text left, image right; stacked: centred, image below
|
|
234
|
+
tagline: Everything you need to integrate with our APIs, from your first request to production.
|
|
235
|
+
actions:
|
|
236
|
+
- text: Get started
|
|
237
|
+
link: /docs/getting-started/
|
|
238
|
+
icon: right-arrow
|
|
239
|
+
- text: API reference
|
|
240
|
+
link: /apis/${apiId}
|
|
241
|
+
variant: minimal
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
{% card-grid %}
|
|
245
|
+
{% card title="Getting Started" icon="rocket" href="/docs/getting-started/" %}
|
|
246
|
+
Authenticate and make your first request.
|
|
247
|
+
{% /card %}
|
|
248
|
+
{% card title="Architecture Overview" icon="book-open" href="/docs/guides/overview/" %}
|
|
249
|
+
How the platform handles caching, rate limits and failover.
|
|
250
|
+
{% /card %}
|
|
251
|
+
{% /card-grid %}
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const STARTER_CONFIG = (
|
|
255
|
+
title,
|
|
256
|
+
apis,
|
|
257
|
+
) => `# yaml-language-server: $schema=./node_modules/@rapidoc/portal/schema.json
|
|
258
|
+
site:
|
|
259
|
+
title: "${title}"
|
|
260
|
+
description: "Official developer portal and interactive API reference."
|
|
261
|
+
# url: "https://developers.example.com" # canonical links and sitemap; a path (https://example.com/developers) is where the portal is served
|
|
262
|
+
# image: "/social-card.png" # preview image for shared links (needs url)
|
|
263
|
+
banner:
|
|
264
|
+
message: "🎉 Welcome to our brand new developer portal!"
|
|
265
|
+
href: "/docs/getting-started"
|
|
266
|
+
|
|
267
|
+
theme:
|
|
268
|
+
primary: "#f59e0b"
|
|
269
|
+
mode: auto
|
|
270
|
+
# css: "./styles/portal.css" # your stylesheet, loaded after the portal's own
|
|
271
|
+
|
|
272
|
+
# Tags added to every page, e.g. analytics:
|
|
273
|
+
# head:
|
|
274
|
+
# - script: { src: "https://plausible.io/js/script.js", defer: true, data-domain: "example.com" }
|
|
275
|
+
|
|
276
|
+
# Row 1 of the header: links and menus right of the search
|
|
277
|
+
header:
|
|
278
|
+
links:
|
|
279
|
+
- label: "Solutions"
|
|
280
|
+
items:
|
|
281
|
+
- label: "Getting Started"
|
|
282
|
+
icon: "rocket"
|
|
283
|
+
description: "Quickstart guide and developer tutorials"
|
|
284
|
+
href: "/docs/getting-started"
|
|
285
|
+
- label: "${apis[0].label}"
|
|
286
|
+
icon: "lightning"
|
|
287
|
+
description: "Interactive OpenAPI documentation"
|
|
288
|
+
href: "/apis/${apis[0].id}"
|
|
289
|
+
- label: "Get started"
|
|
290
|
+
href: "/docs/getting-started"
|
|
291
|
+
style: primary
|
|
292
|
+
|
|
293
|
+
# Row 2 of the header: one tab per entry; a documentation tab has its own sidebar
|
|
294
|
+
navigation:
|
|
295
|
+
- label: "Documentation"
|
|
296
|
+
icon: "book"
|
|
297
|
+
items:
|
|
298
|
+
- label: "Guides"
|
|
299
|
+
items:
|
|
300
|
+
- getting-started
|
|
301
|
+
- guides/overview
|
|
302
|
+
${apis.map((api) => ` - label: "${api.label}"\n icon: "code"\n api: ${api.id}\n`).join("")} - label: "API Catalog"
|
|
303
|
+
icon: "squares-four"
|
|
304
|
+
catalog: true
|
|
305
|
+
|
|
306
|
+
apis:
|
|
307
|
+
${apis.map((api) => ` - id: ${api.id}\n spec: "${api.spec}"\n`).join("")}# Several versions of one API: \`versions\` instead of \`spec\`. The first is the default, at /apis/<id>;
|
|
308
|
+
# the others are at /apis/<id>/<version>, with a version picker on the reference page.
|
|
309
|
+
# versions:
|
|
310
|
+
# - "./specs/api-v2.yaml"
|
|
311
|
+
# - "./specs/api-v1.yaml"
|
|
312
|
+
|
|
313
|
+
# Arazzo workflow files: drawn by {% workflow file="<id>" /%} in docs pages and exported as MCP
|
|
314
|
+
# tools. A source description named like an API id resolves against that API's spec.
|
|
315
|
+
# workflows:
|
|
316
|
+
# - file: "./workflows/checkout.arazzo.yaml"
|
|
317
|
+
|
|
318
|
+
rapidoc:
|
|
319
|
+
render-style: "focused"
|
|
320
|
+
schema-style: "tree"
|
|
321
|
+
|
|
322
|
+
# Each docs page is also published as Markdown, with /llms.txt for AI tools and "Copy page" buttons.
|
|
323
|
+
# llms: false # turns that off
|
|
324
|
+
|
|
325
|
+
# Optional: sign-in through your OpenID Connect provider and access rules, enforced by
|
|
326
|
+
# \`rapidoc serve\` (npm install @rapidoc/server). Secrets are environment variables of the server:
|
|
327
|
+
# RAPIDOC_SESSION_SECRET and RAPIDOC_AUTH_CLIENT_SECRET.
|
|
328
|
+
# auth:
|
|
329
|
+
# issuer: "https://your-tenant.okta.com/oauth2/default"
|
|
330
|
+
# clientId: "portal-web"
|
|
331
|
+
# tryIt: true # Try in the API reference sends the signed-in reader's access token
|
|
332
|
+
# access:
|
|
333
|
+
# default: public
|
|
334
|
+
# rules:
|
|
335
|
+
# - path: /docs/internal/**
|
|
336
|
+
# allow: signed-in
|
|
337
|
+
`;
|
|
338
|
+
|
|
339
|
+
const STARTER_PACKAGE_JSON = (projectName) =>
|
|
340
|
+
JSON.stringify(
|
|
341
|
+
{
|
|
342
|
+
name: projectName,
|
|
343
|
+
version: "0.1.0",
|
|
344
|
+
private: true,
|
|
345
|
+
type: "module",
|
|
346
|
+
scripts: {
|
|
347
|
+
dev: "rapidoc dev",
|
|
348
|
+
build: "rapidoc build",
|
|
349
|
+
validate: "rapidoc validate",
|
|
350
|
+
preview: "npx serve dist",
|
|
351
|
+
},
|
|
352
|
+
// The CLI loads the portal engine for dev/build; listing it here also makes the config's
|
|
353
|
+
// $schema path (node_modules/@rapidoc/portal/schema.json) resolve.
|
|
354
|
+
devDependencies: {
|
|
355
|
+
"@rapidoc/cli": RAPIDOC_PACKAGES_RANGE,
|
|
356
|
+
"@rapidoc/portal": RAPIDOC_PACKAGES_RANGE,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
null,
|
|
360
|
+
2,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const STARTER_GITIGNORE = `node_modules/
|
|
364
|
+
dist/
|
|
365
|
+
.rapidoc-temp/
|
|
366
|
+
.DS_Store
|
|
367
|
+
`;
|
|
368
|
+
|
|
369
|
+
const STARTER_README = (title) => `# ${title}
|
|
370
|
+
|
|
371
|
+
Welcome to your developer portal, powered by **[@rapidoc/portal](https://rapidocui.com)**.
|
|
372
|
+
|
|
373
|
+
## Quick Start
|
|
374
|
+
|
|
375
|
+
\`\`\`bash
|
|
376
|
+
# 1. Install dependencies
|
|
377
|
+
npm install
|
|
378
|
+
|
|
379
|
+
# 2. Live preview with hot reload
|
|
380
|
+
npm run dev
|
|
381
|
+
|
|
382
|
+
# 3. Build the static portal (SSG) into dist/, then preview it
|
|
383
|
+
npm run build
|
|
384
|
+
npm run preview
|
|
385
|
+
\`\`\`
|
|
386
|
+
|
|
387
|
+
Run \`npm run validate\` (for example in CI) to check every spec in \`apis:\` against the OpenAPI schema. It also warns about operations without an \`operationId\`, whose links change when their summary is edited.
|
|
388
|
+
|
|
389
|
+
## Project Structure
|
|
390
|
+
|
|
391
|
+
\`\`\`text
|
|
392
|
+
├── docs/ # Documentation pages (.md, .markdoc, .mdx); docs/index.* is the home page
|
|
393
|
+
├── specs/ # OpenAPI specifications (.yaml, .json)
|
|
394
|
+
├── public/ # Static files (logo, favicon, images), served from /
|
|
395
|
+
├── rapidoc.config.yaml # Portal header, navigation tabs, theme and APIs
|
|
396
|
+
├── package.json
|
|
397
|
+
└── README.md
|
|
398
|
+
\`\`\`
|
|
399
|
+
`;
|
|
400
|
+
|
|
401
|
+
const SPEC_EXTENSIONS = new Set([".yaml", ".yml", ".json"]);
|
|
402
|
+
const SKIPPED_DIRS = new Set(["node_modules", "dist", "build", "coverage", ".rapidoc-temp"]);
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Finds OpenAPI / Swagger documents in a folder (and up to `maxDepth` levels below it), so a portal can be
|
|
406
|
+
* scaffolded around specs that already exist. A file counts when an `openapi:` or `swagger:` key appears
|
|
407
|
+
* in its first few kilobytes. Hidden folders and build output are skipped.
|
|
408
|
+
*
|
|
409
|
+
* @param {string} dir - Folder to search
|
|
410
|
+
* @param {object} [options] - { maxDepth = 3 }
|
|
411
|
+
* @returns {Array<string>} Spec paths relative to `dir`, with forward slashes, sorted
|
|
412
|
+
*/
|
|
413
|
+
export function findOpenApiSpecs(dir, { maxDepth = 3 } = {}) {
|
|
414
|
+
const root = path.resolve(dir);
|
|
415
|
+
const found = [];
|
|
416
|
+
const walk = (current, depth) => {
|
|
417
|
+
let entries;
|
|
418
|
+
try {
|
|
419
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
420
|
+
} catch {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
for (const entry of entries) {
|
|
424
|
+
if (entry.name.startsWith(".")) continue;
|
|
425
|
+
const fullPath = path.join(current, entry.name);
|
|
426
|
+
if (entry.isDirectory()) {
|
|
427
|
+
if (depth < maxDepth && !SKIPPED_DIRS.has(entry.name)) walk(fullPath, depth + 1);
|
|
428
|
+
} else if (
|
|
429
|
+
SPEC_EXTENSIONS.has(path.extname(entry.name).toLowerCase()) &&
|
|
430
|
+
isOpenApiDocument(fullPath)
|
|
431
|
+
) {
|
|
432
|
+
found.push(path.relative(root, fullPath).split(path.sep).join("/"));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
walk(root, 0);
|
|
437
|
+
return found.sort();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function isOpenApiDocument(filePath) {
|
|
441
|
+
const fd = fs.openSync(filePath, "r");
|
|
442
|
+
try {
|
|
443
|
+
const buffer = Buffer.alloc(4096);
|
|
444
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
445
|
+
return /^\s*["']?(openapi|swagger)["']?\s*:/m.test(buffer.toString("utf8", 0, bytesRead));
|
|
446
|
+
} finally {
|
|
447
|
+
fs.closeSync(fd);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// A valid npm package name from a folder name: lowercase, URL-safe, not starting with . or _
|
|
452
|
+
// ("RapiDocPortal" -> "rapidocportal", "My Portal" -> "my-portal").
|
|
453
|
+
function toPackageName(name) {
|
|
454
|
+
const cleaned = name
|
|
455
|
+
.toLowerCase()
|
|
456
|
+
.replace(/[^a-z0-9._~-]+/g, "-")
|
|
457
|
+
.replace(/^[._-]+|-+$/g, "")
|
|
458
|
+
.slice(0, 214);
|
|
459
|
+
return cleaned || "my-dev-portal";
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// API ids and tab labels from spec file names: "specs/orders-api.yaml" -> { id: 'orders-api', label: 'Orders API' }
|
|
463
|
+
function apisFromSpecs(specs) {
|
|
464
|
+
const used = new Set();
|
|
465
|
+
return specs.map((spec) => {
|
|
466
|
+
const base =
|
|
467
|
+
path
|
|
468
|
+
.basename(spec, path.extname(spec))
|
|
469
|
+
.toLowerCase()
|
|
470
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
471
|
+
.replace(/^-|-$/g, "") || "api";
|
|
472
|
+
let id = base;
|
|
473
|
+
for (let n = 2; used.has(id); n += 1) id = `${base}-${n}`;
|
|
474
|
+
used.add(id);
|
|
475
|
+
const label = base
|
|
476
|
+
.replace(/-/g, " ")
|
|
477
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
478
|
+
.replace(/\bApi\b/g, "API");
|
|
479
|
+
return { id, label, spec: `./${spec}` };
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** The site title a folder gets when none is given: `acme-docs` → "Acme Docs". */
|
|
484
|
+
export function defaultSiteTitle(targetDir, name) {
|
|
485
|
+
const folderName = name || path.basename(path.resolve(targetDir)) || "my-dev-portal";
|
|
486
|
+
return folderName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* What scaffoldProject would write, without writing anything: { projectName, siteTitle, apis, files },
|
|
491
|
+
* where files are { path, content }. `init` uses it to list the files --force would overwrite.
|
|
492
|
+
*/
|
|
493
|
+
export function planScaffold(targetDir, options = {}) {
|
|
494
|
+
const resolvedDir = path.resolve(targetDir);
|
|
495
|
+
const folderName = options.name || path.basename(resolvedDir) || "my-dev-portal";
|
|
496
|
+
const projectName = toPackageName(folderName);
|
|
497
|
+
const siteTitle = options.title || defaultSiteTitle(resolvedDir, options.name);
|
|
498
|
+
const withStarterSpec = !options.specs?.length;
|
|
499
|
+
const apis = withStarterSpec
|
|
500
|
+
? [{ id: "core", label: "Core API", spec: "./specs/api.yaml" }]
|
|
501
|
+
: apisFromSpecs(options.specs);
|
|
502
|
+
|
|
503
|
+
const docsDir = path.join(resolvedDir, "docs");
|
|
504
|
+
const guidesDir = path.join(docsDir, "guides");
|
|
505
|
+
const specsDir = path.join(resolvedDir, "specs");
|
|
506
|
+
|
|
507
|
+
const files = [
|
|
508
|
+
{
|
|
509
|
+
path: path.join(resolvedDir, "rapidoc.config.yaml"),
|
|
510
|
+
content: STARTER_CONFIG(siteTitle, apis),
|
|
511
|
+
},
|
|
512
|
+
{ path: path.join(resolvedDir, "package.json"), content: STARTER_PACKAGE_JSON(projectName) },
|
|
513
|
+
{ path: path.join(resolvedDir, ".gitignore"), content: STARTER_GITIGNORE },
|
|
514
|
+
{ path: path.join(resolvedDir, "README.md"), content: STARTER_README(siteTitle) },
|
|
515
|
+
...(withStarterSpec ? [{ path: path.join(specsDir, "api.yaml"), content: STARTER_SPEC }] : []),
|
|
516
|
+
{ path: path.join(docsDir, "index.markdoc"), content: STARTER_HOME(siteTitle, apis[0].id) },
|
|
517
|
+
{ path: path.join(docsDir, "getting-started.md"), content: STARTER_GETTING_STARTED },
|
|
518
|
+
{ path: path.join(guidesDir, "overview.markdoc"), content: STARTER_OVERVIEW(withStarterSpec) },
|
|
519
|
+
];
|
|
520
|
+
return { targetDir: resolvedDir, projectName, siteTitle, apis, files };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Scaffolds a new RapiDoc Developer Portal into target directory. Existing files are left alone unless
|
|
525
|
+
* `overwrite` is set (they are listed in `filesSkipped`), so it can also add a portal to an existing repo.
|
|
526
|
+
*
|
|
527
|
+
* @param {string} targetDir - Destination directory path
|
|
528
|
+
* @param {object} [options] - { name, title, overwrite, specs }. `specs` lists OpenAPI files (relative to
|
|
529
|
+
* targetDir) to use instead of the starter spec; see findOpenApiSpecs.
|
|
530
|
+
* @returns {{ targetDir: string, projectName: string, siteTitle: string, apis: Array<object>,
|
|
531
|
+
* filesCreated: Array<string>, filesSkipped: Array<string> }}
|
|
532
|
+
*/
|
|
533
|
+
export function scaffoldProject(targetDir, options = {}) {
|
|
534
|
+
const {
|
|
535
|
+
targetDir: resolvedDir,
|
|
536
|
+
projectName,
|
|
537
|
+
siteTitle,
|
|
538
|
+
apis,
|
|
539
|
+
files,
|
|
540
|
+
} = planScaffold(targetDir, options);
|
|
541
|
+
for (const file of files) fs.mkdirSync(path.dirname(file.path), { recursive: true });
|
|
542
|
+
|
|
543
|
+
const filesCreated = [];
|
|
544
|
+
const filesSkipped = [];
|
|
545
|
+
for (const file of files) {
|
|
546
|
+
if (!fs.existsSync(file.path) || options.overwrite) {
|
|
547
|
+
fs.writeFileSync(file.path, file.content, "utf8");
|
|
548
|
+
filesCreated.push(file.path);
|
|
549
|
+
} else {
|
|
550
|
+
filesSkipped.push(file.path);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
targetDir: resolvedDir,
|
|
556
|
+
projectName,
|
|
557
|
+
siteTitle,
|
|
558
|
+
apis,
|
|
559
|
+
filesCreated,
|
|
560
|
+
filesSkipped,
|
|
561
|
+
};
|
|
562
|
+
}
|
package/src/wizard.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { defaultSiteTitle } from "./scaffold.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The questions `rapidoc init` and `npm create rapidoc` ask in a terminal, for whatever the command line
|
|
5
|
+
* didn't already say. Each one is also an option (--title, --spec / --starter-spec, --install /
|
|
6
|
+
* --no-install), so a run can be repeated without questions.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} args
|
|
9
|
+
* @param {{ text: Function, multiselect: Function, confirm: Function }} args.ask - prompt functions
|
|
10
|
+
* (@clack/prompts' signatures): the CLI passes its lib/prompt.js, create-rapidoc its own.
|
|
11
|
+
* @param {string} args.targetDir - The portal folder.
|
|
12
|
+
* @param {string[]} args.foundSpecs - OpenAPI files found there (findOpenApiSpecs), relative to it.
|
|
13
|
+
* @param {{ title?: string, specs?: string[], install?: boolean }} args.known - Answers the command line
|
|
14
|
+
* already gave; `undefined` means ask. `specs: []` means the starter spec.
|
|
15
|
+
* @param {boolean} [args.askInstall=true] - Whether to ask about `npm install` (not when the folder
|
|
16
|
+
* already has a package.json, which the scaffold leaves alone).
|
|
17
|
+
* @returns {Promise<{ title?: string, specs?: string[], install?: boolean }>}
|
|
18
|
+
*/
|
|
19
|
+
export async function askInitQuestions({ ask, targetDir, foundSpecs, known, askInstall = true }) {
|
|
20
|
+
const answers = { ...known };
|
|
21
|
+
if (answers.title === undefined) {
|
|
22
|
+
const fallback = defaultSiteTitle(targetDir);
|
|
23
|
+
answers.title = await ask.text({
|
|
24
|
+
message: "Site title",
|
|
25
|
+
placeholder: fallback,
|
|
26
|
+
defaultValue: fallback,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
if (answers.specs === undefined && foundSpecs.length) {
|
|
30
|
+
answers.specs = await ask.multiselect({
|
|
31
|
+
message: `OpenAPI files to include (${foundSpecs.length} found; none selected = a starter spec)`,
|
|
32
|
+
options: foundSpecs.map((spec) => ({ value: spec, label: spec })),
|
|
33
|
+
initialValues: foundSpecs,
|
|
34
|
+
required: false,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (askInstall && answers.install === undefined) {
|
|
38
|
+
answers.install = await ask.confirm({
|
|
39
|
+
message: "Install the dependencies with npm now?",
|
|
40
|
+
initialValue: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return answers;
|
|
44
|
+
}
|