create-thally-docs 0.10.78 → 0.10.80
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/README.md +24 -0
- package/dist/{chunk-VGGE6JIL.js → chunk-DIX4EUI4.js} +33 -354
- package/dist/chunk-J7255UQA.js +34 -0
- package/dist/chunk-KDLP27DS.js +87 -0
- package/dist/chunk-KGKSB3PU.js +222 -0
- package/dist/chunk-L2YCVQBR.js +249 -0
- package/dist/chunk-VGE5L66Z.js +349 -0
- package/dist/{chunk-W5WQ2V7P.js → chunk-WWIATVAD.js} +14 -4
- package/dist/index.js +134 -50
- package/dist/migrate/index.d.ts +3 -1
- package/dist/migrate/index.js +6 -3
- package/dist/migration-worker.d.ts +2 -0
- package/dist/migration-worker.js +37 -0
- package/dist/scaffold.d.ts +6 -0
- package/dist/scaffold.js +4 -2
- package/dist/starter-update.js +2 -1
- package/dist/terminal.d.ts +75 -0
- package/dist/terminal.js +17 -0
- package/package.json +8 -3
- package/dist/chunk-QLMRRNLL.js +0 -162
package/README.md
CHANGED
|
@@ -29,6 +29,30 @@ Dependency installation is deliberately opt-in so the scaffold finishes in
|
|
|
29
29
|
seconds. Pass `--install` to run it immediately, or `--no-install` to skip the
|
|
30
30
|
interactive question explicitly.
|
|
31
31
|
|
|
32
|
+
## Terminal output
|
|
33
|
+
|
|
34
|
+
Interactive terminals show the THALLY wordmark, task headings, and live progress.
|
|
35
|
+
The wordmark adapts to narrow terminals. Output automatically
|
|
36
|
+
uses plain text in CI, when output is not a TTY, with `TERM=dumb`, or when
|
|
37
|
+
`NO_COLOR` is set.
|
|
38
|
+
|
|
39
|
+
Setup and migration end with a highlighted next-action panel. The preview
|
|
40
|
+
command is one copyable line, including `npm install` when needed.
|
|
41
|
+
|
|
42
|
+
Scaffolding and migration keep subprocess logs quiet in interactive terminals.
|
|
43
|
+
Add `--verbose` to see installation and build logs as they happen:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx create-thally-docs my-docs --yes --install --verbose
|
|
47
|
+
npx create-thally-docs migrate https://github.com/your-org/your-docs --platform auto --verbose
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Long-running steps show their current activity and elapsed time. Plain logs report an elapsed heartbeat every 15 seconds. Conversion, rendering, and content validation run off the terminal thread so progress keeps updating during larger imports.
|
|
51
|
+
|
|
52
|
+
Failures always show diagnostics, including without `--verbose`; quiet output retains the last 64 KiB. Non-interactive
|
|
53
|
+
runs retain subprocess output for scripts and CI. These options also work with
|
|
54
|
+
`thally init` and `thally migrate`.
|
|
55
|
+
|
|
32
56
|
## What you get
|
|
33
57
|
|
|
34
58
|
- **MDX content** in `src/content/`, navigation in `docs.json`
|
|
@@ -1,120 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
findPublicSpecs,
|
|
4
|
+
shadowNote,
|
|
5
|
+
specHasHiddenOperations
|
|
6
|
+
} from "./chunk-KDLP27DS.js";
|
|
7
|
+
import {
|
|
5
8
|
readDocsJson,
|
|
6
|
-
scaffold,
|
|
7
9
|
writeDocsJson
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import {
|
|
10
|
-
pruneMissingSiteLinks
|
|
11
|
-
} from "./chunk-4VKRBJV2.js";
|
|
12
|
-
|
|
13
|
-
// src/migrate/index.ts
|
|
14
|
-
import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
|
|
15
|
-
import { tmpdir } from "os";
|
|
16
|
-
import { basename, dirname, isAbsolute, join as join4, relative as relative3, resolve, sep } from "path";
|
|
17
|
-
import {
|
|
18
|
-
cloneGitHubRepository,
|
|
19
|
-
hydrateRemoteApiSpecs,
|
|
20
|
-
migrateRepository,
|
|
21
|
-
migrateUrl,
|
|
22
|
-
parseGitHubRepositoryUrl,
|
|
23
|
-
renderMigrationFiles
|
|
24
|
-
} from "@thallylabs/migrate";
|
|
25
|
-
|
|
26
|
-
// src/public-specs.ts
|
|
27
|
-
import { lstatSync, readFileSync, readdirSync, statSync } from "fs";
|
|
28
|
-
import { join, relative } from "path";
|
|
29
|
-
import { parse as parseYaml } from "yaml";
|
|
30
|
-
var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
31
|
-
var SPEC_EXTENSIONS = /\.(?:json|ya?ml)$/i;
|
|
32
|
-
var MAX_SPEC_BYTES = 1e7;
|
|
33
|
-
var MAX_DEPTH = 8;
|
|
34
|
-
var MAX_ENTRIES = 5e3;
|
|
35
|
-
function shadowNote(urlPath) {
|
|
36
|
-
return urlPath === "openapi.json" || urlPath === "openapi.yaml" ? ` It is also served at /${urlPath} in place of the filtered specification, so visitors get this raw file there.` : "";
|
|
37
|
-
}
|
|
38
|
-
var isFlagged = (value) => value === true || value === "true";
|
|
39
|
-
function specHasHiddenOperations(spec, overrides) {
|
|
40
|
-
if (overrides && typeof overrides === "object" && Object.values(overrides).some((o) => o?.hidden === true)) return true;
|
|
41
|
-
const components = spec.components;
|
|
42
|
-
for (const group of [spec.paths, spec.webhooks, spec["x-webhooks"], components?.pathItems]) {
|
|
43
|
-
if (!group || typeof group !== "object") continue;
|
|
44
|
-
for (const item of Object.values(group)) {
|
|
45
|
-
if (!item || typeof item !== "object") continue;
|
|
46
|
-
const record = item;
|
|
47
|
-
if (isFlagged(record["x-excluded"]) || isFlagged(record["x-hidden"])) return true;
|
|
48
|
-
for (const method of OPENAPI_METHODS) {
|
|
49
|
-
const op = record[method];
|
|
50
|
-
if (op && typeof op === "object" && (isFlagged(op["x-excluded"]) || isFlagged(op["x-hidden"]))) return true;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
function findPublicSpecs(projectDir) {
|
|
57
|
-
const publicDir = join(projectDir, "public");
|
|
58
|
-
const scan = { specs: [], skipped: [] };
|
|
59
|
-
let seen = 0;
|
|
60
|
-
const walk = (dir, depth) => {
|
|
61
|
-
let entries;
|
|
62
|
-
try {
|
|
63
|
-
entries = readdirSync(dir).sort();
|
|
64
|
-
} catch {
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
for (const entry of entries) {
|
|
68
|
-
if (++seen > MAX_ENTRIES) return;
|
|
69
|
-
const full = join(dir, entry);
|
|
70
|
-
let stat;
|
|
71
|
-
try {
|
|
72
|
-
stat = lstatSync(full);
|
|
73
|
-
if (stat.isSymbolicLink()) {
|
|
74
|
-
const target = statSync(full);
|
|
75
|
-
if (!target.isFile()) continue;
|
|
76
|
-
stat = target;
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (stat.isDirectory()) {
|
|
82
|
-
if (depth < MAX_DEPTH && entry !== "node_modules") walk(full, depth + 1);
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (!stat.isFile() || !SPEC_EXTENSIONS.test(entry)) continue;
|
|
86
|
-
const urlPath = relative(publicDir, full).split(/[\\/]/).join("/");
|
|
87
|
-
if (stat.size > MAX_SPEC_BYTES) {
|
|
88
|
-
scan.skipped.push(`public/${urlPath}`);
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
const raw = readFileSync(full, "utf8");
|
|
93
|
-
const parsed = /\.json$/i.test(entry) ? JSON.parse(raw) : parseYaml(raw);
|
|
94
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
95
|
-
const spec = parsed;
|
|
96
|
-
if (typeof spec.openapi !== "string" && typeof spec.swagger !== "string") continue;
|
|
97
|
-
scan.specs.push({ path: `public/${urlPath}`, urlPath, hasHiddenOperations: specHasHiddenOperations(spec) });
|
|
98
|
-
} catch {
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
walk(publicDir, 0);
|
|
103
|
-
return scan;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// src/migrate/validate.ts
|
|
107
|
-
import { spawnSync } from "child_process";
|
|
108
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
109
|
-
import { join as join3 } from "path";
|
|
10
|
+
} from "./chunk-J7255UQA.js";
|
|
110
11
|
|
|
111
12
|
// src/check.ts
|
|
112
|
-
import { existsSync, readFileSync
|
|
113
|
-
import { join
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
14
|
+
import { join, extname, relative } from "path";
|
|
114
15
|
import { execFileSync } from "child_process";
|
|
115
16
|
|
|
116
17
|
// src/frontmatter.ts
|
|
117
|
-
import { parse as
|
|
18
|
+
import { parse as parseYaml } from "yaml";
|
|
118
19
|
function parseFrontmatter(raw) {
|
|
119
20
|
const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
120
21
|
const opening = /^---([^\r\n]*)\r?\n/.exec(source);
|
|
@@ -129,7 +30,7 @@ function parseFrontmatter(raw) {
|
|
|
129
30
|
if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
|
|
130
31
|
return { content, data: {} };
|
|
131
32
|
}
|
|
132
|
-
const parsed =
|
|
33
|
+
const parsed = parseYaml(matter);
|
|
133
34
|
return {
|
|
134
35
|
content,
|
|
135
36
|
data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
|
|
@@ -137,7 +38,7 @@ function parseFrontmatter(raw) {
|
|
|
137
38
|
}
|
|
138
39
|
|
|
139
40
|
// src/check.ts
|
|
140
|
-
import { parse as
|
|
41
|
+
import { parse as parseYaml2 } from "yaml";
|
|
141
42
|
|
|
142
43
|
// src/openapi-publication.ts
|
|
143
44
|
var METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
@@ -398,14 +299,14 @@ function checkDrift(projectDir, file, data, issues) {
|
|
|
398
299
|
function scanMdx(dir, results) {
|
|
399
300
|
let entries;
|
|
400
301
|
try {
|
|
401
|
-
entries =
|
|
302
|
+
entries = readdirSync(dir);
|
|
402
303
|
} catch {
|
|
403
304
|
return;
|
|
404
305
|
}
|
|
405
306
|
for (const entry of entries) {
|
|
406
|
-
const fullPath =
|
|
307
|
+
const fullPath = join(dir, entry);
|
|
407
308
|
try {
|
|
408
|
-
const stat =
|
|
309
|
+
const stat = statSync(fullPath);
|
|
409
310
|
if (stat.isDirectory()) scanMdx(fullPath, results);
|
|
410
311
|
else if (extname(entry).toLowerCase() === ".mdx") results.push(fullPath);
|
|
411
312
|
} catch {
|
|
@@ -621,10 +522,10 @@ function checkUnpublishedOperationPages(projectDir, tabs, pages, issues) {
|
|
|
621
522
|
const source = tab.api.source;
|
|
622
523
|
const overrides = tab.api.overrides;
|
|
623
524
|
if (/^https?:/i.test(source)) return { source, overrides };
|
|
624
|
-
const specPath = source.startsWith("/") ?
|
|
525
|
+
const specPath = source.startsWith("/") ? join(projectDir, "public", source.slice(1)) : join(projectDir, source);
|
|
625
526
|
try {
|
|
626
|
-
const raw =
|
|
627
|
-
return { source, overrides, document: /\.json$/i.test(source) ? JSON.parse(raw) :
|
|
527
|
+
const raw = readFileSync(specPath, "utf8");
|
|
528
|
+
return { source, overrides, document: /\.json$/i.test(source) ? JSON.parse(raw) : parseYaml2(raw) };
|
|
628
529
|
} catch {
|
|
629
530
|
return { source, overrides };
|
|
630
531
|
}
|
|
@@ -643,15 +544,15 @@ function checkUnpublishedOperationPages(projectDir, tabs, pages, issues) {
|
|
|
643
544
|
}
|
|
644
545
|
}
|
|
645
546
|
function validateOpenApi(projectDir, source, issues, overrides) {
|
|
646
|
-
const specPath = source.startsWith("/") ?
|
|
547
|
+
const specPath = source.startsWith("/") ? join(projectDir, "public", source.slice(1)) : join(projectDir, source);
|
|
647
548
|
if (!existsSync(specPath)) {
|
|
648
549
|
issues.push({ severity: "error", message: `API reference points at "${source}" but the file does not exist`, file: source });
|
|
649
550
|
return;
|
|
650
551
|
}
|
|
651
552
|
let spec;
|
|
652
553
|
try {
|
|
653
|
-
const raw =
|
|
654
|
-
spec = source.endsWith(".json") ? JSON.parse(raw) :
|
|
554
|
+
const raw = readFileSync(specPath, "utf8");
|
|
555
|
+
spec = source.endsWith(".json") ? JSON.parse(raw) : parseYaml2(raw);
|
|
655
556
|
} catch (err) {
|
|
656
557
|
issues.push({ severity: "error", message: `OpenAPI spec is not valid ${source.endsWith(".json") ? "JSON" : "YAML"}: ${err.message}`, file: source });
|
|
657
558
|
return;
|
|
@@ -693,13 +594,14 @@ function validateOpenApi(projectDir, source, issues, overrides) {
|
|
|
693
594
|
}
|
|
694
595
|
async function runCheck(projectDir, options) {
|
|
695
596
|
const { fix, ci } = options;
|
|
696
|
-
if (!existsSync(
|
|
697
|
-
console.error(`
|
|
597
|
+
if (!existsSync(join(projectDir, "docs.json"))) {
|
|
598
|
+
if (!options.silent) console.error(`
|
|
698
599
|
\u274C Not a Thally project: docs.json not found in ${projectDir}
|
|
699
600
|
`);
|
|
601
|
+
else options.onIssues?.([{ severity: "error", message: `docs.json not found in ${projectDir}` }]);
|
|
700
602
|
return 1;
|
|
701
603
|
}
|
|
702
|
-
const contentDir =
|
|
604
|
+
const contentDir = join(projectDir, "src", "content");
|
|
703
605
|
const issues = [];
|
|
704
606
|
const config = readDocsJson(projectDir);
|
|
705
607
|
const secondaryLocales = new Set(
|
|
@@ -731,7 +633,7 @@ async function runCheck(projectDir, options) {
|
|
|
731
633
|
issues.push({ severity: "warning", message: `[duplicate] "${dup}" appears more than once in docs.json` });
|
|
732
634
|
}
|
|
733
635
|
for (const pageId of navPageIds) {
|
|
734
|
-
const candidates = [
|
|
636
|
+
const candidates = [join(contentDir, `${pageId}.mdx`), join(contentDir, `${pageId}/index.mdx`)];
|
|
735
637
|
if (!candidates.some((c) => existsSync(c))) {
|
|
736
638
|
issues.push({ severity: "error", message: `"${pageId}" is in docs.json but has no MDX file`, file: `src/content/${pageId}.mdx` });
|
|
737
639
|
}
|
|
@@ -750,13 +652,13 @@ async function runCheck(projectDir, options) {
|
|
|
750
652
|
let content = "";
|
|
751
653
|
let lineOffset = 0;
|
|
752
654
|
try {
|
|
753
|
-
const raw =
|
|
655
|
+
const raw = readFileSync(filePath, "utf8");
|
|
754
656
|
const parsed = parseFrontmatter(raw);
|
|
755
657
|
data = parsed.data;
|
|
756
658
|
content = parsed.content;
|
|
757
659
|
lineOffset = raw.slice(0, raw.indexOf(content)).split("\n").length - 1;
|
|
758
660
|
} catch {
|
|
759
|
-
issues.push({ severity: "error", message: `Could not parse frontmatter`, file:
|
|
661
|
+
issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative(projectDir, filePath) });
|
|
760
662
|
continue;
|
|
761
663
|
}
|
|
762
664
|
const { navPageId } = localizedPage(pageId, secondaryLocales);
|
|
@@ -765,10 +667,10 @@ async function runCheck(projectDir, options) {
|
|
|
765
667
|
addOrphanToNav(projectDir, pageId);
|
|
766
668
|
fixedOrphans.push(pageId);
|
|
767
669
|
} else {
|
|
768
|
-
issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file:
|
|
670
|
+
issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file: relative(projectDir, filePath) });
|
|
769
671
|
}
|
|
770
672
|
}
|
|
771
|
-
const rel2 =
|
|
673
|
+
const rel2 = relative(projectDir, filePath);
|
|
772
674
|
if (!data.title) issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
|
|
773
675
|
if (!data.description) issues.push({ severity: "warning", message: `Missing "description" in frontmatter`, file: rel2 });
|
|
774
676
|
if (typeof data.openapi !== "string" && content.trim().length < 50) {
|
|
@@ -822,7 +724,7 @@ async function runCheck(projectDir, options) {
|
|
|
822
724
|
}
|
|
823
725
|
}
|
|
824
726
|
}
|
|
825
|
-
const publicDir =
|
|
727
|
+
const publicDir = join(projectDir, "public");
|
|
826
728
|
for (const { file, images, offset } of linksByFile) {
|
|
827
729
|
for (const { target, line: contentLine } of images) {
|
|
828
730
|
const line = contentLine + offset;
|
|
@@ -835,7 +737,7 @@ async function runCheck(projectDir, options) {
|
|
|
835
737
|
} catch {
|
|
836
738
|
continue;
|
|
837
739
|
}
|
|
838
|
-
if (!existsSync(
|
|
740
|
+
if (!existsSync(join(publicDir, decoded))) {
|
|
839
741
|
issues.push({ severity: "warning", message: `Image not found: "${target}" has no file under public/`, file, line });
|
|
840
742
|
}
|
|
841
743
|
}
|
|
@@ -848,6 +750,7 @@ async function runCheck(projectDir, options) {
|
|
|
848
750
|
const errors = issues.filter((i) => i.severity === "error");
|
|
849
751
|
options.onIssues?.(issues);
|
|
850
752
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
753
|
+
if (options.silent) return errors.length > 0 ? 1 : 0;
|
|
851
754
|
if (ci) {
|
|
852
755
|
for (const issue of issues) {
|
|
853
756
|
const loc = issue.file ? `file=${issue.file}${issue.line ? `,line=${issue.line}` : ""}` : "";
|
|
@@ -893,231 +796,7 @@ thally check: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
|
893
796
|
return errors.length > 0 ? 1 : 0;
|
|
894
797
|
}
|
|
895
798
|
|
|
896
|
-
// src/migrate/validate.ts
|
|
897
|
-
async function validateMigration(projectDir, skip = false, installationFailed = false) {
|
|
898
|
-
const result = { content: "skipped", build: "skipped", messages: [], diagnostics: [] };
|
|
899
|
-
if (skip) {
|
|
900
|
-
result.messages.push("Validation was explicitly skipped; this import is not verified.");
|
|
901
|
-
return result;
|
|
902
|
-
}
|
|
903
|
-
try {
|
|
904
|
-
result.content = await runCheck(projectDir, { fix: false, ci: true, onIssues: (issues) => {
|
|
905
|
-
result.diagnostics = issues;
|
|
906
|
-
} }) === 0 ? "passed" : "failed";
|
|
907
|
-
if (result.content === "failed") result.messages.push("Content validation failed. Review the check output; source links are not silently rewritten.");
|
|
908
|
-
} catch (error) {
|
|
909
|
-
result.content = "failed";
|
|
910
|
-
result.messages.push(`Content validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
911
|
-
}
|
|
912
|
-
if (installationFailed) {
|
|
913
|
-
result.build = "failed";
|
|
914
|
-
result.messages.push("Dependency installation failed; production build was not attempted. Review the installation output before retrying.");
|
|
915
|
-
return result;
|
|
916
|
-
}
|
|
917
|
-
const packagePath = join3(projectDir, "package.json");
|
|
918
|
-
try {
|
|
919
|
-
const manifest = existsSync2(packagePath) ? JSON.parse(readFileSync3(packagePath, "utf8")) : void 0;
|
|
920
|
-
if (!manifest?.scripts?.build) {
|
|
921
|
-
result.messages.push("No build script is available; production rendering is not verified.");
|
|
922
|
-
return result;
|
|
923
|
-
}
|
|
924
|
-
const build = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build"], {
|
|
925
|
-
cwd: projectDir,
|
|
926
|
-
stdio: "inherit",
|
|
927
|
-
timeout: 10 * 60 * 1e3
|
|
928
|
-
});
|
|
929
|
-
result.build = build.status === 0 && !build.error ? "passed" : "failed";
|
|
930
|
-
if (result.build === "failed") result.messages.push("Production build failed. Review the build output before publishing.");
|
|
931
|
-
} catch (error) {
|
|
932
|
-
result.build = "failed";
|
|
933
|
-
result.messages.push(`Production build failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
934
|
-
}
|
|
935
|
-
return result;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// src/migrate/index.ts
|
|
939
|
-
function projectPath(projectDir, candidate) {
|
|
940
|
-
const target = resolve(projectDir, candidate);
|
|
941
|
-
const fromRoot = relative3(resolve(projectDir), target);
|
|
942
|
-
if (isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${sep}`)) {
|
|
943
|
-
throw new Error(`Generated migration path escapes the project: ${candidate}`);
|
|
944
|
-
}
|
|
945
|
-
return target;
|
|
946
|
-
}
|
|
947
|
-
function ignoreQuarantineDirectory(projectDir) {
|
|
948
|
-
const ignorePath = projectPath(projectDir, ".gitignore");
|
|
949
|
-
const existing = existsSync3(ignorePath) ? readFileSync4(ignorePath, "utf8") : "";
|
|
950
|
-
const rules = existing.split(/\r?\n/).map((line) => line.trim()).filter((line) => /^!?\/?migration-quarantine(?:\/(?:\*\*?)?)?$/.test(line));
|
|
951
|
-
if (rules.length > 0 && !rules[rules.length - 1].startsWith("!")) return;
|
|
952
|
-
const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
953
|
-
writeFileSync(ignorePath, `${existing}${separator}
|
|
954
|
-
# Access-restricted pages withheld by migration: local only, never commit or deploy
|
|
955
|
-
/migration-quarantine/
|
|
956
|
-
`);
|
|
957
|
-
}
|
|
958
|
-
function readExistingConfig(projectDir) {
|
|
959
|
-
const configPath = projectPath(projectDir, "docs.json");
|
|
960
|
-
if (!existsSync3(configPath)) return void 0;
|
|
961
|
-
return JSON.parse(readFileSync4(configPath, "utf8"));
|
|
962
|
-
}
|
|
963
|
-
function resetFreshMigrationContent(projectDir) {
|
|
964
|
-
const contentDirectory = projectPath(projectDir, "src/content");
|
|
965
|
-
rmSync(contentDirectory, { recursive: true, force: true });
|
|
966
|
-
mkdirSync(contentDirectory, { recursive: true });
|
|
967
|
-
for (const sampleSpec of ["openapi.yaml", "openapi.json"]) {
|
|
968
|
-
rmSync(projectPath(projectDir, sampleSpec), { force: true });
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
async function discoverMigration(options) {
|
|
972
|
-
const url = new URL(options.sourceUrl);
|
|
973
|
-
if (url.hostname.toLowerCase() !== "github.com") {
|
|
974
|
-
console.log(` \u{1F310} Discovering public docs at ${url.origin}${url.pathname}...`);
|
|
975
|
-
return migrateUrl({
|
|
976
|
-
sourceUrl: options.sourceUrl,
|
|
977
|
-
platform: options.platform,
|
|
978
|
-
maxPages: options.maxPages,
|
|
979
|
-
fetcher: options.fetcher
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
|
-
const source = parseGitHubRepositoryUrl(options.sourceUrl);
|
|
983
|
-
if (options.branch) source.branch = options.branch;
|
|
984
|
-
const temporaryRoot = mkdtempSync(join4(tmpdir(), "thally-migrate-"));
|
|
985
|
-
const cloneDir = join4(temporaryRoot, "repository");
|
|
986
|
-
console.log(` \u{1F4E6} Cloning ${source.owner}/${source.repo}...`);
|
|
987
|
-
try {
|
|
988
|
-
const cloneWarnings = [];
|
|
989
|
-
await cloneGitHubRepository(source, cloneDir, cloneWarnings);
|
|
990
|
-
const bundle = migrateRepository({
|
|
991
|
-
repositoryDir: cloneDir,
|
|
992
|
-
sourceUrl: options.sourceUrl,
|
|
993
|
-
docsDir: options.docsDir ?? (source.docsDir || void 0),
|
|
994
|
-
platform: options.platform
|
|
995
|
-
});
|
|
996
|
-
const hydrated = await hydrateRemoteApiSpecs(bundle, options.fetcher);
|
|
997
|
-
return cloneWarnings.length > 0 ? { ...hydrated, warnings: [...cloneWarnings, ...hydrated.warnings] } : hydrated;
|
|
998
|
-
} finally {
|
|
999
|
-
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
function publicSpecWarning(projectDir, bundle) {
|
|
1003
|
-
const { specs, skipped } = findPublicSpecs(projectDir);
|
|
1004
|
-
if (specs.length === 0 && skipped.length === 0) return null;
|
|
1005
|
-
const migrated = new Set(bundle.assets.filter((asset) => asset.projectRelative).map((asset) => basename(asset.path)));
|
|
1006
|
-
const lines = [
|
|
1007
|
-
...specs.map((spec) => `${spec.path}${migrated.has(basename(spec.path)) ? " (same file name as a migrated spec)" : ""}${spec.hasHiddenOperations ? " (contains x-excluded/x-hidden operations)" : ""}${shadowNote(spec.urlPath) ? ` (answers /${spec.urlPath} in place of the filtered specification)` : ""}`),
|
|
1008
|
-
...skipped.map((path) => `${path} (too large to inspect)`)
|
|
1009
|
-
];
|
|
1010
|
-
return {
|
|
1011
|
-
code: "unsupported-config",
|
|
1012
|
-
message: `Existing OpenAPI files under public/ are served publicly as-is: ${lines.join("; ")}. Migrated specs are written to openapi/, and re-running migration does not remove old copies from public/. Delete any old copy manually (nothing was deleted), or its hidden operations stay downloadable.`
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
async function migrateDocs(options) {
|
|
1016
|
-
const projectDir = resolve(options.projectDir);
|
|
1017
|
-
const bundle = await discoverMigration(options);
|
|
1018
|
-
if (!options.into) {
|
|
1019
|
-
console.log(`
|
|
1020
|
-
\u{1F3D7} Scaffolding new project at ${projectDir}...`);
|
|
1021
|
-
await scaffold({
|
|
1022
|
-
projectDir,
|
|
1023
|
-
projectName: options.projectName ?? bundle.site?.name ?? "My Docs",
|
|
1024
|
-
description: bundle.site?.description ?? `Documentation migrated from ${new URL(options.sourceUrl).hostname}`,
|
|
1025
|
-
brandPreset: "primary",
|
|
1026
|
-
// Source provenance is not the destination repository. Migrated pages
|
|
1027
|
-
// live at new paths, so source URLs cannot power edit/issue actions.
|
|
1028
|
-
repoUrl: "",
|
|
1029
|
-
doInstall: false,
|
|
1030
|
-
colors: bundle.site?.colors
|
|
1031
|
-
});
|
|
1032
|
-
resetFreshMigrationContent(projectDir);
|
|
1033
|
-
pruneMissingSiteLinks(projectDir, new Set(bundle.pages.map((page) => page.id)));
|
|
1034
|
-
} else if (!existsSync3(projectDir)) {
|
|
1035
|
-
throw new Error(`Project directory "${projectDir}" does not exist. Use without --into to scaffold a new one.`);
|
|
1036
|
-
}
|
|
1037
|
-
if (!options.into) {
|
|
1038
|
-
const starterConfig = readExistingConfig(projectDir);
|
|
1039
|
-
if (starterConfig?.markdown) bundle.docsConfig.markdown ??= starterConfig.markdown;
|
|
1040
|
-
if (!bundle.docsConfig.navbar?.primary) {
|
|
1041
|
-
bundle.docsConfig.navbar = { ...bundle.docsConfig.navbar, primary: null };
|
|
1042
|
-
}
|
|
1043
|
-
bundle.docsConfig.i18n ??= { defaultLocale: "en", locales: [{ code: "en", label: "English" }] };
|
|
1044
|
-
}
|
|
1045
|
-
if (options.into) {
|
|
1046
|
-
const stale = publicSpecWarning(projectDir, bundle);
|
|
1047
|
-
if (stale) bundle.warnings.unshift(stale);
|
|
1048
|
-
}
|
|
1049
|
-
const rendered = renderMigrationFiles(bundle, {
|
|
1050
|
-
existingConfig: options.into ? readExistingConfig(projectDir) : void 0,
|
|
1051
|
-
existingComponentRegistry: existsSync3(projectPath(projectDir, "src/mdx/custom-components.tsx")) ? readFileSync4(projectPath(projectDir, "src/mdx/custom-components.tsx"), "utf8") : void 0
|
|
1052
|
-
});
|
|
1053
|
-
for (const file of rendered) {
|
|
1054
|
-
const destination = projectPath(projectDir, file.path);
|
|
1055
|
-
mkdirSync(dirname(destination), { recursive: true });
|
|
1056
|
-
writeFileSync(destination, file.content);
|
|
1057
|
-
}
|
|
1058
|
-
if (bundle.quarantinedFiles?.length) ignoreQuarantineDirectory(projectDir);
|
|
1059
|
-
const format = (warning) => `${warning.message}${warning.source ? ` (${warning.source})` : ""}`;
|
|
1060
|
-
for (const warning of bundle.warnings.filter((item) => item.code !== "gated-page")) console.warn(` \u26A0 ${format(warning)}`);
|
|
1061
|
-
const gatedWarnings = bundle.warnings.filter((item) => item.code === "gated-page");
|
|
1062
|
-
if (gatedWarnings.length > 0) {
|
|
1063
|
-
console.warn("\n \u{1F512} ACCESS-RESTRICTED CONTENT \u2014 review before publishing");
|
|
1064
|
-
for (const warning of gatedWarnings) console.warn(` \u{1F512} ${format(warning)}`);
|
|
1065
|
-
if (bundle.droppedGatedPages) {
|
|
1066
|
-
console.warn(` \u{1F512} ${bundle.droppedGatedPages} access-restricted page(s) were dropped by the file limit: not published and not saved under migration-quarantine/; recover them from the source repository.`);
|
|
1067
|
-
}
|
|
1068
|
-
if (bundle.quarantinedFiles?.length || gatedWarnings.some((warning) => !/dashboard access settings/i.test(warning.message))) {
|
|
1069
|
-
console.warn(" \u{1F512} Before publishing, review migration-quarantine/assets/ and the dashboard access settings of the source site.");
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
console.log(` \u2713 Imported ${bundle.pages.length} pages and ${bundle.assets.length} assets from ${bundle.platform}.`);
|
|
1073
|
-
let installationFailed = false;
|
|
1074
|
-
if (!options.skipValidation) console.log(" Migration validation runs project code locally, including imported MDX and components.");
|
|
1075
|
-
if (!options.into && !options.skipValidation) {
|
|
1076
|
-
try {
|
|
1077
|
-
installDeps(projectDir);
|
|
1078
|
-
} catch {
|
|
1079
|
-
installationFailed = true;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
console.log("\n Validating imported documentation...");
|
|
1083
|
-
const validation = await validateMigration(projectDir, options.skipValidation, installationFailed);
|
|
1084
|
-
const quarantinedPages = (bundle.quarantinedFiles ?? []).filter((file) => /\.mdx?$/i.test(file.path)).length;
|
|
1085
|
-
const reportPath = projectPath(projectDir, "migration-report.json");
|
|
1086
|
-
writeFileSync(reportPath, `${JSON.stringify({
|
|
1087
|
-
version: 1,
|
|
1088
|
-
sourceUrl: `${new URL(options.sourceUrl).origin}${new URL(options.sourceUrl).pathname}`,
|
|
1089
|
-
platform: bundle.platform,
|
|
1090
|
-
pages: bundle.pages.length,
|
|
1091
|
-
assets: bundle.assets.length,
|
|
1092
|
-
components: bundle.componentFiles?.length ?? 0,
|
|
1093
|
-
quarantined: quarantinedPages,
|
|
1094
|
-
quarantinedAssets: (bundle.quarantinedFiles?.length ?? 0) - quarantinedPages,
|
|
1095
|
-
droppedGatedPages: bundle.droppedGatedPages ?? 0,
|
|
1096
|
-
warnings: bundle.warnings,
|
|
1097
|
-
validation
|
|
1098
|
-
}, null, 2)}
|
|
1099
|
-
`);
|
|
1100
|
-
for (const message of validation.messages) console.warn(` \u26A0 ${message}`);
|
|
1101
|
-
console.log(` Migration report: ${reportPath}`);
|
|
1102
|
-
if (validation.content === "passed" && validation.build === "passed") {
|
|
1103
|
-
console.log(` \u2713 Content and production build passed.${bundle.warnings.length ? " Review the migration warnings for compatibility limitations." : ""}`);
|
|
1104
|
-
} else {
|
|
1105
|
-
console.warn(" Import retained, but validation is incomplete. Do not publish without reviewing the report.");
|
|
1106
|
-
}
|
|
1107
|
-
if (!options.into) initGit(projectDir);
|
|
1108
|
-
return {
|
|
1109
|
-
pagesWritten: bundle.pages.length,
|
|
1110
|
-
assetsWritten: bundle.assets.length,
|
|
1111
|
-
projectDir,
|
|
1112
|
-
platform: bundle.platform,
|
|
1113
|
-
warnings: bundle.warnings,
|
|
1114
|
-
validation,
|
|
1115
|
-
reportPath
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
799
|
export {
|
|
1120
800
|
parseFrontmatter,
|
|
1121
|
-
runCheck
|
|
1122
|
-
migrateDocs
|
|
801
|
+
runCheck
|
|
1123
802
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/docs-json.ts
|
|
4
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
function readDocsJson(projectDir) {
|
|
7
|
+
const docsPath = join(projectDir, "docs.json");
|
|
8
|
+
const raw = readFileSync(docsPath, "utf8");
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
function writeDocsJson(projectDir, config) {
|
|
12
|
+
const docsPath = join(projectDir, "docs.json");
|
|
13
|
+
writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
14
|
+
}
|
|
15
|
+
function resetTrackingConfig(projectDir) {
|
|
16
|
+
const config = readDocsJson(projectDir);
|
|
17
|
+
if (config.tracking) {
|
|
18
|
+
delete config.tracking;
|
|
19
|
+
writeDocsJson(projectDir, config);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function writeTrackingConfig(projectDir, repos) {
|
|
23
|
+
if (repos.length === 0) return;
|
|
24
|
+
const config = readDocsJson(projectDir);
|
|
25
|
+
config.tracking = { repos: repos.map((r) => ({ owner: r.owner, repo: r.repo, branch: "main" })) };
|
|
26
|
+
writeDocsJson(projectDir, config);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
readDocsJson,
|
|
31
|
+
writeDocsJson,
|
|
32
|
+
resetTrackingConfig,
|
|
33
|
+
writeTrackingConfig
|
|
34
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/public-specs.ts
|
|
4
|
+
import { lstatSync, readFileSync, readdirSync, statSync } from "fs";
|
|
5
|
+
import { join, relative } from "path";
|
|
6
|
+
import { parse as parseYaml } from "yaml";
|
|
7
|
+
var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
|
|
8
|
+
var SPEC_EXTENSIONS = /\.(?:json|ya?ml)$/i;
|
|
9
|
+
var MAX_SPEC_BYTES = 1e7;
|
|
10
|
+
var MAX_DEPTH = 8;
|
|
11
|
+
var MAX_ENTRIES = 5e3;
|
|
12
|
+
function shadowNote(urlPath) {
|
|
13
|
+
return urlPath === "openapi.json" || urlPath === "openapi.yaml" ? ` It is also served at /${urlPath} in place of the filtered specification, so visitors get this raw file there.` : "";
|
|
14
|
+
}
|
|
15
|
+
var isFlagged = (value) => value === true || value === "true";
|
|
16
|
+
function specHasHiddenOperations(spec, overrides) {
|
|
17
|
+
if (overrides && typeof overrides === "object" && Object.values(overrides).some((o) => o?.hidden === true)) return true;
|
|
18
|
+
const components = spec.components;
|
|
19
|
+
for (const group of [spec.paths, spec.webhooks, spec["x-webhooks"], components?.pathItems]) {
|
|
20
|
+
if (!group || typeof group !== "object") continue;
|
|
21
|
+
for (const item of Object.values(group)) {
|
|
22
|
+
if (!item || typeof item !== "object") continue;
|
|
23
|
+
const record = item;
|
|
24
|
+
if (isFlagged(record["x-excluded"]) || isFlagged(record["x-hidden"])) return true;
|
|
25
|
+
for (const method of OPENAPI_METHODS) {
|
|
26
|
+
const op = record[method];
|
|
27
|
+
if (op && typeof op === "object" && (isFlagged(op["x-excluded"]) || isFlagged(op["x-hidden"]))) return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
function findPublicSpecs(projectDir) {
|
|
34
|
+
const publicDir = join(projectDir, "public");
|
|
35
|
+
const scan = { specs: [], skipped: [] };
|
|
36
|
+
let seen = 0;
|
|
37
|
+
const walk = (dir, depth) => {
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(dir).sort();
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (++seen > MAX_ENTRIES) return;
|
|
46
|
+
const full = join(dir, entry);
|
|
47
|
+
let stat;
|
|
48
|
+
try {
|
|
49
|
+
stat = lstatSync(full);
|
|
50
|
+
if (stat.isSymbolicLink()) {
|
|
51
|
+
const target = statSync(full);
|
|
52
|
+
if (!target.isFile()) continue;
|
|
53
|
+
stat = target;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (stat.isDirectory()) {
|
|
59
|
+
if (depth < MAX_DEPTH && entry !== "node_modules") walk(full, depth + 1);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!stat.isFile() || !SPEC_EXTENSIONS.test(entry)) continue;
|
|
63
|
+
const urlPath = relative(publicDir, full).split(/[\\/]/).join("/");
|
|
64
|
+
if (stat.size > MAX_SPEC_BYTES) {
|
|
65
|
+
scan.skipped.push(`public/${urlPath}`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const raw = readFileSync(full, "utf8");
|
|
70
|
+
const parsed = /\.json$/i.test(entry) ? JSON.parse(raw) : parseYaml(raw);
|
|
71
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
72
|
+
const spec = parsed;
|
|
73
|
+
if (typeof spec.openapi !== "string" && typeof spec.swagger !== "string") continue;
|
|
74
|
+
scan.specs.push({ path: `public/${urlPath}`, urlPath, hasHiddenOperations: specHasHiddenOperations(spec) });
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
walk(publicDir, 0);
|
|
80
|
+
return scan;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
shadowNote,
|
|
85
|
+
specHasHiddenOperations,
|
|
86
|
+
findPublicSpecs
|
|
87
|
+
};
|