docnest 0.1.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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +83 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +79 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +814 -0
- package/package.json +60 -0
- package/server/favicon.ico +0 -0
- package/server/static/css/highlight.css +72 -0
- package/server/static/css/styles.css +5762 -0
- package/server/static/favicon.ico +0 -0
- package/server/static/img/404-deep-space-light.webp +0 -0
- package/server/static/img/404-deep-space.webp +0 -0
- package/server/static/js/core/doc-client-nav.js +260 -0
- package/server/static/js/core/doc-pdf-export.js +579 -0
- package/server/static/js/core/mermaid-init.js +36 -0
- package/server/static/js/core/sidebar.js +141 -0
- package/server/static/js/core/theme.js +138 -0
- package/server/static/js/diagram/diagram-controls.js +90 -0
- package/server/static/js/diagram/diagram-download.js +594 -0
- package/server/static/js/diagram/diagram-drag.js +157 -0
- package/server/static/js/diagram/diagram-viewer.js +405 -0
- package/server/static/js/diagram/diagram-zoom.js +67 -0
- package/server/static/js/utils/app.js +14 -0
- package/server/static/js/utils/modal-stack.js +33 -0
- package/server/views/dir.ejs +142 -0
- package/server/views/doc.ejs +483 -0
- package/server/views/index.ejs +77 -0
- package/server/views/not-found.ejs +57 -0
- package/server/views/partials/breadcrumbs.ejs +15 -0
- package/server/views/partials/diagram-viewer.ejs +16 -0
- package/server/views/partials/header.ejs +37 -0
- package/server/views/partials/sidebar.ejs +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DocNest contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# DocNest
|
|
2
|
+
|
|
3
|
+
DocNest 是一个可复用的本地 Markdown 文档中心。文档内容归使用它的项目所有,DocNest 只提供查看、导航、Mermaid 图表、导出和本地 Watch 能力。
|
|
4
|
+
|
|
5
|
+
## 快速接入
|
|
6
|
+
|
|
7
|
+
在项目根目录执行:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install docnest@0.1.0
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
项目默认读取 `docs/`,可直接启动:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx docnest serve
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
默认只监听 `127.0.0.1`,从 3000 开始查找可用端口。启动时会自动打开浏览器;使用 `--no-open` 可关闭。
|
|
20
|
+
|
|
21
|
+
## 配置
|
|
22
|
+
|
|
23
|
+
复制 `docnest.config.example.mjs` 为项目根目录的 `docnest.config.mjs`,配置项目自己的标题、目录顺序、存储键前缀和 PDF 水印。
|
|
24
|
+
|
|
25
|
+
水印默认关闭。开启后,PDF 导出弹窗会读取配置文字,并允许本次导出临时修改;临时修改不会写回文件或浏览器存储。
|
|
26
|
+
|
|
27
|
+
如需显式指定端口、文档目录或监听地址:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx docnest serve --port 3000 --docs-dir docs --host 127.0.0.1
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`--host 0.0.0.0` 只应由项目自己的生产启动适配层显式使用;DocNest 不提供远程发布、SSH、Docker、云厂商流水线、SSO、Redis 或对象存储能力。
|
|
34
|
+
|
|
35
|
+
若宿主项目需要固定生产端口,可在适配层设置 `DOCS_PORT_FALLBACK=false`;普通本地启动保持从起始端口向后查找空闲端口。
|
|
36
|
+
|
|
37
|
+
## 嵌入现有 Node 服务
|
|
38
|
+
|
|
39
|
+
需要接入项目自己的鉴权或文档来源时,在加载 `docnest/server` 前注入中间件和文档存储适配器,然后使用导出的 Express/Socket.IO 实例:
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
globalThis.__DOCNEST_BEFORE_DOCUMENT_ROUTES__ = [projectAuthMiddleware]
|
|
43
|
+
globalThis.__DOCNEST_DOCUMENT_STORE__ = projectDocumentStore
|
|
44
|
+
process.env.DOCS_AUTOSTART = 'false'
|
|
45
|
+
|
|
46
|
+
const { app, io, startServer } = require('docnest/server')
|
|
47
|
+
io.use(projectSocketAuth)
|
|
48
|
+
startServer(3000)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
存储适配器实现 `name`、`readDir`、`fileExists`、`readFile`、`readAsset`,可选实现 `ready`;这样文档内容仍由宿主项目提供,DocNest 只负责展示和交互。
|
|
52
|
+
|
|
53
|
+
## 开发
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install
|
|
57
|
+
npm test
|
|
58
|
+
npm run pack:check
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
最低支持 Node.js 20,验证 Node.js 20 和 22。
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
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 node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_url_1 = require("node:url");
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
function parseArgs(args) {
|
|
11
|
+
const options = {};
|
|
12
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
13
|
+
const arg = args[index];
|
|
14
|
+
const next = args[index + 1];
|
|
15
|
+
if (arg === '--port' && next) {
|
|
16
|
+
options.port = Number(next);
|
|
17
|
+
index += 1;
|
|
18
|
+
}
|
|
19
|
+
else if (arg === '--host' && next) {
|
|
20
|
+
options.host = next;
|
|
21
|
+
index += 1;
|
|
22
|
+
}
|
|
23
|
+
else if (arg === '--docs-dir' && next) {
|
|
24
|
+
options.docsDir = next;
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
else if (arg === '--no-open') {
|
|
28
|
+
options.openBrowser = false;
|
|
29
|
+
}
|
|
30
|
+
else if (arg === '--open') {
|
|
31
|
+
options.openBrowser = true;
|
|
32
|
+
}
|
|
33
|
+
else if (arg === '--no-watch') {
|
|
34
|
+
options.watch = false;
|
|
35
|
+
}
|
|
36
|
+
else if (arg === '--watch') {
|
|
37
|
+
options.watch = true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return options;
|
|
41
|
+
}
|
|
42
|
+
function applyCliOverrides(config, options) {
|
|
43
|
+
return {
|
|
44
|
+
...config,
|
|
45
|
+
docsDir: options.docsDir ? node_path_1.default.resolve(config.projectRoot, options.docsDir) : config.docsDir,
|
|
46
|
+
server: {
|
|
47
|
+
...config.server,
|
|
48
|
+
...(Number.isInteger(options.port) && options.port && options.port > 0 ? { port: options.port } : {}),
|
|
49
|
+
...(options.host ? { host: options.host } : {}),
|
|
50
|
+
...(typeof options.openBrowser === 'boolean' ? { openBrowser: options.openBrowser } : {}),
|
|
51
|
+
...(typeof options.watch === 'boolean' ? { watch: options.watch } : {}),
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function exposeConfig(config) {
|
|
56
|
+
process.env.DOCS_ROOT = config.docsDir;
|
|
57
|
+
process.env.DOCS_TITLE = config.site.title;
|
|
58
|
+
process.env.DOCS_SUBTITLE = config.site.subtitle;
|
|
59
|
+
process.env.DOCS_LOGO = config.site.logo;
|
|
60
|
+
process.env.DOCS_STORAGE_KEY_PREFIX = config.site.storageKeyPrefix;
|
|
61
|
+
process.env.DOCS_ROOT_DIRECTORY_ORDER = JSON.stringify(config.navigation.rootDirectoryOrder);
|
|
62
|
+
process.env.DOCS_WATERMARK_ENABLED = String(config.export.watermark.enabled);
|
|
63
|
+
process.env.DOCS_WATERMARK_TEXT = config.export.watermark.text;
|
|
64
|
+
process.env.DOCS_HOST = config.server.host;
|
|
65
|
+
process.env.DOCS_PORT_START = String(config.server.port);
|
|
66
|
+
process.env.DOCS_WATCH_ENABLED = String(config.server.watch);
|
|
67
|
+
process.env.DOCS_OPEN_BROWSER = String(config.server.openBrowser);
|
|
68
|
+
}
|
|
69
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
70
|
+
async function main() {
|
|
71
|
+
const [command = 'serve', ...args] = process.argv.slice(2);
|
|
72
|
+
if (command !== 'serve') {
|
|
73
|
+
throw new Error(`未知命令:${command}。当前仅支持 docnest serve。`);
|
|
74
|
+
}
|
|
75
|
+
const config = applyCliOverrides(await (0, config_1.loadConfig)(), parseArgs(args));
|
|
76
|
+
exposeConfig(config);
|
|
77
|
+
await dynamicImport(`${(0, node_url_1.pathToFileURL)(node_path_1.default.join(__dirname, 'server.js')).href}?v=${Date.now()}`);
|
|
78
|
+
}
|
|
79
|
+
main().catch((error) => {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
console.error(`DocNest 启动失败:${message}`);
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type DocNestConfig = {
|
|
2
|
+
docsDir?: string;
|
|
3
|
+
site?: {
|
|
4
|
+
title?: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
logo?: string;
|
|
7
|
+
storageKeyPrefix?: string;
|
|
8
|
+
};
|
|
9
|
+
navigation?: {
|
|
10
|
+
rootDirectoryOrder?: string[];
|
|
11
|
+
};
|
|
12
|
+
server?: {
|
|
13
|
+
host?: string;
|
|
14
|
+
port?: number;
|
|
15
|
+
watch?: boolean;
|
|
16
|
+
openBrowser?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export?: {
|
|
19
|
+
watermark?: {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
text?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export declare function defineConfig(config: DocNestConfig): DocNestConfig;
|
|
26
|
+
export type ResolvedDocNestConfig = Required<DocNestConfig> & {
|
|
27
|
+
projectRoot: string;
|
|
28
|
+
docsDir: string;
|
|
29
|
+
site: Required<NonNullable<DocNestConfig['site']>>;
|
|
30
|
+
navigation: Required<NonNullable<DocNestConfig['navigation']>>;
|
|
31
|
+
server: Required<NonNullable<DocNestConfig['server']>>;
|
|
32
|
+
export: {
|
|
33
|
+
watermark: Required<NonNullable<NonNullable<DocNestConfig['export']>['watermark']>>;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export declare function loadConfig(projectRoot?: string): Promise<ResolvedDocNestConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
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.defineConfig = defineConfig;
|
|
7
|
+
exports.loadConfig = loadConfig;
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_url_1 = require("node:url");
|
|
11
|
+
function defineConfig(config) {
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
const DEFAULT_CONFIG_FILE = 'docnest.config.mjs';
|
|
15
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
16
|
+
async function readProjectName(projectRoot) {
|
|
17
|
+
try {
|
|
18
|
+
const packageJson = JSON.parse(await promises_1.default.readFile(node_path_1.default.join(projectRoot, 'package.json'), 'utf8'));
|
|
19
|
+
if (typeof packageJson.name === 'string' && packageJson.name.trim()) {
|
|
20
|
+
return packageJson.name.trim();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// package.json is optional for a document-only folder.
|
|
25
|
+
}
|
|
26
|
+
return node_path_1.default.basename(projectRoot);
|
|
27
|
+
}
|
|
28
|
+
function normalizePort(value, fallback) {
|
|
29
|
+
const port = Number(value);
|
|
30
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
31
|
+
return fallback;
|
|
32
|
+
return port;
|
|
33
|
+
}
|
|
34
|
+
async function loadConfig(projectRoot = process.cwd()) {
|
|
35
|
+
const configPath = node_path_1.default.join(projectRoot, DEFAULT_CONFIG_FILE);
|
|
36
|
+
let userConfig = {};
|
|
37
|
+
try {
|
|
38
|
+
const loaded = await dynamicImport(`${(0, node_url_1.pathToFileURL)(configPath).href}?v=${Date.now()}`);
|
|
39
|
+
userConfig = loaded.default ?? {};
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
43
|
+
if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'ENOENT')
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
const projectName = await readProjectName(projectRoot);
|
|
47
|
+
const site = userConfig.site ?? {};
|
|
48
|
+
const navigation = userConfig.navigation ?? {};
|
|
49
|
+
const server = userConfig.server ?? {};
|
|
50
|
+
const watermark = userConfig.export?.watermark ?? {};
|
|
51
|
+
const storageKeyPrefix = site.storageKeyPrefix?.trim() || `docnest:${projectName}`;
|
|
52
|
+
return {
|
|
53
|
+
projectRoot,
|
|
54
|
+
docsDir: node_path_1.default.resolve(projectRoot, userConfig.docsDir?.trim() || 'docs'),
|
|
55
|
+
site: {
|
|
56
|
+
title: site.title?.trim() || projectName,
|
|
57
|
+
subtitle: site.subtitle?.trim() || '本地 Markdown 文档服务',
|
|
58
|
+
logo: site.logo?.trim() || '',
|
|
59
|
+
storageKeyPrefix,
|
|
60
|
+
},
|
|
61
|
+
navigation: {
|
|
62
|
+
rootDirectoryOrder: Array.isArray(navigation.rootDirectoryOrder)
|
|
63
|
+
? navigation.rootDirectoryOrder.filter((item) => typeof item === 'string')
|
|
64
|
+
: [],
|
|
65
|
+
},
|
|
66
|
+
server: {
|
|
67
|
+
host: server.host?.trim() || '127.0.0.1',
|
|
68
|
+
port: normalizePort(server.port, 3000),
|
|
69
|
+
watch: server.watch ?? true,
|
|
70
|
+
openBrowser: server.openBrowser ?? true,
|
|
71
|
+
},
|
|
72
|
+
export: {
|
|
73
|
+
watermark: {
|
|
74
|
+
enabled: watermark.enabled ?? false,
|
|
75
|
+
text: watermark.text?.trim() || site.title?.trim() || projectName,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare const app: any;
|
|
2
|
+
declare const server: any;
|
|
3
|
+
declare const io: any;
|
|
4
|
+
declare const DOCS_DIR: any;
|
|
5
|
+
declare function startServer(port?: number, attemptsLeft?: number): any;
|
|
6
|
+
export type DocNestDocumentEntry = {
|
|
7
|
+
name: string;
|
|
8
|
+
isDirectory: boolean;
|
|
9
|
+
isFile: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type DocNestDocumentStore = {
|
|
12
|
+
name: string;
|
|
13
|
+
rootLabel?: string;
|
|
14
|
+
readDir: (relativePath: string) => Promise<DocNestDocumentEntry[]>;
|
|
15
|
+
fileExists: (relativePath: string) => Promise<boolean>;
|
|
16
|
+
readFile: (relativePath: string) => Promise<string>;
|
|
17
|
+
readAsset: (relativePath: string) => Promise<Uint8Array>;
|
|
18
|
+
ready?: () => Promise<void>;
|
|
19
|
+
};
|
|
20
|
+
export { app, io, server, startServer, DOCS_DIR as docsDir, };
|