@marlinjai/clearify 1.11.2 → 1.13.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
@@ -14,6 +14,7 @@ An open-source documentation site generator. Turn markdown into beautiful docs.
14
14
  - **OpenAPI API Reference** — custom-built renderer for OpenAPI specs with code examples, schema viewer, and full design system integration
15
15
  - **Multi-section support** — pill-based section switcher (e.g. public docs + internal docs)
16
16
  - **Auto changelog** — drop a `CHANGELOG.md` in your project root, it appears as `/changelog`
17
+ - **README as landing page** — optionally include your project's `README.md` as the docs overview page
17
18
 
18
19
  ## Quick Start
19
20
 
@@ -0,0 +1,225 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "name": {
6
+ "type": "string"
7
+ },
8
+ "siteUrl": {
9
+ "type": "string"
10
+ },
11
+ "theme": {
12
+ "type": "object",
13
+ "properties": {
14
+ "primaryColor": {
15
+ "type": "string"
16
+ },
17
+ "mode": {
18
+ "type": "string",
19
+ "enum": [
20
+ "light",
21
+ "dark",
22
+ "auto"
23
+ ]
24
+ }
25
+ },
26
+ "additionalProperties": false
27
+ },
28
+ "logo": {
29
+ "type": "object",
30
+ "properties": {
31
+ "light": {
32
+ "type": "string"
33
+ },
34
+ "dark": {
35
+ "type": "string"
36
+ }
37
+ },
38
+ "additionalProperties": false
39
+ },
40
+ "links": {
41
+ "type": "object",
42
+ "propertyNames": {
43
+ "type": "string"
44
+ },
45
+ "additionalProperties": {
46
+ "type": "string"
47
+ }
48
+ },
49
+ "sections": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "object",
53
+ "properties": {
54
+ "label": {
55
+ "type": "string"
56
+ },
57
+ "docsDir": {
58
+ "type": "string"
59
+ },
60
+ "basePath": {
61
+ "type": "string"
62
+ },
63
+ "draft": {
64
+ "type": "boolean"
65
+ },
66
+ "sitemap": {
67
+ "type": "boolean"
68
+ },
69
+ "exclude": {
70
+ "type": "array",
71
+ "items": {
72
+ "type": "string"
73
+ }
74
+ },
75
+ "git": {
76
+ "type": "object",
77
+ "properties": {
78
+ "repo": {
79
+ "type": "string"
80
+ },
81
+ "ref": {
82
+ "type": "string"
83
+ },
84
+ "path": {
85
+ "type": "string"
86
+ },
87
+ "sparse": {
88
+ "type": "boolean"
89
+ }
90
+ },
91
+ "required": [
92
+ "repo"
93
+ ],
94
+ "additionalProperties": false
95
+ }
96
+ },
97
+ "required": [
98
+ "label",
99
+ "docsDir"
100
+ ],
101
+ "additionalProperties": false
102
+ }
103
+ },
104
+ "hub": {
105
+ "type": "object",
106
+ "properties": {
107
+ "projects": {
108
+ "default": [],
109
+ "type": "array",
110
+ "items": {
111
+ "type": "object",
112
+ "properties": {
113
+ "name": {
114
+ "type": "string"
115
+ },
116
+ "description": {
117
+ "type": "string"
118
+ },
119
+ "href": {
120
+ "type": "string"
121
+ },
122
+ "repo": {
123
+ "type": "string"
124
+ },
125
+ "status": {
126
+ "default": "active",
127
+ "type": "string",
128
+ "enum": [
129
+ "active",
130
+ "beta",
131
+ "planned",
132
+ "deprecated"
133
+ ]
134
+ },
135
+ "icon": {
136
+ "type": "string"
137
+ },
138
+ "tags": {
139
+ "type": "array",
140
+ "items": {
141
+ "type": "string"
142
+ }
143
+ },
144
+ "group": {
145
+ "type": "string"
146
+ },
147
+ "mode": {
148
+ "type": "string",
149
+ "enum": [
150
+ "link",
151
+ "embed",
152
+ "inject"
153
+ ]
154
+ },
155
+ "git": {
156
+ "type": "object",
157
+ "properties": {
158
+ "repo": {
159
+ "type": "string"
160
+ },
161
+ "ref": {
162
+ "type": "string"
163
+ },
164
+ "path": {
165
+ "type": "string"
166
+ },
167
+ "sparse": {
168
+ "type": "boolean"
169
+ }
170
+ },
171
+ "required": [
172
+ "repo"
173
+ ],
174
+ "additionalProperties": false
175
+ },
176
+ "embedSections": {
177
+ "anyOf": [
178
+ {
179
+ "type": "string",
180
+ "const": "all"
181
+ },
182
+ {
183
+ "type": "string",
184
+ "const": "public"
185
+ },
186
+ {
187
+ "type": "array",
188
+ "items": {
189
+ "type": "string"
190
+ }
191
+ }
192
+ ]
193
+ },
194
+ "injectInto": {
195
+ "type": "string"
196
+ },
197
+ "docsPath": {
198
+ "type": "string"
199
+ }
200
+ },
201
+ "required": [
202
+ "name",
203
+ "description",
204
+ "status"
205
+ ],
206
+ "additionalProperties": false
207
+ }
208
+ },
209
+ "scan": {
210
+ "type": "string"
211
+ },
212
+ "cacheDir": {
213
+ "type": "string"
214
+ }
215
+ },
216
+ "required": [
217
+ "projects"
218
+ ],
219
+ "additionalProperties": false
220
+ }
221
+ },
222
+ "additionalProperties": false,
223
+ "$id": "https://docs.lumitra.co/schemas/clearify-data.json",
224
+ "title": "Clearify Data Config"
225
+ }
@@ -6,7 +6,7 @@ import {
6
6
  import { z } from "zod";
7
7
  import { resolve, basename, dirname } from "path";
8
8
  import { pathToFileURL } from "url";
9
- import { existsSync, readFileSync, rmSync } from "fs";
9
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, rmSync, symlinkSync, readdirSync, statSync } from "fs";
10
10
  import { tmpdir } from "os";
