@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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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);
@@ -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
+ }