create-rapidoc 0.0.0-stage → 0.1.0

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