mirror of
https://github.com/prdlk/leetcode.git
synced 2026-11-02 13:12:44 +00:00
refactor(cli): generate docs/site.json manifest and update docs site references
This commit is contained in:
1 parent
0b004fcba2
commit
e9d40020f4
3 files changed
+82
-137
No files matched your search
@@ -21,14 +21,14 @@
|
|||||||
* tagged commit — the last main commit before the finish. Problems still open
|
* tagged commit — the last main commit before the finish. Problems still open
|
||||||
* at the finish are left out: the release records what finished, and an open
|
* at the finish are left out: the release records what finished, and an open
|
||||||
* optional problem is tracked by its issue (or its Review day), not here.
|
* optional problem is tracked by its issue (or its Review day), not here.
|
||||||
* apps/docs/lib/releases.ts reads these `### Closed …` tables back for the
|
* The docs site (prdlk/cs `lib/releases.ts`) reads these `### Closed …`
|
||||||
* docs `/changelog` timeline, so their row shape is its contract.
|
* tables back for its `/changelog` timeline, so their row shape is its contract.
|
||||||
*
|
*
|
||||||
* The tag is annotated and backdated (tagger date = finish), so
|
* The tag is annotated and backdated (tagger date = finish), so
|
||||||
* `git tag --sort=taggerdate` reads as the campaign's history even for tags
|
* `git tag --sort=taggerdate` reads as the campaign's history even for tags
|
||||||
* minted by a backfill. The release's own published date cannot be backdated
|
* minted by a backfill. The release's own published date cannot be backdated
|
||||||
* (the API has no field for it), so the docs changelog dates each entry from
|
* (the API has no field for it), so the docs changelog dates each entry from
|
||||||
* this timever tag instead (apps/docs/lib/releases.ts)
|
* this timever tag instead (prdlk/cs `lib/releases.ts`)
|
||||||
* — the tag format is that contract.
|
* — the tag format is that contract.
|
||||||
*
|
*
|
||||||
* Reconciles instead of reacting: every finished topic or review without a
|
* Reconciles instead of reacting: every finished topic or review without a
|
||||||
@@ -59,7 +59,7 @@ const DRY =
|
|||||||
process.env.DRY_RUN === "1" ||
|
process.env.DRY_RUN === "1" ||
|
||||||
process.env.DRY_RUN === "true";
|
process.env.DRY_RUN === "true";
|
||||||
|
|
||||||
/** The docs site (apps/docs/blume.config.ts `deployment.site`); sync publishes `/<num>` short links. */
|
/** The docs site (prdlk/cs `blume.config.ts` `deployment.site`); it publishes sync's `/<num>` short links. */
|
||||||
const DOCS_SITE = "https://lc.prd.lk";
|
const DOCS_SITE = "https://lc.prd.lk";
|
||||||
const TIMEVER = "https://gist.github.com/twolfson/de1b004dd22536b8e668";
|
const TIMEVER = "https://gist.github.com/twolfson/de1b004dd22536b8e668";
|
||||||
|
|
||||||
|
|||||||
+75
-130
@@ -1,6 +1,13 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
/**
|
/**
|
||||||
* Sync work/1 leetcode solutions into apps/docs/content/ pages.
|
* Sync work/1 leetcode solutions into docs/ pages, plus docs/site.json.
|
||||||
|
*
|
||||||
|
* docs/ is content only — MDX pages and one generated JSON manifest, no Blume
|
||||||
|
* config. The docs site itself (config, components, islands, the Appendix)
|
||||||
|
* lives in prdlk/cs and reads docs/ over HTTP with Blume's `mdxRemote()`
|
||||||
|
* source, which fetches `.md`/`.mdx` and nothing else: no `meta.ts`. So the
|
||||||
|
* sidebar meta a folder `meta.ts` used to carry, and the `/<num>` short links,
|
||||||
|
* travel in `docs/site.json` instead, which the site fetches at the same ref.
|
||||||
*
|
*
|
||||||
* - New problems get a full page (frontmatter, warning, examples,
|
* - New problems get a full page (frontmatter, warning, examples,
|
||||||
* constraints, solution) in the gold-standard format.
|
* constraints, solution) in the gold-standard format.
|
||||||
@@ -15,9 +22,9 @@
|
|||||||
* `topicTags` once — when the page is created, or on any later run that
|
* `topicTags` once — when the page is created, or on any later run that
|
||||||
* finds the key missing — and never refreshed after, so a sync with nothing
|
* finds the key missing — and never refreshed after, so a sync with nothing
|
||||||
* new makes no request. Delete the key to refetch it. The page itself
|
* new makes no request. Delete the key to refetch it. The page itself
|
||||||
* renders no badges: the docs MDX `wrapper`
|
* renders no badges: the site's MDX `wrapper` (prdlk/cs
|
||||||
* (`apps/docs/components/ProblemBadges.astro`) turns `topics:` and the
|
* `components/ProblemBadges.astro`) turns `topics:` and the `video:` key
|
||||||
* `video:` key `videos.ts` writes into the badge row under the description.
|
* `videos.ts` writes into the badge row under the description.
|
||||||
* - Every planned page (a problem the README plan lists) carries its GitHub
|
* - Every planned page (a problem the README plan lists) carries its GitHub
|
||||||
* problem issue as frontmatter `issue:` (the URL of the `problem`-labelled
|
* problem issue as frontmatter `issue:` (the URL of the `problem`-labelled
|
||||||
* issue titled `LC <num> · …`), rendered by the same wrapper as a GitHub
|
* issue titled `LC <num> · …`), rendered by the same wrapper as a GitHub
|
||||||
@@ -27,18 +34,11 @@
|
|||||||
* (never on a sync with nothing new); offline, or with no `gh` auth, the
|
* (never on a sync with nothing new); offline, or with no `gh` auth, the
|
||||||
* page is written without it and the next sync retries. Delete the key to
|
* page is written without it and the next sync retries. Delete the key to
|
||||||
* refetch it.
|
* refetch it.
|
||||||
* - Every tag a page carries that no campaign topic teaches (not a key of
|
|
||||||
* `apps/docs/topic-landings.ts`) belongs in the Appendix glossary,
|
|
||||||
* `apps/docs/appendix.ts`, which is hand-written: sync only checks it,
|
|
||||||
* warning about a carried tag with no term (its badge falls back to
|
|
||||||
* LeetCode's list), a term whose tag a topic now teaches, and a term icon
|
|
||||||
* that is missing, a topic's (`TOPIC_ICONS`), or another term's — every
|
|
||||||
* badge linking to the term shows it.
|
|
||||||
*
|
*
|
||||||
* The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`,
|
* The sidebar group is the campaign TOPIC from the README table (`tree-dfs/`).
|
||||||
* ordered by topic number via a script-owned `meta.ts`). A problem outside the
|
* A problem outside the plan (warm-ups) has no topic, so it groups under the
|
||||||
* plan (warm-ups) has no topic, so it groups under the data-structure CATEGORY
|
* data-structure CATEGORY its file sits under in
|
||||||
* its file sits under in `work/1/<Difficulty>/<Category>/` instead.
|
* `work/1/<Difficulty>/<Category>/` instead.
|
||||||
*
|
*
|
||||||
* Topics nest under their README PHASE: `(01-linear-structures)/tree-dfs/`.
|
* Topics nest under their README PHASE: `(01-linear-structures)/tree-dfs/`.
|
||||||
* The phase folder is a Blume `(group)` — a sidebar section adding no route
|
* The phase folder is a Blume `(group)` — a sidebar section adding no route
|
||||||
@@ -49,31 +49,33 @@
|
|||||||
* `tree-dfs/(problems)/226_invert-binary-tree.mdx`: no route segment either,
|
* `tree-dfs/(problems)/226_invert-binary-tree.mdx`: no route segment either,
|
||||||
* and Blume strips the `<num>_` prefix from the route while sorting by it, so
|
* and Blume strips the `<num>_` prefix from the route while sorting by it, so
|
||||||
* the URL is still `/tree-dfs/invert-binary-tree`. Sync never writes or moves
|
* the URL is still `/tree-dfs/invert-binary-tree`. Sync never writes or moves
|
||||||
* an `index.mdx`, and never removes a folder that still holds one. A phase
|
* an `index.mdx`, and never removes a folder that still holds one. An
|
||||||
* folder's `meta.ts` is written once, when missing, then hand-owned (its
|
* unplanned category group has no phase and sits at the docs/ root.
|
||||||
* title is yours to rename); a topic's is script-owned. An unplanned category
|
*
|
||||||
* group has no phase and sits at the content root.
|
* `docs/site.json` is wholly script-owned and rewritten whenever it differs:
|
||||||
|
* - `folders`: Blume folder meta keyed by folder path under docs/ — exactly
|
||||||
|
* the key a `meta.ts` in that folder would have. A phase is `N. Name` in
|
||||||
|
* phase order; a topic is its exact name (the folder slug loses `/` and
|
||||||
|
* case), curriculum order, `TOPIC_ICONS` icon and `display: "page"`, so it
|
||||||
|
* is one sidebar row drilling into its own panel. `(problems)` has none:
|
||||||
|
* Blume's humanized "Problems" is the label.
|
||||||
|
* - `redirects`: one `{ from: "/<num>", to: "/<topic>/<slug>" }` per page, in
|
||||||
|
* number order — exact root-relative paths; Blume prepends the deployment
|
||||||
|
* base itself.
|
||||||
*
|
*
|
||||||
* Only the first-solve bucket is read: work/3 and work/7 hold blind re-solves
|
* Only the first-solve bucket is read: work/3 and work/7 hold blind re-solves
|
||||||
* of problems whose page already exists (see work.ts), and a fresh scaffold
|
* of problems whose page already exists (see work.ts), and a fresh scaffold
|
||||||
* there would otherwise wipe the page's `## Solution`.
|
* there would otherwise wipe the page's `## Solution`.
|
||||||
*
|
|
||||||
* Every page also gets a short link: `/<num>` → `/<topic>/<slug>` (so
|
|
||||||
* `/leetcode/217` opens Contains Duplicate). The rules live in the generated
|
|
||||||
* `apps/docs/redirects.ts`, which `blume.config.ts` imports — Blume takes exact
|
|
||||||
* root-relative paths and prepends the deployment base itself.
|
|
||||||
*/
|
*/
|
||||||
import { mkdir, readdir, rename, rmdir, unlink } from "node:fs/promises";
|
import { mkdir, readdir, rename, rmdir } from "node:fs/promises";
|
||||||
import { basename, dirname, join } from "node:path";
|
import { basename, dirname, join } from "node:path";
|
||||||
|
|
||||||
import { APPENDIX } from "../docs/appendix.ts";
|
|
||||||
import { TOPIC_LANDINGS } from "../docs/topic-landings.ts";
|
|
||||||
import { github } from "./github.ts";
|
import { github } from "./github.ts";
|
||||||
|
|
||||||
const ROOT = join(import.meta.dir, "..", "..");
|
const ROOT = join(import.meta.dir, "..", "..");
|
||||||
const WORK = join(ROOT, "work", "1");
|
const WORK = join(ROOT, "work", "1");
|
||||||
const DOCS = join(ROOT, "apps", "docs", "content");
|
const DOCS = join(ROOT, "docs");
|
||||||
const REDIRECTS = join(ROOT, "apps", "docs", "redirects.ts");
|
const MANIFEST = join(DOCS, "site.json");
|
||||||
|
|
||||||
type Lang = "js" | "py";
|
type Lang = "js" | "py";
|
||||||
|
|
||||||
@@ -327,32 +329,34 @@ function phaseDir(phase: Phase): string {
|
|||||||
return `(${String(phase.order).padStart(2, "0")}-${slugify(phase.name)})`;
|
return `(${String(phase.order).padStart(2, "0")}-${slugify(phase.name)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A sidebar group's Blume folder meta, as `docs/site.json` carries it. */
|
||||||
* Blume `(group)` meta for a phase: the overview page's heading, in phase
|
interface FolderMeta {
|
||||||
* order. Written only when the folder has none — the title is hand-owned after
|
title: string;
|
||||||
* that. Nothing is lost by never rewriting it: the order and name it is built
|
order: number;
|
||||||
* from are also the folder name (`phaseDir`), so a README change to either
|
icon?: string;
|
||||||
* lands in a new folder that gets a fresh meta.
|
display?: "page";
|
||||||
*/
|
|
||||||
function phaseMeta(phase: Phase): string {
|
|
||||||
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ title: ${JSON.stringify(`Phase ${phase.order}: ${phase.name}`)}, order: ${phase.order} });\n`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Blume folder meta for a topic: exact label (the folder slug loses `/` and
|
* Folder meta for a phase `(group)`: the overview page's heading, `N. Name`,
|
||||||
* case), curriculum order, the sidebar icon, and `display: "page"` so the
|
* in phase order. Derived like everything in the manifest, so a README rename
|
||||||
* topic is one row under its phase heading that drills into its own panel
|
* renames the heading — and lands in a new folder (`phaseDir`) anyway.
|
||||||
* (the landing page, then its `(problems)` list). An unplanned category group
|
|
||||||
* (warm-ups) has no README number and sorts at 0, ahead of phase 1, so the
|
|
||||||
* sidebar opens on the basics.
|
|
||||||
*/
|
*/
|
||||||
function groupMeta(topic: Topic): string {
|
function phaseMeta(phase: Phase): FolderMeta {
|
||||||
|
return { title: `${phase.order}. ${phase.name}`, order: phase.order };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Folder meta for a topic: exact label, curriculum order, the sidebar icon,
|
||||||
|
* and `display: "page"` so the topic is one row under its phase heading that
|
||||||
|
* drills into its own panel (the landing page, then its `(problems)` list).
|
||||||
|
* An unplanned category group (warm-ups) has no README number and sorts at 0,
|
||||||
|
* ahead of phase 1, so the sidebar opens on the basics.
|
||||||
|
*/
|
||||||
|
function groupMeta(topic: Topic): FolderMeta {
|
||||||
const icon = TOPIC_ICONS[topic.name];
|
const icon = TOPIC_ICONS[topic.name];
|
||||||
if (!icon) console.warn(`no icon for group ${JSON.stringify(topic.name)}: add it to TOPIC_ICONS`);
|
if (!icon) console.warn(`no icon for group ${JSON.stringify(topic.name)}: add it to TOPIC_ICONS`);
|
||||||
const fields = [`title: ${JSON.stringify(topic.name)}`, `order: ${topic.order ?? 0}`];
|
return { title: topic.name, order: topic.order ?? 0, ...(icon ? { icon } : {}), display: "page" };
|
||||||
if (icon) fields.push(`icon: ${JSON.stringify(icon)}`);
|
|
||||||
fields.push(`display: "page"`);
|
|
||||||
return `import { defineMeta } from "blume";\n\nexport default defineMeta({ ${fields.join(", ")} });\n`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LeetCode topic tags ──────────────────────────────────────────
|
// ── LeetCode topic tags ──────────────────────────────────────────
|
||||||
@@ -399,14 +403,6 @@ function topicsYaml(tags: TopicTag[]): string {
|
|||||||
return ["topics:", ...tags.map((t) => ` - { name: ${JSON.stringify(t.name)}, slug: ${JSON.stringify(t.slug)} }`)].join("\n");
|
return ["topics:", ...tags.map((t) => ` - { name: ${JSON.stringify(t.name)}, slug: ${JSON.stringify(t.slug)} }`)].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The frontmatter's `topics:` as sync wrote them (or a human edited them); [] when absent. */
|
|
||||||
function frontmatterTopics(page: string): TopicTag[] {
|
|
||||||
const fm = page.match(/^---\n([\s\S]*?)\n---/)?.[1];
|
|
||||||
const data: unknown = fm ? Bun.YAML.parse(fm) : undefined;
|
|
||||||
if (!data || typeof data !== "object" || !("topics" in data) || !Array.isArray(data.topics)) return [];
|
|
||||||
return data.topics.filter((t): t is TopicTag => typeof t?.name === "string" && typeof t?.slug === "string");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── GitHub problem issues ────────────────────────────────────────
|
// ── GitHub problem issues ────────────────────────────────────────
|
||||||
|
|
||||||
let issueUrls: Promise<Map<number, string> | undefined> | undefined;
|
let issueUrls: Promise<Map<number, string> | undefined> | undefined;
|
||||||
@@ -701,11 +697,9 @@ for (const rel of (await Array.fromAsync(new Bun.Glob("**/*.mdx").scan(DOCS))).s
|
|||||||
|
|
||||||
const report: [string, string, string][] = [];
|
const report: [string, string, string][] = [];
|
||||||
const ordered = [...solutions.values()].sort((a, b) => a.num - b.num);
|
const ordered = [...solutions.values()].sort((a, b) => a.num - b.num);
|
||||||
const groups = new Map<string, Topic>(); // topic folder -> topic, for meta.ts
|
const folders = new Map<string, FolderMeta>(); // folder under docs/ -> its sidebar meta, for site.json
|
||||||
const phases = new Map<string, Phase>(); // phase folder -> phase, for meta.ts
|
|
||||||
const vacated = new Set<string>(); // folders a page moved out of; removed below if empty
|
const vacated = new Set<string>(); // folders a page moved out of; removed below if empty
|
||||||
const redirects: string[] = []; // `{ from: "/<num>", to: "/<topic>/<slug>" }` rows, numeric order
|
const redirects: { from: string; to: string }[] = []; // one per page, numeric order
|
||||||
const tags = new Map<string, TopicTag>(); // every tag slug any problem page carries -> tag
|
|
||||||
|
|
||||||
for (const sol of ordered) {
|
for (const sol of ordered) {
|
||||||
const langs = Object.keys(sol.code).sort().join("+");
|
const langs = Object.keys(sol.code).sort().join("+");
|
||||||
@@ -715,13 +709,12 @@ for (const sol of ordered) {
|
|||||||
// leetcode-number prefix: numeric sidebar order, prefix stripped from the URL.
|
// leetcode-number prefix: numeric sidebar order, prefix stripped from the URL.
|
||||||
const topic = topics.get(sol.num) ?? topicByName.get(sol.category) ?? { name: sol.category };
|
const topic = topics.get(sol.num) ?? topicByName.get(sol.category) ?? { name: sol.category };
|
||||||
const dir = topicDir(topic);
|
const dir = topicDir(topic);
|
||||||
groups.set(dir, topic);
|
if (!folders.has(dir)) folders.set(dir, groupMeta(topic));
|
||||||
if (topic.phase) phases.set(phaseDir(topic.phase), topic.phase);
|
if (topic.phase) folders.set(phaseDir(topic.phase), phaseMeta(topic.phase));
|
||||||
const name = `${dir}/(problems)/${sol.num}_${sol.slug}.mdx`;
|
const name = `${dir}/(problems)/${sol.num}_${sol.slug}.mdx`;
|
||||||
const existing = pages.get(sol.num);
|
const existing = pages.get(sol.num);
|
||||||
await mkdir(join(DOCS, dirname(name)), { recursive: true });
|
await mkdir(join(DOCS, dirname(name)), { recursive: true });
|
||||||
|
|
||||||
let written: string; // the page as it stands after this run, for its topics
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const page = await Bun.file(join(DOCS, existing)).text();
|
const page = await Bun.file(join(DOCS, existing)).text();
|
||||||
const at = page.indexOf("## Solution");
|
const at = page.indexOf("## Solution");
|
||||||
@@ -743,89 +736,41 @@ for (const sol of ordered) {
|
|||||||
actions.push("updated");
|
actions.push("updated");
|
||||||
}
|
}
|
||||||
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
|
report.push([workRef, name, actions.join(" + ") || "skipped (in sync)"]);
|
||||||
written = next;
|
|
||||||
} else {
|
} else {
|
||||||
const page = renderPage(sol, await fetchTopicTags(sol), await issueUrl(sol));
|
await Bun.write(join(DOCS, name), renderPage(sol, await fetchTopicTags(sol), await issueUrl(sol)));
|
||||||
await Bun.write(join(DOCS, name), page);
|
|
||||||
report.push([workRef, name, "created"]);
|
report.push([workRef, name, "created"]);
|
||||||
written = page;
|
|
||||||
}
|
}
|
||||||
for (const tag of frontmatterTopics(written)) tags.set(tag.slug, tag);
|
// The route drops the phase `(group)`, `(problems)` and the number prefix.
|
||||||
// The route drops the phase `(group)` and the page's number prefix.
|
redirects.push({ from: `/${sol.num}`, to: `/${slugify(topic.name)}/${sol.slug}` });
|
||||||
redirects.push(` { from: "/${sol.num}", to: ${JSON.stringify(`/${slugify(topic.name)}/${sol.slug}`)} },`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Script-owned short links, one per page; imported by blume.config.ts.
|
// The site's one generated input besides the pages; script-owned, rewritten
|
||||||
|
// only when it differs. Folders sorted by path so a diff reads by location.
|
||||||
{
|
{
|
||||||
const module = [
|
const manifest = `${JSON.stringify(
|
||||||
"// Generated by `bun run sync` (apps/cli/sync.ts) — do not edit.",
|
{ folders: Object.fromEntries([...folders].sort(([a], [b]) => (a < b ? -1 : 1))), redirects },
|
||||||
"// `/leetcode/<num>` → that problem's page; exact paths, base added by Blume.",
|
null,
|
||||||
"export default [",
|
2,
|
||||||
...redirects,
|
)}\n`;
|
||||||
"];",
|
if (!(await Bun.file(MANIFEST).exists()) || (await Bun.file(MANIFEST).text()) !== manifest) {
|
||||||
"",
|
await Bun.write(MANIFEST, manifest);
|
||||||
].join("\n");
|
report.push([`${folders.size} folders, ${redirects.length} redirects`, "docs/site.json", "written"]);
|
||||||
if (!(await Bun.file(REDIRECTS).exists()) || (await Bun.file(REDIRECTS).text()) !== module) {
|
|
||||||
await Bun.write(REDIRECTS, module);
|
|
||||||
report.push([`${redirects.length} redirects`, "apps/docs/redirects.ts", "written"]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every carried tag no campaign topic teaches is an Appendix term, filed by
|
// A folder every page left behind is dead: drop it, then the folders above it
|
||||||
// hand in apps/docs/appendix.ts: warn about the gaps and the dead weight. A
|
// that emptied too (`(problems)`, its topic, the phase). A folder that still
|
||||||
// term's icon is also the icon on every problem-page badge linking to its
|
// holds anything (a hand-written index.mdx, another topic) stops the climb.
|
||||||
// card, so it must not be a topic group's (`TOPIC_ICONS`), so a term badge
|
|
||||||
// never reads as a topic's, nor another term's.
|
|
||||||
for (const tag of [...tags.values()].sort((a, b) => a.slug.localeCompare(b.slug))) {
|
|
||||||
if (!TOPIC_LANDINGS[tag.slug] && !APPENDIX[tag.slug])
|
|
||||||
console.warn(`tag ${tag.slug}: no Appendix term; add ${JSON.stringify(tag.name)} to apps/docs/appendix.ts`);
|
|
||||||
}
|
|
||||||
const topicIcons = new Set(Object.values(TOPIC_ICONS));
|
|
||||||
const termIcons = new Map<string, string>(); // icon -> the term that took it first
|
|
||||||
for (const [slug, term] of Object.entries(APPENDIX)) {
|
|
||||||
if (TOPIC_LANDINGS[slug]) console.warn(`appendix ${slug}: a campaign topic teaches it now; delete its term`);
|
|
||||||
if (topicIcons.has(term.icon)) console.warn(`appendix ${slug}: icon ${JSON.stringify(term.icon)} is a topic's (TOPIC_ICONS); pick another`);
|
|
||||||
else if (termIcons.has(term.icon)) console.warn(`appendix ${slug}: icon ${JSON.stringify(term.icon)} is ${termIcons.get(term.icon)}'s too; pick another`);
|
|
||||||
else termIcons.set(term.icon, slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase metas: created when missing (an empty file counts as missing), never
|
|
||||||
// rewritten, so a renamed phase title survives every sync.
|
|
||||||
for (const [dir, phase] of phases) {
|
|
||||||
const file = Bun.file(join(DOCS, dir, "meta.ts"));
|
|
||||||
if ((await file.exists()) && file.size > 0) continue;
|
|
||||||
await Bun.write(file, phaseMeta(phase));
|
|
||||||
report.push([`phase ${phase.order}`, `${dir}/meta.ts`, "created"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Script-owned folder metas — one per live topic folder — rewritten only when
|
|
||||||
// they differ.
|
|
||||||
const metas: [label: string, dir: string, meta: string][] = [...groups].map(([dir, topic]) => [
|
|
||||||
`topic ${topic.order ?? "-"}`,
|
|
||||||
dir,
|
|
||||||
groupMeta(topic),
|
|
||||||
]);
|
|
||||||
for (const [label, dir, meta] of metas) {
|
|
||||||
const path = join(DOCS, dir, "meta.ts");
|
|
||||||
if ((await Bun.file(path).exists()) && (await Bun.file(path).text()) === meta) continue;
|
|
||||||
await Bun.write(path, meta);
|
|
||||||
report.push([label, `${dir}/meta.ts`, "written"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A folder every page left behind is dead: drop its meta and the folder itself,
|
|
||||||
// then the phase folder above it if that emptied too. A folder that still holds
|
|
||||||
// anything else (a hand-written index.mdx, another topic) stops the climb.
|
|
||||||
for (const start of vacated) {
|
for (const start of vacated) {
|
||||||
for (let dir = start; dir !== "."; dir = dirname(dir)) {
|
for (let dir = start; dir !== "."; dir = dirname(dir)) {
|
||||||
if (groups.has(dir) || phases.has(dir)) break;
|
if (folders.has(dir)) break;
|
||||||
let left: string[];
|
let left: string[];
|
||||||
try {
|
try {
|
||||||
left = await readdir(join(DOCS, dir));
|
left = await readdir(join(DOCS, dir));
|
||||||
} catch {
|
} catch {
|
||||||
break; // already removed via a sibling's climb
|
break; // already removed via a sibling's climb
|
||||||
}
|
}
|
||||||
if (left.some((f) => f !== "meta.ts")) break;
|
if (left.length) break;
|
||||||
if (left.length) await unlink(join(DOCS, dir, "meta.ts"));
|
|
||||||
await rmdir(join(DOCS, dir));
|
await rmdir(join(DOCS, dir));
|
||||||
report.push(["", dir, "removed (empty)"]);
|
report.push(["", dir, "removed (empty)"]);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -3,8 +3,8 @@
|
|||||||
* Find the NeetCode explanation video for every docs page that has none.
|
* Find the NeetCode explanation video for every docs page that has none.
|
||||||
*
|
*
|
||||||
* A finished solution page carries frontmatter `video: <YouTube id>` —
|
* A finished solution page carries frontmatter `video: <YouTube id>` —
|
||||||
* NeetCode's walkthrough of the problem — which the docs MDX `wrapper`
|
* NeetCode's walkthrough of the problem — which the site's MDX `wrapper`
|
||||||
* (`apps/docs/components/ProblemBadges.astro`) renders as the red
|
* (prdlk/cs `components/ProblemBadges.astro`) renders as the red
|
||||||
* "Explanation" badge beside the topic badges. `sync.ts` never writes that key
|
* "Explanation" badge beside the topic badges. `sync.ts` never writes that key
|
||||||
* (it owns `icon:`, `topics:` and `## Solution`), so pages accumulate without
|
* (it owns `icon:`, `topics:` and `## Solution`), so pages accumulate without
|
||||||
* one. This script lists every numbered page lacking `video:` and looks each
|
* one. This script lists every numbered page lacking `video:` and looks each
|
||||||
@@ -25,7 +25,7 @@ import { join } from "node:path";
|
|||||||
import { printTable } from "./report.ts";
|
import { printTable } from "./report.ts";
|
||||||
import { ROOT } from "./work.ts";
|
import { ROOT } from "./work.ts";
|
||||||
|
|
||||||
const DOCS = join(ROOT, "apps", "docs", "content");
|
const DOCS = join(ROOT, "docs");
|
||||||
const WRITE = process.argv.includes("--write");
|
const WRITE = process.argv.includes("--write");
|
||||||
|
|
||||||
/** Channel handles searched in order; the first exact match wins. */
|
/** Channel handles searched in order; the first exact match wins. */
|
||||||
|
|||||||
Reference in new issue
Block a user