doc-repo 0.1.0-alpha.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.ja.md +162 -0
- package/README.md +162 -0
- package/dist/cli/exitCode.d.ts +2 -0
- package/dist/cli/exitCode.js +3 -0
- package/dist/cli/formatResultMessage.d.ts +2 -0
- package/dist/cli/formatResultMessage.js +18 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +43 -0
- package/dist/core/parser/convertMarkdown.d.ts +4 -0
- package/dist/core/parser/convertMarkdown.js +127 -0
- package/dist/core/scanner/detectRoot.d.ts +2 -0
- package/dist/core/scanner/detectRoot.js +22 -0
- package/dist/core/scanner/scanMarkdown.d.ts +2 -0
- package/dist/core/scanner/scanMarkdown.js +22 -0
- package/dist/core/site/atomicPublish.d.ts +1 -0
- package/dist/core/site/atomicPublish.js +21 -0
- package/dist/core/site/buildSiteBundle.d.ts +2 -0
- package/dist/core/site/buildSiteBundle.js +79 -0
- package/dist/core/site/copyAssets.d.ts +1 -0
- package/dist/core/site/copyAssets.js +8 -0
- package/dist/core/site/generateSite.d.ts +2 -0
- package/dist/core/site/generateSite.js +101 -0
- package/dist/core/site/renderPages.d.ts +2 -0
- package/dist/core/site/renderPages.js +82 -0
- package/dist/shared/errors.d.ts +9 -0
- package/dist/shared/errors.js +28 -0
- package/dist/shared/logger.d.ts +7 -0
- package/dist/shared/logger.js +16 -0
- package/dist/shared/sitePaths.d.ts +5 -0
- package/dist/shared/sitePaths.js +28 -0
- package/dist/shared/types.d.ts +45 -0
- package/dist/shared/types.js +1 -0
- package/package.json +64 -0
- package/templates/app.js +236 -0
- package/templates/page.html +23 -0
- package/templates/styles.css +157 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import { convertMarkdown } from "../parser/convertMarkdown.js";
|
|
3
|
+
import { AppError } from "../../shared/errors.js";
|
|
4
|
+
const createDir = (name) => ({
|
|
5
|
+
type: "dir",
|
|
6
|
+
name,
|
|
7
|
+
dirs: new Map(),
|
|
8
|
+
files: [],
|
|
9
|
+
});
|
|
10
|
+
const finalize = (node) => {
|
|
11
|
+
const childDirs = [...node.dirs.values()]
|
|
12
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
13
|
+
.map((dir) => ({
|
|
14
|
+
type: "dir",
|
|
15
|
+
name: dir.name,
|
|
16
|
+
children: finalize(dir),
|
|
17
|
+
}));
|
|
18
|
+
const childFiles = node.files.sort((a, b) => a.name.localeCompare(b.name));
|
|
19
|
+
return [...childDirs, ...childFiles];
|
|
20
|
+
};
|
|
21
|
+
const addFileToTree = (root, relativePath, id) => {
|
|
22
|
+
const segments = relativePath.split("/");
|
|
23
|
+
const fileName = segments.pop() ?? relativePath;
|
|
24
|
+
let cursor = root;
|
|
25
|
+
for (const segment of segments) {
|
|
26
|
+
if (!cursor.dirs.has(segment)) {
|
|
27
|
+
cursor.dirs.set(segment, createDir(segment));
|
|
28
|
+
}
|
|
29
|
+
cursor = cursor.dirs.get(segment);
|
|
30
|
+
}
|
|
31
|
+
cursor.files.push({
|
|
32
|
+
type: "file",
|
|
33
|
+
name: fileName,
|
|
34
|
+
id,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
export const buildSiteBundle = async (files) => {
|
|
38
|
+
const pages = [];
|
|
39
|
+
const root = createDir("root");
|
|
40
|
+
// 実在する全ページのID集合を先に構築し、リンク解決の唯一の正とする。
|
|
41
|
+
const knownIds = new Set(files.map((file) => file.relativePath.replace(/\.md$/i, "")));
|
|
42
|
+
// ベース名 → ID。同名が複数ある場合は曖昧なので除外し、一意なものだけ救済対象にする。
|
|
43
|
+
const basenameCounts = new Map();
|
|
44
|
+
const basenameToId = new Map();
|
|
45
|
+
for (const id of knownIds) {
|
|
46
|
+
const basename = id.split("/").pop() ?? id;
|
|
47
|
+
basenameCounts.set(basename, (basenameCounts.get(basename) ?? 0) + 1);
|
|
48
|
+
basenameToId.set(basename, id);
|
|
49
|
+
}
|
|
50
|
+
const uniqueBasenames = new Map();
|
|
51
|
+
for (const [basename, count] of basenameCounts) {
|
|
52
|
+
if (count === 1) {
|
|
53
|
+
uniqueBasenames.set(basename, basenameToId.get(basename));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const id = file.relativePath.replace(/\.md$/i, "");
|
|
58
|
+
let converted;
|
|
59
|
+
try {
|
|
60
|
+
const source = await fs.readFile(file.absolutePath, "utf8");
|
|
61
|
+
converted = convertMarkdown(source, file.relativePath, knownIds, uniqueBasenames);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
65
|
+
throw new AppError(`Markdown の読み取りまたは変換に失敗しました: ${file.relativePath} (${detail})`, "MARKDOWN_READ_OR_CONVERT_FAILED", `対象ファイルを確認してください: ${file.relativePath}`);
|
|
66
|
+
}
|
|
67
|
+
pages.push({
|
|
68
|
+
id,
|
|
69
|
+
title: converted.title,
|
|
70
|
+
relativePath: file.relativePath,
|
|
71
|
+
html: converted.html,
|
|
72
|
+
});
|
|
73
|
+
addFileToTree(root, file.relativePath, id);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
pages,
|
|
77
|
+
tree: finalize(root),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const copyAssets: (templatesDir: string, stagingDir: string) => Promise<void>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
const ASSET_FILES = ["styles.css"];
|
|
4
|
+
export const copyAssets = async (templatesDir, stagingDir) => {
|
|
5
|
+
for (const file of ASSET_FILES) {
|
|
6
|
+
await fs.copyFile(path.join(templatesDir, file), path.join(stagingDir, file));
|
|
7
|
+
}
|
|
8
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { detectRoot } from "../scanner/detectRoot.js";
|
|
5
|
+
import { scanMarkdown } from "../scanner/scanMarkdown.js";
|
|
6
|
+
import { buildSiteBundle } from "./buildSiteBundle.js";
|
|
7
|
+
import { renderPages } from "./renderPages.js";
|
|
8
|
+
import { copyAssets } from "./copyAssets.js";
|
|
9
|
+
import { atomicPublish } from "./atomicPublish.js";
|
|
10
|
+
import { AppError, toUserGuidance } from "../../shared/errors.js";
|
|
11
|
+
import { createLogger } from "../../shared/logger.js";
|
|
12
|
+
const resolveTargetDir = async (rootDir, scopePath) => {
|
|
13
|
+
if (!scopePath) {
|
|
14
|
+
return rootDir;
|
|
15
|
+
}
|
|
16
|
+
const targetDir = path.resolve(rootDir, scopePath);
|
|
17
|
+
const relativeToRoot = path.relative(rootDir, targetDir);
|
|
18
|
+
if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) {
|
|
19
|
+
throw new AppError("指定した生成範囲がリポジトリルート配下ではありません。", "SCOPE_OUTSIDE_ROOT", "リポジトリルートからの相対パスで、配下のフォルダを指定してください。");
|
|
20
|
+
}
|
|
21
|
+
if (!(await fs.pathExists(targetDir))) {
|
|
22
|
+
throw new AppError(`指定した生成範囲が見つかりません: ${scopePath}`, "SCOPE_NOT_FOUND", "存在するフォルダを、リポジトリルートからの相対パスで指定してください。");
|
|
23
|
+
}
|
|
24
|
+
const stat = await fs.stat(targetDir);
|
|
25
|
+
if (!stat.isDirectory()) {
|
|
26
|
+
throw new AppError(`指定した生成範囲はフォルダではありません: ${scopePath}`, "SCOPE_NOT_DIRECTORY", "生成範囲にはフォルダを指定してください。");
|
|
27
|
+
}
|
|
28
|
+
return targetDir;
|
|
29
|
+
};
|
|
30
|
+
const resolveRequestedTargetPath = (rootDir, scopePath) => {
|
|
31
|
+
if (!scopePath) {
|
|
32
|
+
return rootDir;
|
|
33
|
+
}
|
|
34
|
+
return path.resolve(rootDir, scopePath);
|
|
35
|
+
};
|
|
36
|
+
const resolveTemplatesDir = async (cwd) => {
|
|
37
|
+
const cwdTemplatesDir = path.resolve(cwd, "templates");
|
|
38
|
+
if (await fs.pathExists(cwdTemplatesDir)) {
|
|
39
|
+
return cwdTemplatesDir;
|
|
40
|
+
}
|
|
41
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const bundledTemplatesDir = path.resolve(moduleDir, "../../../templates");
|
|
43
|
+
if (await fs.pathExists(bundledTemplatesDir)) {
|
|
44
|
+
return bundledTemplatesDir;
|
|
45
|
+
}
|
|
46
|
+
throw new AppError("テンプレートディレクトリが見つかりません。", "TEMPLATE_MISSING", "プロジェクトルートに templates/ が存在するか、パッケージに templates/ が同梱されていることを確認してください。");
|
|
47
|
+
};
|
|
48
|
+
export const generateSite = async (context) => {
|
|
49
|
+
const logger = createLogger();
|
|
50
|
+
const warnings = [];
|
|
51
|
+
let outputDir = path.join(path.resolve(context.cwd), ".doc-repo");
|
|
52
|
+
let targetPath = path.resolve(context.cwd);
|
|
53
|
+
try {
|
|
54
|
+
const detected = await detectRoot(context.cwd);
|
|
55
|
+
const rootDir = detected.detectedRoot;
|
|
56
|
+
outputDir = path.join(rootDir, ".doc-repo");
|
|
57
|
+
const stagingDir = path.join(rootDir, `.doc-repo.__staging__.${Date.now()}`);
|
|
58
|
+
targetPath = resolveRequestedTargetPath(rootDir, context.scopePath);
|
|
59
|
+
const targetDir = await resolveTargetDir(rootDir, context.scopePath);
|
|
60
|
+
const templatesDir = await resolveTemplatesDir(context.cwd);
|
|
61
|
+
if (detected.usedFallback) {
|
|
62
|
+
warnings.push("Git ルートが見つからなかったため、カレントディレクトリを対象に処理しました。");
|
|
63
|
+
}
|
|
64
|
+
const markdownFiles = await scanMarkdown(rootDir, targetDir);
|
|
65
|
+
if (markdownFiles.length === 0) {
|
|
66
|
+
warnings.push("Markdown ファイルが 0 件でした。空サイトを生成しました。");
|
|
67
|
+
}
|
|
68
|
+
logger.info(`対象ルート: ${rootDir}`);
|
|
69
|
+
logger.info(`生成対象: ${targetDir}`);
|
|
70
|
+
logger.info(`検出Markdown数: ${markdownFiles.length}`);
|
|
71
|
+
await fs.ensureDir(stagingDir);
|
|
72
|
+
const bundle = await buildSiteBundle(markdownFiles);
|
|
73
|
+
await renderPages(templatesDir, stagingDir, bundle);
|
|
74
|
+
await copyAssets(templatesDir, stagingDir);
|
|
75
|
+
await atomicPublish(stagingDir, outputDir);
|
|
76
|
+
return {
|
|
77
|
+
status: "success",
|
|
78
|
+
exitCode: 0,
|
|
79
|
+
outputDir,
|
|
80
|
+
targetPath,
|
|
81
|
+
markdownFileCount: markdownFiles.length,
|
|
82
|
+
message: "ドキュメントサイトの生成に成功しました。",
|
|
83
|
+
warnings,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const guidance = toUserGuidance(error);
|
|
88
|
+
logger.error(guidance.reason);
|
|
89
|
+
return {
|
|
90
|
+
status: "failure",
|
|
91
|
+
exitCode: 1,
|
|
92
|
+
outputDir,
|
|
93
|
+
targetPath,
|
|
94
|
+
markdownFileCount: 0,
|
|
95
|
+
message: "ドキュメントサイトの生成に失敗しました。",
|
|
96
|
+
warnings,
|
|
97
|
+
errorReason: guidance.reason,
|
|
98
|
+
hint: guidance.hint,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { docHref, siteRootPrefix } from "../../shared/sitePaths.js";
|
|
4
|
+
const TEMPLATE_FILE = "page.html";
|
|
5
|
+
const escapeHtml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6
|
+
// テンプレートのプレースホルダ置換。$ 連鎖を解釈させないため split/join を使う。
|
|
7
|
+
const fill = (template, key, value) => template.split(key).join(value);
|
|
8
|
+
// サイドバーのツリーを、現在ページからの相対 .html リンクとして静的 HTML にレンダリングする。
|
|
9
|
+
// フォルダは <details>/<summary> でネイティブに開閉でき、JS は不要。
|
|
10
|
+
const renderTree = (nodes, currentRelativePath, currentId) => {
|
|
11
|
+
const renderNodes = (items) => {
|
|
12
|
+
const lis = items
|
|
13
|
+
.map((item) => {
|
|
14
|
+
if (item.type === "dir") {
|
|
15
|
+
return `<li><details open><summary>${escapeHtml(item.name)}</summary>${renderNodes(item.children)}</details></li>`;
|
|
16
|
+
}
|
|
17
|
+
const href = docHref(currentRelativePath, item.id);
|
|
18
|
+
const selected = item.id === currentId ? ' class="selected" aria-current="page"' : "";
|
|
19
|
+
return `<li><a href="${href}"${selected}>${escapeHtml(item.name)}</a></li>`;
|
|
20
|
+
})
|
|
21
|
+
.join("");
|
|
22
|
+
return `<ul>${lis}</ul>`;
|
|
23
|
+
};
|
|
24
|
+
return renderNodes(nodes);
|
|
25
|
+
};
|
|
26
|
+
const renderPageHtml = (template, page, tree) => {
|
|
27
|
+
const stylesHref = `${siteRootPrefix(page.relativePath)}styles.css`;
|
|
28
|
+
const homeHref = `${siteRootPrefix(page.relativePath)}index.html`;
|
|
29
|
+
const sidebar = renderTree(tree, page.relativePath, page.id);
|
|
30
|
+
let html = fill(template, "__TITLE__", escapeHtml(page.title));
|
|
31
|
+
html = fill(html, "__STYLES_HREF__", stylesHref);
|
|
32
|
+
html = fill(html, "__HOME_HREF__", homeHref);
|
|
33
|
+
html = fill(html, "__SIDEBAR__", sidebar);
|
|
34
|
+
html = fill(html, "__ARTICLE__", page.html);
|
|
35
|
+
return html;
|
|
36
|
+
};
|
|
37
|
+
// ルート index.html: ホームページ(README 優先、なければ先頭ページ)へリダイレクトする静的 HTML。
|
|
38
|
+
const pickHomePage = (pages) => pages.find((page) => page.id === "README" || page.id === "readme") ?? pages[0];
|
|
39
|
+
const renderIndexHtml = (bundle) => {
|
|
40
|
+
const home = pickHomePage(bundle.pages);
|
|
41
|
+
if (!home) {
|
|
42
|
+
return [
|
|
43
|
+
"<!doctype html>",
|
|
44
|
+
'<html lang="ja">',
|
|
45
|
+
"<head>",
|
|
46
|
+
'<meta charset="utf-8" />',
|
|
47
|
+
"<title>doc-repo</title>",
|
|
48
|
+
'<link rel="stylesheet" href="./styles.css" />',
|
|
49
|
+
"</head>",
|
|
50
|
+
"<body>",
|
|
51
|
+
'<main class="content"><article><p class="muted">No Markdown files found.</p></article></main>',
|
|
52
|
+
"</body>",
|
|
53
|
+
"</html>",
|
|
54
|
+
"",
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
const homeHref = `./${docHref("index.md", home.id)}`;
|
|
58
|
+
return [
|
|
59
|
+
"<!doctype html>",
|
|
60
|
+
'<html lang="ja">',
|
|
61
|
+
"<head>",
|
|
62
|
+
'<meta charset="utf-8" />',
|
|
63
|
+
`<meta http-equiv="refresh" content="0; url=${homeHref}" />`,
|
|
64
|
+
`<link rel="canonical" href="${homeHref}" />`,
|
|
65
|
+
"<title>doc-repo</title>",
|
|
66
|
+
"</head>",
|
|
67
|
+
"<body>",
|
|
68
|
+
`<p>移動中です… <a href="${homeHref}">${escapeHtml(home.title)}</a></p>`,
|
|
69
|
+
"</body>",
|
|
70
|
+
"</html>",
|
|
71
|
+
"",
|
|
72
|
+
].join("\n");
|
|
73
|
+
};
|
|
74
|
+
// 各 Markdown を 1 対 1 で実 HTML に出力し、ルート index.html も生成する。
|
|
75
|
+
export const renderPages = async (templatesDir, stagingDir, bundle) => {
|
|
76
|
+
const template = await fs.readFile(path.join(templatesDir, TEMPLATE_FILE), "utf8");
|
|
77
|
+
for (const page of bundle.pages) {
|
|
78
|
+
const outputPath = path.join(stagingDir, ...page.id.split("/")) + ".html";
|
|
79
|
+
await fs.outputFile(outputPath, renderPageHtml(template, page, bundle.tree), "utf8");
|
|
80
|
+
}
|
|
81
|
+
await fs.outputFile(path.join(stagingDir, "index.html"), renderIndexHtml(bundle), "utf8");
|
|
82
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class AppError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
hint;
|
|
4
|
+
constructor(message, code, hint) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "AppError";
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.hint = hint;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export const toUserGuidance = (error) => {
|
|
12
|
+
if (error instanceof AppError) {
|
|
13
|
+
return {
|
|
14
|
+
reason: `${error.code}: ${error.message}`,
|
|
15
|
+
hint: error.hint,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (error instanceof Error) {
|
|
19
|
+
return {
|
|
20
|
+
reason: error.message,
|
|
21
|
+
hint: "実行ディレクトリの権限と入力ファイルを確認して再実行してください。",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
reason: "Unknown error",
|
|
26
|
+
hint: "詳細ログを確認し、問題が解消しない場合は再実行してください。",
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const createLogger = () => {
|
|
2
|
+
const log = (level, message) => {
|
|
3
|
+
const prefix = `[doc-repo:${level}]`;
|
|
4
|
+
const line = `${prefix} ${message}`;
|
|
5
|
+
if (level === "error") {
|
|
6
|
+
console.error(line);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
console.log(line);
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
log,
|
|
13
|
+
info: (message) => log("info", message),
|
|
14
|
+
error: (message) => log("error", message),
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const pageDirDepth: (relativePath: string) => number;
|
|
2
|
+
export declare const siteRootPrefix: (relativePath: string) => string;
|
|
3
|
+
export declare const repoRootPrefix: (relativePath: string) => string;
|
|
4
|
+
export declare const docHref: (currentRelativePath: string, targetId: string) => string;
|
|
5
|
+
export declare const assetHref: (currentRelativePath: string, pathFromRoot: string, suffix: string) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
// マルチページ静的サイトのリンク計算ヘルパー。
|
|
3
|
+
// 出力は `.doc-repo/<id>.html` のミラー構造になり、ページ間リンクはブラウザネイティブの相対パスで解決する。
|
|
4
|
+
const toPosix = (value) => value.split(path.sep).join(path.posix.sep);
|
|
5
|
+
// 各セグメントを URL エンコードしつつ、`/`・`.`・`..` は保つ。
|
|
6
|
+
// 日本語やスペースを含む ID/パスでも file:// で確実に解決できるようにする。
|
|
7
|
+
const encodePath = (value) => value
|
|
8
|
+
.split("/")
|
|
9
|
+
.map((segment) => (segment === "" || segment === "." || segment === ".." ? segment : encodeURIComponent(segment)))
|
|
10
|
+
.join("/");
|
|
11
|
+
// ページの出力ディレクトリの深さ(セグメント数)。ルート直下のページは 0。
|
|
12
|
+
export const pageDirDepth = (relativePath) => {
|
|
13
|
+
const dir = path.posix.dirname(toPosix(relativePath));
|
|
14
|
+
return dir === "." || dir === "" ? 0 : dir.split("/").length;
|
|
15
|
+
};
|
|
16
|
+
// ページ出力ディレクトリから `.doc-repo` ルートへ戻る相対プレフィックス(styles.css 等のサイト内資産向け)。
|
|
17
|
+
export const siteRootPrefix = (relativePath) => "../".repeat(pageDirDepth(relativePath));
|
|
18
|
+
// ページ出力ディレクトリからリポジトリルートへ戻る相対プレフィックス(`.doc-repo` を抜ける画像・添付向け)。
|
|
19
|
+
export const repoRootPrefix = (relativePath) => "../".repeat(pageDirDepth(relativePath) + 1);
|
|
20
|
+
// 現在ページから対象ページ ID(拡張子なし)への相対 `.html` リンク。
|
|
21
|
+
export const docHref = (currentRelativePath, targetId) => {
|
|
22
|
+
const currentDir = path.posix.dirname(toPosix(currentRelativePath));
|
|
23
|
+
const baseDir = currentDir === "." ? "" : currentDir;
|
|
24
|
+
const relative = path.posix.relative(baseDir, toPosix(targetId));
|
|
25
|
+
return `${encodePath(relative)}.html`;
|
|
26
|
+
};
|
|
27
|
+
// リポジトリ相対の資産パス(画像・添付など)を、現在ページからの相対パスへ変換する。
|
|
28
|
+
export const assetHref = (currentRelativePath, pathFromRoot, suffix) => `${repoRootPrefix(currentRelativePath)}${encodePath(pathFromRoot)}${suffix}`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type ExecutionStatus = "success" | "failure";
|
|
2
|
+
export interface MarkdownFile {
|
|
3
|
+
absolutePath: string;
|
|
4
|
+
relativePath: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SitePage {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
relativePath: string;
|
|
10
|
+
html: string;
|
|
11
|
+
}
|
|
12
|
+
export interface TreeDirNode {
|
|
13
|
+
type: "dir";
|
|
14
|
+
name: string;
|
|
15
|
+
children: TreeNode[];
|
|
16
|
+
}
|
|
17
|
+
export interface TreeFileNode {
|
|
18
|
+
type: "file";
|
|
19
|
+
name: string;
|
|
20
|
+
id: string;
|
|
21
|
+
}
|
|
22
|
+
export type TreeNode = TreeDirNode | TreeFileNode;
|
|
23
|
+
export interface SiteBundle {
|
|
24
|
+
pages: SitePage[];
|
|
25
|
+
tree: TreeNode[];
|
|
26
|
+
}
|
|
27
|
+
export interface RootDetectionResult {
|
|
28
|
+
detectedRoot: string;
|
|
29
|
+
usedFallback: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface GenerationContext {
|
|
32
|
+
cwd: string;
|
|
33
|
+
scopePath?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface GenerationResult {
|
|
36
|
+
status: ExecutionStatus;
|
|
37
|
+
exitCode: number;
|
|
38
|
+
outputDir: string;
|
|
39
|
+
targetPath: string;
|
|
40
|
+
markdownFileCount: number;
|
|
41
|
+
message: string;
|
|
42
|
+
warnings: string[];
|
|
43
|
+
errorReason?: string;
|
|
44
|
+
hint?: string;
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "doc-repo",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Generate a static documentation site from repository Markdown files.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"markdown",
|
|
7
|
+
"documentation",
|
|
8
|
+
"static-site",
|
|
9
|
+
"cli",
|
|
10
|
+
"repository",
|
|
11
|
+
"docs"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Ryuta Otsuka",
|
|
16
|
+
"url": "https://github.com/Ryuta1005"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Ryuta1005/doc-repo.git"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/Ryuta1005/doc-repo/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/Ryuta1005/doc-repo#readme",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"templates",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"bin": {
|
|
33
|
+
"doc-repo": "dist/cli/index.js"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
37
|
+
"prebuild": "npm run clean",
|
|
38
|
+
"build": "tsc -p tsconfig.build.json",
|
|
39
|
+
"dev": "tsx src/cli/index.ts",
|
|
40
|
+
"typecheck": "tsc --noEmit -p tsconfig.json && tsc -p tsconfig.tests.json",
|
|
41
|
+
"test": "vitest run --coverage",
|
|
42
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"commander": "^15.0.0",
|
|
49
|
+
"fast-glob": "^3.3.3",
|
|
50
|
+
"fs-extra": "^11.3.5",
|
|
51
|
+
"markdown-it": "^14.2.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/fs-extra": "^11.0.4",
|
|
55
|
+
"@types/jsdom": "^28.0.3",
|
|
56
|
+
"@types/markdown-it": "^14.1.2",
|
|
57
|
+
"@types/node": "^25.9.2",
|
|
58
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
59
|
+
"jsdom": "^29.1.1",
|
|
60
|
+
"tsx": "^4.22.4",
|
|
61
|
+
"typescript": "^6.0.3",
|
|
62
|
+
"vitest": "^4.1.8"
|
|
63
|
+
}
|
|
64
|
+
}
|