@theholocron/holocron-plugin-fern 3.43.0 → 3.45.1
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 +17 -9
- package/dist/index.d.mts +60 -0
- package/dist/index.mjs +148 -0
- package/package.json +7 -7
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
|
|
38
|
-
|
|
|
39
|
-
| `domain`
|
|
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. **
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
navigation
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { AuthError, ResolveTokenInput, Wiki, WikiDnsRecord, WikiProvisionOpts, WikiProxyConfig } 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
|
+
proxyConfig(): WikiProxyConfig | null;
|
|
44
|
+
dnsRecord(): WikiDnsRecord | null;
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/auth.d.ts
|
|
48
|
+
declare const resolveToken: (input?: ResolveTokenInput) => string;
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/index.d.ts
|
|
51
|
+
type FernPluginOptions = FernWikiOptions;
|
|
52
|
+
declare function wiki(opts: FernPluginOptions): Wiki;
|
|
53
|
+
declare function createPlugin(options?: FernPluginOptions): {
|
|
54
|
+
name: string;
|
|
55
|
+
capabilities: {
|
|
56
|
+
wiki: () => Wiki;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { AuthError, FERN_VERSION, FernPluginOptions, FernWiki, type FernWikiOptions, type ResolveTokenInput, createPlugin, resolveToken, wiki };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
proxyConfig() {
|
|
30
|
+
if (!this.opts.domain) return null;
|
|
31
|
+
return {
|
|
32
|
+
target: "https://app.buildwithfern.com",
|
|
33
|
+
headers: { "X-Fern-Host": this.opts.domain.split("/")[0] }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
dnsRecord() {
|
|
37
|
+
const { org, domain, fernOrg } = this.opts;
|
|
38
|
+
if (!domain) return null;
|
|
39
|
+
const resolvedFernOrg = fernOrg ?? org ?? "holocron";
|
|
40
|
+
const hostname = domain.split("/")[0];
|
|
41
|
+
return {
|
|
42
|
+
zone: hostname.split(".").slice(-2).join("."),
|
|
43
|
+
cname: hostname,
|
|
44
|
+
target: `${resolvedFernOrg}.docs.buildwithfern.com`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
async function writeFernConfig({ repoRoot, fernOrg }) {
|
|
49
|
+
const fernDir = join(repoRoot, "fern");
|
|
50
|
+
await mkdir(fernDir, { recursive: true });
|
|
51
|
+
const org = fernOrg ?? "holocron";
|
|
52
|
+
const content = JSON.stringify({
|
|
53
|
+
organization: org,
|
|
54
|
+
version: FERN_VERSION
|
|
55
|
+
}, null, 2) + "\n";
|
|
56
|
+
await writeFile(join(fernDir, "fern.config.json"), content, "utf8");
|
|
57
|
+
return `fern.config.json: org=${org}, version=${FERN_VERSION}`;
|
|
58
|
+
}
|
|
59
|
+
async function writeFernDocsYml({ repoRoot, fernOrg, repo, name, domain }) {
|
|
60
|
+
const fernDir = join(repoRoot, "fern");
|
|
61
|
+
await mkdir(fernDir, { recursive: true });
|
|
62
|
+
const docsPath = join(fernDir, "docs.yml");
|
|
63
|
+
const resolvedFernOrg = fernOrg ?? "holocron";
|
|
64
|
+
const repoName = repo?.split("/").pop();
|
|
65
|
+
let instanceUrl;
|
|
66
|
+
let customDomain;
|
|
67
|
+
let multiSource = false;
|
|
68
|
+
if (domain) {
|
|
69
|
+
const slashIdx = domain.indexOf("/");
|
|
70
|
+
if (slashIdx !== -1) {
|
|
71
|
+
instanceUrl = `${resolvedFernOrg}.docs.buildwithfern.com/${domain.slice(slashIdx + 1)}`;
|
|
72
|
+
customDomain = domain;
|
|
73
|
+
multiSource = true;
|
|
74
|
+
} else if (repoName) {
|
|
75
|
+
instanceUrl = `${resolvedFernOrg}.docs.buildwithfern.com/${repoName}`;
|
|
76
|
+
customDomain = `${domain}/${repoName}`;
|
|
77
|
+
multiSource = true;
|
|
78
|
+
} else {
|
|
79
|
+
instanceUrl = `${resolvedFernOrg}.docs.buildwithfern.com`;
|
|
80
|
+
customDomain = domain;
|
|
81
|
+
}
|
|
82
|
+
} else instanceUrl = `${resolvedFernOrg}.docs.buildwithfern.com`;
|
|
83
|
+
let existing = null;
|
|
84
|
+
try {
|
|
85
|
+
existing = await readFile(docsPath, "utf8");
|
|
86
|
+
} catch {}
|
|
87
|
+
if (existing !== null) {
|
|
88
|
+
const updated = updateInstancesBlock(existing, instanceUrl, customDomain, multiSource);
|
|
89
|
+
if (updated !== existing) {
|
|
90
|
+
await writeFile(docsPath, updated, "utf8");
|
|
91
|
+
const domainPart = customDomain ? `, domain=${customDomain}${multiSource ? " (multi-source)" : ""}` : "";
|
|
92
|
+
return `docs.yml: updated instances (url=${instanceUrl}${domainPart})`;
|
|
93
|
+
}
|
|
94
|
+
return "docs.yml: instances block already up to date";
|
|
95
|
+
}
|
|
96
|
+
const lines = [
|
|
97
|
+
`# yaml-language-server: $schema=https://schema.buildwithfern.dev/docs-yml.json`,
|
|
98
|
+
``,
|
|
99
|
+
`instances:`,
|
|
100
|
+
` - url: ${instanceUrl}`
|
|
101
|
+
];
|
|
102
|
+
if (customDomain) lines.push(` custom-domain: ${customDomain}`);
|
|
103
|
+
if (multiSource) lines.push(` multi-source: true`);
|
|
104
|
+
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`, ``);
|
|
105
|
+
await writeFile(docsPath, lines.join("\n"), "utf8");
|
|
106
|
+
const domainSummary = customDomain ? `, domain=${customDomain}${multiSource ? " (multi-source)" : ""}` : "";
|
|
107
|
+
return `docs.yml: url=${instanceUrl}${domainSummary}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Replace the `instances:` block in an existing docs.yml, preserving
|
|
111
|
+
* everything else (navigation, colors, layout, title, etc.).
|
|
112
|
+
*
|
|
113
|
+
* Uses a regex to match the block (the `instances:` key and all following
|
|
114
|
+
* indented / list lines) and replaces it in-place. Returns the original
|
|
115
|
+
* string unchanged when the block already matches.
|
|
116
|
+
*/
|
|
117
|
+
function updateInstancesBlock(content, instanceUrl, customDomain, multiSource) {
|
|
118
|
+
const blockRe = /^instances:(?:\n[ \t][^\n]*)*/m;
|
|
119
|
+
const match = content.match(blockRe);
|
|
120
|
+
if (!match) return content;
|
|
121
|
+
const newLines = [`instances:`, ` - url: ${instanceUrl}`];
|
|
122
|
+
if (customDomain) newLines.push(` custom-domain: ${customDomain}`);
|
|
123
|
+
if (multiSource) newLines.push(` multi-source: true`);
|
|
124
|
+
const newBlock = newLines.join("\n");
|
|
125
|
+
if (match[0] === newBlock) return content;
|
|
126
|
+
return content.replace(blockRe, newBlock);
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/auth.ts
|
|
130
|
+
const resolveToken = createResolveToken({
|
|
131
|
+
envName: "HOLOCRON_FERN_TOKEN",
|
|
132
|
+
vendorEnvName: "FERN_TOKEN",
|
|
133
|
+
keyringService: "fern",
|
|
134
|
+
errorMessage: "no Fern token found. Set HOLOCRON_FERN_TOKEN / FERN_TOKEN, or run: holocron auth set fern <token>"
|
|
135
|
+
});
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/index.ts
|
|
138
|
+
function wiki(opts) {
|
|
139
|
+
return new FernWiki(opts);
|
|
140
|
+
}
|
|
141
|
+
function createPlugin(options = {}) {
|
|
142
|
+
return {
|
|
143
|
+
name: "@theholocron/holocron-plugin-fern",
|
|
144
|
+
capabilities: { wiki: () => wiki(options) }
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
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.
|
|
3
|
+
"version": "3.45.1",
|
|
4
4
|
"description": "Holocron plugin for Fern. Implements the wiki capability for engineering knowledge publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fern",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"dist"
|
|
31
31
|
],
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@theholocron/eslint-config": "^7.
|
|
34
|
-
"@theholocron/tsconfig": "^7.
|
|
35
|
-
"@theholocron/tsdown-config": "^7.
|
|
36
|
-
"@theholocron/vitest-config": "^7.
|
|
33
|
+
"@theholocron/eslint-config": "^7.29.0",
|
|
34
|
+
"@theholocron/tsconfig": "^7.29.0",
|
|
35
|
+
"@theholocron/tsdown-config": "^7.29.0",
|
|
36
|
+
"@theholocron/vitest-config": "^7.29.0",
|
|
37
37
|
"@types/node": "^26",
|
|
38
38
|
"@vitest/coverage-v8": "^4.1.10",
|
|
39
39
|
"@vitest/eslint-plugin": "^1.6.27",
|
|
@@ -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.
|
|
47
|
+
"@theholocron/cli": "3.45.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@theholocron/cli": "3.
|
|
50
|
+
"@theholocron/cli": "3.45.1"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=22"
|