@theholocron/holocron-plugin-fern 3.43.0 → 3.45.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/README.md CHANGED
@@ -27,6 +27,9 @@ pnpm add -D @theholocron/holocron-plugin-fern
27
27
 
28
28
  // With an explicit basepath
29
29
  "wiki": ["fern", { "domain": "wiki.theholocron.dev/myrepo" }],
30
+
31
+ // When the Fern workspace slug differs from config.org
32
+ "wiki": ["fern", { "domain": "wiki.theholocron.dev", "fernOrg": "holocron" }],
30
33
  },
31
34
  }
32
35
 
@@ -34,20 +37,25 @@ pnpm add -D @theholocron/holocron-plugin-fern
34
37
 
35
38
  ### Options
36
39
 
37
- | Option | Required | Description |
38
- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
39
- | `domain` | No | Base domain (`"wiki.theholocron.dev"`) or full path (`"wiki.theholocron.dev/myrepo"`). When a base domain is given, the repo name is appended automatically as a basepath. |
40
+ | Option | Required | Description |
41
+ | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | `domain` | No | Base domain (`"wiki.theholocron.dev"`) or full path (`"wiki.theholocron.dev/myrepo"`). When a base domain is given, the repo name is appended automatically as a basepath. |
43
+ | `fernOrg` | No | Fern workspace slug. Defaults to `config.org`. Set this when the Fern workspace name differs from the GitHub org (e.g. workspace `"holocron"`, org `"theholocron"`). |
40
44
 
41
45
  ## What `holocron setup` does
42
46
 
43
47
  1. **Writes `fern/fern.config.json`** — Fern workspace org name and pinned
44
48
  CLI version. Always overwritten.
45
- 2. **Scaffolds `fern/docs.yml`** — Instance URL, optional custom domain with
46
- `multi-source: true`, and navigation pointing at `docs/decisions/` and
47
- `docs/engineering/`. Skipped if the file already exists so hand-edited
48
- navigation entries are preserved.
49
-
50
- Both files must be committed the CI workflow reads them at deploy time.
49
+ 2. **Updates `fern/docs.yml`** — On first run, scaffolds the file with
50
+ instance URL, optional custom domain, and navigation stubs. On subsequent
51
+ runs, updates only the `instances:` block (URL, `custom-domain`,
52
+ `multi-source`) while leaving hand-edited navigation, colors, and layout
53
+ untouched.
54
+ 3. **Provisions DNS** When a `domain` is set and a `dns` provider is
55
+ configured, upserts a CNAME record pointing the hostname at
56
+ `<fernOrg>.docs.buildwithfern.com`.
57
+
58
+ Both `fern/` files must be committed — the CI workflow reads them at deploy time.
51
59
 
52
60
  ## Custom domain and multi-source routing
53
61
 
