const fs = require("fs"); const path = require("path"); const child_process = require("child_process"); const ROOT = __dirname; const CONFIG_PATH = path.join(ROOT, "config.json"); const CONFIGS_DIR = path.join(ROOT, "configs"); const DOCS_DIR = path.join(ROOT, "docs"); function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, "utf-8")); } function sanitizeName(raw) { return String(raw || "").trim() .replace(/[\\/:*?"<>|]/g, "_") .replace(/\s+/g, " ") .replace(/\.+$/, "") .slice(0, 60) .trim(); } function parseSiteIds(rawSiteId) { const list = Array.isArray(rawSiteId) ? rawSiteId : String(rawSiteId).split(","); return list.map((id) => String(id).trim()).filter((id) => id.length > 0); } function resolveSiteData(json) { if (json?.Response?.Data) return json.Response.Data; if (json?.Response?.Site?.Data) return json.Response.Site.Data; if (json?.Response?.Data?.SiteSettings) return json.Response.Data; if (json?.Response?.SiteSettings) return json.Response; throw new Error("getsiteレスポンスの想定構造が見つかりません。Response.Data を含むJSONを渡してください。"); } function resolveTableCellValue(row, header) { if (row[header] != null) return row[header]; const normalized = String(header) .replace(/\s+/g, "") .replace(/[()]/g, "") .replace(///g, "/"); if (row[normalized] != null) return row[normalized]; const aliasMap = { "順番": ["No", "Order", "Index"], "列名": ["Column", "ColumnName", "Field", "Name"], "列名(内部)": ["Column", "ColumnName", "Field", "Name"], "表示ラベル": ["Label", "LabelText", "DisplayName", "Title"], "説明/入力ガイド": ["Description", "DescriptionText", "ToolTip", "InputHelpText"], "備考": ["Remarks", "Remark", "Notes"], "種別": ["Type", "Kind"], "対象": ["Target", "TargetId"], "権限値": ["Permission", "PermissionValue", "Level", "Value"], "タブ/キー": ["Tab", "Key", "TabKey"], "項目一覧": ["Items", "ItemList"], "項目": ["Field", "Column", "ColumnName"], "権限設定": ["Permission", "PermissionSetting", "Value"], "ファイル": ["File", "Path"], "要約": ["Summary", "Description"], "操作": ["Operation", "Name"], "状態の変化": ["Change", "Transition"], "説明": ["Description", "Detail"], "起点列": ["From", "FromColumn", "FromField"], "参照先SiteId": ["TargetSite", "TargetSiteId", "SiteId"], "参照先の値→コピー先": ["To", "ToColumn", "ToField"], "JsonFormat": ["JsonFormat", "Format"], "ラベル": ["Label", "LabelText"], "内容": ["Content", "Value", "Body"], }; for (const alias of aliasMap[header] || []) { if (row[alias] != null) return row[alias]; } return ""; } function formatMarkdownTable(header, rows) { if (!rows || rows.length === 0) return ""; const heads = header.map((h) => `| ${h} `).join("") + "|"; const sep = header.map(() => "| --- ").join("") + "|"; const body = rows .map((row) => { return header .map((cell) => { const value = resolveTableCellValue(row, cell); return `| ${String(value).replace(/\|/g, "\\|")} `; }) .join("") + "|"; }) .join("\n"); return `${heads}\n${sep}\n${body}\n`; } function inferColumnType(column) { const name = String(column.ColumnName || column.Name || ""); if (/^(Class[A-Z]|Class\d{3}|Class)/.test(name)) return "文字列"; if (/^Num/.test(name)) return "数値"; if (/^Date/.test(name)) return "日付"; if (/^Description/.test(name)) return "説明"; if (/^Check/.test(name)) return "チェック"; if (/^Attachments?/.test(name)) return "添付"; if (/^(Owner|Manager|Assignee|CreatedBy|UpdatedBy|Updator|Users?)/.test(name)) return "ユーザー"; if (/^Status$/i.test(name)) return "状態"; if (/Id$/.test(name) && !/^Title$/i.test(name)) return "ID"; if (/Title/i.test(name)) return "タイトル"; return column.Type || column.FieldType || "不明"; } function getArray(value) { if (!value) return []; return Array.isArray(value) ? value : [value]; } function formatValue(value) { if (value === null || value === undefined) return ""; if (typeof value === "boolean") return value ? "true" : "false"; if (Array.isArray(value)) return value.join(", "); return String(value); } function findExtractionDir(siteId) { if (!fs.existsSync(CONFIGS_DIR)) return null; const candidates = fs.readdirSync(CONFIGS_DIR).filter((name) => { const full = path.join(CONFIGS_DIR, name); return fs.statSync(full).isDirectory() && name.startsWith(`site-${siteId}`); }); return candidates[0] ? path.join(CONFIGS_DIR, candidates[0]) : null; } function loadManifest(siteId) { const dir = findExtractionDir(siteId); if (!dir) return null; const manifestPath = path.join(dir, "manifest.json"); if (!fs.existsSync(manifestPath)) return null; return readJson(manifestPath); } function getSiteJsonPath(siteId) { const candidate = path.join(CONFIGS_DIR, `site-${siteId}_latest.json`); if (fs.existsSync(candidate)) return candidate; const all = fs.readdirSync(CONFIGS_DIR).filter((name) => name.startsWith(`site-${siteId}_`) && name.endsWith(".json")); return all.length > 0 ? path.join(CONFIGS_DIR, all[0]) : null; } function buildBasicInfoTable(data, settings) { const rows = [ { Key: "TenantId", Value: formatValue(data.TenantId) }, { Key: "Title", Value: formatValue(data.Title) }, { Key: "ReferenceType", Value: formatValue(data.ReferenceType) }, { Key: "ParentId", Value: formatValue(data.ParentId) }, { Key: "InheritPermission", Value: formatValue(data.InheritPermission) }, { Key: "Publish", Value: formatValue(data.Publish) }, { Key: "DisableCrossSearch", Value: formatValue(data.DisableCrossSearch) }, { Key: "Creator", Value: formatValue(data.Creator) }, { Key: "Updator", Value: formatValue(data.Updator || data.UpdatedBy) }, { Key: "CreatedTime", Value: formatValue(data.CreatedTime) }, { Key: "UpdatedTime", Value: formatValue(data.UpdatedTime) }, { Key: "SiteSettings.Version", Value: formatValue(settings?.Version) }, ]; return rows.map((row) => `| ${row.Key} | ${row.Value} |`).join("\n") + "\n"; } function inferLinkDescription(link) { const from = link.From || link.FromColumn || link.FromField || "(不明)"; const to = link.To || link.ToColumn || link.ToField || "(不明)"; return `${from} → ${to}`; } function scriptSummary(filePath) { if (!fs.existsSync(filePath)) return ""; const content = fs.readFileSync(filePath, "utf-8"); const line = content.split(/\r?\n/).find((l) => l.trim().length > 0); return line ? line.slice(0, 120) : ""; } function generateSpecMarkdown(siteId, data, settings, manifest) { const title = data.Title || `Site ${siteId}`; const specLines = []; specLines.push(`# サイト仕様書:${title}(SiteId: ${siteId})\n`); specLines.push(`- 取得元データ: [site-${siteId}_latest.json](./site-${siteId}_latest.json)`); specLines.push(`- 取得日時: ${formatValue(data.UpdatedTime || data.CreatedTime || "不明")}`); specLines.push(`- 設定バージョン: ${formatValue(settings?.Version || "不明")}\n`); specLines.push(`## 1. サイト基本情報`); specLines.push("| 項目 | 値 |\n| --- | --- |\n" + buildBasicInfoTable(data, settings)); const permissions = settings?.Permissions || data.Permissions || []; if (permissions.length > 0) { specLines.push(`## 2. アクセス権限(Permissions)`); const permRows = permissions.map((perm) => ({ Type: formatValue(perm.Type || perm.PermissionType || perm.AuthorityType), Target: formatValue(perm.TargetId || perm.Target || perm.RoleId || perm.Value), Level: formatValue(perm.Permission || perm.Value || perm.Level), })); specLines.push(formatMarkdownTable(["種別", "対象", "権限値"], permRows)); specLines.push(`※ 権限値は環境のロール定義に依存します。`); } specLines.push(`## 3. 画面構成`); const gridColumns = settings?.GridColumns || []; if (gridColumns.length > 0) { specLines.push(`### 3.1 一覧画面(GridColumns)`); const gridRows = gridColumns.map((col, index) => ({ No: index + 1, Column: formatValue(col.ColumnName || col.Column || col.Name || col.Field), Label: formatValue(col.LabelText || col.DisplayName || col.Title || ""), })); specLines.push(formatMarkdownTable(["順番", "列名", "表示ラベル"], gridRows)); } const editorHash = settings?.EditorColumnHash || {}; const sections = settings?.Sections || []; if (Object.keys(editorHash).length > 0 || sections.length > 0) { specLines.push(`### 3.2 編集画面レイアウト(EditorColumnHash / Sections)`); if (Object.keys(editorHash).length > 0) { const editorRows = Object.keys(editorHash).map((tabKey) => ({ Tab: tabKey, Items: formatValue(getArray(editorHash[tabKey]).join(", ")), })); specLines.push(formatMarkdownTable(["タブ/キー", "項目一覧"], editorRows)); } if (sections.length > 0) { const sectionRows = sections.map((section) => ({ Id: formatValue(section.Id), Label: formatValue(section.LabelText || section.Name || ""), AllowExpand: formatValue(section.AllowExpand), Expand: formatValue(section.Expand), })); specLines.push(`\n#### Sections`); specLines.push(formatMarkdownTable(["Id", "ラベル", "AllowExpand", "Expand"], sectionRows)); } } const titleColumns = settings?.TitleColumns || []; if (titleColumns.length > 0) { specLines.push(`### 3.3 タイトル表示(TitleColumns / TitleSeparator)`); specLines.push(`- タイトル項目: ${formatValue(titleColumns.join(" + "))}`); specLines.push(`- 区切り文字: ${formatValue(settings?.TitleSeparator || "")}`); } const columns = settings?.Columns || []; if (columns.length > 0) { specLines.push(`## 4. 項目定義(Columns)`); const colRows = columns.map((col) => ({ Column: formatValue(col.ColumnName || col.Name || col.Field || col.Id), Type: inferColumnType(col), Label: formatValue(col.LabelText || col.DisplayName || col.Title || ""), Description: formatValue(col.Description || col.ToolTip || col.InputHelpText || ""), Remarks: [col.NoWrap ? "NoWrap" : null, col.FieldCss ? "FieldCss" : null, col.ExtendedHtmlAfterControl ? "ExtendedHtmlAfterControl" : null].filter(Boolean).join(", "), })); specLines.push(formatMarkdownTable(["列名(内部)", "種別", "表示ラベル", "説明/入力ガイド", "備考"], colRows)); const extendedItems = columns.filter((col) => col.ExtendedHtmlAfterControl); if (extendedItems.length > 0) { specLines.push(`## 4.1 項目ごとの補足HTML(ExtendedHtmlAfterControl)`); const extRows = extendedItems.map((col) => ({ Column: formatValue(col.ColumnName || col.Name || col.Field), Label: formatValue(col.LabelText || col.DisplayName || col.Title || ""), Content: formatValue(col.ExtendedHtmlAfterControl), })); specLines.push(formatMarkdownTable(["列名", "ラベル", "内容"], extRows)); } } const createPerm = settings?.PermissionForCreating || []; const updatePerm = settings?.PermissionForUpdating || []; if (createPerm.length > 0 || updatePerm.length > 0) { specLines.push(`## 5. 作成・更新権限(フィールド単位)`); if (createPerm.length > 0) { specLines.push(`### PermissionForCreating`); const createRows = createPerm.map((item) => ({ Field: formatValue(item.ColumnName || item.Field || item.Column), Permission: formatValue(item.Required || item.Permission || item.Value), })); specLines.push(formatMarkdownTable(["項目", "権限設定"], createRows)); } if (updatePerm.length > 0) { specLines.push(`### PermissionForUpdating`); const updateRows = updatePerm.map((item) => ({ Field: formatValue(item.ColumnName || item.Field || item.Column), Permission: formatValue(item.Required || item.Permission || item.Value), })); specLines.push(formatMarkdownTable(["項目", "権限設定"], updateRows)); } } const aggregations = settings?.Aggregations || []; if (aggregations.length > 0) { specLines.push(`## 6. 集計設定(Aggregations)`); const aggRows = aggregations.map((item) => ({ Id: formatValue(item.Id), GroupBy: formatValue(item.GroupBy), Type: formatValue(item.Type), Target: formatValue(item.Target), })); specLines.push(formatMarkdownTable(["Id", "GroupBy", "Type", "Target"], aggRows)); } const links = settings?.Links || []; if (links.length > 0) { specLines.push(`## 7. 他サイト連携(Links / ルックアップ)`); const linkRows = links.map((link) => ({ From: formatValue(link.From || link.FromColumn || link.FromField), TargetSite: formatValue(link.SiteId || link.TargetSiteId || link.ReferenceSiteId), To: formatValue(link.To || link.ToColumn || link.ToField), JsonFormat: formatValue(link.JsonFormat || link.Format), })); specLines.push(formatMarkdownTable(["起点列", "参照先SiteId", "参照先の値→コピー先", "JsonFormat"], linkRows)); } const processes = manifest ? readJson(path.join(findExtractionDir(siteId), "processes.json")) : settings?.Processes || []; if (processes.length > 0) { specLines.push(`## 8. プロセス設定(Processes)`); specLines.push(`- 定義ファイル: [configs/${path.relative(ROOT, path.join(findExtractionDir(siteId), "processes.json")).replace(/\\/g, "/")}]`); const procRows = processes.map((proc) => ({ Id: formatValue(proc.Id), Name: formatValue(proc.Name), DisplayName: formatValue(proc.DisplayName || proc.Name), Condition: formatValue(`${proc.CurrentStatus || ""} → ${proc.ChangedStatus || ""}`), Action: formatValue(proc.OnClick || proc.Action || proc.Name), })); specLines.push(formatMarkdownTable(["Id", "Name", "DisplayName", "実行条件(Current→Changed)", "動作"], procRows)); } if (manifest?.Styles?.length > 0) { specLines.push(`## 9. スタイル(Styles)`); const styleRows = manifest.Styles.map((style) => ({ Id: formatValue(style.Id), Title: formatValue(style.Title), File: `[${style.File}](./${path.posix.join(path.basename(findExtractionDir(siteId)), "styles", style.File)})`, Remarks: formatValue(style.Disabled ? "Disabled" : ""), })); specLines.push(formatMarkdownTable(["Id", "Title", "ファイル", "備考"], styleRows)); } const scripts = manifest?.Scripts || []; const serverScripts = manifest?.ServerScripts || []; if (scripts.length > 0 || serverScripts.length > 0) { specLines.push(`## 10. スクリプト(Scripts)/サーバースクリプト(ServerScripts)`); if (scripts.length > 0) { const scriptRows = scripts.map((script) => ({ Id: formatValue(script.Id), Title: formatValue(script.Title), File: `[${script.File}](./${path.posix.join(path.basename(findExtractionDir(siteId)), "scripts", script.File)})`, Summary: formatValue(scriptSummary(path.join(findExtractionDir(siteId), "scripts", script.File))), })); specLines.push(`### Scripts`); specLines.push(formatMarkdownTable(["Id", "Title", "ファイル", "要約"], scriptRows)); } if (serverScripts.length > 0) { const serverRows = serverScripts.map((script) => ({ Id: formatValue(script.Id), Title: formatValue(script.Title), File: `[${script.File}](./${path.posix.join(path.basename(findExtractionDir(siteId)), "serverscripts", script.File)})`, Summary: formatValue(scriptSummary(path.join(findExtractionDir(siteId), "serverscripts", script.File))), })); specLines.push(`### ServerScripts`); specLines.push(formatMarkdownTable(["Id", "Title", "ファイル", "要約"], serverRows)); } } const otherSettings = { ...settings }; ["Permissions", "GridColumns", "EditorColumnHash", "Sections", "TitleColumns", "TitleSeparator", "Columns", "PermissionForCreating", "PermissionForUpdating", "Aggregations", "Links", "Processes", "Scripts", "Styles", "ServerScripts"].forEach((k) => delete otherSettings[k]); const otherEntries = Object.entries(otherSettings).filter(([key, value]) => value !== null && value !== undefined && value !== "" && !(Array.isArray(value) && value.length === 0) && !(typeof value === "object" && Object.keys(value).length === 0)); if (otherEntries.length > 0) { specLines.push(`## 11. その他設定`); specLines.push(`| 設定キー | 値 |\n| --- | --- |`); otherEntries.forEach(([key, value]) => { specLines.push(`| ${key} | ${formatValue(value)} |`); }); } const alertRows = []; if (settings?.Scripts?.some((script) => /API_KEY|SECRET|Bearer\s+|token|webhook|password/i.test(script.Body || ""))) { alertRows.push("抽出されたスクリプト内に API キーやシークレット、Webhook URL、Bearerトークンなどの機密情報が含まれている可能性があります。内容を目視で確認してください。"); } if (alertRows.length > 0) { specLines.push(`## 気になる点(レビュー観点)`); alertRows.forEach((line) => specLines.push(`- ${line}`)); } return specLines.join("\n\n"); } function generateOverviewMarkdown(siteId, data, settings, manifest) { const title = data.Title || `Site ${siteId}`; const sanitizedTitle = sanitizeName(title); const overviewLines = []; overviewLines.push(`# 概要書:${title}`); overviewLines.push(`- 技術仕様(項目のシステム上の名称・設定値等)は [../configs/site-${siteId}_spec.md](../configs/site-${siteId}_spec.md) を参照してください。`); overviewLines.push(`- 本書は業務担当者向けに機能面のみを平易に説明するものです。\n`); overviewLines.push(`## 1. これは何か`); overviewLines.push(`このサイトは「${title}」で、${formatValue(data.ReferenceType || "データ")}を管理・参照するための画面です。`); const columns = getArray(settings?.Columns || []).filter((col) => col.LabelText || col.DisplayName || col.Title); if (columns.length > 0) { overviewLines.push(`## 2. 登場する情報(入力項目)`); const items = columns.map((col) => ({ Category: inferColumnType(col), Field: formatValue(col.LabelText || col.DisplayName || col.Title), Notes: col.Hide ? "非表示項目" : formatValue(col.Description || col.ToolTip || ""), })); overviewLines.push(formatMarkdownTable(["分類", "項目", "内容"], items)); } const processes = manifest ? readJson(path.join(findExtractionDir(siteId), "processes.json")) : settings?.Processes || []; if (processes.length > 0) { overviewLines.push(`## 3. 業務の流れ(ボタン操作)`); const flowRows = processes.map((proc) => ({ Operation: formatValue(proc.Name || proc.DisplayName || "プロセス"), Change: formatValue(proc.CurrentStatus || "") + (proc.ChangedStatus ? ` → ${proc.ChangedStatus}` : ""), Description: formatValue(proc.Description || proc.DisplayName || `状態を変更するプロセスです。`), })); overviewLines.push(formatMarkdownTable(["操作", "状態の変化", "説明"], flowRows)); } const notifications = getArray(settings?.Notifications || data.Notifications || []); if (notifications.length > 0) { overviewLines.push(`## 4. 通知される場面`); notifications.forEach((noti) => { const who = formatValue(noti.To || noti.Target || noti.Recipient || noti.Role); const when = formatValue(noti.When || noti.Trigger || noti.Condition || "不明"); const what = formatValue(noti.Message || noti.Subject || noti.Template || "通知が送信されます。"); overviewLines.push(`- ${when} に ${who} へ通知: ${what}`); }); } const links = getArray(settings?.Links || []); if (links.length > 0) { overviewLines.push(`## 5. 関連する仕組み(マスタ連携)`); links.forEach((link) => { const desc = inferLinkDescription(link); overviewLines.push(`- ${formatValue(link.Description || link.Title || "関連マスタ連携")}: ${desc}`); }); } if (columns.length === 0 && processes.length === 0 && notifications.length === 0 && links.length === 0) { overviewLines.push(`## 5. 関連する仕組み(マスタ連携)`); overviewLines.push(`- このサイトは現時点では項目・プロセス・通知・他サイト連携の情報が限定的です。`); } return { markdown: overviewLines.join("\n\n"), sanitizedTitle }; } function generateAllDocs() { if (!fs.existsSync(CONFIG_PATH)) { console.error(`[エラー] 設定ファイルが見つかりません: ${CONFIG_PATH}`); process.exit(1); } const config = readJson(CONFIG_PATH); const siteIds = parseSiteIds(config.SiteId); if (siteIds.length === 0) { console.error("[エラー] config.json の SiteId から有効なサイトIDを取得できませんでした。"); process.exit(1); } ensureDir(CONFIGS_DIR); ensureDir(DOCS_DIR); console.log("[INFO] まず既存サイト情報を取得・抽出します..."); try { child_process.execFileSync("node", [path.join(ROOT, "get-site-config.js")], { stdio: "inherit", cwd: ROOT, }); } catch (err) { console.error("[エラー] get-site-config.js の実行に失敗しました。取得処理を確認してください。"); process.exit(1); } for (const siteId of siteIds) { console.log(`\n==== ドキュメント生成: SiteId=${siteId} ==== `); const siteJsonPath = getSiteJsonPath(siteId); if (!siteJsonPath) { console.error(`[警告] site-${siteId}_latest.json が見つかりません。スキップします。`); continue; } const json = readJson(siteJsonPath); let data; try { data = resolveSiteData(json); } catch (err) { console.error(`[エラー] ${siteJsonPath} のデータ構造を解釈できませんでした: ${err.message}`); continue; } const settings = data.SiteSettings || {}; const manifest = loadManifest(siteId); const specMarkdown = generateSpecMarkdown(siteId, data, settings, manifest); const specPath = path.join(CONFIGS_DIR, `site-${siteId}_spec.md`); fs.writeFileSync(specPath, specMarkdown, "utf-8"); console.log(`[OK] 仕様書生成: ${specPath}`); const { markdown: overviewMarkdown, sanitizedTitle } = generateOverviewMarkdown(siteId, data, settings, manifest); const overviewName = `site-${siteId}_${sanitizedTitle}_overview.md`; const overviewPath = path.join(DOCS_DIR, overviewName); fs.writeFileSync(overviewPath, overviewMarkdown, "utf-8"); console.log(`[OK] 概要書生成: ${overviewPath}`); try { child_process.execFileSync("node", [path.join(ROOT, "md-to-pdf.js"), overviewPath], { stdio: "inherit", cwd: ROOT, }); console.log(`[OK] PDF生成完了: ${overviewPath.replace(/\.md$/i, ".pdf")}`); } catch (err) { console.error(`[エラー] md-to-pdf.js による PDF 生成に失敗しました: ${overviewPath}`); console.error(err.message || err); } } } if (require.main === module) { generateAllDocs(); }