@brandon_m_behring/book-scaffold-astro 4.14.3 → 4.15.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/components/CodeBlock.astro +20 -7
- package/components/CodeRef.astro +15 -2
- package/components/PocLayout.astro +10 -2
- package/components/Practice.astro +11 -2
- package/components/StatusBadge.astro +18 -9
- package/dist/index.d.ts +71 -3
- package/dist/index.mjs +92 -3
- package/dist/schemas.d.ts +1 -1
- package/package.json +1 -1
- package/src/lib/assert-prop.ts +30 -0
- package/src/lib/repo-url.ts +82 -17
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* build time, with syntax highlighting and a "View on GitHub" header
|
|
5
5
|
* link pointing to the exact line range.
|
|
6
6
|
*
|
|
7
|
-
* Reads the file from the repo root (
|
|
8
|
-
*
|
|
9
|
-
* to Astro's `<Code>`
|
|
7
|
+
* Reads the file from the code repo root (the consumer's project root by
|
|
8
|
+
* default; set BOOK_REPO_ROOT for a monorepo subdir layout), slices `lines`
|
|
9
|
+
* ("N-M" or "N"), and hands the snippet to Astro's `<Code>` for Shiki.
|
|
10
10
|
*
|
|
11
11
|
* Build fails if:
|
|
12
12
|
* - The file does not exist on disk
|
|
@@ -28,6 +28,7 @@ import { Code } from 'astro:components';
|
|
|
28
28
|
import { readFileSync, existsSync } from 'node:fs';
|
|
29
29
|
import { resolve } from 'node:path';
|
|
30
30
|
import { buildGithubUrl } from '../src/lib/repo-url';
|
|
31
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
31
32
|
|
|
32
33
|
interface Props {
|
|
33
34
|
src: string;
|
|
@@ -37,11 +38,15 @@ interface Props {
|
|
|
37
38
|
|
|
38
39
|
const { src, lines, lang } = Astro.props;
|
|
39
40
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
41
|
+
// #109: resolve src relative to the code repo root. Defaults to the consumer's
|
|
42
|
+
// project root (process.cwd() during astro build — book + code in one repo);
|
|
43
|
+
// set BOOK_REPO_ROOT when the code lives elsewhere (e.g. a monorepo where the
|
|
44
|
+
// book is a subdir, the old guides/web/ → '../..' case). Using an env + cwd
|
|
42
45
|
// (not import.meta.dirname) survives Astro's bundling — at runtime
|
|
43
46
|
// import.meta.dirname points to the emitted .mjs chunk, not the source.
|
|
44
|
-
const REPO_ROOT =
|
|
47
|
+
const REPO_ROOT = process.env.BOOK_REPO_ROOT
|
|
48
|
+
? resolve(process.env.BOOK_REPO_ROOT)
|
|
49
|
+
: process.cwd();
|
|
45
50
|
const absPath = resolve(REPO_ROOT, src);
|
|
46
51
|
|
|
47
52
|
if (!existsSync(absPath)) {
|
|
@@ -102,7 +107,15 @@ const inferLang = (path: string): string => {
|
|
|
102
107
|
|
|
103
108
|
const language = (lang ?? inferLang(src)) as Parameters<typeof Code>[0]['lang'];
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
// #109: fail loud rather than link the wrong repo (see CodeRef for the rationale).
|
|
111
|
+
if (!bookConfig.githubRepo) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`<CodeBlock src="${src}">: no GitHub repo resolved. Set githubRepo in ` +
|
|
114
|
+
`defineBookConfig(), or add a "repository" field to package.json (or an ` +
|
|
115
|
+
`origin git remote) so the scaffold can auto-detect owner/repo.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const githubUrl = buildGithubUrl(bookConfig.githubRepo, bookConfig.githubBranch, src, startLine, endLine);
|
|
106
119
|
const rangeText = startLine === endLine ? `:${startLine}` : `:${startLine}-${endLine}`;
|
|
107
120
|
const displayPath = `${src}${rangeText}`;
|
|
108
121
|
---
|
package/components/CodeRef.astro
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
3
|
* CodeRef — inline reference to a specific file (and optional line range)
|
|
4
|
-
* in the
|
|
4
|
+
* in the book's GitHub repo (resolved per book — #109: defineBookConfig
|
|
5
|
+
* `githubRepo`, else auto-detected from package.json / git remote).
|
|
5
6
|
*
|
|
6
7
|
* Maps the LaTeX `\code{path:N}` and `\path{path}` idioms (which only
|
|
7
8
|
* styled monospace text) to a real hyperlink that lands the reader on
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
* synthesized.
|
|
27
28
|
*/
|
|
28
29
|
import { buildGithubUrl } from '../src/lib/repo-url';
|
|
30
|
+
import bookConfig from 'virtual:book-scaffold/book-config';
|
|
29
31
|
|
|
30
32
|
interface Props {
|
|
31
33
|
path: string;
|
|
@@ -34,7 +36,18 @@ interface Props {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const { path, line, lineEnd } = Astro.props;
|
|
37
|
-
|
|
39
|
+
|
|
40
|
+
// #109: fail loud rather than link to the wrong repo. `githubRepo` is null
|
|
41
|
+
// only when neither defineBookConfig({ githubRepo }) nor auto-detection
|
|
42
|
+
// (package.json repository / git remote) resolved one.
|
|
43
|
+
if (!bookConfig.githubRepo) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`<CodeRef path="${path}">: no GitHub repo resolved. Set githubRepo in ` +
|
|
46
|
+
`defineBookConfig(), or add a "repository" field to package.json (or an ` +
|
|
47
|
+
`origin git remote) so the scaffold can auto-detect owner/repo.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const url = buildGithubUrl(bookConfig.githubRepo, bookConfig.githubBranch, path, line, lineEnd);
|
|
38
51
|
const hasSlot = await Astro.slots.has('default');
|
|
39
52
|
const basename = path.split('/').pop() ?? path;
|
|
40
53
|
const defaultText =
|
|
@@ -11,12 +11,20 @@
|
|
|
11
11
|
* Closed `kind` union: discriminated literal type. To add a 6th kind in
|
|
12
12
|
* a future release, expand the union + add a CSS block in poc-layouts.css.
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
import { assertEnumProp } from '../src/lib/assert-prop';
|
|
15
|
+
|
|
16
|
+
const POC_LAYOUT_KINDS = ['tutorial', 'how-to', 'tldr', 'part-summary', 'cheat-sheet'] as const;
|
|
17
|
+
export type PocLayoutKind = (typeof POC_LAYOUT_KINDS)[number];
|
|
15
18
|
|
|
16
19
|
interface Props {
|
|
17
20
|
kind: PocLayoutKind;
|
|
18
21
|
}
|
|
19
|
-
|
|
22
|
+
// #109 sweep: fail loud on an out-of-union kind instead of emitting a
|
|
23
|
+
// poc-layout-<bogus> class that matches no CSS rule.
|
|
24
|
+
const kind = assertEnumProp(Astro.props.kind, POC_LAYOUT_KINDS, {
|
|
25
|
+
component: 'PocLayout',
|
|
26
|
+
prop: 'kind',
|
|
27
|
+
});
|
|
20
28
|
---
|
|
21
29
|
<div class={`poc-layout poc-layout-${kind}`}>
|
|
22
30
|
<slot />
|
|
@@ -11,12 +11,21 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Family: book-genre (cross-profile).
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
import { assertEnumProp } from '../src/lib/assert-prop';
|
|
15
|
+
|
|
16
|
+
const PRACTICE_DIFFICULTIES = ['1', '2', '3', '4'] as const;
|
|
17
|
+
type Difficulty = (typeof PRACTICE_DIFFICULTIES)[number];
|
|
15
18
|
interface Props {
|
|
16
19
|
id: string;
|
|
17
20
|
difficulty: Difficulty;
|
|
18
21
|
}
|
|
19
|
-
const { id
|
|
22
|
+
const { id } = Astro.props;
|
|
23
|
+
// #109 sweep: coerce (tolerates difficulty={3} numeric usage), then fail loud
|
|
24
|
+
// on anything outside 1-4 instead of rendering empty / NaN diamond markers.
|
|
25
|
+
const difficulty = assertEnumProp(String(Astro.props.difficulty), PRACTICE_DIFFICULTIES, {
|
|
26
|
+
component: 'Practice',
|
|
27
|
+
prop: 'difficulty',
|
|
28
|
+
});
|
|
20
29
|
const anchorId = `practice-${id}`;
|
|
21
30
|
const filled = Number.parseInt(difficulty, 10);
|
|
22
31
|
const markers = '◆'.repeat(filled) + '◇'.repeat(4 - filled);
|
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
* Internal frontmatter retains the 7-state value for STATUS.md alignment;
|
|
12
12
|
* this component is the public-facing translator.
|
|
13
13
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
import { assertEnumProp } from '../src/lib/assert-prop';
|
|
15
|
+
|
|
16
|
+
const INTERNAL_STATUSES = [
|
|
17
|
+
'implemented',
|
|
18
|
+
'chapter_only',
|
|
19
|
+
'reading_only',
|
|
20
|
+
'prose_only',
|
|
21
|
+
'code_only',
|
|
22
|
+
'scaffolded',
|
|
23
|
+
'planned',
|
|
24
|
+
] as const;
|
|
25
|
+
type InternalStatus = (typeof INTERNAL_STATUSES)[number];
|
|
22
26
|
|
|
23
27
|
type PublicStatus = 'published' | 'draft' | 'coming-soon';
|
|
24
28
|
|
|
@@ -42,7 +46,12 @@ interface Props {
|
|
|
42
46
|
status: InternalStatus;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
// #109 sweep: fail loud on an unknown status instead of rendering an empty
|
|
50
|
+
// badge label (PUBLIC_OF[undefined] -> undefined -> blank).
|
|
51
|
+
const status = assertEnumProp(Astro.props.status, INTERNAL_STATUSES, {
|
|
52
|
+
component: 'StatusBadge',
|
|
53
|
+
prop: 'status',
|
|
54
|
+
});
|
|
46
55
|
const publicStatus = PUBLIC_OF[status];
|
|
47
56
|
const label = PUBLIC_LABEL[publicStatus];
|
|
48
57
|
---
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration } from 'astro';
|
|
2
|
-
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Y as volatilityLevels, h as ChaptersRenderer, r as academicParts, o as Style } from './types-
|
|
3
|
-
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, m as Provenance, R as ResearchPortfolioChapter, n as RouteToggles, S as StatusBadge, p as StyleInput, T as ToolsChapter, V as VolatilityBadge, q as academicChapterSchema, s as changeKinds, t as changelogSchema, u as chapterStatus, v as citationBackstops, w as composeStyles, x as courseNotesChapterSchema, y as defineProfile, z as defineStyle, D as minimalChapterSchema, E as normalizeFrontmatterConfig, G as patternCategories, H as patternsSchema, I as provenanceObject, J as provenanceSchema, K as researchPortfolioChapterSchema, L as resolvePreset, N as resolveProfile, O as sourceTiers, Q as sourceTiersResearch, U as sourcesSchema, W as toolSlugs, X as toolsChapterSchema } from './types-
|
|
2
|
+
import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Y as volatilityLevels, h as ChaptersRenderer, r as academicParts, o as Style } from './types-CjOseOSG.js';
|
|
3
|
+
export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, m as Provenance, R as ResearchPortfolioChapter, n as RouteToggles, S as StatusBadge, p as StyleInput, T as ToolsChapter, V as VolatilityBadge, q as academicChapterSchema, s as changeKinds, t as changelogSchema, u as chapterStatus, v as citationBackstops, w as composeStyles, x as courseNotesChapterSchema, y as defineProfile, z as defineStyle, D as minimalChapterSchema, E as normalizeFrontmatterConfig, G as patternCategories, H as patternsSchema, I as provenanceObject, J as provenanceSchema, K as researchPortfolioChapterSchema, L as resolvePreset, N as resolveProfile, O as sourceTiers, Q as sourceTiersResearch, U as sourcesSchema, W as toolSlugs, X as toolsChapterSchema } from './types-CjOseOSG.js';
|
|
4
4
|
import 'astro/zod';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -252,6 +252,74 @@ interface ResolvedTheoremLabel {
|
|
|
252
252
|
*/
|
|
253
253
|
declare function theoremLabel(props: TheoremLabelProps): ResolvedTheoremLabel;
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* repo-url — resolve and build GitHub source links for CodeRef / CodeBlock.
|
|
257
|
+
*
|
|
258
|
+
* The repo is no longer hardcoded (#109). `buildGithubUrl` takes an explicit
|
|
259
|
+
* `repo` + `branch`; the integration resolves them per book — from
|
|
260
|
+
* `defineBookConfig({ githubRepo })` (override), else auto-detected from the
|
|
261
|
+
* consumer's own `package.json` `repository` (or git remote) via
|
|
262
|
+
* `parseRepoSlug`. When nothing resolves, the components throw rather than
|
|
263
|
+
* emit links to the wrong repo (the old silent default was
|
|
264
|
+
* `brandon-behring/post_transformers`).
|
|
265
|
+
*/
|
|
266
|
+
/** Default branch when a book configures a repo but no branch. */
|
|
267
|
+
declare const DEFAULT_GITHUB_BRANCH = "main";
|
|
268
|
+
/**
|
|
269
|
+
* Derive an `owner/repo` slug from an npm `repository` field (string or
|
|
270
|
+
* `{ url }`) or a raw git remote URL. Handles https, ssh, the `git+` prefix,
|
|
271
|
+
* a trailing `.git`, and the `github:owner/repo` npm shorthand. Returns null
|
|
272
|
+
* for anything that isn't a recognizable GitHub repo — no silent guess.
|
|
273
|
+
*/
|
|
274
|
+
declare function parseRepoSlug(repository: string | {
|
|
275
|
+
url?: string;
|
|
276
|
+
} | null | undefined): string | null;
|
|
277
|
+
/** Extract the `origin` remote URL from the text of a `.git/config` file. */
|
|
278
|
+
declare function originUrlFromGitConfig(gitConfigText: string): string | null;
|
|
279
|
+
/**
|
|
280
|
+
* Resolve a GitHub `owner/repo` by precedence (#109) — the rule that keeps a
|
|
281
|
+
* book from ever silently linking to the wrong repo:
|
|
282
|
+
* 1. explicit `override` (defineBookConfig `githubRepo`)
|
|
283
|
+
* 2. the consumer's `package.json` `repository`
|
|
284
|
+
* 3. the `origin` remote in `.git/config`
|
|
285
|
+
* 4. null — the components then throw rather than guess.
|
|
286
|
+
*/
|
|
287
|
+
declare function resolveGithubRepo(sources: {
|
|
288
|
+
override?: string | null;
|
|
289
|
+
packageJsonRepository?: string | {
|
|
290
|
+
url?: string;
|
|
291
|
+
} | null;
|
|
292
|
+
gitConfigText?: string | null;
|
|
293
|
+
}): string | null;
|
|
294
|
+
/**
|
|
295
|
+
* Build a GitHub line-anchor URL for an explicit repo + branch.
|
|
296
|
+
* buildGithubUrl('o/r', 'main', 'a/b.py') -> https://github.com/o/r/blob/main/a/b.py
|
|
297
|
+
* buildGithubUrl('o/r', 'main', 'a/b.py', 42) -> …/a/b.py#L42
|
|
298
|
+
* buildGithubUrl('o/r', 'main', 'a/b.py', 42, 58) -> …/a/b.py#L42-L58
|
|
299
|
+
*/
|
|
300
|
+
declare function buildGithubUrl(repo: string, branch: string, path: string, line?: number, lineEnd?: number): string;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* assert-prop — shared fail-loud validator for closed-union component props
|
|
304
|
+
* (#109 / the v4.15.0 parseProps sweep).
|
|
305
|
+
*
|
|
306
|
+
* Astro has no runtime prop validation, so a closed-union prop given an
|
|
307
|
+
* out-of-range value silently produces a broken render — an empty StatusBadge
|
|
308
|
+
* label, a `poc-layout-<bogus>` class that matches no CSS, NaN difficulty
|
|
309
|
+
* markers. This is the same silent-degradation class as the #121 Theorem bug.
|
|
310
|
+
* `assertEnumProp` converts it into a loud, actionable build-time throw.
|
|
311
|
+
*/
|
|
312
|
+
/**
|
|
313
|
+
* Return `value` when it's one of `allowed`; otherwise throw an actionable
|
|
314
|
+
* error naming the component, prop, the offending value, and the legal set.
|
|
315
|
+
* Never returns a fallback — a closed union with no valid value is an authoring
|
|
316
|
+
* error that should stop the build, not render something wrong.
|
|
317
|
+
*/
|
|
318
|
+
declare function assertEnumProp<T extends string>(value: unknown, allowed: readonly T[], ctx: {
|
|
319
|
+
component: string;
|
|
320
|
+
prop: string;
|
|
321
|
+
}): T;
|
|
322
|
+
|
|
255
323
|
/**
|
|
256
324
|
* src/styles/built-in.ts — toolkit-shipped Styles, one per BookPreset (v4.0.0).
|
|
257
325
|
*
|
|
@@ -353,4 +421,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
|
|
|
353
421
|
*/
|
|
354
422
|
declare function defineTips(opts: TipsConfigInput): TipsConfig;
|
|
355
423
|
|
|
356
|
-
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, KIND_LABEL, type ResolvedTheoremLabel, Style, THEOREM_KINDS, type TheoremKind, type TheoremLabelProps, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, bookScaffoldIntegration, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, researchPortfolioStyle, theoremLabel, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
|
424
|
+
export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, KIND_LABEL, type ResolvedTheoremLabel, Style, THEOREM_KINDS, type TheoremKind, type TheoremLabelProps, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, assertEnumProp, bookScaffoldIntegration, buildGithubUrl, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, originUrlFromGitConfig, parseRepoSlug, researchPortfolioStyle, resolveGithubRepo, theoremLabel, toolsChaptersRenderer, toolsStyle, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -788,6 +788,8 @@ function resolveProfile(explicit) {
|
|
|
788
788
|
|
|
789
789
|
// src/integration.ts
|
|
790
790
|
import { fileURLToPath } from "url";
|
|
791
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
792
|
+
import { join } from "path";
|
|
791
793
|
|
|
792
794
|
// src/lib/define-style.ts
|
|
793
795
|
function defineStyle(opts) {
|
|
@@ -855,6 +857,44 @@ function normalizeFrontmatterConfig(v) {
|
|
|
855
857
|
return v;
|
|
856
858
|
}
|
|
857
859
|
|
|
860
|
+
// src/lib/repo-url.ts
|
|
861
|
+
var DEFAULT_GITHUB_BRANCH = "main";
|
|
862
|
+
function parseRepoSlug(repository) {
|
|
863
|
+
const raw = typeof repository === "string" ? repository : repository && typeof repository === "object" ? repository.url : void 0;
|
|
864
|
+
if (!raw || typeof raw !== "string") return null;
|
|
865
|
+
const s = raw.trim();
|
|
866
|
+
if (s === "") return null;
|
|
867
|
+
const shorthand = s.match(/^github:([\w.-]+)\/([\w.-]+?)(?:\.git)?$/i);
|
|
868
|
+
if (shorthand) return `${shorthand[1]}/${shorthand[2]}`;
|
|
869
|
+
const m = s.replace(/^git\+/, "").match(/github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:[/#?].*)?$/i);
|
|
870
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
871
|
+
}
|
|
872
|
+
function originUrlFromGitConfig(gitConfigText) {
|
|
873
|
+
const m = gitConfigText.match(/\[remote "origin"\][^[]*?url\s*=\s*(\S+)/);
|
|
874
|
+
return m ? m[1] : null;
|
|
875
|
+
}
|
|
876
|
+
function resolveGithubRepo(sources) {
|
|
877
|
+
if (sources.override) return sources.override;
|
|
878
|
+
const fromPkg = parseRepoSlug(sources.packageJsonRepository ?? null);
|
|
879
|
+
if (fromPkg) return fromPkg;
|
|
880
|
+
if (sources.gitConfigText) {
|
|
881
|
+
const fromGit = parseRepoSlug(originUrlFromGitConfig(sources.gitConfigText));
|
|
882
|
+
if (fromGit) return fromGit;
|
|
883
|
+
}
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
function buildGithubUrl(repo, branch, path, line, lineEnd) {
|
|
887
|
+
const cleanPath = path.replace(/^\/+/, "");
|
|
888
|
+
let url = `https://github.com/${repo}/blob/${branch}/${cleanPath}`;
|
|
889
|
+
if (line !== void 0) {
|
|
890
|
+
url += `#L${line}`;
|
|
891
|
+
if (lineEnd !== void 0 && lineEnd !== line) {
|
|
892
|
+
url += `-L${lineEnd}`;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return url;
|
|
896
|
+
}
|
|
897
|
+
|
|
858
898
|
// src/mdx-components-resolver.ts
|
|
859
899
|
import { existsSync as existsSync2 } from "fs";
|
|
860
900
|
import { resolve } from "path";
|
|
@@ -915,6 +955,23 @@ function makeBookConfigVitePlugin(config) {
|
|
|
915
955
|
}
|
|
916
956
|
};
|
|
917
957
|
}
|
|
958
|
+
function resolveBookGithubRepo(override, consumerRoot) {
|
|
959
|
+
let packageJsonRepository = null;
|
|
960
|
+
let gitConfigText = null;
|
|
961
|
+
try {
|
|
962
|
+
packageJsonRepository = JSON.parse(readFileSync2(join(consumerRoot, "package.json"), "utf8")).repository ?? null;
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
gitConfigText = readFileSync2(join(consumerRoot, ".git", "config"), "utf8");
|
|
967
|
+
} catch {
|
|
968
|
+
}
|
|
969
|
+
return resolveGithubRepo({
|
|
970
|
+
override,
|
|
971
|
+
packageJsonRepository,
|
|
972
|
+
gitConfigText
|
|
973
|
+
});
|
|
974
|
+
}
|
|
918
975
|
var PACKAGE_NAME = "@brandon_m_behring/book-scaffold-astro";
|
|
919
976
|
var ROUTE_REGISTRY = {
|
|
920
977
|
references: { pattern: "/references", file: "references.astro" },
|
|
@@ -968,7 +1025,10 @@ function bookScaffoldIntegration(opts) {
|
|
|
968
1025
|
// v4.6.0: book-level author + SEO config, propagated through the
|
|
969
1026
|
// (renamed) book-config virtual module to Base.astro + Chapter.astro.
|
|
970
1027
|
author,
|
|
971
|
-
seo
|
|
1028
|
+
seo,
|
|
1029
|
+
// v4.15.0 (#109): optional GitHub repo/branch override for CodeRef/CodeBlock.
|
|
1030
|
+
githubRepo,
|
|
1031
|
+
githubBranch
|
|
972
1032
|
} = opts;
|
|
973
1033
|
const def = PROFILES[profile];
|
|
974
1034
|
const fmNormalized = normalizeFrontmatterConfig(userOverrides.frontmatter);
|
|
@@ -1009,6 +1069,8 @@ function bookScaffoldIntegration(opts) {
|
|
|
1009
1069
|
}
|
|
1010
1070
|
const consumerRoot = fileURLToPath(config.root);
|
|
1011
1071
|
const resolvedMdxPath = resolveMdxComponentsPath(consumerRoot, mdxComponentsModule);
|
|
1072
|
+
const resolvedGithubRepo = resolveBookGithubRepo(githubRepo, consumerRoot);
|
|
1073
|
+
const resolvedGithubBranch = githubBranch ?? DEFAULT_GITHUB_BRANCH;
|
|
1012
1074
|
const presetLiteral = JSON.stringify(profile);
|
|
1013
1075
|
const enabledRouteNames = Object.entries(enabledRoutes).filter(([, on]) => on).map(([name]) => name);
|
|
1014
1076
|
updateConfig({
|
|
@@ -1024,7 +1086,9 @@ function bookScaffoldIntegration(opts) {
|
|
|
1024
1086
|
seo: {
|
|
1025
1087
|
ogImage: seo?.ogImage ?? null,
|
|
1026
1088
|
twitterHandle: seo?.twitterHandle ?? null
|
|
1027
|
-
}
|
|
1089
|
+
},
|
|
1090
|
+
githubRepo: resolvedGithubRepo,
|
|
1091
|
+
githubBranch: resolvedGithubBranch
|
|
1028
1092
|
})
|
|
1029
1093
|
],
|
|
1030
1094
|
define: {
|
|
@@ -1157,7 +1221,10 @@ async function defineBookConfig(opts) {
|
|
|
1157
1221
|
// Base.astro + Chapter.astro. `seo.sitemap` is NOT passed through —
|
|
1158
1222
|
// it's consumed below at config-time by the @astrojs/sitemap call.
|
|
1159
1223
|
author: opts.author,
|
|
1160
|
-
seo: opts.seo ? { ogImage: opts.seo.ogImage, twitterHandle: opts.seo.twitterHandle } : void 0
|
|
1224
|
+
seo: opts.seo ? { ogImage: opts.seo.ogImage, twitterHandle: opts.seo.twitterHandle } : void 0,
|
|
1225
|
+
// v4.15.0 (#109): repo/branch override; integration auto-detects when undefined.
|
|
1226
|
+
githubRepo: opts.githubRepo,
|
|
1227
|
+
githubBranch: opts.githubBranch
|
|
1161
1228
|
}),
|
|
1162
1229
|
...mergedExtraIntegrations
|
|
1163
1230
|
];
|
|
@@ -1199,6 +1266,9 @@ async function defineBookConfig(opts) {
|
|
|
1199
1266
|
// v4.6.0: strip new book-level SEO opts (author + seo block).
|
|
1200
1267
|
author: _author,
|
|
1201
1268
|
seo: _seo,
|
|
1269
|
+
// v4.15.0: strip repo opts so they don't leak into AstroUserConfig.
|
|
1270
|
+
githubRepo: _githubRepo,
|
|
1271
|
+
githubBranch: _githubBranch,
|
|
1202
1272
|
...rest
|
|
1203
1273
|
} = opts;
|
|
1204
1274
|
void _styles;
|
|
@@ -1215,6 +1285,8 @@ async function defineBookConfig(opts) {
|
|
|
1215
1285
|
void _portfolio;
|
|
1216
1286
|
void _author;
|
|
1217
1287
|
void _seo;
|
|
1288
|
+
void _githubRepo;
|
|
1289
|
+
void _githubBranch;
|
|
1218
1290
|
const katexExternals = wantsKatex ? [] : ["remark-math", "rehype-katex", "katex"];
|
|
1219
1291
|
const restVite = rest.vite ?? {};
|
|
1220
1292
|
const restSsr = restVite.ssr ?? {};
|
|
@@ -1294,6 +1366,17 @@ function theoremLabel(props) {
|
|
|
1294
1366
|
return { kind: raw, fullLabel };
|
|
1295
1367
|
}
|
|
1296
1368
|
|
|
1369
|
+
// src/lib/assert-prop.ts
|
|
1370
|
+
function assertEnumProp(value, allowed, ctx) {
|
|
1371
|
+
if (typeof value === "string" && allowed.includes(value)) {
|
|
1372
|
+
return value;
|
|
1373
|
+
}
|
|
1374
|
+
const got = value === void 0 ? "nothing" : JSON.stringify(value);
|
|
1375
|
+
throw new Error(
|
|
1376
|
+
`<${ctx.component}>: ${ctx.prop}=${got} is not one of ${allowed.join(", ")}.`
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1297
1380
|
// src/styles/built-in.ts
|
|
1298
1381
|
var academicStyle = defineStyle({
|
|
1299
1382
|
name: "academic",
|
|
@@ -1340,6 +1423,7 @@ export {
|
|
|
1340
1423
|
BRANDON_PORTFOLIO_DEFAULT,
|
|
1341
1424
|
BUILTIN_STYLES,
|
|
1342
1425
|
BookConfigError,
|
|
1426
|
+
DEFAULT_GITHUB_BRANCH,
|
|
1343
1427
|
KIND_LABEL,
|
|
1344
1428
|
THEOREM_KINDS,
|
|
1345
1429
|
UNKNOWN_PART_ORDINAL,
|
|
@@ -1350,7 +1434,9 @@ export {
|
|
|
1350
1434
|
academicPartOrdinal,
|
|
1351
1435
|
academicParts,
|
|
1352
1436
|
academicStyle,
|
|
1437
|
+
assertEnumProp,
|
|
1353
1438
|
bookScaffoldIntegration,
|
|
1439
|
+
buildGithubUrl,
|
|
1354
1440
|
changeKinds,
|
|
1355
1441
|
changelogSchema,
|
|
1356
1442
|
chapterSortKey,
|
|
@@ -1370,12 +1456,15 @@ export {
|
|
|
1370
1456
|
minimalChapterSchema,
|
|
1371
1457
|
minimalStyle,
|
|
1372
1458
|
normalizeFrontmatterConfig,
|
|
1459
|
+
originUrlFromGitConfig,
|
|
1460
|
+
parseRepoSlug,
|
|
1373
1461
|
patternCategories,
|
|
1374
1462
|
patternsSchema,
|
|
1375
1463
|
provenanceObject,
|
|
1376
1464
|
provenanceSchema,
|
|
1377
1465
|
researchPortfolioChapterSchema,
|
|
1378
1466
|
researchPortfolioStyle,
|
|
1467
|
+
resolveGithubRepo,
|
|
1379
1468
|
resolvePreset,
|
|
1380
1469
|
resolveProfile,
|
|
1381
1470
|
sourceTiers,
|
package/dist/schemas.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brandon_m_behring/book-scaffold-astro",
|
|
3
3
|
"description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.15.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assert-prop — shared fail-loud validator for closed-union component props
|
|
3
|
+
* (#109 / the v4.15.0 parseProps sweep).
|
|
4
|
+
*
|
|
5
|
+
* Astro has no runtime prop validation, so a closed-union prop given an
|
|
6
|
+
* out-of-range value silently produces a broken render — an empty StatusBadge
|
|
7
|
+
* label, a `poc-layout-<bogus>` class that matches no CSS, NaN difficulty
|
|
8
|
+
* markers. This is the same silent-degradation class as the #121 Theorem bug.
|
|
9
|
+
* `assertEnumProp` converts it into a loud, actionable build-time throw.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Return `value` when it's one of `allowed`; otherwise throw an actionable
|
|
14
|
+
* error naming the component, prop, the offending value, and the legal set.
|
|
15
|
+
* Never returns a fallback — a closed union with no valid value is an authoring
|
|
16
|
+
* error that should stop the build, not render something wrong.
|
|
17
|
+
*/
|
|
18
|
+
export function assertEnumProp<T extends string>(
|
|
19
|
+
value: unknown,
|
|
20
|
+
allowed: readonly T[],
|
|
21
|
+
ctx: { component: string; prop: string },
|
|
22
|
+
): T {
|
|
23
|
+
if (typeof value === 'string' && (allowed as readonly string[]).includes(value)) {
|
|
24
|
+
return value as T;
|
|
25
|
+
}
|
|
26
|
+
const got = value === undefined ? 'nothing' : JSON.stringify(value);
|
|
27
|
+
throw new Error(
|
|
28
|
+
`<${ctx.component}>: ${ctx.prop}=${got} is not one of ${allowed.join(', ')}.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
package/src/lib/repo-url.ts
CHANGED
|
@@ -1,27 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* repo-url — resolve and build GitHub source links for CodeRef / CodeBlock.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* The repo is no longer hardcoded (#109). `buildGithubUrl` takes an explicit
|
|
5
|
+
* `repo` + `branch`; the integration resolves them per book — from
|
|
6
|
+
* `defineBookConfig({ githubRepo })` (override), else auto-detected from the
|
|
7
|
+
* consumer's own `package.json` `repository` (or git remote) via
|
|
8
|
+
* `parseRepoSlug`. When nothing resolves, the components throw rather than
|
|
9
|
+
* emit links to the wrong repo (the old silent default was
|
|
10
|
+
* `brandon-behring/post_transformers`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Default branch when a book configures a repo but no branch. */
|
|
14
|
+
export const DEFAULT_GITHUB_BRANCH = 'main';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Derive an `owner/repo` slug from an npm `repository` field (string or
|
|
18
|
+
* `{ url }`) or a raw git remote URL. Handles https, ssh, the `git+` prefix,
|
|
19
|
+
* a trailing `.git`, and the `github:owner/repo` npm shorthand. Returns null
|
|
20
|
+
* for anything that isn't a recognizable GitHub repo — no silent guess.
|
|
11
21
|
*/
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
export function parseRepoSlug(
|
|
23
|
+
repository: string | { url?: string } | null | undefined,
|
|
24
|
+
): string | null {
|
|
25
|
+
const raw =
|
|
26
|
+
typeof repository === 'string'
|
|
27
|
+
? repository
|
|
28
|
+
: repository && typeof repository === 'object'
|
|
29
|
+
? repository.url
|
|
30
|
+
: undefined;
|
|
31
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
32
|
+
const s = raw.trim();
|
|
33
|
+
if (s === '') return null;
|
|
34
|
+
|
|
35
|
+
// npm shorthand: github:owner/repo
|
|
36
|
+
const shorthand = s.match(/^github:([\w.-]+)\/([\w.-]+?)(?:\.git)?$/i);
|
|
37
|
+
if (shorthand) return `${shorthand[1]}/${shorthand[2]}`;
|
|
38
|
+
|
|
39
|
+
// https://github.com/owner/repo(.git), git+https://…, ssh git@github.com:owner/repo(.git)
|
|
40
|
+
const m = s
|
|
41
|
+
.replace(/^git\+/, '')
|
|
42
|
+
.match(/github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?(?:[/#?].*)?$/i);
|
|
43
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Extract the `origin` remote URL from the text of a `.git/config` file. */
|
|
47
|
+
export function originUrlFromGitConfig(gitConfigText: string): string | null {
|
|
48
|
+
const m = gitConfigText.match(/\[remote "origin"\][^[]*?url\s*=\s*(\S+)/);
|
|
49
|
+
return m ? m[1]! : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve a GitHub `owner/repo` by precedence (#109) — the rule that keeps a
|
|
54
|
+
* book from ever silently linking to the wrong repo:
|
|
55
|
+
* 1. explicit `override` (defineBookConfig `githubRepo`)
|
|
56
|
+
* 2. the consumer's `package.json` `repository`
|
|
57
|
+
* 3. the `origin` remote in `.git/config`
|
|
58
|
+
* 4. null — the components then throw rather than guess.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveGithubRepo(sources: {
|
|
61
|
+
override?: string | null;
|
|
62
|
+
packageJsonRepository?: string | { url?: string } | null;
|
|
63
|
+
gitConfigText?: string | null;
|
|
64
|
+
}): string | null {
|
|
65
|
+
if (sources.override) return sources.override;
|
|
66
|
+
const fromPkg = parseRepoSlug(sources.packageJsonRepository ?? null);
|
|
67
|
+
if (fromPkg) return fromPkg;
|
|
68
|
+
if (sources.gitConfigText) {
|
|
69
|
+
const fromGit = parseRepoSlug(originUrlFromGitConfig(sources.gitConfigText));
|
|
70
|
+
if (fromGit) return fromGit;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
15
74
|
|
|
16
75
|
/**
|
|
17
|
-
* Build a GitHub line-anchor URL.
|
|
18
|
-
* buildGithubUrl('
|
|
19
|
-
* buildGithubUrl('
|
|
20
|
-
* buildGithubUrl('
|
|
76
|
+
* Build a GitHub line-anchor URL for an explicit repo + branch.
|
|
77
|
+
* buildGithubUrl('o/r', 'main', 'a/b.py') -> https://github.com/o/r/blob/main/a/b.py
|
|
78
|
+
* buildGithubUrl('o/r', 'main', 'a/b.py', 42) -> …/a/b.py#L42
|
|
79
|
+
* buildGithubUrl('o/r', 'main', 'a/b.py', 42, 58) -> …/a/b.py#L42-L58
|
|
21
80
|
*/
|
|
22
|
-
export function buildGithubUrl(
|
|
81
|
+
export function buildGithubUrl(
|
|
82
|
+
repo: string,
|
|
83
|
+
branch: string,
|
|
84
|
+
path: string,
|
|
85
|
+
line?: number,
|
|
86
|
+
lineEnd?: number,
|
|
87
|
+
): string {
|
|
23
88
|
const cleanPath = path.replace(/^\/+/, '');
|
|
24
|
-
let url =
|
|
89
|
+
let url = `https://github.com/${repo}/blob/${branch}/${cleanPath}`;
|
|
25
90
|
if (line !== undefined) {
|
|
26
91
|
url += `#L${line}`;
|
|
27
92
|
if (lineEnd !== undefined && lineEnd !== line) {
|