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 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.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
+ }
@@ -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
+ }