@docubook/docs-tree 1.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 +76 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +35 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +86 -0
- package/package.json +30 -0
- package/src/cli.ts +39 -0
- package/src/index.ts +114 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @docubook/docs-tree
|
|
2
|
+
|
|
3
|
+
Prebuild navigation tree for DocuBook documentation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### npm
|
|
8
|
+
```bash
|
|
9
|
+
npm install --save-dev @docubook/docs-tree
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
### pnpm
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add -D @docubook/docs-tree
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### yarn
|
|
18
|
+
```bash
|
|
19
|
+
yarn add -D @docubook/docs-tree
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### bun
|
|
23
|
+
```bash
|
|
24
|
+
bun add -d @docubook/docs-tree
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### npm
|
|
30
|
+
```bash
|
|
31
|
+
npx @docubook/docs-tree ./docs ./docu.json ./lib/docs-tree.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### pnpm
|
|
35
|
+
```bash
|
|
36
|
+
pnpm dlx @docubook/docs-tree ./docs ./docu.json ./lib/docs-tree.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### yarn
|
|
40
|
+
```bash
|
|
41
|
+
yarn dlx @docubook/docs-tree ./docs ./docu.json ./lib/docs-tree.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### bun
|
|
45
|
+
```bash
|
|
46
|
+
bunx @docubook/docs-tree ./docs ./docu.json ./lib/docs-tree.json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or run with default paths:
|
|
50
|
+
```bash
|
|
51
|
+
npx @docubook/docs-tree # Uses ./docs, ./docu.json, ./docs-tree.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Implementation in DocuBook Projects
|
|
55
|
+
|
|
56
|
+
Add to your `package.json` scripts:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"scripts": {
|
|
61
|
+
"prebuild": "pnpx @docubook/docs-tree ./docs ./docu.json ./lib/docs-tree.json",
|
|
62
|
+
"build": "pnpm run prebuild && next build"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Import in your routes file:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// lib/route.ts
|
|
71
|
+
import docsTree from "@/lib/docs-tree.json";
|
|
72
|
+
|
|
73
|
+
export const ROUTES = docsTree;
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The navigation tree will be prebuilt before each production build, with intelligent caching to skip regeneration when no changes are detected.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const index_1 = require("./index");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
// Default values
|
|
11
|
+
const defaultDocsDir = './docs';
|
|
12
|
+
const defaultConfigPath = './docu.json';
|
|
13
|
+
const defaultOutputPath = './docs-tree.json';
|
|
14
|
+
let docsDir;
|
|
15
|
+
let configPath;
|
|
16
|
+
let outputPath;
|
|
17
|
+
if (args.length === 0) {
|
|
18
|
+
// Use defaults
|
|
19
|
+
docsDir = defaultDocsDir;
|
|
20
|
+
configPath = defaultConfigPath;
|
|
21
|
+
outputPath = defaultOutputPath;
|
|
22
|
+
}
|
|
23
|
+
else if (args.length === 3) {
|
|
24
|
+
[docsDir, configPath, outputPath] = args;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.error('Usage: docs-tree [docs-dir] [config-path] [output-path]');
|
|
28
|
+
console.error('If no arguments provided, defaults will be used:');
|
|
29
|
+
console.error(` docs-dir: ${defaultDocsDir}`);
|
|
30
|
+
console.error(` config-path: ${defaultConfigPath}`);
|
|
31
|
+
console.error(` output-path: ${defaultOutputPath}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const builder = new index_1.DocsTreeBuilder(path_1.default.resolve(docsDir), path_1.default.resolve(configPath), path_1.default.resolve(outputPath));
|
|
35
|
+
builder.build().catch(console.error);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface DocuConfig {
|
|
2
|
+
routes: any[];
|
|
3
|
+
}
|
|
4
|
+
export interface EachRoute {
|
|
5
|
+
title: string;
|
|
6
|
+
href: string;
|
|
7
|
+
noLink?: boolean;
|
|
8
|
+
context?: any;
|
|
9
|
+
items?: EachRoute[];
|
|
10
|
+
}
|
|
11
|
+
export declare class DocsTreeBuilder {
|
|
12
|
+
private docsDir;
|
|
13
|
+
private configPath;
|
|
14
|
+
private outputPath;
|
|
15
|
+
private cachePath;
|
|
16
|
+
constructor(docsDir: string, configPath: string, outputPath: string);
|
|
17
|
+
private getHash;
|
|
18
|
+
private scanDocs;
|
|
19
|
+
private buildRoutes;
|
|
20
|
+
build(): Promise<void>;
|
|
21
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DocsTreeBuilder = void 0;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
class DocsTreeBuilder {
|
|
11
|
+
constructor(docsDir, configPath, outputPath) {
|
|
12
|
+
this.docsDir = path_1.default.resolve(docsDir);
|
|
13
|
+
this.configPath = path_1.default.resolve(configPath);
|
|
14
|
+
this.outputPath = path_1.default.resolve(outputPath);
|
|
15
|
+
this.cachePath = path_1.default.join(path_1.default.dirname(this.outputPath), '.docs-tree-cache.json');
|
|
16
|
+
}
|
|
17
|
+
async getHash() {
|
|
18
|
+
const configContent = await fs_extra_1.default.readFile(this.configPath, 'utf-8');
|
|
19
|
+
const docsStats = await fs_extra_1.default.stat(this.docsDir);
|
|
20
|
+
const hash = crypto_1.default.createHash('md5');
|
|
21
|
+
hash.update(configContent);
|
|
22
|
+
hash.update(docsStats.mtime.toISOString());
|
|
23
|
+
return hash.digest('hex');
|
|
24
|
+
}
|
|
25
|
+
async scanDocs(dir) {
|
|
26
|
+
const items = await fs_extra_1.default.readdir(dir);
|
|
27
|
+
const files = [];
|
|
28
|
+
for (const item of items) {
|
|
29
|
+
const fullPath = path_1.default.join(dir, item);
|
|
30
|
+
const stat = await fs_extra_1.default.stat(fullPath);
|
|
31
|
+
if (stat.isFile() && (item.endsWith('.mdx') || item.endsWith('.md'))) {
|
|
32
|
+
files.push(item.replace(/\.(mdx|md)$/, ''));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
async buildRoutes(routes, basePath = '') {
|
|
38
|
+
const result = [];
|
|
39
|
+
for (const route of routes) {
|
|
40
|
+
const fullHref = basePath + route.href;
|
|
41
|
+
const routeItem = {
|
|
42
|
+
title: route.title,
|
|
43
|
+
href: route.href, // keep relative
|
|
44
|
+
noLink: route.noLink,
|
|
45
|
+
context: route.context
|
|
46
|
+
};
|
|
47
|
+
if (route.items) {
|
|
48
|
+
routeItem.items = await this.buildRoutes(route.items, fullHref);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// Jika tidak ada items, cek apakah ada folder atau file di docs
|
|
52
|
+
const docsSubDir = path_1.default.join(this.docsDir, fullHref.replace(/^\//, ''));
|
|
53
|
+
if (await fs_extra_1.default.pathExists(docsSubDir)) {
|
|
54
|
+
const files = await this.scanDocs(docsSubDir);
|
|
55
|
+
routeItem.items = files.map(file => ({
|
|
56
|
+
title: file.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
|
57
|
+
href: `/${file}`
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
result.push(routeItem);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
async build() {
|
|
66
|
+
const currentHash = await this.getHash();
|
|
67
|
+
// Cek cache
|
|
68
|
+
if (await fs_extra_1.default.pathExists(this.cachePath)) {
|
|
69
|
+
const cache = await fs_extra_1.default.readJson(this.cachePath);
|
|
70
|
+
if (cache.hash === currentHash && await fs_extra_1.default.pathExists(this.outputPath)) {
|
|
71
|
+
console.log('Using cached docs-tree.json');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Baca config
|
|
76
|
+
const config = await fs_extra_1.default.readJson(this.configPath);
|
|
77
|
+
// Build routes
|
|
78
|
+
const navigationRoutes = await this.buildRoutes(config.routes);
|
|
79
|
+
// Write output
|
|
80
|
+
await fs_extra_1.default.writeJson(this.outputPath, navigationRoutes, { spaces: 2 });
|
|
81
|
+
// Update cache
|
|
82
|
+
await fs_extra_1.default.writeJson(this.cachePath, { hash: currentHash });
|
|
83
|
+
console.log('Generated docs-tree.json');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.DocsTreeBuilder = DocsTreeBuilder;
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@docubook/docs-tree",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Prebuild navigation tree for DocuBook documentation",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"@docubook/docs-tree": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["docubook", "navigation", "tree", "docs"],
|
|
14
|
+
"homepage": "https://docubook.pro/",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/DocuBook/docubook"
|
|
18
|
+
},
|
|
19
|
+
"author": "wildan.nrs",
|
|
20
|
+
"author-url": "https://wildan.dev",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/fs-extra": "^11.0.0",
|
|
24
|
+
"@types/node": "^20.0.0",
|
|
25
|
+
"typescript": "^5.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"fs-extra": "^11.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { DocsTreeBuilder } from './index';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
|
|
8
|
+
// Default values
|
|
9
|
+
const defaultDocsDir = './docs';
|
|
10
|
+
const defaultConfigPath = './docu.json';
|
|
11
|
+
const defaultOutputPath = './docs-tree.json';
|
|
12
|
+
|
|
13
|
+
let docsDir: string;
|
|
14
|
+
let configPath: string;
|
|
15
|
+
let outputPath: string;
|
|
16
|
+
|
|
17
|
+
if (args.length === 0) {
|
|
18
|
+
// Use defaults
|
|
19
|
+
docsDir = defaultDocsDir;
|
|
20
|
+
configPath = defaultConfigPath;
|
|
21
|
+
outputPath = defaultOutputPath;
|
|
22
|
+
} else if (args.length === 3) {
|
|
23
|
+
[docsDir, configPath, outputPath] = args;
|
|
24
|
+
} else {
|
|
25
|
+
console.error('Usage: docs-tree [docs-dir] [config-path] [output-path]');
|
|
26
|
+
console.error('If no arguments provided, defaults will be used:');
|
|
27
|
+
console.error(` docs-dir: ${defaultDocsDir}`);
|
|
28
|
+
console.error(` config-path: ${defaultConfigPath}`);
|
|
29
|
+
console.error(` output-path: ${defaultOutputPath}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const builder = new DocsTreeBuilder(
|
|
34
|
+
path.resolve(docsDir),
|
|
35
|
+
path.resolve(configPath),
|
|
36
|
+
path.resolve(outputPath)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
builder.build().catch(console.error);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
export interface DocuConfig {
|
|
6
|
+
routes: any[];
|
|
7
|
+
// tambahkan field lain jika perlu
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EachRoute {
|
|
11
|
+
title: string;
|
|
12
|
+
href: string;
|
|
13
|
+
noLink?: boolean;
|
|
14
|
+
context?: any;
|
|
15
|
+
items?: EachRoute[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class DocsTreeBuilder {
|
|
19
|
+
private docsDir: string;
|
|
20
|
+
private configPath: string;
|
|
21
|
+
private outputPath: string;
|
|
22
|
+
private cachePath: string;
|
|
23
|
+
|
|
24
|
+
constructor(docsDir: string, configPath: string, outputPath: string) {
|
|
25
|
+
this.docsDir = path.resolve(docsDir);
|
|
26
|
+
this.configPath = path.resolve(configPath);
|
|
27
|
+
this.outputPath = path.resolve(outputPath);
|
|
28
|
+
this.cachePath = path.join(path.dirname(this.outputPath), '.docs-tree-cache.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private async getHash(): Promise<string> {
|
|
32
|
+
const configContent = await fs.readFile(this.configPath, 'utf-8');
|
|
33
|
+
const docsStats = await fs.stat(this.docsDir);
|
|
34
|
+
const hash = crypto.createHash('md5');
|
|
35
|
+
hash.update(configContent);
|
|
36
|
+
hash.update(docsStats.mtime.toISOString());
|
|
37
|
+
return hash.digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async scanDocs(dir: string): Promise<string[]> {
|
|
41
|
+
const items = await fs.readdir(dir);
|
|
42
|
+
const files: string[] = [];
|
|
43
|
+
|
|
44
|
+
for (const item of items) {
|
|
45
|
+
const fullPath = path.join(dir, item);
|
|
46
|
+
const stat = await fs.stat(fullPath);
|
|
47
|
+
|
|
48
|
+
if (stat.isFile() && (item.endsWith('.mdx') || item.endsWith('.md'))) {
|
|
49
|
+
files.push(item.replace(/\.(mdx|md)$/, ''));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return files;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async buildRoutes(routes: any[], basePath: string = ''): Promise<EachRoute[]> {
|
|
57
|
+
const result: EachRoute[] = [];
|
|
58
|
+
|
|
59
|
+
for (const route of routes) {
|
|
60
|
+
const fullHref = basePath + route.href;
|
|
61
|
+
const routeItem: EachRoute = {
|
|
62
|
+
title: route.title,
|
|
63
|
+
href: route.href, // keep relative
|
|
64
|
+
noLink: route.noLink,
|
|
65
|
+
context: route.context
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (route.items) {
|
|
69
|
+
routeItem.items = await this.buildRoutes(route.items, fullHref);
|
|
70
|
+
} else {
|
|
71
|
+
// Jika tidak ada items, cek apakah ada folder atau file di docs
|
|
72
|
+
const docsSubDir = path.join(this.docsDir, fullHref.replace(/^\//, ''));
|
|
73
|
+
if (await fs.pathExists(docsSubDir)) {
|
|
74
|
+
const files = await this.scanDocs(docsSubDir);
|
|
75
|
+
routeItem.items = files.map(file => ({
|
|
76
|
+
title: file.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
|
77
|
+
href: `/${file}`
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
result.push(routeItem);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async build(): Promise<void> {
|
|
89
|
+
const currentHash = await this.getHash();
|
|
90
|
+
|
|
91
|
+
// Cek cache
|
|
92
|
+
if (await fs.pathExists(this.cachePath)) {
|
|
93
|
+
const cache = await fs.readJson(this.cachePath);
|
|
94
|
+
if (cache.hash === currentHash && await fs.pathExists(this.outputPath)) {
|
|
95
|
+
console.log('Using cached docs-tree.json');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Baca config
|
|
101
|
+
const config: DocuConfig = await fs.readJson(this.configPath);
|
|
102
|
+
|
|
103
|
+
// Build routes
|
|
104
|
+
const navigationRoutes = await this.buildRoutes(config.routes);
|
|
105
|
+
|
|
106
|
+
// Write output
|
|
107
|
+
await fs.writeJson(this.outputPath, navigationRoutes, { spaces: 2 });
|
|
108
|
+
|
|
109
|
+
// Update cache
|
|
110
|
+
await fs.writeJson(this.cachePath, { hash: currentHash });
|
|
111
|
+
|
|
112
|
+
console.log('Generated docs-tree.json');
|
|
113
|
+
}
|
|
114
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|