docula 1.14.0 → 2.0.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
@@ -56,7 +56,7 @@ You can build Docula as a standalone binary that runs without Node.js installed.
56
56
 
57
57
  ## Building the Binary
58
58
 
59
- Requires Node.js >= 20 to build (the resulting binary does not need Node.js to run).
59
+ Requires Node.js >= 25.7.0 to build (the resulting binary does not need Node.js to run). The build uses [tsdown's `exe` option](https://tsdown.dev/options/exe), which wraps Node.js's [Single Executable Applications](https://nodejs.org/api/single-executable-applications.html) feature added in Node 25.7.
60
60
 
61
61
  ```bash
62
62
  pnpm install
@@ -69,7 +69,7 @@ This produces a platform-specific binary at `dist/docula` (or `dist/docula.exe`
69
69
 
70
70
  1. Embeds all built-in templates (modern, classic) into the bundle as base64
71
71
  2. Bundles all source code and dependencies into a single CJS file via [tsdown](https://tsdown.dev/)
72
- 3. Uses tsdown's built-in `exe` option to create a Node.js SEA binary (blob generation, injection, and code signing are handled automatically)
72
+ 3. Uses tsdown's built-in `exe` option to create a Node.js SEA binary blob generation, injection, and (on macOS hosts) ad-hoc code signing are handled automatically
73
73
 
74
74
  ## Testing the Binary
75
75
 
@@ -91,7 +91,7 @@ After building, test it locally:
91
91
 
92
92
  ## Cross-Platform Binaries
93
93
 
94
- Node.js SEA cannot cross-compile — the binary matches the OS and architecture it was built on. The CI workflow (`.github/workflows/build-binaries.yaml`) builds for all platforms using a matrix strategy:
94
+ The CI workflow (`.github/workflows/build-binaries.yaml`) builds each platform natively on its own runner. Native builds avoid the cross-compile signing pitfall on Apple Silicon, where unsigned Mach-O binaries are killed on launch.
95
95
 
96
96
  | Platform | Runner | Artifact |
97
97
  |---|---|---|
package/dist/docula.d.ts CHANGED
@@ -215,6 +215,7 @@ type DoculaData = {
215
215
  changelogUrl: string;
216
216
  editPageUrl?: string;
217
217
  openGraph?: DoculaOpenGraph;
218
+ faviconUrl?: string;
218
219
  };
219
220
  type DoculaTemplates = {
220
221
  home: string;
package/dist/docula.js CHANGED
@@ -11,8 +11,9 @@ import { Writr, Writr as Writr$1 } from "writr";
11
11
  import { blue, bold, cyan, dim, gray, green, magenta, red, white, yellow } from "colorette";
12
12
  import { CacheableNet } from "@cacheable/net";
13
13
  import os from "node:os";
14
+ import sea from "node:sea";
14
15
  //#region package.json
15
- var version = "1.14.0";
16
+ var version = "2.0.0";
16
17
  var package_default = {
17
18
  name: "docula",
18
19
  version,
@@ -27,12 +28,12 @@ var package_default = {
27
28
  "url": "git+https://github.com/jaredwray/docula.git"
28
29
  },
29
30
  author: "Jared Wray <me@jaredwray.com>",
30
- engines: { "node": ">=20" },
31
+ engines: { "node": "^22.18.0 || >=24.0.0" },
31
32
  license: "MIT",
32
33
  scripts: {
33
34
  "clean": "rimraf ./dist ./coverage ./node_modules ./package-lock.json ./yarn.lock ./pnpm-lock.yaml ./site/dist",
34
35
  "build": "pnpm generate-init-file && tsdown",
35
- "build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts && tsx scripts/build-sea.ts",
36
+ "build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts",
36
37
  "generate-embedded-templates": "tsx scripts/generate-embedded-templates.ts",
37
38
  "lint": "biome check --write --error-on-warnings",
38
39
  "lint:ci": "biome check --error-on-warnings",
@@ -69,39 +70,39 @@ var package_default = {
69
70
  ],
70
71
  bin: { "docula": "./bin/docula.js" },
71
72
  dependencies: {
72
- "@ai-sdk/anthropic": "^3.0.69",
73
- "@ai-sdk/google": "^3.0.63",
74
- "@ai-sdk/openai": "^3.0.53",
73
+ "@ai-sdk/anthropic": "^3.0.77",
74
+ "@ai-sdk/google": "^3.0.72",
75
+ "@ai-sdk/openai": "^3.0.63",
75
76
  "@cacheable/net": "^2.0.7",
76
- "ai": "^6.0.164",
77
+ "ai": "^6.0.178",
77
78
  "colorette": "^2.0.20",
78
- "ecto": "^4.8.4",
79
+ "ecto": "^4.8.5",
79
80
  "hashery": "^2.0.0",
80
- "jiti": "^2.6.1",
81
+ "jiti": "^2.7.0",
81
82
  "serve-handler": "^6.1.7",
82
83
  "update-notifier": "^7.3.1",
83
- "writr": "^6.1.1"
84
+ "writr": "^6.1.2"
84
85
  },
85
86
  devDependencies: {
86
- "@biomejs/biome": "^2.4.12",
87
- "@playwright/test": "^1.59.1",
88
- "@types/node": "^25.6.0",
87
+ "@biomejs/biome": "^2.4.15",
88
+ "@playwright/test": "^1.60.0",
89
+ "@types/node": "^24.12.4",
89
90
  "@types/serve-handler": "^6.1.4",
90
91
  "@types/update-notifier": "^6.0.8",
91
- "@vitest/coverage-v8": "^4.1.4",
92
+ "@vitest/coverage-v8": "^4.1.6",
92
93
  "dotenv": "^17.4.2",
93
- "postject": "1.0.0-alpha.6",
94
94
  "rimraf": "^6.1.3",
95
- "tsdown": "^0.21.9",
95
+ "tsdown": "^0.22.0",
96
96
  "tsx": "^4.21.0",
97
- "typescript": "^6.0.2",
98
- "vitest": "^4.1.4"
97
+ "typescript": "^6.0.3",
98
+ "vitest": "^4.1.6"
99
99
  },
100
100
  files: [
101
101
  "dist",
102
102
  "templates",
103
103
  "bin"
104
- ]
104
+ ],
105
+ packageManager: "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d"
105
106
  };
106
107
  //#endregion
107
108
  //#region src/builder-ai.ts
@@ -118,15 +119,15 @@ async function createAIModel(ai) {
118
119
  switch (ai.provider) {
119
120
  case "anthropic": {
120
121
  const { createAnthropic } = await import("@ai-sdk/anthropic");
121
- return createAnthropic({ apiKey: ai.apiKey })(ai.model ?? "claude-haiku-4-5");
122
+ return createAnthropic(ai.apiKey ? { apiKey: ai.apiKey } : {})(ai.model || "claude-haiku-4-5");
122
123
  }
123
124
  case "openai": {
124
125
  const { createOpenAI } = await import("@ai-sdk/openai");
125
- return createOpenAI({ apiKey: ai.apiKey })(ai.model ?? "gpt-4o-mini-latest");
126
+ return createOpenAI(ai.apiKey ? { apiKey: ai.apiKey } : {})(ai.model || "gpt-4o-mini");
126
127
  }
127
128
  case "google": {
128
129
  const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
129
- return createGoogleGenerativeAI({ apiKey: ai.apiKey })(ai.model ?? "gemini-2.5-flash-lite");
130
+ return createGoogleGenerativeAI(ai.apiKey ? { apiKey: ai.apiKey } : {})(ai.model || "gemini-2.5-flash-lite");
130
131
  }
131
132
  default: return;
132
133
  }
@@ -597,6 +598,7 @@ function extractResponses(operation, spec) {
597
598
  responses.push({
598
599
  statusCode,
599
600
  statusClass: getStatusClass(statusCode),
601
+ /* v8 ignore next */
600
602
  description: response.description ?? "",
601
603
  contentType,
602
604
  schemaProperties,
@@ -1171,6 +1173,7 @@ function hasAssetsChanged(hash, sitePath, previousAssets, autoReadme) {
1171
1173
  for (const file of [
1172
1174
  "favicon.ico",
1173
1175
  "logo.svg",
1176
+ "logo.png",
1174
1177
  "logo_horizontal.png",
1175
1178
  "variables.css",
1176
1179
  "api/swagger.json",
@@ -2695,12 +2698,7 @@ var DoculaOptions = class {
2695
2698
  * Returns true when running as a single-executable application (SEA).
2696
2699
  */
2697
2700
  function isSEA() {
2698
- try {
2699
- return Boolean(process.sea);
2700
- } catch {
2701
- /* v8 ignore next -- @preserve */
2702
- return false;
2703
- }
2701
+ return sea.isSea();
2704
2702
  }
2705
2703
  /**
2706
2704
  * Returns the deterministic temp directory path for extracted templates.
@@ -2831,6 +2829,12 @@ var DoculaBuilder = class {
2831
2829
  editPageUrl: this.options.editPageUrl,
2832
2830
  openGraph: this.options.openGraph
2833
2831
  };
2832
+ const resolvedFavicon = [
2833
+ "favicon.ico",
2834
+ "logo.svg",
2835
+ "logo.png"
2836
+ ].find((file) => fs.existsSync(path.join(this.options.sitePath, file)));
2837
+ if (resolvedFavicon) doculaData.faviconUrl = buildUrlPath(this.options.baseUrl, resolvedFavicon);
2834
2838
  if (siteReadmeExists) currentAssetHashes["README.md"] = hashFile(this._hash, path.join(this.options.sitePath, "README.md"));
2835
2839
  else if (autoReadmeResult) currentAssetHashes.__autoReadme = hashFile(this._hash, autoReadmeResult.sourcePath);
2836
2840
  if (Array.isArray(this.options.openApiUrl)) doculaData.openApiSpecs = this.options.openApiUrl.map((spec) => ({
@@ -2966,6 +2970,10 @@ var DoculaBuilder = class {
2966
2970
  await fs.promises.copyFile(`${siteRelativePath}/logo.svg`, `${this.options.output}/logo.svg`);
2967
2971
  this._console.fileCopied("logo.svg");
2968
2972
  }
2973
+ if (!hashAssetAndCheckSkip(this._hash, path.join(siteRelativePath, "logo.png"), path.join(this.options.output, "logo.png"), "logo.png", previousAssets, currentAssetHashes)) {
2974
+ await fs.promises.copyFile(path.join(siteRelativePath, "logo.png"), path.join(this.options.output, "logo.png"));
2975
+ this._console.fileCopied("logo.png");
2976
+ }
2969
2977
  if (!hashAssetAndCheckSkip(this._hash, `${siteRelativePath}/logo_horizontal.png`, `${this.options.output}/logo_horizontal.png`, "logo_horizontal.png", previousAssets, currentAssetHashes)) {
2970
2978
  await fs.promises.copyFile(`${siteRelativePath}/logo_horizontal.png`, `${this.options.output}/logo_horizontal.png`);
2971
2979
  this._console.fileCopied("logo_horizontal.png");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docula",
3
- "version": "1.14.0",
3
+ "version": "2.0.0",
4
4
  "description": "Beautiful Website for Your Projects",
5
5
  "type": "module",
6
6
  "main": "./dist/docula.js",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "author": "Jared Wray <me@jaredwray.com>",
19
19
  "engines": {
20
- "node": ">=20"
20
+ "node": "^22.18.0 || >=24.0.0"
21
21
  },
22
22
  "license": "MIT",
23
23
  "keywords": [
@@ -40,33 +40,32 @@
40
40
  "docula": "./bin/docula.js"
41
41
  },
42
42
  "dependencies": {
43
- "@ai-sdk/anthropic": "^3.0.69",
44
- "@ai-sdk/google": "^3.0.63",
45
- "@ai-sdk/openai": "^3.0.53",
43
+ "@ai-sdk/anthropic": "^3.0.77",
44
+ "@ai-sdk/google": "^3.0.72",
45
+ "@ai-sdk/openai": "^3.0.63",
46
46
  "@cacheable/net": "^2.0.7",
47
- "ai": "^6.0.164",
47
+ "ai": "^6.0.178",
48
48
  "colorette": "^2.0.20",
49
- "ecto": "^4.8.4",
49
+ "ecto": "^4.8.5",
50
50
  "hashery": "^2.0.0",
51
- "jiti": "^2.6.1",
51
+ "jiti": "^2.7.0",
52
52
  "serve-handler": "^6.1.7",
53
53
  "update-notifier": "^7.3.1",
54
- "writr": "^6.1.1"
54
+ "writr": "^6.1.2"
55
55
  },
56
56
  "devDependencies": {
57
- "@biomejs/biome": "^2.4.12",
58
- "@playwright/test": "^1.59.1",
59
- "@types/node": "^25.6.0",
57
+ "@biomejs/biome": "^2.4.15",
58
+ "@playwright/test": "^1.60.0",
59
+ "@types/node": "^24.12.4",
60
60
  "@types/serve-handler": "^6.1.4",
61
61
  "@types/update-notifier": "^6.0.8",
62
- "@vitest/coverage-v8": "^4.1.4",
62
+ "@vitest/coverage-v8": "^4.1.6",
63
63
  "dotenv": "^17.4.2",
64
- "postject": "1.0.0-alpha.6",
65
64
  "rimraf": "^6.1.3",
66
- "tsdown": "^0.21.9",
65
+ "tsdown": "^0.22.0",
67
66
  "tsx": "^4.21.0",
68
- "typescript": "^6.0.2",
69
- "vitest": "^4.1.4"
67
+ "typescript": "^6.0.3",
68
+ "vitest": "^4.1.6"
70
69
  },
71
70
  "files": [
72
71
  "dist",
@@ -76,7 +75,7 @@
76
75
  "scripts": {
77
76
  "clean": "rimraf ./dist ./coverage ./node_modules ./package-lock.json ./yarn.lock ./pnpm-lock.yaml ./site/dist",
78
77
  "build": "pnpm generate-init-file && tsdown",
79
- "build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts && tsx scripts/build-sea.ts",
78
+ "build:binary": "pnpm generate-init-file && pnpm generate-embedded-templates && tsdown --config tsdown.config.binary.ts",
80
79
  "generate-embedded-templates": "tsx scripts/generate-embedded-templates.ts",
81
80
  "lint": "biome check --write --error-on-warnings",
82
81
  "lint:ci": "biome check --error-on-warnings",
@@ -31,4 +31,4 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
31
31
  rel="stylesheet"
32
32
  />
33
33
  <link rel="stylesheet" href="/css/highlight/styles/base16/dracula.min.css" />
34
- <link rel="icon" href="/favicon.ico" />
34
+ {{#if faviconUrl}}<link rel="icon" href="{{faviconUrl}}" />{{/if}}
@@ -398,7 +398,7 @@ body {
398
398
  .copy-code-btn { position: absolute; top: 8px; right: 8px; padding: 4px; line-height: 0; border-radius: 4px; background: transparent; color: var(--muted); border: none; cursor: pointer; opacity: 0; transition: opacity 0.15s; }
399
399
  pre:hover .copy-code-btn { opacity: 1; }
400
400
  .copy-code-btn:hover { color: var(--fg); }
401
- .article__main img, .changelog-entry-body img { cursor: zoom-in; }
401
+ .article__main img:not(a img), .changelog-entry-body img:not(a img) { cursor: zoom-in; }
402
402
  .lightbox-overlay { display: none; position: fixed; inset: 0; z-index: 200; background: rgba(0, 0, 0, 0.8); justify-content: center; align-items: center; cursor: pointer; }
403
403
  .lightbox-overlay--visible { display: flex !important; }
404
404
  .lightbox-overlay img { max-width: 90vw; max-height: 90vh; border-radius: 8px; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); cursor: default; }
@@ -22,7 +22,7 @@ j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
22
22
  <link rel="stylesheet" href="{{baseUrl}}/css/variables.css">
23
23
  <link rel="stylesheet" href="{{baseUrl}}/css/styles.css">
24
24
  <link rel="stylesheet" href="{{baseUrl}}/css/highlight/styles/base16/docula.css">
25
- <link rel="icon" href="{{baseUrl}}/favicon.ico">
25
+ {{#if faviconUrl}}<link rel="icon" href="{{faviconUrl}}">{{/if}}
26
26
  <script>
27
27
  (function(){
28
28
  window.__doculaThemeKey = 'docula:theme:' + ({{#if siteUrl}}'{{siteUrl}}'{{else}}location.origin{{/if}}).replace(/^https?:\/\//, '');
@@ -236,10 +236,6 @@
236
236
  storedSectionState = JSON.parse(localStorage.getItem(SIDEBAR_STORAGE_KEY) || '{}');
237
237
  } catch (e) { storedSectionState = {}; }
238
238
 
239
- const activeSidebarLink = document.querySelector('.nav-sidebar__item--active');
240
- const activeSection = activeSidebarLink ? activeSidebarLink.closest('.nav-sidebar__section--collapsible') : null;
241
- const defaultOpenSection = activeSection || collapsibleSections[0];
242
-
243
239
  const setSectionOpen = (section, toggle, open) => {
244
240
  toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
245
241
  section.classList.toggle('nav-sidebar__section--collapsed', !open);
@@ -255,7 +251,7 @@
255
251
 
256
252
  const key = 'section-' + idx;
257
253
  const hasStored = Object.prototype.hasOwnProperty.call(storedSectionState, key);
258
- const isOpen = hasStored ? !!storedSectionState[key] : section === defaultOpenSection;
254
+ const isOpen = hasStored ? !!storedSectionState[key] : true;
259
255
  setSectionOpen(section, toggle, isOpen);
260
256
 
261
257
  toggle.addEventListener('click', () => {
@@ -324,6 +320,7 @@
324
320
  document.body.appendChild(lightboxOverlay);
325
321
 
326
322
  document.querySelectorAll('.article__main img, .changelog-entry-body img').forEach(function(img) {
323
+ if (img.closest('a')) return;
327
324
  img.addEventListener('click', function() {
328
325
  lightboxImg.src = img.src;
329
326
  lightboxOverlay.classList.add('lightbox-overlay--visible');