@@ -0,0 +1,59 @@
1
+ import { AuthError, ResolveTokenInput, Wiki, WikiDnsRecord, WikiProvisionOpts } from "@theholocron/cli";
2
+ //#region src/capabilities/wiki.d.ts
3
+ declare const FERN_VERSION = "5.35.4";
4
+ interface FernWikiOptions {
5
+ /** Absolute path to the working repo root. Injected by the loader from RuntimeContext. */
6
+ repoRoot?: string;
7
+ /** Active org name (e.g. "theholocron"). Injected by the loader from RuntimeContext. */
8
+ org?: string;
9
+ /**
10
+ * "owner/name" repo coordinate. Injected by the loader from RuntimeContext.
11
+ * Used to derive the per-repo basepath when multi-source custom domain is configured.
12
+ */
13
+ repo?: string;
14
+ /**
15
+ * Base domain for the Fern docs site (e.g. "wiki.theholocron.dev").
16
+ *
17
+ * When provided without a path, the repo name is appended automatically:
18
+ * domain "wiki.theholocron.dev" + repo "owner/holocron"
19
+ * → custom-domain: wiki.theholocron.dev/holocron
20
+ * → multi-source: true
21
+ *
22
+ * You can also supply the full path explicitly:
23
+ * domain "wiki.theholocron.dev/holocron"
24
+ *
25
+ * Note: password protection is configured in the Fern Dashboard
26
+ * (https://dashboard.buildwithfern.com) — it is not configurable via docs.yml.
27
+ */
28
+ domain?: string;
29
+ /**
30
+ * Fern workspace org slug. Defaults to `org` (the GitHub org name).
31
+ * Set this explicitly when the Fern workspace name differs from the
32
+ * GitHub org — e.g. the workspace was registered as "holocron" while
33
+ * the GitHub org is "theholocron".
34
+ */
35
+ fernOrg?: string;
36
+ }
37
+ declare class FernWiki implements Wiki {
38
+ private readonly opts;
39
+ readonly key: "wiki";
40
+ readonly providerName = "fern";
41
+ constructor(opts: FernWikiOptions);
42
+ provision(callOpts?: WikiProvisionOpts): Promise<string>;
43
+ dnsRecord(): WikiDnsRecord | null;
44
+ }
45
+ //#endregion
46
+ //#region src/auth.d.ts
47
+ declare const resolveToken: (input?: ResolveTokenInput) => string;
48
+ //#endregion
49
+ //#region src/index.d.ts
50
+ type FernPluginOptions = FernWikiOptions;
51
+ declare function wiki(opts: FernPluginOptions): Wiki;
52
+ declare function createPlugin(options?: FernPluginOptions): {
53
+ name: string;
54
+ capabilities: {
55
+ wiki: () => Wiki;
56
+ };
57
+ };
58
+ //#endregion
59
+ export { AuthError, FERN_VERSION, FernPluginOptions, FernWiki, type FernWikiOptions, type ResolveTokenInput, createPlugin, resolveToken, wiki };
package/dist/index.mjs ADDED
@@ -0,0 +1,141 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { AuthError, createResolveToken } from "@theholocron/cli";
4
+ //#region src/capabilities/wiki.ts
5
+ const FERN_VERSION = "5.35.4";
6
+ var FernWiki = class {
7
+ opts;
8
+ key = "wiki";
9
+ providerName = "fern";
10
+ constructor(opts) {
11
+ this.opts = opts;
12
+ }
13
+ async provision(callOpts) {
14
+ const repoRoot = this.opts.repoRoot ?? process.cwd();
15
+ const { org, repo, domain, fernOrg } = this.opts;
16
+ const name = callOpts?.name;
17
+ const resolvedFernOrg = fernOrg ?? org;
18
+ return `${await writeFernConfig({
19
+ repoRoot,
20
+ fernOrg: resolvedFernOrg
21
+ })}; ${await writeFernDocsYml({
22
+ repoRoot,
23
+ fernOrg: resolvedFernOrg,
24
+ repo,
25
+ name,
26
+ domain
27
+ })}`;
28
+ }
29
+ dnsRecord() {
30
+ const { org, domain, fernOrg } = this.opts;
31
+ if (!domain) return null;
32
+ const resolvedFernOrg = fernOrg ?? org ?? "holocron";
33
+ const hostname = domain.split("/")[0];
34
+ return {
35
+ zone: hostname.split(".").slice(-2).join("."),
36
+ cname: hostname,
37
+ target: `${resolvedFernOrg}.docs.buildwithfern.com`
38
+ };
39
+ }
40
+ };
41
+ async function writeFernConfig({ repoRoot, fernOrg }) {
42
+ const fernDir = join(repoRoot, "fern");
43
+ await mkdir(fernDir, { recursive: true });
44
+ const org = fernOrg ?? "holocron";
45
+ const content = JSON.stringify({
46
+ organization: org,
47
+ version: FERN_VERSION
48
+ }, null, 2) + "\n";
49
+ await writeFile(join(fernDir, "fern.config.json"), content, "utf8");
50
+ return `fern.config.json: org=${org}, version=${FERN_VERSION}`;
51
+ }
52
+ async function writeFernDocsYml({ repoRoot, fernOrg, repo, name, domain }) {
53
+ const fernDir = join(repoRoot, "fern");
54
+ await mkdir(fernDir, { recursive: true });
55
+ const docsPath = join(fernDir, "docs.yml");
56
+ const resolvedFernOrg = fernOrg ?? "holocron";
57
+ const repoName = repo?.split("/").pop();
58
+ let instanceUrl;
59
+ let customDomain;
60
+ let multiSource = false;
61
+ if (domain) {
62
+ const slashIdx = domain.indexOf("/");
63
+ if (slashIdx !== -1) {
64
+ instanceUrl = `${resolvedFernOrg}.docs.buildwithfern.com/${domain.slice(slashIdx + 1)}`;
65
+ customDomain = domain;
66
+ multiSource = true;
67
+ } else if (repoName) {
68
+ instanceUrl = `${resolvedFernOrg}.docs.buildwithfern.com/${repoName}`;
69
+ customDomain = `${domain}/${repoName}`;
70
+ multiSource = true;
71
+ } else {
72
+ instanceUrl = `${resolvedFernOrg}.docs.buildwithfern.com`;
73
+ customDomain = domain;
74
+ }
75
+ } else instanceUrl = `${resolvedFernOrg}.docs.buildwithfern.com`;
76
+ let existing = null;
77
+ try {
78
+ existing = await readFile(docsPath, "utf8");
79
+ } catch {}
80
+ if (existing !== null) {
81
+ const updated = updateInstancesBlock(existing, instanceUrl, customDomain, multiSource);
82
+ if (updated !== existing) {
83
+ await writeFile(docsPath, updated, "utf8");
84
+ const domainPart = customDomain ? `, domain=${customDomain}${multiSource ? " (multi-source)" : ""}` : "";
85
+ return `docs.yml: updated instances (url=${instanceUrl}${domainPart})`;
86
+ }
87
+ return "docs.yml: instances block already up to date";
88
+ }
89
+ const lines = [
90
+ `# yaml-language-server: $schema=https://schema.buildwithfern.dev/docs-yml.json`,
91
+ ``,
92
+ `instances:`,
93
+ ` - url: ${instanceUrl}`
94
+ ];
95
+ if (customDomain) lines.push(` custom-domain: ${customDomain}`);
96
+ if (multiSource) lines.push(` multi-source: true`);
97
+ lines.push(``, `title: ${name ?? resolvedFernOrg} Engineering`, ``, `layout:`, ` page-width: full`, ` tabs-placement: header`, ` searchbar-placement: header`, ``, `tabs:`, ` decisions:`, ` display-name: Decisions`, ` icon: fa-duotone fa-scale-balanced`, ` engineering:`, ` display-name: Engineering`, ` icon: fa-duotone fa-gear`, ``, `navigation:`, ` - tab: decisions`, ` layout:`, ` - section: Architecture Decision Records`, ` contents:`, ` - page: Index`, ` path: ../docs/decisions/README.md`, ` - tab: engineering`, ` layout:`, ` - section: Overview`, ` contents:`, ` - page: Engineering Home`, ` path: ../docs/engineering/README.md`, ``, `colors:`, ` accent-primary:`, ` dark: "#70E155"`, ` light: "#008700"`, ``, `logo:`, ` height: 20`, ``, `metadata:`, ` og:dynamic: true`, ``);
98
+ await writeFile(docsPath, lines.join("\n"), "utf8");
99
+ const domainSummary = customDomain ? `, domain=${customDomain}${multiSource ? " (multi-source)" : ""}` : "";
100
+ return `docs.yml: url=${instanceUrl}${domainSummary}`;
101
+ }
102
+ /**
103
+ * Replace the `instances:` block in an existing docs.yml, preserving
104
+ * everything else (navigation, colors, layout, title, etc.).
105
+ *
106
+ * Uses a regex to match the block (the `instances:` key and all following
107
+ * indented / list lines) and replaces it in-place. Returns the original
108
+ * string unchanged when the block already matches.
109
+ */
110
+ function updateInstancesBlock(content, instanceUrl, customDomain, multiSource) {
111
+ const blockRe = /^instances:(?:\n[ \t][^\n]*)*/m;
112
+ const match = content.match(blockRe);
113
+ if (!match) return content;
114
+ const newLines = [`instances:`, ` - url: ${instanceUrl}`];
115
+ if (customDomain) newLines.push(` custom-domain: ${customDomain}`);
116
+ if (multiSource) newLines.push(` multi-source: true`);
117
+ const newBlock = newLines.join("\n");
118
+ if (match[0] === newBlock) return content;
119
+ return content.replace(blockRe, newBlock);
120
+ }
121
+ //#endregion
122
+ //#region src/auth.ts
123
+ const resolveToken = createResolveToken({
124
+ envName: "HOLOCRON_FERN_TOKEN",
125
+ vendorEnvName: "FERN_TOKEN",
126
+ keyringService: "fern",
127
+ errorMessage: "no Fern token found. Set HOLOCRON_FERN_TOKEN / FERN_TOKEN, or run: holocron auth set fern <token>"
128
+ });
129
+ //#endregion
130
+ //#region src/index.ts
131
+ function wiki(opts) {
132
+ return new FernWiki(opts);
133
+ }
134
+ function createPlugin(options = {}) {
135
+ return {
136
+ name: "@theholocron/holocron-plugin-fern",
137
+ capabilities: { wiki: () => wiki(options) }
138
+ };
139
+ }
140
+ //#endregion
141
+ export { AuthError, FERN_VERSION, FernWiki, createPlugin, resolveToken, wiki };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-fern",
3
- "version": "3.43.0",
3
+ "version": "3.45.0",
4
4
  "description": "Holocron plugin for Fern. Implements the wiki capability for engineering knowledge publishing.",
5
5
  "keywords": [
6
6
  "fern",
@@ -44,10 +44,10 @@
44
44
  "tsx": "4.23.12",
45
45
  "typescript": "^5.9.3",
46
46
  "vitest": "^4.1.10",
47
- "@theholocron/cli": "3.43.0"
47
+ "@theholocron/cli": "3.45.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@theholocron/cli": "3.43.0"
50
+ "@theholocron/cli": "3.45.0"
51
51
  },
52
52
  "engines": {
53
53
  "node": ">=22"