docula 1.13.0 → 1.14.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/dist/docula.d.ts CHANGED
@@ -83,6 +83,7 @@ type ApiOperation = {
83
83
  id: string;
84
84
  method: string;
85
85
  methodUpper: string;
86
+ methodShort: string;
86
87
  path: string;
87
88
  summary: string;
88
89
  description: string;
@@ -448,6 +449,7 @@ declare class DoculaBuilder {
448
449
  private readonly _console;
449
450
  private readonly _hash;
450
451
  onReleaseChangelog?: (entries: DoculaChangelogEntry[], console: DoculaConsole) => Promise<DoculaChangelogEntry[]> | DoculaChangelogEntry[];
452
+ onAutoReadme?: (content: string, sourcePath: string, console: DoculaConsole) => Promise<string> | string;
451
453
  get console(): DoculaConsole;
452
454
  constructor(options?: DoculaBuilderOptions, engineOptions?: any);
453
455
  get options(): DoculaOptions;
package/dist/docula.js CHANGED
@@ -12,7 +12,7 @@ import { blue, bold, cyan, dim, gray, green, magenta, red, white, yellow } from
12
12
  import { CacheableNet } from "@cacheable/net";
13
13
  import os from "node:os";
14
14
  //#region package.json
15
- var version = "1.13.0";
15
+ var version = "1.14.0";
16
16
  var package_default = {
17
17
  name: "docula",
18
18
  version,
@@ -69,36 +69,33 @@ var package_default = {
69
69
  ],
70
70
  bin: { "docula": "./bin/docula.js" },
71
71
  dependencies: {
72
- "@ai-sdk/anthropic": "^3.0.63",
73
- "@ai-sdk/google": "^3.0.52",
74
- "@ai-sdk/openai": "^3.0.47",
75
- "@cacheable/net": "^2.0.6",
76
- "ai": "^6.0.134",
72
+ "@ai-sdk/anthropic": "^3.0.69",
73
+ "@ai-sdk/google": "^3.0.63",
74
+ "@ai-sdk/openai": "^3.0.53",
75
+ "@cacheable/net": "^2.0.7",
76
+ "ai": "^6.0.164",
77
77
  "colorette": "^2.0.20",
78
- "ecto": "^4.8.3",
79
- "feed": "^5.2.0",
80
- "hashery": "^1.5.1",
78
+ "ecto": "^4.8.4",
79
+ "hashery": "^2.0.0",
81
80
  "jiti": "^2.6.1",
82
81
  "serve-handler": "^6.1.7",
83
82
  "update-notifier": "^7.3.1",
84
- "writr": "^6.1.0"
83
+ "writr": "^6.1.1"
85
84
  },
86
85
  devDependencies: {
87
- "@biomejs/biome": "^2.4.8",
88
- "@playwright/test": "^1.58.2",
89
- "@types/express": "^5.0.6",
90
- "@types/js-yaml": "^4.0.9",
91
- "@types/node": "^25.5.0",
86
+ "@biomejs/biome": "^2.4.12",
87
+ "@playwright/test": "^1.59.1",
88
+ "@types/node": "^25.6.0",
92
89
  "@types/serve-handler": "^6.1.4",
93
90
  "@types/update-notifier": "^6.0.8",
94
- "@vitest/coverage-v8": "^4.1.0",
95
- "dotenv": "^17.3.1",
91
+ "@vitest/coverage-v8": "^4.1.4",
92
+ "dotenv": "^17.4.2",
96
93
  "postject": "1.0.0-alpha.6",
97
94
  "rimraf": "^6.1.3",
98
- "tsdown": "^0.21.7",
95
+ "tsdown": "^0.21.9",
99
96
  "tsx": "^4.21.0",
100
- "typescript": "^5.9.3",
101
- "vitest": "^4.1.0"
97
+ "typescript": "^6.0.2",
98
+ "vitest": "^4.1.4"
102
99
  },
103
100
  files: [
104
101
  "dist",
@@ -379,10 +376,13 @@ function parseOpenApiSpec(specJson) {
379
376
  const requestBody = extractRequestBody(operation, spec);
380
377
  const responses = extractResponses(operation, spec);
381
378
  const codeExamples = generateCodeExamples(method, pathStr, servers.length > 0 ? servers[0].url : "", parameters, requestBody);
379
+ const operationId = operation.operationId ?? `${method}-${pathStr.replaceAll(/[^a-zA-Z0-9]/g, "-")}`;
380
+ const methodUpper = method.toUpperCase();
382
381
  const apiOperation = {
383
- id: slugify(operation.operationId ?? `${method}-${pathStr.replaceAll(/[^a-zA-Z0-9]/g, "-")}`),
382
+ id: slugify(operationId),
384
383
  method,
385
- methodUpper: method.toUpperCase(),
384
+ methodUpper,
385
+ methodShort: methodUpper === "DELETE" ? "DEL" : methodUpper === "OPTIONS" ? "OPT" : methodUpper,
386
386
  path: pathStr,
387
387
  summary: operation.summary ?? "",
388
388
  description: operation.description ?? "",
@@ -1093,6 +1093,22 @@ function hashFile(hash, filePath) {
1093
1093
  const content = fs.readFileSync(filePath);
1094
1094
  return hash.toHashSync(content);
1095
1095
  }
1096
+ function tryHashFile(hash, filePath) {
1097
+ try {
1098
+ return hashFile(hash, filePath);
1099
+ } catch (error) {
1100
+ if (error.code === "ENOENT") return;
1101
+ /* v8 ignore next 2 -- @preserve */
1102
+ throw error;
1103
+ }
1104
+ }
1105
+ function hashConfigFile(hash, sitePath) {
1106
+ for (const name of ["docula.config.ts", "docula.config.mjs"]) {
1107
+ const configPath = path.join(sitePath, name);
1108
+ if (fs.existsSync(configPath)) return hashFile(hash, configPath);
1109
+ }
1110
+ return "";
1111
+ }
1096
1112
  function hashOptions(hash, options) {
1097
1113
  const relevant = {
1098
1114
  siteUrl: options.siteUrl,
@@ -1119,19 +1135,29 @@ function hashOptions(hash, options) {
1119
1135
  ai: options.ai,
1120
1136
  googleTagManager: options.googleTagManager
1121
1137
  };
1122
- return hash.toHashSync(JSON.stringify(relevant));
1138
+ const optionsHash = hash.toHashSync(JSON.stringify(relevant));
1139
+ const configHash = hashConfigFile(hash, options.sitePath);
1140
+ return hash.toHashSync(`${optionsHash}:${configHash}`);
1123
1141
  }
1124
1142
  function hashTemplateDirectory(hash, templatePath) {
1125
1143
  /* v8 ignore next 3 -- @preserve */
1126
1144
  if (!fs.existsSync(templatePath)) return "";
1127
- const hashes = listFilesRecursive(templatePath).map((f) => hashFile(hash, path.join(templatePath, f)));
1145
+ const files = listFilesRecursive(templatePath);
1146
+ const hashes = [];
1147
+ for (const f of files) {
1148
+ const fileHash = tryHashFile(hash, path.join(templatePath, f));
1149
+ if (fileHash !== void 0) hashes.push(fileHash);
1150
+ }
1128
1151
  return hash.toHashSync(hashes.join(""));
1129
1152
  }
1130
1153
  function hashSourceFiles(hash, dir) {
1131
1154
  const hashes = {};
1132
1155
  if (!fs.existsSync(dir)) return hashes;
1133
1156
  const files = listFilesRecursive(dir);
1134
- for (const file of files) hashes[file] = hashFile(hash, path.join(dir, file));
1157
+ for (const file of files) {
1158
+ const fileHash = tryHashFile(hash, path.join(dir, file));
1159
+ if (fileHash !== void 0) hashes[file] = fileHash;
1160
+ }
1135
1161
  return hashes;
1136
1162
  }
1137
1163
  function recordsEqual(a, b) {
@@ -2739,6 +2765,7 @@ var DoculaBuilder = class {
2739
2765
  _console;
2740
2766
  _hash = new Hashery();
2741
2767
  onReleaseChangelog;
2768
+ onAutoReadme;
2742
2769
  get console() {
2743
2770
  return this._console;
2744
2771
  }
@@ -3014,9 +3041,16 @@ var DoculaBuilder = class {
3014
3041
  if (packageJson.name && typeof packageJson.name === "string") readmeContent = `# ${packageJson.name}\n\n${readmeContent}`;
3015
3042
  } catch {}
3016
3043
  }
3044
+ let content = readmeContent;
3045
+ if (this.onAutoReadme) try {
3046
+ content = await this.onAutoReadme(content, cwdReadmePath, this._console);
3047
+ } catch (error) {
3048
+ const message = error instanceof Error ? error.message : String(error);
3049
+ this._console.error(`onAutoReadme error: ${message}`);
3050
+ }
3017
3051
  return {
3018
3052
  sourcePath: cwdReadmePath,
3019
- content: readmeContent
3053
+ content
3020
3054
  };
3021
3055
  }
3022
3056
  async getGithubData(githubPath) {
@@ -3396,6 +3430,8 @@ var Docula = class {
3396
3430
  const builder = new DoculaBuilder(Object.assign(this.options, { console: this._console }));
3397
3431
  /* v8 ignore next 4 -- @preserve */
3398
3432
  if (this._configFileModule.onReleaseChangelog) builder.onReleaseChangelog = this._configFileModule.onReleaseChangelog;
3433
+ /* v8 ignore next 4 -- @preserve */
3434
+ if (this._configFileModule.onAutoReadme) builder.onAutoReadme = this._configFileModule.onAutoReadme;
3399
3435
  await builder.build();
3400
3436
  return builder;
3401
3437
  }
@@ -3496,12 +3532,16 @@ var Docula = class {
3496
3532
  }
3497
3533
  else {
3498
3534
  const { createJiti } = await import("jiti");
3499
- this._configFileModule = await createJiti(import.meta.url, { interopDefault: true }).import(absolutePath);
3535
+ const jiti = createJiti(import.meta.url, { interopDefault: true });
3536
+ this._configFileModule = await jiti.import(absolutePath);
3500
3537
  }
3501
3538
  return;
3502
3539
  }
3503
3540
  /* v8 ignore next -- @preserve */
3504
- if (fs.existsSync(mjsConfigFile)) this._configFileModule = await import(pathToFileURL(path.resolve(mjsConfigFile)).href);
3541
+ if (fs.existsSync(mjsConfigFile)) {
3542
+ const absolutePath = path.resolve(mjsConfigFile);
3543
+ this._configFileModule = await import(pathToFileURL(absolutePath).href);
3544
+ }
3505
3545
  }
3506
3546
  /**
3507
3547
  * Watch the site path for file changes and rebuild on change
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docula",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Beautiful Website for Your Projects",
5
5
  "type": "module",
6
6
  "main": "./dist/docula.js",
@@ -40,36 +40,33 @@
40
40
  "docula": "./bin/docula.js"
41
41
  },
42
42
  "dependencies": {
43
- "@ai-sdk/anthropic": "^3.0.63",
44
- "@ai-sdk/google": "^3.0.52",
45
- "@ai-sdk/openai": "^3.0.47",
46
- "@cacheable/net": "^2.0.6",
47
- "ai": "^6.0.134",
43
+ "@ai-sdk/anthropic": "^3.0.69",
44
+ "@ai-sdk/google": "^3.0.63",
45
+ "@ai-sdk/openai": "^3.0.53",
46
+ "@cacheable/net": "^2.0.7",
47
+ "ai": "^6.0.164",
48
48
  "colorette": "^2.0.20",
49
- "ecto": "^4.8.3",
50
- "feed": "^5.2.0",
51
- "hashery": "^1.5.1",
49
+ "ecto": "^4.8.4",
50
+ "hashery": "^2.0.0",
52
51
  "jiti": "^2.6.1",
53
52
  "serve-handler": "^6.1.7",
54
53
  "update-notifier": "^7.3.1",
55
- "writr": "^6.1.0"
54
+ "writr": "^6.1.1"
56
55
  },
57
56
  "devDependencies": {
58
- "@biomejs/biome": "^2.4.8",
59
- "@playwright/test": "^1.58.2",
60
- "@types/express": "^5.0.6",
61
- "@types/js-yaml": "^4.0.9",
62
- "@types/node": "^25.5.0",
57
+ "@biomejs/biome": "^2.4.12",
58
+ "@playwright/test": "^1.59.1",
59
+ "@types/node": "^25.6.0",
63
60
  "@types/serve-handler": "^6.1.4",
64
61
  "@types/update-notifier": "^6.0.8",
65
- "@vitest/coverage-v8": "^4.1.0",
66
- "dotenv": "^17.3.1",
62
+ "@vitest/coverage-v8": "^4.1.4",
63
+ "dotenv": "^17.4.2",
67
64
  "postject": "1.0.0-alpha.6",
68
65
  "rimraf": "^6.1.3",
69
- "tsdown": "^0.21.7",
66
+ "tsdown": "^0.21.9",
70
67
  "tsx": "^4.21.0",
71
- "typescript": "^5.9.3",
72
- "vitest": "^4.1.0"
68
+ "typescript": "^6.0.2",
69
+ "vitest": "^4.1.4"
73
70
  },
74
71
  "files": [
75
72
  "dist",
@@ -39,8 +39,8 @@
39
39
  <div class="api-sidebar__group-items">
40
40
  {{#each this.operations}}
41
41
  <a href="#{{this.id}}" class="api-sidebar__item" data-method="{{this.method}}" data-path="{{this.path}}">
42
- <span class="method-badge method-badge--{{this.method}}">{{this.methodUpper}}</span>
43
42
  <span class="api-sidebar__item-path">{{this.path}}</span>
43
+ <span class="method-badge method-badge--{{this.method}}">{{this.methodShort}}</span>
44
44
  </a>
45
45
  {{/each}}
46
46
  </div>
@@ -100,17 +100,20 @@
100
100
  }
101
101
 
102
102
  .api-sidebar__group-items {
103
- padding: 2px 0 8px 0;
103
+ padding: 4px 0 8px 12px;
104
+ margin-left: 10px;
105
+ border-left: 1px solid var(--border);
104
106
  }
105
107
 
106
108
  .api-sidebar__item {
107
109
  display: flex;
108
110
  align-items: center;
109
- gap: 8px;
110
- padding: 4px 10px;
111
+ justify-content: space-between;
112
+ gap: 12px;
113
+ padding: 8px 10px;
111
114
  font-size: 13px;
112
115
  border-radius: 4px;
113
- color: var(--fg);
116
+ color: var(--muted-fg);
114
117
  white-space: nowrap;
115
118
  overflow: hidden;
116
119
  text-overflow: ellipsis;
@@ -125,8 +128,23 @@
125
128
  }
126
129
 
127
130
  .api-sidebar__item-path {
131
+ min-width: 0;
128
132
  overflow: hidden;
129
133
  text-overflow: ellipsis;
134
+ color: var(--muted-fg);
135
+ }
136
+
137
+ .api-sidebar__item--active .api-sidebar__item-path {
138
+ color: var(--fg);
139
+ }
140
+
141
+ .api-sidebar__item .method-badge {
142
+ background: transparent;
143
+ padding: 0;
144
+ min-width: 0;
145
+ border-radius: 0;
146
+ font-size: 11px;
147
+ letter-spacing: 0.5px;
130
148
  }
131
149
 
132
150
  /* Method Badges */
@@ -289,6 +289,44 @@ body {
289
289
  letter-spacing: 0.24px;
290
290
  }
291
291
 
292
+ .nav-sidebar__title:has(.nav-sidebar__toggle) {
293
+ padding: 0;
294
+ }
295
+
296
+ .nav-sidebar__toggle {
297
+ all: unset;
298
+ box-sizing: border-box;
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: space-between;
302
+ width: 100%;
303
+ cursor: pointer;
304
+ padding: 0 8px 0 10px;
305
+ font: inherit;
306
+ color: inherit;
307
+ text-transform: inherit;
308
+ letter-spacing: inherit;
309
+ }
310
+
311
+ .nav-sidebar__toggle:focus-visible {
312
+ outline: 2px solid var(--accent, currentColor);
313
+ outline-offset: 2px;
314
+ border-radius: 4px;
315
+ }
316
+
317
+ .nav-sidebar__chevron {
318
+ flex-shrink: 0;
319
+ transition: transform 150ms ease;
320
+ }
321
+
322
+ .nav-sidebar__section--collapsed .nav-sidebar__chevron {
323
+ transform: rotate(-90deg);
324
+ }
325
+
326
+ .nav-sidebar__section--collapsed .nav-sidebar__list {
327
+ display: none;
328
+ }
329
+
292
330
  .nav-sidebar__list {
293
331
  margin-block: 2px;
294
332
  }
@@ -227,6 +227,48 @@
227
227
  }
228
228
  });
229
229
 
230
+ // Sidebar section collapse/expand
231
+ const SIDEBAR_STORAGE_KEY = 'docula:sidebar-sections';
232
+ const collapsibleSections = document.querySelectorAll('.nav-sidebar__section--collapsible');
233
+ if (collapsibleSections.length > 0) {
234
+ let storedSectionState = {};
235
+ try {
236
+ storedSectionState = JSON.parse(localStorage.getItem(SIDEBAR_STORAGE_KEY) || '{}');
237
+ } catch (e) { storedSectionState = {}; }
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
+ const setSectionOpen = (section, toggle, open) => {
244
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
245
+ section.classList.toggle('nav-sidebar__section--collapsed', !open);
246
+ };
247
+
248
+ collapsibleSections.forEach((section, idx) => {
249
+ const toggle = section.querySelector('.nav-sidebar__toggle');
250
+ const list = section.querySelector('.nav-sidebar__list');
251
+ if (!toggle || !list) return;
252
+ const listId = 'nav-sidebar-section-' + idx;
253
+ list.id = listId;
254
+ toggle.setAttribute('aria-controls', listId);
255
+
256
+ const key = 'section-' + idx;
257
+ const hasStored = Object.prototype.hasOwnProperty.call(storedSectionState, key);
258
+ const isOpen = hasStored ? !!storedSectionState[key] : section === defaultOpenSection;
259
+ setSectionOpen(section, toggle, isOpen);
260
+
261
+ toggle.addEventListener('click', () => {
262
+ const next = toggle.getAttribute('aria-expanded') !== 'true';
263
+ setSectionOpen(section, toggle, next);
264
+ storedSectionState[key] = next;
265
+ try {
266
+ localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(storedSectionState));
267
+ } catch (e) { /* storage unavailable */ }
268
+ });
269
+ });
270
+ }
271
+
230
272
  // Active header nav link highlighting
231
273
  const navLinks = document.querySelectorAll('.header-bottom__item');
232
274
  navLinks.forEach((link) => {
@@ -1,7 +1,12 @@
1
1
  {{#forEach sidebarItems}}
2
2
  {{#if children}}
3
- <section class="nav-sidebar__section">
4
- <h2 class="nav-sidebar__title">{{name}}</h2>
3
+ <section class="nav-sidebar__section nav-sidebar__section--collapsible">
4
+ <h2 class="nav-sidebar__title">
5
+ <button type="button" class="nav-sidebar__toggle" aria-expanded="true">
6
+ <span class="nav-sidebar__toggle-label">{{name}}</span>
7
+ <svg class="nav-sidebar__chevron" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
8
+ </button>
9
+ </h2>
5
10
  <ul class="nav-sidebar__list">
6
11
  {{#forEach children}}
7
12
  <li>