11
11
  import { randomBytes } from "crypto";
12
12
  var NavigationItemSchema = z.lazy(
@@ -51,13 +51,15 @@ var HubProjectSchema = z.object({
51
51
  icon: z.string().optional(),
52
52
  tags: z.array(z.string()).optional(),
53
53
  group: z.string().optional(),
54
- mode: z.enum(["link", "embed"]).optional(),
54
+ mode: z.enum(["link", "embed", "inject"]).optional(),
55
55
  git: RemoteGitSourceSchema.optional(),
56
56
  embedSections: z.union([
57
57
  z.literal("all"),
58
58
  z.literal("public"),
59
59
  z.array(z.string())
60
- ]).optional()
60
+ ]).optional(),
61
+ injectInto: z.string().optional(),
62
+ docsPath: z.string().optional()
61
63
  });
62
64
  var HubProjectPartialSchema = z.object({
63
65
  description: z.string(),
@@ -98,8 +100,55 @@ var ClearifyConfigSchema = z.object({
98
100
  hub: HubConfigSchema,
99
101
  hubProject: HubProjectPartialSchema,
100
102
  customCss: z.string().optional(),
101
- headTags: z.array(z.string()).optional()
103
+ headTags: z.array(z.string()).optional(),
104
+ includeReadme: z.boolean().default(false)
102
105
  });
106
+ var ClearifyDataSchema = z.object({
107
+ name: z.string().optional(),
108
+ siteUrl: z.string().optional(),
109
+ theme: z.object({
110
+ primaryColor: z.string().optional(),
111
+ mode: z.enum(["light", "dark", "auto"]).optional()
112
+ }).optional(),
113
+ logo: z.object({
114
+ light: z.string().optional(),
115
+ dark: z.string().optional()
116
+ }).optional(),
117
+ links: z.record(z.string(), z.string()).optional(),
118
+ sections: z.array(SectionConfigSchema).optional(),
119
+ hub: HubConfigSchema
120
+ });
121
+ function deepMergeJsonWins(base, json) {
122
+ const result = { ...base };
123
+ for (const key of Object.keys(json)) {
124
+ const jsonVal = json[key];
125
+ if (jsonVal === void 0) continue;
126
+ const baseVal = base[key];
127
+ if (baseVal !== null && jsonVal !== null && typeof baseVal === "object" && typeof jsonVal === "object" && !Array.isArray(baseVal) && !Array.isArray(jsonVal)) {
128
+ result[key] = deepMergeJsonWins(baseVal, jsonVal);
129
+ } else {
130
+ result[key] = jsonVal;
131
+ }
132
+ }
133
+ return result;
134
+ }
135
+ function loadDataConfig(root) {
136
+ const dataPath = resolve(root, "clearify.data.json");
137
+ if (!existsSync(dataPath)) return {};
138
+ try {
139
+ const raw = JSON.parse(readFileSync(dataPath, "utf-8"));
140
+ return ClearifyDataSchema.parse(raw);
141
+ } catch {
142
+ return {};
143
+ }
144
+ }
145
+ function writeDataConfig(root, data) {
146
+ const validated = ClearifyDataSchema.parse(data);
147
+ const dataPath = resolve(root, "clearify.data.json");
148
+ const tmpPath = dataPath + ".tmp";
149
+ writeFileSync(tmpPath, JSON.stringify(validated, null, 2) + "\n", "utf-8");
150
+ renameSync(tmpPath, dataPath);
151
+ }
103
152
  var defaultConfig = {
104
153
  name: "Documentation",
105
154
  docsDir: "./docs",
@@ -176,20 +225,24 @@ async function loadUserConfig(root) {
176
225
  "clearify.config.js",
177
226
  "clearify.config.mjs"
178
227
  ];
228
+ let tsConfig = {};
179
229
  for (const file of configFiles) {
180
230
  const configPath = resolve(root, file);
181
231
  if (existsSync(configPath)) {
182
232
  try {
183
233
  if (file.endsWith(".ts")) {
184
- return await loadTsConfig(configPath);
234
+ tsConfig = await loadTsConfig(configPath);
235
+ } else {
236
+ const mod = await import(pathToFileURL(configPath).href);
237
+ tsConfig = mod.default ?? mod;
185
238
  }
186
- const mod = await import(pathToFileURL(configPath).href);
187
- return mod.default ?? mod;
239
+ break;
188
240
  } catch {
189
241
  }
190
242
  }
191
243
  }
192
- return {};
244
+ const jsonData = loadDataConfig(root);
245
+ return deepMergeJsonWins(tsConfig, jsonData);
193
246
  }
194
247
  function resolveConfig(userConfig = {}, root) {
195
248
  const parsed = ClearifyConfigSchema.parse(userConfig);
@@ -247,8 +300,43 @@ async function resolveSections(config, root) {
247
300
  }
248
301
  ];
249
302
  }
303
+ function buildStagingOverlay(stagingDir, originalDir, injections) {
304
+ if (existsSync(stagingDir)) {
305
+ rmSync(stagingDir, { recursive: true, force: true });
306
+ }
307
+ mkdirSync(stagingDir, { recursive: true });
308
+ const ancestorDirs = /* @__PURE__ */ new Set();
309
+ for (const targetRelPath of injections.keys()) {
310
+ const parts = targetRelPath.split("/");
311
+ for (let i = 1; i <= parts.length; i++) {
312
+ ancestorDirs.add(parts.slice(0, i).join("/"));
313
+ }
314
+ }
315
+ function mirrorDir(srcDir, destDir, relPrefix) {
316
+ if (!existsSync(srcDir)) return;
317
+ const entries = readdirSync(srcDir);
318
+ for (const entry of entries) {
319
+ const srcPath = resolve(srcDir, entry);
320
+ const destPath = resolve(destDir, entry);
321
+ const relPath = relPrefix ? `${relPrefix}/${entry}` : entry;
322
+ if (ancestorDirs.has(relPath) && statSync(srcPath).isDirectory()) {
323
+ mkdirSync(destPath, { recursive: true });
324
+ mirrorDir(srcPath, destPath, relPath);
325
+ } else {
326
+ symlinkSync(srcPath, destPath);
327
+ }
328
+ }
329
+ }
330
+ mirrorDir(originalDir, stagingDir, "");
331
+ for (const [relPath, sourcePath] of injections) {
332
+ const destPath = resolve(stagingDir, relPath);
333
+ const parentDir = dirname(destPath);
334
+ mkdirSync(parentDir, { recursive: true });
335
+ symlinkSync(sourcePath, destPath);
336
+ }
337
+ }
250
338
  async function scanHubProjects(config, root) {
251
- if (!config.hub?.scan && !config.hub?.projects?.some((p) => p.mode === "embed")) {
339
+ if (!config.hub?.scan && !config.hub?.projects?.some((p) => p.mode === "embed" || p.mode === "inject")) {
252
340
  return config;
253
341
  }
254
342
  const scannedProjects = [];
@@ -320,10 +408,69 @@ async function scanHubProjects(config, root) {
320
408
  );
321
409
  }
322
410
  }
323
- const finalSections = [...config.sections ?? [], ...embedSections];
411
+ const injectionsBySection = /* @__PURE__ */ new Map();
412
+ for (const project of merged) {
413
+ if (project.mode !== "inject" || !project.git) continue;
414
+ const cacheDir = config.hub?.cacheDir ? resolve(root, config.hub.cacheDir) : resolve(root, "node_modules/.cache/clearify-remote");
415
+ try {
416
+ const { resolveRemoteSource } = await import("./core/remote.js");
417
+ const { localPath, cached } = await resolveRemoteSource(project.git, cacheDir);
418
+ const docsPath = project.docsPath ?? "docs";
419
+ const absoluteDocsPath = resolve(localPath, docsPath);
420
+ if (!existsSync(absoluteDocsPath)) {
421
+ console.warn(
422
+ ` \u26A0 Hub inject "${project.name}": docs path "${docsPath}" not found in cloned repo.`
423
+ );
424
+ continue;
425
+ }
426
+ const targetLabel = project.injectInto;
427
+ if (!targetLabel) {
428
+ console.warn(` \u26A0 Hub inject "${project.name}": missing injectInto field.`);
429
+ continue;
430
+ }
431
+ if (!injectionsBySection.has(targetLabel)) {
432
+ injectionsBySection.set(targetLabel, /* @__PURE__ */ new Map());
433
+ }
434
+ const slugifiedName = slugify(project.name);
435
+ const relPath = project.group ? `projects/${project.group}/${slugifiedName}` : `projects/${slugifiedName}`;
436
+ injectionsBySection.get(targetLabel).set(relPath, absoluteDocsPath);
437
+ console.log(
438
+ ` Hub inject "${project.name}": ${cached ? "cached" : "cloned"} \u2192 ${relPath}`
439
+ );
440
+ } catch (err) {
441
+ console.warn(
442
+ ` \u26A0 Hub inject "${project.name}" failed:`,
443
+ err instanceof Error ? err.message : err
444
+ );
445
+ }
446
+ }
447
+ let modifiedSections = config.sections ? [...config.sections] : void 0;
448
+ if (injectionsBySection.size > 0 && modifiedSections) {
449
+ for (const [sectionLabel, injections] of injectionsBySection) {
450
+ const sectionIdx = modifiedSections.findIndex((s) => s.label === sectionLabel);
451
+ if (sectionIdx === -1) {
452
+ console.warn(` \u26A0 Hub inject: target section "${sectionLabel}" not found.`);
453
+ continue;
454
+ }
455
+ const section = modifiedSections[sectionIdx];
456
+ const sectionId = slugify(section.label);
457
+ const originalDocsDir = section.docsDir.startsWith("/") ? section.docsDir : resolve(root, section.docsDir);
458
+ const stagingDir = resolve(root, `node_modules/.cache/clearify-inject/${sectionId}`);
459
+ buildStagingOverlay(stagingDir, originalDocsDir, injections);
460
+ modifiedSections[sectionIdx] = {
461
+ ...section,
462
+ docsDir: stagingDir
463
+ };
464
+ console.log(
465
+ ` Hub inject: staging overlay for "${sectionLabel}" \u2192 ${stagingDir}`
466
+ );
467
+ }
468
+ }
469
+ const finalSections = [...modifiedSections ?? config.sections ?? [], ...embedSections];
470
+ const sectionsChanged = embedSections.length > 0 || injectionsBySection.size > 0;
324
471
  return {
325
472
  ...config,
326
- ...embedSections.length > 0 ? { sections: finalSections } : {},
473
+ ...sectionsChanged ? { sections: finalSections } : {},
327
474
  hub: {
328
475
  ...config.hub,
329
476
  projects: merged
@@ -332,6 +479,10 @@ async function scanHubProjects(config, root) {
332
479
  }
333
480
 
334
481
  export {
482
+ ClearifyDataSchema,
483
+ deepMergeJsonWins,
484
+ loadDataConfig,
485
+ writeDataConfig,
335
486
  defaultConfig,
336
487
  loadUserConfig,
337
488
  resolveConfig,
@@ -133,9 +133,30 @@ function buildRoutes(docs, sectionId) {
133
133
  ...sectionId ? { sectionId } : {}
134
134
  }));
135
135
  }
136
+ function extractReadmeTitle(content) {
137
+ const match = content.match(/^#\s+(.+)$/m);
138
+ return match ? match[1].trim() : "Overview";
139
+ }
136
140
  function buildSectionData(section, rootFiles, customNavigation) {
137
141
  const docs = scanDocs(section.docsDir, section.exclude, section.basePath);
138
142
  if (section.basePath === "/") {
143
+ if (rootFiles?.readmePath && existsSync(rootFiles.readmePath)) {
144
+ const hasIndex = docs.some((d) => d.routePath === "/");
145
+ if (!hasIndex) {
146
+ const content = readFileSync(rootFiles.readmePath, "utf-8");
147
+ const { data } = matter(content);
148
+ const title = extractReadmeTitle(content);
149
+ docs.unshift({
150
+ filePath: rootFiles.readmePath,
151
+ routePath: "/",
152
+ frontmatter: {
153
+ title: data.title ?? title,
154
+ description: data.description ?? "Project overview",
155
+ order: -1
156
+ }
157
+ });
158
+ }
159
+ }
139
160
  if (rootFiles?.changelogPath && existsSync(rootFiles.changelogPath)) {
140
161
  const content = readFileSync(rootFiles.changelogPath, "utf-8");
141
162
  const { data } = matter(content);