tadforce 0.0.0-stage → 0.3.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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +202 -0
- package/NOTICE +11 -0
- package/README.md +266 -2
- package/assets/logo-mark.svg +24 -0
- package/assets/logo.svg +31 -0
- package/bin/tadforce.js +4 -0
- package/commands/tadforce-review.md +8 -0
- package/commands/tadforce-update.md +6 -0
- package/commands/tadforce.md +10 -0
- package/package.json +60 -4
- package/skills/tadforce/SKILL.md +73 -0
- package/skills/tadforce/references/axes-rules.md +29 -0
- package/skills/tadforce/references/evidence-rules.md +40 -0
- package/skills/tadforce/references/exclusion-heuristics.md +40 -0
- package/skills/tadforce/references/figure-rules.md +42 -0
- package/skills/tadforce/references/markdown-conventions.md +67 -0
- package/skills/tadforce/references/modules/agentforce-ai.md +24 -0
- package/skills/tadforce/references/modules/analytics.md +19 -0
- package/skills/tadforce/references/modules/commerce.md +25 -0
- package/skills/tadforce/references/modules/data-cloud.md +23 -0
- package/skills/tadforce/references/modules/experience-cloud.md +24 -0
- package/skills/tadforce/references/modules/field-service.md +25 -0
- package/skills/tadforce/references/modules/industries.md +21 -0
- package/skills/tadforce/references/modules/integration-platforms.md +19 -0
- package/skills/tadforce/references/modules/marketing.md +24 -0
- package/skills/tadforce/references/modules/omnistudio.md +24 -0
- package/skills/tadforce/references/modules/platform-events.md +19 -0
- package/skills/tadforce/references/modules/revenue-cpq.md +25 -0
- package/skills/tadforce/references/modules/sales-cloud.md +26 -0
- package/skills/tadforce/references/modules/security-shield.md +18 -0
- package/skills/tadforce/references/modules/service-cloud.md +27 -0
- package/skills/tadforce/references/naming-external-systems.md +32 -0
- package/skills/tadforce/references/questions.md +59 -0
- package/skills/tadforce/references/salesforce-skills.md +27 -0
- package/skills/tadforce/references/workflow.md +95 -0
- package/skills/tadforce/references/writing-rules.md +47 -0
- package/skills/tadforce-review/SKILL.md +53 -0
- package/skills/tadforce-update/SKILL.md +48 -0
- package/src/build/brand.js +107 -0
- package/src/build/browser.js +70 -0
- package/src/build/directives.js +293 -0
- package/src/build/export.js +60 -0
- package/src/build/figures.js +119 -0
- package/src/build/mermaid.js +107 -0
- package/src/build/render.js +299 -0
- package/src/build/serve.js +59 -0
- package/src/cli.js +68 -0
- package/src/commands/docs.js +204 -0
- package/src/commands/doctor.js +370 -0
- package/src/commands/init.js +132 -0
- package/src/commands/org.js +151 -0
- package/src/commands/review.js +35 -0
- package/src/commands/spec.js +91 -0
- package/src/commands/update.js +154 -0
- package/src/facts/extractors/apex.js +111 -0
- package/src/facts/extractors/automation.js +85 -0
- package/src/facts/extractors/data.js +94 -0
- package/src/facts/extractors/integration.js +76 -0
- package/src/facts/extractors/misc.js +100 -0
- package/src/facts/extractors/security.js +92 -0
- package/src/facts/extractors/ui.js +120 -0
- package/src/facts/fingerprints.js +32 -0
- package/src/facts/heuristics/exclusions.js +143 -0
- package/src/facts/heuristics/modules.js +85 -0
- package/src/facts/heuristics/naming.js +31 -0
- package/src/facts/heuristics/signals.js +76 -0
- package/src/facts/index.js +197 -0
- package/src/facts/org.js +87 -0
- package/src/facts/xml.js +85 -0
- package/src/sf/sf.js +120 -0
- package/src/util/args.js +21 -0
- package/src/util/config.js +104 -0
- package/src/util/fsx.js +58 -0
- package/src/util/i18n.js +49 -0
- package/src/util/log.js +31 -0
- package/src/util/paths.js +11 -0
- package/src/validate/index.js +126 -0
- package/templates/axes/en/00-document-control.md +3 -0
- package/templates/axes/en/01-summary.md +7 -0
- package/templates/axes/en/02-method.md +9 -0
- package/templates/axes/en/03-findings.md +19 -0
- package/templates/axes/en/04-register.md +3 -0
- package/templates/axes/en/05-roadmap.md +3 -0
- package/templates/axes/en/99-appendix.md +5 -0
- package/templates/axes/fr/00-document-control.md +3 -0
- package/templates/axes/fr/01-summary.md +7 -0
- package/templates/axes/fr/02-method.md +9 -0
- package/templates/axes/fr/03-findings.md +19 -0
- package/templates/axes/fr/04-register.md +3 -0
- package/templates/axes/fr/05-roadmap.md +3 -0
- package/templates/axes/fr/99-appendix.md +5 -0
- package/templates/html/cover.html +12 -0
- package/templates/html/editor.js +502 -0
- package/templates/html/style.css +255 -0
- package/templates/html/template.html +88 -0
- package/templates/i18n/en.json +366 -0
- package/templates/i18n/fr.json +366 -0
- package/templates/manifest/package-tad.xml +543 -0
- package/templates/specs/sections.yml +494 -0
- package/templates/tad/en/00-document-control.md +7 -0
- package/templates/tad/en/01-summary.md +17 -0
- package/templates/tad/en/02-purpose.md +35 -0
- package/templates/tad/en/03-context.md +17 -0
- package/templates/tad/en/04-general.md +29 -0
- package/templates/tad/en/05-functional.md +7 -0
- package/templates/tad/en/06-data.md +71 -0
- package/templates/tad/en/07-application.md +49 -0
- package/templates/tad/en/08-ui.md +41 -0
- package/templates/tad/en/09-integrations.md +53 -0
- package/templates/tad/en/10-security.md +59 -0
- package/templates/tad/en/11-reporting.md +13 -0
- package/templates/tad/en/12-operations.md +25 -0
- package/templates/tad/en/13-nfr.md +9 -0
- package/templates/tad/en/14-decisions.md +3 -0
- package/templates/tad/en/15-outlook.md +3 -0
- package/templates/tad/en/99-appendices.md +119 -0
- package/templates/tad/fr/00-document-control.md +7 -0
- package/templates/tad/fr/01-summary.md +17 -0
- package/templates/tad/fr/02-purpose.md +35 -0
- package/templates/tad/fr/03-context.md +17 -0
- package/templates/tad/fr/04-general.md +29 -0
- package/templates/tad/fr/05-functional.md +7 -0
- package/templates/tad/fr/06-data.md +71 -0
- package/templates/tad/fr/07-application.md +49 -0
- package/templates/tad/fr/08-ui.md +41 -0
- package/templates/tad/fr/09-integrations.md +53 -0
- package/templates/tad/fr/10-security.md +59 -0
- package/templates/tad/fr/11-reporting.md +13 -0
- package/templates/tad/fr/12-operations.md +25 -0
- package/templates/tad/fr/13-nfr.md +9 -0
- package/templates/tad/fr/14-decisions.md +3 -0
- package/templates/tad/fr/15-outlook.md +3 -0
- package/templates/tad/fr/99-appendices.md +119 -0
- package/templates/vendor/LICENSE-html-docx-js.txt +21 -0
- package/templates/vendor/LICENSE-html2canvas.txt +22 -0
- package/templates/vendor/LICENSE-mermaid.txt +21 -0
- package/templates/vendor/html-docx.js +13216 -0
- package/templates/vendor/html2canvas.min.js +20 -0
- package/templates/vendor/mermaid.min.js +2314 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
/** Find a Chromium-based browser already installed (Chrome, Edge, Chromium) or a Puppeteer download. */
|
|
7
|
+
export function findBrowser() {
|
|
8
|
+
const env = process.env.TADFORCE_CHROME || process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH;
|
|
9
|
+
if (env && fs.existsSync(env)) return env;
|
|
10
|
+
const pf = process.env.ProgramFiles || 'C:\\Program Files';
|
|
11
|
+
const pf86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
12
|
+
const local = process.env.LOCALAPPDATA || '';
|
|
13
|
+
const candidates = process.platform === 'win32'
|
|
14
|
+
? [
|
|
15
|
+
path.join(pf, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
16
|
+
path.join(pf86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
17
|
+
path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
18
|
+
path.join(pf86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
19
|
+
path.join(pf, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
20
|
+
]
|
|
21
|
+
: process.platform === 'darwin'
|
|
22
|
+
? ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', '/Applications/Chromium.app/Contents/MacOS/Chromium']
|
|
23
|
+
: [];
|
|
24
|
+
for (const c of candidates) if (c && fs.existsSync(c)) return c;
|
|
25
|
+
if (process.platform !== 'win32') {
|
|
26
|
+
for (const bin of ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'microsoft-edge']) {
|
|
27
|
+
const r = spawnSync('which', [bin], { encoding: 'utf8' });
|
|
28
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Browsers downloaded with `npx @puppeteer/browsers install chrome-headless-shell@stable`
|
|
32
|
+
for (const base of [path.join(os.homedir(), '.cache', 'puppeteer'), path.join(process.cwd(), 'chrome-headless-shell'), path.join(process.cwd(), 'chrome')]) {
|
|
33
|
+
if (!fs.existsSync(base)) continue;
|
|
34
|
+
const hit = findExecutable(base, 4);
|
|
35
|
+
if (hit) return hit;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findExecutable(dir, depth) {
|
|
41
|
+
if (depth < 0) return null;
|
|
42
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
43
|
+
const full = path.join(dir, e.name);
|
|
44
|
+
if (e.isFile() && /^(chrome|chrome-headless-shell)(\.exe)?$/.test(e.name)) return full;
|
|
45
|
+
if (e.isDirectory()) { const r = findExecutable(full, depth - 1); if (r) return r; }
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let shared = null;
|
|
51
|
+
|
|
52
|
+
/** Launch (once) a headless browser. Caller closes it with closeBrowser(). */
|
|
53
|
+
export async function getBrowser() {
|
|
54
|
+
if (shared) return shared;
|
|
55
|
+
const executablePath = findBrowser();
|
|
56
|
+
if (!executablePath) {
|
|
57
|
+
throw new Error('No Chrome, Edge or Chromium found. Install one, or run `npx @puppeteer/browsers install chrome-headless-shell@stable` and set TADFORCE_CHROME to its path.');
|
|
58
|
+
}
|
|
59
|
+
const { default: puppeteer } = await import('puppeteer-core');
|
|
60
|
+
shared = await puppeteer.launch({
|
|
61
|
+
executablePath,
|
|
62
|
+
headless: true,
|
|
63
|
+
args: ['--disable-gpu', '--allow-file-access-from-files', '--no-first-run', '--no-default-browser-check', ...(process.platform === 'linux' ? ['--no-sandbox'] : [])],
|
|
64
|
+
});
|
|
65
|
+
return shared;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function closeBrowser() {
|
|
69
|
+
if (shared) { const b = shared; shared = null; await b.close(); }
|
|
70
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Deterministic content generated from facts: appendices, ERDs, figures, KPI tiles, placeholders.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { t, formatNumber } from '../util/i18n.js';
|
|
4
|
+
import { exists, readText } from '../util/fsx.js';
|
|
5
|
+
import { contextView, layersBlock } from './figures.js';
|
|
6
|
+
|
|
7
|
+
const cell = (v) => String(v ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
8
|
+
const code = (v) => (v ? '`' + String(v).replace(/`/g, '') + '`' : '—');
|
|
9
|
+
const list = (xs, n = 8, dict) => (xs.length > n ? xs.slice(0, n).map(code).join(', ') + ' ' + t(dict, 'annex.more', { n: xs.length - n }) : xs.map(code).join(', ') || '—');
|
|
10
|
+
|
|
11
|
+
export function mdTable(headers, rows) {
|
|
12
|
+
if (!rows.length) return '';
|
|
13
|
+
return ['| ' + headers.map(cell).join(' | ') + ' |', '|' + headers.map(() => '---').join('|') + '|', ...rows.map((r) => '| ' + r.map(cell).join(' | ') + ' |')].join('\n');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Read a dotted path from an object: get(facts, 'counts.apex.classes'). */
|
|
17
|
+
export function get(obj, p) {
|
|
18
|
+
return p.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const truthy = (v) => (Array.isArray(v) ? v.length > 0 : v && typeof v === 'object' ? Object.keys(v).length > 0 : !!v);
|
|
22
|
+
|
|
23
|
+
/** <!-- tad:if modules.omnistudio --> … <!-- tad:else --> … <!-- tad:endif --> (nestable, `!` negates). */
|
|
24
|
+
export function resolveConditionals(text, scope) {
|
|
25
|
+
const out = [];
|
|
26
|
+
const stack = [];
|
|
27
|
+
for (const line of text.split('\n')) {
|
|
28
|
+
const mIf = /^\s*<!--\s*tad:if\s+(!?)([\w.|]+)\s*-->\s*$/.exec(line);
|
|
29
|
+
const mElse = /^\s*<!--\s*tad:else\s*-->\s*$/.test(line);
|
|
30
|
+
const mEnd = /^\s*<!--\s*tad:endif\s*-->\s*$/.test(line);
|
|
31
|
+
const active = stack.every((s) => s.on);
|
|
32
|
+
if (mIf) {
|
|
33
|
+
let v = mIf[2].split('|').some((p) => truthy(get(scope, p)));
|
|
34
|
+
if (mIf[1]) v = !v;
|
|
35
|
+
stack.push({ on: v, parentOn: active });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (mElse && stack.length) { const s = stack[stack.length - 1]; s.on = !s.on; continue; }
|
|
39
|
+
if (mEnd && stack.length) { stack.pop(); continue; }
|
|
40
|
+
if (active) out.push(line);
|
|
41
|
+
}
|
|
42
|
+
return out.join('\n');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveIncludes(text, baseDir) {
|
|
46
|
+
return text.replace(/^@@INCLUDE:(.+?)@@[ \t]*$/gm, (m, rel) => {
|
|
47
|
+
const p = path.resolve(baseDir, rel.trim());
|
|
48
|
+
if (!exists(p)) return `> [!WARN]\n> Missing include: \`${rel}\``;
|
|
49
|
+
const content = readText(p);
|
|
50
|
+
return /\.svg$/i.test(p) ? '<div class="fig-body hand">' + content.replace(/\n\n/g, '\n') + '</div>' : content;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** {{n:path}} numbers (from counts, then facts), {{v:path}} raw values, {{cfg:path}} configuration. */
|
|
55
|
+
export function resolveValues(text, ctx, problems) {
|
|
56
|
+
return text.replace(/\{\{(n|v|cfg):([\w.-]+)\}\}/g, (m, kind, p) => {
|
|
57
|
+
const src = kind === 'cfg' ? ctx.cfg : ctx.facts;
|
|
58
|
+
let v = kind === 'n' ? (get(ctx.facts.counts, p) ?? get(ctx.facts, p)) : get(src, p);
|
|
59
|
+
if (kind === 'n' && Array.isArray(v)) v = v.length;
|
|
60
|
+
if (v === undefined || v === null) { problems.push({ level: 'error', code: 'unknown-placeholder', message: `Unknown placeholder ${m}` }); return `⟨${m.slice(2, -2)}⟩`; }
|
|
61
|
+
return kind === 'n' && typeof v === 'number' ? formatNumber(v, ctx.lang) : String(v);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------- appendices
|
|
66
|
+
function annexes(ctx) {
|
|
67
|
+
const { facts: f, dict, cfg, glossary } = ctx;
|
|
68
|
+
const A = (k) => t(dict, 'annex.' + k);
|
|
69
|
+
const yes = A('yes'), no = A('no');
|
|
70
|
+
const kindsLabel = (ks) => ks.filter((k) => dict.kinds[k]).map((k) => dict.kinds[k]).join(', ') || '—';
|
|
71
|
+
const local = f.data.objects.filter((o) => !o.namespace);
|
|
72
|
+
const n = (x) => formatNumber(x, ctx.lang);
|
|
73
|
+
return {
|
|
74
|
+
'doc-control': () => mdTable([t(dict, 'control.element'), t(dict, 'control.value')], [
|
|
75
|
+
[t(dict, 'control.title'), `${ctx.docName || t(dict, 'meta.docName')} — ${cfg.document.title}`],
|
|
76
|
+
[t(dict, 'control.reference'), ctx.doc === 'axes' ? cfg.document.reference + '-AX' : cfg.document.reference],
|
|
77
|
+
[t(dict, 'control.client'), cfg.client.name],
|
|
78
|
+
...(cfg.integrator.name ? [[t(dict, 'control.integrator'), cfg.integrator.name]] : []),
|
|
79
|
+
...(cfg.document.author ? [[t(dict, 'control.preparedBy'), cfg.document.author]] : []),
|
|
80
|
+
[t(dict, 'control.version'), `${cfg.document.version}${cfg.document.status ? ' — ' + cfg.document.status : ''}`],
|
|
81
|
+
[t(dict, 'control.date'), cfg.document.date],
|
|
82
|
+
[t(dict, 'control.classification'), cfg.document.classification],
|
|
83
|
+
[t(dict, 'control.scope'), t(dict, 'control.scopeValue', { environment: cfg.document.environment, client: cfg.client.name, date: cfg.document.date })
|
|
84
|
+
+ (cfg.scope && (cfg.scope.include || []).length ? ' — ' + t(dict, 'control.scopeLimited', { scope: cfg.scope.include.map((x) => '`' + x + '`').join(', ') }) : '')],
|
|
85
|
+
]),
|
|
86
|
+
'version-history': () => mdTable([t(dict, 'control.version'), t(dict, 'control.date'), t(dict, 'control.author'), t(dict, 'control.purpose')],
|
|
87
|
+
(cfg.document.history && cfg.document.history.length ? cfg.document.history : [{ version: cfg.document.version, date: cfg.document.date, author: [cfg.document.author, cfg.integrator.name].filter(Boolean).join(' — ') || cfg.client.name, purpose: t(dict, 'control.firstVersion', { client: cfg.client.name }) }])
|
|
88
|
+
.map((h) => [h.version, h.date, h.author, h.purpose])),
|
|
89
|
+
legend: () => `<div class="legend">\n<span class="lg actor">${t(dict, 'legend.actor')}</span>\n<span class="lg sf">${t(dict, 'legend.sf')}</span>\n<span class="lg ext">${t(dict, 'legend.ext')}</span>\n<span class="lg data">${t(dict, 'legend.data')}</span>\n</div>`,
|
|
90
|
+
'key-figures': () => mdTable([A('domain'), A('count')], [
|
|
91
|
+
[t(dict, 'keyfig.apps'), n(f.counts.ui.apps)], [t(dict, 'keyfig.pages'), n(f.counts.ui.flexipages)], [t(dict, 'keyfig.lwc'), n(f.counts.ui.lwc)],
|
|
92
|
+
...(f.counts.ui.aura ? [[t(dict, 'keyfig.aura'), n(f.counts.ui.aura)]] : []), ...(f.counts.ui.vfPages ? [[t(dict, 'keyfig.vf'), n(f.counts.ui.vfPages)]] : []),
|
|
93
|
+
[t(dict, 'keyfig.classes'), n(f.counts.apex.classes)], [t(dict, 'keyfig.tests'), n(f.counts.apex.tests)], [t(dict, 'keyfig.triggers'), n(f.counts.apex.triggers)],
|
|
94
|
+
[t(dict, 'keyfig.flows'), `${n(f.counts.flows.active)} / ${n(f.counts.flows.total)}`],
|
|
95
|
+
...(f.modules.omnistudio ? [[t(dict, 'keyfig.os'), n(f.counts.omni.scriptsActive)], [t(dict, 'keyfig.ips'), n(f.counts.omni.ipsActive)], [t(dict, 'keyfig.dms'), n(f.counts.omni.dataMappers)]] : []),
|
|
96
|
+
[t(dict, 'keyfig.objects'), n(f.counts.objects.custom)], [t(dict, 'keyfig.fields'), n(f.counts.fields.custom)], [t(dict, 'keyfig.cmdt'), `${n(f.counts.cmdt.types)} / ${n(f.counts.cmdt.records)}`],
|
|
97
|
+
[t(dict, 'keyfig.psg'), `${n(f.counts.security.permissionSetGroups)} / ${n(f.counts.security.permissionSets)}`], [t(dict, 'keyfig.roles'), n(f.counts.security.roles)],
|
|
98
|
+
[t(dict, 'keyfig.nc'), `${n(f.counts.integration.namedCredentials)} / ${n(f.counts.integration.externalCredentials)}`], [t(dict, 'keyfig.rest'), n(f.counts.integration.restResources)],
|
|
99
|
+
[t(dict, 'keyfig.reports'), `${n(f.counts.misc.reports)} / ${n(f.counts.misc.dashboards)}`],
|
|
100
|
+
]),
|
|
101
|
+
products: () => mdTable([A('product'), A('evidence')], [
|
|
102
|
+
...f.modules.clouds.map((x) => [x.name, x.evidence.map((e) => (e === 'metadata' ? A('evMetadata') : e.replace(/^(namespace|object) (.+)$/, (m0, k, v) => A(k === 'namespace' ? 'evNamespace' : 'evObject') + ' ' + code(v)))).join(', ')]),
|
|
103
|
+
...(f.modules.omniChannel && !f.modules.clouds.some((x) => x.id === 'serviceCloud') ? [['Omni-Channel', `${f.misc.signals.serviceChannels} service channels`]] : []),
|
|
104
|
+
...(f.modules.cti ? [['CTI / call centers', f.integration.callCenters.map((x) => code(x.name)).join(', ')]] : []),
|
|
105
|
+
...f.namespaces.filter((x) => !f.modules.clouds.some((cl) => cl.evidence.includes('namespace ' + x.namespace))).map((x) => [x.product || A('managed'), `${code(x.namespace)} · ${n(x.references)} references${x.installed ? ', installed package' : ''}`]),
|
|
106
|
+
]),
|
|
107
|
+
naming: () => mdTable([A('family'), A('convention'), A('examples')], [
|
|
108
|
+
...f.naming.apex.suffixes.map((s) => ['Apex', `*${s.value}`, n(s.count)]),
|
|
109
|
+
...['apex', 'triggers', 'lwc', 'flows', 'objects', 'permissionSets'].flatMap((k) => f.naming[k].prefixes.map((p) => [k === 'apex' ? 'Apex' : k === 'lwc' ? 'LWC' : k[0].toUpperCase() + k.slice(1), `${p.value}*`, n(p.count)])),
|
|
110
|
+
]),
|
|
111
|
+
objects: () => mdTable([A('apiName'), A('label'), A('owd'), A('fields'), A('recordTypes'), A('relations'), A('description')],
|
|
112
|
+
local.filter((o) => o.kind === 'custom').map((o) => [code(o.name), o.label || '—', `${o.sharingModel || '—'} / ${o.externalSharingModel || '—'}`, o.customFields, o.recordTypes.length, o.relations.length, o.description || '—'])),
|
|
113
|
+
'standard-objects': () => mdTable([A('apiName'), A('fields'), A('recordTypes'), A('owd')],
|
|
114
|
+
local.filter((o) => o.kind === 'standard' && (o.customFields > 0 || o.recordTypes.length)).map((o) => [code(o.name), o.customFields, o.recordTypes.length, `${o.sharingModel || '—'} / ${o.externalSharingModel || '—'}`])),
|
|
115
|
+
'external-ids': () => mdTable([A('object'), A('field')], local.flatMap((o) => o.externalIds.map((x) => [code(o.name), code(x)]))),
|
|
116
|
+
'record-types': () => mdTable([A('object'), A('recordTypes')], local.filter((o) => o.recordTypes.length).map((o) => [code(o.name), o.recordTypes.filter((r) => r.active).map((r) => code(r.name)).join(', ') || '—'])),
|
|
117
|
+
'validation-rules': () => mdTable([A('object'), A('rule'), A('status')], local.flatMap((o) => o.validationRules.map((v) => [code(o.name), code(v.name), v.active ? A('active') : A('inactive')]))),
|
|
118
|
+
cmdt: () => mdTable([A('type'), A('records')], f.data.cmdtTypes.map((x) => [code(x.type), n(x.records)])),
|
|
119
|
+
'platform-events': () => mdTable([A('apiName'), A('label')], local.filter((o) => o.kind === 'platformEvent').map((o) => [code(o.name), o.label || '—'])),
|
|
120
|
+
volumes: () => (f.org && f.org.volumes ? mdTable([A('object'), A('records'), A('lastCreated')], f.org.volumes.filter((v) => v.records > 0).sort((a, b) => b.records - a.records).map((v) => [code(v.object), n(v.records), v.lastCreated || '—'])) : ''),
|
|
121
|
+
'apex-summary': () => mdTable([A('kinds'), A('count')], Object.entries(dict.kinds).map(([k, label]) => [label, f.apex.classes.filter((c) => !c.test && c.kinds.includes(k)).length]).filter(([, c]) => c > 0).map(([l, c]) => [l, n(c)])),
|
|
122
|
+
triggers: () => mdTable([A('trigger'), A('object'), A('events')], f.apex.triggers.map((x) => [code(x.name), code(x.object), x.events.join(', ')])),
|
|
123
|
+
async: () => mdTable([A('class'), A('kinds')], f.apex.classes.filter((c) => !c.test && c.kinds.some((k) => ['batch', 'queueable', 'schedulable', 'future'].includes(k))).map((c) => [code(c.name), kindsLabel(c.kinds.filter((k) => ['batch', 'queueable', 'schedulable', 'future', 'stateful', 'callout'].includes(k)))])),
|
|
124
|
+
rest: () => mdTable([A('url'), A('class'), A('kinds')], f.apex.classes.filter((c) => c.restUrl || c.kinds.includes('soap')).map((c) => [code(c.restUrl || 'SOAP'), code(c.name), c.restUrl ? 'Apex REST' : 'Apex SOAP'])),
|
|
125
|
+
flows: () => mdTable([A('apiName'), A('label'), A('processType'), A('start'), A('object'), A('status')], f.automation.flows.map((x) => [code(x.name), x.label || '—', x.processType || '—', [x.triggerType, x.recordTriggerType, x.scheduleFrequency].filter(Boolean).join(' · ') || '—', x.object ? code(x.object) : '—', x.status])),
|
|
126
|
+
'omni-scripts': () => mdTable([A('apiName'), A('label'), A('type'), A('language'), A('status')], f.automation.omnistudio.omniScripts.filter((x) => x.active).map((x) => [code(x.name), x.label || '—', `${x.type || ''}/${x.subType || ''}`, x.language || '—', A('active')])),
|
|
127
|
+
'integration-procedures': () => mdTable([A('apiName'), A('label'), A('type'), A('status')], f.automation.omnistudio.integrationProcedures.filter((x) => x.active).map((x) => [code(x.name), x.label || '—', `${x.type || ''}/${x.subType || ''}`, A('active')])),
|
|
128
|
+
'data-mappers': () => mdTable([A('apiName'), A('type')], f.automation.omnistudio.dataMappers.map((x) => [code(x.name), x.type || '—'])),
|
|
129
|
+
apps: () => mdTable([A('apiName'), A('label'), A('navigation'), A('formFactors')], f.ui.apps.map((a) => [code(a.name), a.label || '—', a.navType || '—', a.formFactors.join(', ') || '—'])),
|
|
130
|
+
flexipages: () => mdTable([A('apiName'), A('type'), A('object'), A('components')], f.ui.flexipages.map((p) => [code(p.name), p.type || '—', p.object ? code(p.object) : '—', list(p.customComponents, 6, dict)])),
|
|
131
|
+
lwc: () => mdTable([A('apiName'), A('placement'), A('controllers')], f.ui.lwc.map((l) => [code(l.name), l.placedIn.length ? [...new Set(l.placedIn.map((u) => u.name))].slice(0, 4).map(code).join(', ') + (l.placedIn.length > 4 ? ' …' : '') : '—', list(l.apex, 4, dict)])),
|
|
132
|
+
aura: () => mdTable([A('apiName'), A('type'), A('placement')], f.ui.aura.map((a) => [code(a.name), a.type, a.placedIn.length ? a.placedIn.slice(0, 4).map((u) => code(u.name)).join(', ') : '—'])),
|
|
133
|
+
visualforce: () => mdTable([A('apiName'), A('controllers')], f.ui.vfPages.map((p) => [code(p.name), [p.controller, ...p.extensions].filter(Boolean).map(code).join(', ') || '—'])),
|
|
134
|
+
psg: () => mdTable([A('apiName'), A('label'), A('permissionSets')].concat(f.org && f.org.psgAssignments ? [A('users')] : []), f.security.permissionSetGroups.map((g) => {
|
|
135
|
+
const row = [code(g.name), g.label || '—', list(g.permissionSets, 10, dict)];
|
|
136
|
+
if (f.org && f.org.psgAssignments) row.push(n((f.org.psgAssignments.find((a) => a.group === g.name) || {}).users || 0));
|
|
137
|
+
return row;
|
|
138
|
+
})),
|
|
139
|
+
'permission-sets': () => mdTable([A('apiName'), A('label'), A('license')], f.security.permissionSets.map((p) => [code(p.name), p.label || '—', p.license || '—'])),
|
|
140
|
+
profiles: () => mdTable([A('name'), A('license')].concat(f.org && f.org.usersByProfile ? [A('users')] : []), f.security.profiles.map((p) => {
|
|
141
|
+
const row = [code(p.name), p.userLicense || '—'];
|
|
142
|
+
if (f.org && f.org.usersByProfile) row.push(n((f.org.usersByProfile.find((u) => u.profile === p.name) || {}).users || 0));
|
|
143
|
+
return row;
|
|
144
|
+
})),
|
|
145
|
+
roles: () => mdTable([A('apiName'), A('label'), A('parent')], f.security.roles.map((r) => [code(r.name), r.label || '—', r.parent ? code(r.parent) : '—'])),
|
|
146
|
+
owd: () => mdTable([A('object'), A('owd')], local.filter((o) => o.sharingModel && ['custom', 'standard'].includes(o.kind)).map((o) => [code(o.name), `${o.sharingModel} / ${o.externalSharingModel || '—'}`])),
|
|
147
|
+
'sharing-rules': () => mdTable([A('object'), A('rule'), A('kind'), A('access')], f.security.sharingRules.flatMap((s) => s.rules.map((r) => [code(s.object), code(r.name), r.kind, r.access || '—']))),
|
|
148
|
+
queues: () => mdTable([A('apiName'), A('label'), A('object')], f.security.queues.map((q) => [code(q.name), q.label || '—', q.objects.map(code).join(', ') || '—'])),
|
|
149
|
+
'named-credentials': () => mdTable([A('apiName'), A('label'), A('type'), A('endpoint'), A('externalCredential')], f.integration.namedCredentials.map((x) => [code(x.name), x.label || '—', x.type, x.host || '—', x.externalCredential ? code(x.externalCredential) : '—'])),
|
|
150
|
+
'external-credentials': () => mdTable([A('apiName'), A('label'), A('protocol')], f.integration.externalCredentials.map((x) => [code(x.name), x.label || '—', x.protocol || '—'])),
|
|
151
|
+
'connected-apps': () => mdTable([A('apiName'), A('label'), A('scopes')], f.integration.connectedApps.map((x) => [code(x.name), x.label || '—', x.scopes.join(', ') || (x.saml ? 'SAML' : '—')])),
|
|
152
|
+
'auth-providers': () => mdTable([A('apiName'), A('label'), A('type'), A('handler')], f.integration.authProviders.map((x) => [code(x.name), x.label || '—', x.type || '—', x.registrationHandler ? code(x.registrationHandler) : '—'])),
|
|
153
|
+
'allowed-domains': () => mdTable([A('kind'), A('apiName'), A('endpoint')], [
|
|
154
|
+
...f.integration.remoteSites.map((x) => ['Remote Site', code(x.name), x.host || '—']),
|
|
155
|
+
...f.integration.cspTrustedSites.map((x) => ['CSP Trusted Site', code(x.name), x.host || '—']),
|
|
156
|
+
...f.integration.corsOrigins.map((x) => ['CORS', code(x.name), x.host || '—']),
|
|
157
|
+
]),
|
|
158
|
+
'call-centers': () => mdTable([A('apiName'), A('label'), A('endpoint')], f.integration.callCenters.map((x) => [code(x.name), x.label || '—', x.adapterHost || '—'])),
|
|
159
|
+
systems: () => mdTable([A('system'), A('description'), A('endpoints')], f.systems.named.map((s) => [s.name, s.description || '—', s.endpoints.map((e) => `${code(e.name)}`).join(', ') || '—'])),
|
|
160
|
+
reports: () => mdTable([A('folder'), A('count')], f.misc.reports.folders.map((fo) => [code(fo), n((f.misc.reports.byFolder || {})[fo] || 0)])),
|
|
161
|
+
labels: () => mdTable([A('apiName'), A('kind')], f.misc.labels.slice(0, 40).map((l) => [code(l.name), l.category || '—'])),
|
|
162
|
+
'scheduled-jobs': () => (f.org && f.org.scheduledJobs ? mdTable([A('name'), A('type'), A('cron'), A('status')], f.org.scheduledJobs.map((j) => [j.name, j.type || '—', code(j.cron), j.state])) : ''),
|
|
163
|
+
packages: () => (f.org && f.org.packages && f.org.packages.length
|
|
164
|
+
? mdTable([A('product'), A('namespace'), A('version')], f.org.packages.map((p) => [p.name, p.namespace ? code(p.namespace) : '—', p.version || '—']))
|
|
165
|
+
: mdTable([A('namespace'), A('product')], f.namespaces.map((x) => [code(x.namespace), x.product || A('managed')]))),
|
|
166
|
+
exclusions: () => mdTable([A('type'), A('apiName'), A('reason')], f.exclusions.applied.map((x) => [x.type, code(x.name), x.reason || '—'])),
|
|
167
|
+
inventory: () => mdTable([A('type'), A('count')], Object.entries(f.misc.folders).filter(([, c]) => c > 0).map(([k, c]) => [code(k), n(c)])),
|
|
168
|
+
traceability: () => mdTable([A('chapter'), A('metadata')], Object.entries(dict.traceability).map(([k, v]) => [ctx.chapterTitle(k), v])),
|
|
169
|
+
glossary: () => mdTable([A('term'), A('definition')], [
|
|
170
|
+
...(glossary.terms || []).filter((x) => x.definition).map((x) => [x.term || x.use, x.definition]),
|
|
171
|
+
...Object.entries(dict.glossary).map(([k, v]) => [k, v]),
|
|
172
|
+
].sort((a, b) => a[0].localeCompare(b[0]))),
|
|
173
|
+
signals: () => mdTable([A('signal'), A('pillar'), A('priority'), A('observed'), A('evidence')], f.signals.map((s) => [t(dict, 'signals.' + s.id), t(dict, 'pillars.' + s.pillar), `<span class="b ${s.priority}">${s.priority.toUpperCase()}</span>`, String(s.value), s.evidence.length ? list(s.evidence.map((e) => e.split(' ')[0]), 6, dict) : '—'])),
|
|
174
|
+
'signals-trusted': () => signalTable(ctx, 'trusted'),
|
|
175
|
+
'signals-easy': () => signalTable(ctx, 'easy'),
|
|
176
|
+
'signals-adaptable': () => signalTable(ctx, 'adaptable'),
|
|
177
|
+
'exclusion-candidates': () => mdTable([A('type'), A('apiName'), A('reason')], f.exclusions.candidates.map((c) => [c.type, code(c.name), c.reasons.join('; ')])),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function signalTable(ctx, pillar) {
|
|
182
|
+
const { facts: f, dict } = ctx;
|
|
183
|
+
const A = (k) => t(dict, 'annex.' + k);
|
|
184
|
+
return mdTable([A('signal'), A('priority'), A('observed'), A('evidence')], f.signals.filter((s) => s.pillar === pillar).map((s) => [t(dict, 'signals.' + s.id), `<span class="b ${s.priority}">${s.priority.toUpperCase()}</span>`, String(s.value), s.evidence.length ? s.evidence.slice(0, 6).map((e) => '`' + e + '`').join(', ') + (s.total > 6 ? ' ' + t(dict, 'annex.more', { n: s.total - 6 }) : '') : '—']));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------- ERD, figures, KPIs
|
|
188
|
+
|
|
189
|
+
/** Mermaid ER diagram limited to Lookup / Master-Detail relationships between the given objects. */
|
|
190
|
+
export function erd(facts, objectNames, caption) {
|
|
191
|
+
const byName = new Map(facts.data.objects.map((o) => [o.name, o]));
|
|
192
|
+
let names = objectNames;
|
|
193
|
+
if (names.length === 1 && names[0] === 'auto') {
|
|
194
|
+
const degree = {};
|
|
195
|
+
for (const o of facts.data.objects.filter((x) => !x.namespace)) {
|
|
196
|
+
for (const r of o.relations) for (const to of r.to) { if (to === o.name || to === 'User' || to === 'Group') continue; degree[o.name] = (degree[o.name] || 0) + 1; degree[to] = (degree[to] || 0) + 1; }
|
|
197
|
+
}
|
|
198
|
+
names = Object.entries(degree).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 12).map(([k]) => k);
|
|
199
|
+
}
|
|
200
|
+
const set = new Set(names);
|
|
201
|
+
const lines = ['erDiagram'];
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
for (const nm of names) {
|
|
204
|
+
const o = byName.get(nm);
|
|
205
|
+
if (!o) continue;
|
|
206
|
+
for (const r of o.relations) {
|
|
207
|
+
for (const to of r.to) {
|
|
208
|
+
if (!set.has(to) || to === nm) continue;
|
|
209
|
+
const key = `${to}>${nm}>${r.field}`;
|
|
210
|
+
if (seen.has(key)) continue;
|
|
211
|
+
seen.add(key);
|
|
212
|
+
const card = r.type === 'MasterDetail' ? '||--|{' : '|o--o{';
|
|
213
|
+
lines.push(` ${ent(to)} ${card} ${ent(nm)} : "${r.field}"`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const nm of names) if (!lines.some((l) => l.includes(` ${ent(nm)} `) || l.endsWith(` ${ent(nm)}`))) lines.push(` ${ent(nm)} {\n Id id\n }`);
|
|
218
|
+
return '```mermaid\n' + lines.join('\n') + '\n```\nFigure: ' + (caption || 'Entity-relationship model') + '\n';
|
|
219
|
+
}
|
|
220
|
+
const ent = (n) => n.replace(/[^A-Za-z0-9_]/g, '_');
|
|
221
|
+
|
|
222
|
+
function rolesFigure(facts, caption) {
|
|
223
|
+
const roles = facts.security.roles;
|
|
224
|
+
if (!roles.length) return '';
|
|
225
|
+
const id = new Map(roles.map((r, i) => [r.name, 'r' + i]));
|
|
226
|
+
const depth = (r, d = 0) => (d > 12 || !r.parent || !id.has(r.parent) ? d : depth(roles.find((x) => x.name === r.parent), d + 1));
|
|
227
|
+
const shown = roles.filter((r) => depth(r) <= (roles.length > 40 ? 2 : 5));
|
|
228
|
+
const lines = ['flowchart TB'];
|
|
229
|
+
for (const r of shown) lines.push(` ${id.get(r.name)}["${(r.label || r.name).replace(/"/g, "'")}"]:::${r.parent ? 'sf' : 'data'}`);
|
|
230
|
+
for (const r of shown) if (r.parent && id.has(r.parent) && shown.some((s) => s.name === r.parent)) lines.push(` ${id.get(r.parent)} --> ${id.get(r.name)}`);
|
|
231
|
+
return '```mermaid\n' + lines.join('\n') + '\n```\nFigure: ' + caption + '\n';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function integrationsFigure(facts, cfg, dict, caption) {
|
|
235
|
+
const lines = ['flowchart LR', ` SF["${(t(dict, 'context.salesforce') + ' — ' + (cfg.client.short || cfg.client.name)).replace(/"/g, "'")}"]:::sf`];
|
|
236
|
+
let i = 0;
|
|
237
|
+
for (const s of facts.systems.named) {
|
|
238
|
+
const k = 's' + i++;
|
|
239
|
+
lines.push(` ${k}["${s.name.replace(/"/g, "'")}"]:::ext`);
|
|
240
|
+
const ncs = s.endpoints.filter((e) => e.kind === 'namedCredential').map((e) => e.name);
|
|
241
|
+
const label = ncs.length ? ncs.slice(0, 2).join(', ') : [...new Set(s.endpoints.map((e) => e.kind))].join(', ');
|
|
242
|
+
const inbound = s.endpoints.every((e) => e.kind === 'connectedApp' || e.kind === 'authProvider');
|
|
243
|
+
lines.push(inbound ? ` ${k} -- "${label}" --> SF` : ` SF -- "${label}" --> ${k}`);
|
|
244
|
+
}
|
|
245
|
+
const hosts = [...new Set(facts.systems.unnamed.filter((e) => e.host).map((e) => e.host))].slice(0, 8);
|
|
246
|
+
for (const h of hosts) { const k = 's' + i++; lines.push(` ${k}["${h}"]:::ext`); lines.push(` SF --> ${k}`); }
|
|
247
|
+
if (facts.counts.integration.restResources) lines.push(` IN["${t(dict, 'context.callers')}"]:::actor`, ` IN -- "Apex REST" --> SF`);
|
|
248
|
+
if (i === 0 && !facts.counts.integration.restResources) return '';
|
|
249
|
+
return '```mermaid\n' + lines.join('\n') + '\n```\nFigure: ' + caption + '\n';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function kpis(ctx) {
|
|
253
|
+
const { facts: f, dict } = ctx;
|
|
254
|
+
const n = (x) => formatNumber(x, ctx.lang);
|
|
255
|
+
const tile = (href, value, label) => `<a class="kpi" href="#${href}"><b>${value}</b><span>${label}</span></a>`;
|
|
256
|
+
const tiles = [
|
|
257
|
+
tile('annex-apps', n(f.counts.ui.apps), t(dict, 'kpi.apps')),
|
|
258
|
+
tile('annex-objects', n(f.counts.objects.custom), t(dict, 'kpi.objects')),
|
|
259
|
+
tile('annex-apex', n(f.counts.apex.classes), t(dict, 'kpi.classes', { tests: n(f.counts.apex.tests) })),
|
|
260
|
+
tile('annex-lwc', n(f.counts.ui.lwc), t(dict, 'kpi.lwc')),
|
|
261
|
+
f.modules.omnistudio && tile('annex-omnistudio', `${n(f.counts.omni.scriptsActive)} / ${n(f.counts.omni.ipsActive)}`, t(dict, 'kpi.omni')),
|
|
262
|
+
tile('annex-automation', n(f.counts.flows.active), t(dict, 'kpi.flows')),
|
|
263
|
+
f.counts.cmdt.records && tile('annex-cmdt', n(f.counts.cmdt.records), t(dict, 'kpi.cmdt', { types: n(f.counts.cmdt.types) })),
|
|
264
|
+
f.counts.integration.systems ? tile('annex-integration', n(f.counts.integration.systems), t(dict, 'kpi.systems')) : tile('annex-integration', n(f.counts.integration.namedCredentials + f.counts.integration.restResources), t(dict, 'kpi.integrations')),
|
|
265
|
+
].filter(Boolean);
|
|
266
|
+
return `<div class="kpis">\n${tiles.join('\n')}\n</div>`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Replace every @@DIRECTIVE@@ line by generated content. Unknown directives are reported. */
|
|
270
|
+
export function resolveDirectives(text, ctx, problems) {
|
|
271
|
+
const A = annexes(ctx);
|
|
272
|
+
return text.replace(/^@@([A-Z]+)(?::([^@|]*))?(?:\|([^@]*))?@@[ \t]*$/gm, (m, kind, arg = '', caption = '') => {
|
|
273
|
+
arg = arg.trim(); caption = caption.trim();
|
|
274
|
+
if (kind === 'ANNEX') {
|
|
275
|
+
const fn = A[arg];
|
|
276
|
+
if (!fn) { problems.push({ level: 'error', code: 'unknown-annex', message: `Unknown appendix generator @@ANNEX:${arg}@@` }); return ''; }
|
|
277
|
+
const out = fn();
|
|
278
|
+
return out || `<p class="empty-annex">${t(ctx.dict, 'annex.none')}</p>`;
|
|
279
|
+
}
|
|
280
|
+
if (kind === 'ERD') return erd(ctx.facts, arg.split(',').map((s) => s.trim()).filter(Boolean), caption);
|
|
281
|
+
if (kind === 'KPIS') return kpis(ctx);
|
|
282
|
+
if (kind === 'FIGURE') {
|
|
283
|
+
if (arg === 'context') return '<div class="fig-body hand">' + contextView(ctx.facts, ctx.glossary, ctx.cfg, ctx.dict) + '</div>\n<p class="figcap">Figure — ' + (caption || t(ctx.dict, 'context.caption')) + '</p>';
|
|
284
|
+
if (arg === 'layers') return layersBlock(ctx.facts, ctx.dict);
|
|
285
|
+
if (arg === 'roles') return rolesFigure(ctx.facts, caption || 'Role hierarchy');
|
|
286
|
+
if (arg === 'integrations') return integrationsFigure(ctx.facts, ctx.cfg, ctx.dict, caption || 'Integration map');
|
|
287
|
+
problems.push({ level: 'error', code: 'unknown-figure', message: `Unknown figure @@FIGURE:${arg}@@` });
|
|
288
|
+
return '';
|
|
289
|
+
}
|
|
290
|
+
problems.push({ level: 'error', code: 'unknown-directive', message: `Unknown directive ${m}` });
|
|
291
|
+
return '';
|
|
292
|
+
});
|
|
293
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { getBrowser } from './browser.js';
|
|
4
|
+
|
|
5
|
+
/** PDF through the browser print engine (same layout as « Print / export › PDF »). */
|
|
6
|
+
export async function exportPdf(htmlPath, pdfPath) {
|
|
7
|
+
const browser = await getBrowser();
|
|
8
|
+
const page = await browser.newPage();
|
|
9
|
+
try {
|
|
10
|
+
await page.goto(pathToFileURL(htmlPath).href, { waitUntil: 'load', timeout: 120000 });
|
|
11
|
+
await page.emulateMediaType('print');
|
|
12
|
+
await page.pdf({ path: pdfPath, printBackground: true, preferCSSPageSize: true, displayHeaderFooter: false, timeout: 300000 });
|
|
13
|
+
} finally {
|
|
14
|
+
await page.close();
|
|
15
|
+
}
|
|
16
|
+
return fs.statSync(pdfPath).size;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** DOCX through the page's own export (same result as « Print / export › Word »). */
|
|
20
|
+
export async function exportDocx(htmlPath, docxPath) {
|
|
21
|
+
const browser = await getBrowser();
|
|
22
|
+
const page = await browser.newPage();
|
|
23
|
+
try {
|
|
24
|
+
await page.setViewport({ width: 1400, height: 1000 });
|
|
25
|
+
await page.goto(pathToFileURL(htmlPath).href, { waitUntil: 'load', timeout: 120000 });
|
|
26
|
+
const b64 = await page.evaluate(async () => {
|
|
27
|
+
let blob = null;
|
|
28
|
+
const orig = URL.createObjectURL.bind(URL);
|
|
29
|
+
URL.createObjectURL = function (b) { blob = b; return orig(b); };
|
|
30
|
+
HTMLAnchorElement.prototype.click = function () {};
|
|
31
|
+
document.getElementById('doDocx').click();
|
|
32
|
+
for (let i = 0; i < 480 && !blob; i++) await new Promise((r) => setTimeout(r, 250));
|
|
33
|
+
if (!blob) return null;
|
|
34
|
+
const bytes = new Uint8Array(await blob.arrayBuffer());
|
|
35
|
+
let s = '';
|
|
36
|
+
for (let i = 0; i < bytes.length; i += 8192) s += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
|
|
37
|
+
return btoa(s);
|
|
38
|
+
});
|
|
39
|
+
if (!b64) throw new Error('The page did not produce a DOCX file.');
|
|
40
|
+
fs.writeFileSync(docxPath, Buffer.from(b64, 'base64'));
|
|
41
|
+
} finally {
|
|
42
|
+
await page.close();
|
|
43
|
+
}
|
|
44
|
+
return fs.statSync(docxPath).size;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** PNG screenshot of a page region (used for README visuals). */
|
|
48
|
+
export async function screenshot(htmlPath, pngPath, { width = 1440, height = 900, script } = {}) {
|
|
49
|
+
const browser = await getBrowser();
|
|
50
|
+
const page = await browser.newPage();
|
|
51
|
+
try {
|
|
52
|
+
await page.setViewport({ width, height, deviceScaleFactor: 1 });
|
|
53
|
+
await page.goto(pathToFileURL(htmlPath).href, { waitUntil: 'load' });
|
|
54
|
+
if (script) await page.evaluate(script);
|
|
55
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
56
|
+
await page.screenshot({ path: pngPath });
|
|
57
|
+
} finally {
|
|
58
|
+
await page.close();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Data-driven figures drawn as SVG (context view) or HTML blocks (logical layers).
|
|
2
|
+
import { t } from '../util/i18n.js';
|
|
3
|
+
|
|
4
|
+
const STY = {
|
|
5
|
+
actor: ['#F1E8FF', '#7B3FE4', '#2A1458'],
|
|
6
|
+
sf: ['#E8F1FF', '#2F6FE4', '#0B2545'],
|
|
7
|
+
ext: ['#FFF2E2', '#E0861F', '#4A2A05'],
|
|
8
|
+
data: ['#E7F6EC', '#2E9E5B', '#0E3B22'],
|
|
9
|
+
warn: ['#FDECEF', '#D6336C', '#5A0A23'],
|
|
10
|
+
};
|
|
11
|
+
const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
12
|
+
const clip = (s, n) => { s = String(s || ''); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
|
|
13
|
+
|
|
14
|
+
export class Svg {
|
|
15
|
+
constructor(w, h, title) { this.w = w; this.h = h; this.title = title; this.parts = []; }
|
|
16
|
+
box(x, y, w, h, kind, lines, { pill = false } = {}) {
|
|
17
|
+
const [fill, stroke, ink] = STY[kind];
|
|
18
|
+
this.parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${pill ? h / 2 : 8}" fill="${fill}" stroke="${stroke}" stroke-width="1.4"/>`);
|
|
19
|
+
lines.forEach((tx, i) => {
|
|
20
|
+
const ty = y + h / 2 + (i - (lines.length - 1) / 2) * 16 + 5;
|
|
21
|
+
this.parts.push(`<text x="${x + w / 2}" y="${ty}" text-anchor="middle" font-size="${i === 0 ? 13.5 : 12}" font-weight="${i === 0 ? 600 : 400}" fill="${ink}">${esc(tx)}</text>`);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
group(x, y, w, h, title) {
|
|
25
|
+
this.parts.push(`<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="14" fill="#F7F9FC" stroke="#C9D4E3" stroke-width="1.4" stroke-dasharray="5 4"/>`);
|
|
26
|
+
this.parts.push(`<text x="${x + 14}" y="${y + 20}" font-size="13" font-weight="700" fill="#33475B">${esc(title)}</text>`);
|
|
27
|
+
}
|
|
28
|
+
arrow(pts, { label, dashed = false, both = false } = {}) {
|
|
29
|
+
const d = 'M' + pts.map(([a, b]) => `${a},${b}`).join(' L');
|
|
30
|
+
this.parts.push(`<path d="${d}" fill="none" stroke="#5B6B7D" stroke-width="1.5"${dashed ? ' stroke-dasharray="5 4"' : ''} marker-end="url(#ah)"${both ? ' marker-start="url(#ahs)"' : ''}/>`);
|
|
31
|
+
if (label) {
|
|
32
|
+
const [x1, y1] = pts[0], [x2, y2] = pts[pts.length - 1];
|
|
33
|
+
const lx = (x1 + x2) / 2, ly = (y1 + y2) / 2 - 6;
|
|
34
|
+
this.parts.push(`<rect x="${lx - label.length * 3.4 - 4}" y="${ly - 12}" width="${label.length * 6.8 + 8}" height="16" rx="4" fill="#fff"/>`);
|
|
35
|
+
this.parts.push(`<text x="${lx}" y="${ly}" text-anchor="middle" font-size="11.5" fill="#33475B">${esc(label)}</text>`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
render() {
|
|
39
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${this.w} ${this.h}" role="img" aria-label="${esc(this.title)}" style="font-family:Segoe UI,Arial,sans-serif;max-width:100%">` +
|
|
40
|
+
`<defs><marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#5B6B7D"/></marker>` +
|
|
41
|
+
`<marker id="ahs" viewBox="0 0 10 10" refX="1" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M10,0 L0,5 L10,10 z" fill="#5B6B7D"/></marker></defs>` +
|
|
42
|
+
this.parts.join('') + '</svg>';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const KIND_LABEL = { namedCredential: 'Named Credential', remoteSite: 'Remote Site', cspTrustedSite: 'CSP', authProvider: 'SSO', connectedApp: 'Connected App', callCenter: 'CTI' };
|
|
47
|
+
|
|
48
|
+
/** Context view: users on the left, the Salesforce org in the middle, connected systems on the right. */
|
|
49
|
+
export function contextView(facts, glossary, cfg, dict) {
|
|
50
|
+
const actors = (glossary.actors || []).map((a) => [a.name, a.channel || a.description || '']).slice(0, 6);
|
|
51
|
+
if (!actors.length) {
|
|
52
|
+
for (const a of facts.ui.apps.slice(0, 5)) actors.push([a.label || a.name, a.console ? 'Console' : 'Lightning']);
|
|
53
|
+
if (!actors.length) actors.push([t(dict, 'context.users'), 'Lightning']);
|
|
54
|
+
}
|
|
55
|
+
const systems = facts.systems.named.map((s) => [s.name, [...new Set(s.endpoints.map((e) => KIND_LABEL[e.kind] || e.kind))].join(' · ')]);
|
|
56
|
+
const seenHosts = new Set();
|
|
57
|
+
for (const e of facts.systems.unnamed) {
|
|
58
|
+
const key = e.host || e.name;
|
|
59
|
+
if (seenHosts.has(key) || systems.length >= 8) continue;
|
|
60
|
+
seenHosts.add(key);
|
|
61
|
+
systems.push([e.host || e.name, KIND_LABEL[e.kind] || e.kind]);
|
|
62
|
+
}
|
|
63
|
+
const rest = facts.counts.integration.restResources;
|
|
64
|
+
if (rest) systems.push([t(dict, 'context.callers'), `${t(dict, 'context.rest')} · ${rest} API`]);
|
|
65
|
+
|
|
66
|
+
const m = facts.modules;
|
|
67
|
+
const caps = m.clouds.map((x) => x.name.replace(/ \(.*\)$/, ''));
|
|
68
|
+
for (const p of m.products) if (!caps.some((x) => p.startsWith(x))) caps.push(p);
|
|
69
|
+
if (m.omniChannel && !caps.includes('Omni-Channel')) caps.push('Omni-Channel');
|
|
70
|
+
if (m.cti) caps.push('CTI');
|
|
71
|
+
if (!caps.length) caps.push('Lightning Platform');
|
|
72
|
+
|
|
73
|
+
const rows = Math.max(actors.length, systems.length, 4);
|
|
74
|
+
const H = Math.max(360, 70 + rows * 64);
|
|
75
|
+
const svg = new Svg(1180, H, t(dict, 'context.caption'));
|
|
76
|
+
// actors
|
|
77
|
+
const ay = (i, n) => 50 + i * ((H - 80) / n);
|
|
78
|
+
actors.forEach(([name, sub], i) => svg.box(20, ay(i, actors.length) + 6, 230, 50, 'actor', [clip(name, 30), clip(sub, 34)]));
|
|
79
|
+
// salesforce org
|
|
80
|
+
const gx = 320, gw = 520;
|
|
81
|
+
svg.group(gx, 30, gw, H - 50, `${t(dict, 'context.salesforce')} — ${cfg.client.short || cfg.client.name}`);
|
|
82
|
+
const apps = facts.ui.apps.filter((a) => a.label).slice(0, 4);
|
|
83
|
+
const appW = apps.length ? (gw - 40 - (apps.length - 1) * 10) / apps.length : 0;
|
|
84
|
+
apps.forEach((a, i) => svg.box(gx + 20 + i * (appW + 10), 56, appW, 46, 'sf', [clip(a.label, Math.floor(appW / 8)), a.console ? 'Console' : 'Lightning']));
|
|
85
|
+
const capY = apps.length ? 122 : 60;
|
|
86
|
+
svg.parts.push(`<text x="${gx + 20}" y="${capY}" font-size="12" font-weight="700" fill="#33475B">${esc(t(dict, 'context.platform'))}</text>`);
|
|
87
|
+
const capList = caps.slice(0, 8);
|
|
88
|
+
const capW = (gw - 40 - 10) / 2;
|
|
89
|
+
capList.forEach((c, i) => svg.box(gx + 20 + (i % 2) * (capW + 10), capY + 10 + Math.floor(i / 2) * 40, capW, 32, 'sf', [clip(c, Math.floor(capW / 7.4))], { pill: true }));
|
|
90
|
+
const dataY = capY + 20 + Math.ceil(capList.length / 2) * 40;
|
|
91
|
+
svg.parts.push(`<text x="${gx + 20}" y="${dataY}" font-size="12" font-weight="700" fill="#33475B">${esc(t(dict, 'context.data'))}</text>`);
|
|
92
|
+
svg.box(gx + 20, dataY + 10, capW, 40, 'data', [t(dict, 'context.objects', { n: facts.counts.objects.custom })]);
|
|
93
|
+
svg.box(gx + 30 + capW, dataY + 10, capW, 40, 'data', [t(dict, 'context.cmdt', { n: facts.counts.cmdt.types })]);
|
|
94
|
+
// external systems
|
|
95
|
+
const sy = (i, n) => 50 + i * ((H - 80) / Math.max(n, 1));
|
|
96
|
+
systems.forEach(([name, sub], i) => svg.box(930, sy(i, systems.length) + 6, 230, 50, 'ext', [clip(name, 30), clip(sub, 34)]));
|
|
97
|
+
// arrows
|
|
98
|
+
actors.forEach((_, i) => svg.arrow([[250, ay(i, actors.length) + 31], [gx, ay(i, actors.length) + 31]]));
|
|
99
|
+
systems.forEach((s, i) => {
|
|
100
|
+
const y = sy(i, systems.length) + 31;
|
|
101
|
+
const inbound = s[0] === t(dict, 'context.callers');
|
|
102
|
+
svg.arrow(inbound ? [[930, y], [gx + gw, y]] : [[gx + gw, y], [930, y]], { dashed: inbound });
|
|
103
|
+
});
|
|
104
|
+
return svg.render();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Logical layers block (HTML), with counts from facts. */
|
|
108
|
+
export function layersBlock(facts, dict) {
|
|
109
|
+
const c = facts.counts;
|
|
110
|
+
const L = (k, n) => t(dict, `layers.${k}`, { n });
|
|
111
|
+
const span = (s) => `<span>${esc(s)}</span>`;
|
|
112
|
+
const row = (cls, title, items) => `<div class="layer${cls ? ' ' + cls : ''}"><div class="ln">${esc(title)}</div><div class="li">${items.filter(Boolean).map(span).join('')}</div></div>`;
|
|
113
|
+
const pres = [c.ui.apps && L('apps', c.ui.apps), c.ui.flexipages && L('pages', c.ui.flexipages), c.ui.lwc && L('lwc', c.ui.lwc) + (c.ui.aura ? ' · ' + L('aura', c.ui.aura) : ''), c.omni.scriptsActive && L('os', c.omni.scriptsActive), c.ui.vfPages && L('vf', c.ui.vfPages)];
|
|
114
|
+
const orch = [c.flows.active && L('flows', c.flows.active), c.omni.ipsActive && L('ips', c.omni.ipsActive), c.omni.dataMappers && L('dms', c.omni.dataMappers)];
|
|
115
|
+
const svc = [c.apex.triggers && L('triggers', c.apex.triggers), c.apex.auraEnabled && L('controllers', c.apex.auraEnabled), L('services', c.apex.classes), (c.apex.batch + c.apex.queueable + c.apex.schedulable + c.apex.future) && L('async')];
|
|
116
|
+
const data = [L('standard'), c.objects.custom && L('custom', c.objects.custom), c.cmdt.types && L('cmdt', c.cmdt.types), L('files')];
|
|
117
|
+
const integ = [c.integration.namedCredentials && L('nc', c.integration.namedCredentials), c.integration.externalCredentials && L('ec', c.integration.externalCredentials), c.integration.restResources && L('rest', c.integration.restResources), c.integration.connectedApps && L('apps2', c.integration.connectedApps)];
|
|
118
|
+
return `<div class="layers">\n${row('', L('presentation'), pres)}\n${row('', L('orchestration'), orch.length && orch.some(Boolean) ? orch : ['—'])}\n${row('', L('services'), svc)}\n${row('data', L('data'), data)}\n${row('int', L('integration'), integ.some(Boolean) ? integ : ['—'])}\n</div>\n<p class="figcap">Figure — ${esc(L('caption'))}</p>`;
|
|
119
|
+
}
|