- Nx 22.7 monorepo (pnpm 11.1, TypeScript 5.9, Node 24) - apps/api: NestJS 11 (CJS conforme CODING-RULES.md PGD-DB-004) - apps/web: React 19 + Vite 8 (ESM) - libs/shared/api-interface: Zod contract base - Docker Compose dev: Postgres 18, Valkey 8, MinIO, Mailpit - WDS artifacts: - design-artifacts/A-Product-Brief/ (5 docs canônicos + 16 dialogs) - design-artifacts/B-Trigger-Map/ (hub + 4 personas + feature impact) - Stack canon: STACK.md v2.2 + CODING-RULES.md v2.0 + brand.md - AGENTS.md + README.md como entrada para devs/agentes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
202 lines
5.5 KiB
JavaScript
202 lines
5.5 KiB
JavaScript
// wds-nav.js — WDS scaffold: generate/update navigation links across all pages in a scenario
|
|
// Usage: node src/scripts/wds-nav.js --scenario "01 Onboarding"
|
|
// node src/scripts/wds-nav.js --all
|
|
|
|
'use strict';
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
function parseArgs(argv) {
|
|
const args = {};
|
|
for (let i = 0; i < argv.length; i++) {
|
|
if (argv[i].startsWith('--')) {
|
|
const key = argv[i].slice(2);
|
|
const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : true;
|
|
args[key] = value;
|
|
if (value !== true) i++;
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function toSlug(str) {
|
|
return str.toLowerCase().replaceAll(/\s+/g, '-');
|
|
}
|
|
|
|
function printUsage() {
|
|
process.stdout.write(
|
|
[
|
|
'Usage: node src/scripts/wds-nav.js --scenario "01 Onboarding"',
|
|
' node src/scripts/wds-nav.js --all',
|
|
'',
|
|
'Options:',
|
|
' --scenario Scenario name or slug to update',
|
|
' --all Update all scenarios',
|
|
' --output Base path (default: current directory)',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
}
|
|
|
|
// Build a human-readable page name from the slug for nav labels
|
|
// e.g. "01-start" -> "01 Start"
|
|
function slugToLabel(slug) {
|
|
return slug
|
|
.split('-')
|
|
.map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
.join(' ');
|
|
}
|
|
|
|
function buildNavRow(prev, next) {
|
|
const leftPart = prev ? `← [${slugToLabel(prev.slug)}](../${prev.slug}/${prev.slug}.md)` : '←';
|
|
const rightPart = next ? `[${slugToLabel(next.slug)} →](../${next.slug}/${next.slug}.md)` : '→';
|
|
return `${leftPart} | ${rightPart}`;
|
|
}
|
|
|
|
// Replace all 3 occurrences of navigation rows in a page file.
|
|
// Nav rows are lines matching the pattern: starts with '←' or contains '| [' and ends with '→'
|
|
// We identify them by a simple pattern: line that starts with "←" (after trim).
|
|
function updateNavInContent(content, navRow) {
|
|
const lines = content.split('\n');
|
|
const result = [];
|
|
let navCount = 0;
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
// Match navigation rows: lines that start with "←" (our nav format)
|
|
if (trimmed.startsWith('←')) {
|
|
result.push(navRow);
|
|
navCount++;
|
|
} else {
|
|
result.push(line);
|
|
}
|
|
}
|
|
|
|
return { content: result.join('\n'), navCount };
|
|
}
|
|
|
|
function getPageFolders(scenarioDir) {
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(scenarioDir, { withFileTypes: true });
|
|
} catch {
|
|
return [];
|
|
}
|
|
|
|
return entries
|
|
.filter((e) => e.isDirectory())
|
|
.map((e) => e.name)
|
|
.filter((name) => {
|
|
// Must have a matching .md file inside
|
|
const mdFile = path.join(scenarioDir, name, `${name}.md`);
|
|
return fs.existsSync(mdFile);
|
|
})
|
|
.sort(); // Sort alphabetically — page numbers ensure correct order
|
|
}
|
|
|
|
function processScenario(scenariosBase, scenarioSlug) {
|
|
const scenarioDir = path.join(scenariosBase, scenarioSlug);
|
|
|
|
if (!fs.existsSync(scenarioDir)) {
|
|
process.stderr.write(`Error: Scenario not found: ${scenarioDir}\n`);
|
|
return false;
|
|
}
|
|
|
|
const pageSlugs = getPageFolders(scenarioDir);
|
|
|
|
if (pageSlugs.length === 0) {
|
|
process.stdout.write(` ${scenarioSlug}: no pages found, skipping.\n`);
|
|
return true;
|
|
}
|
|
|
|
let updated = 0;
|
|
|
|
for (let i = 0; i < pageSlugs.length; i++) {
|
|
const slug = pageSlugs[i];
|
|
const prev = i > 0 ? { slug: pageSlugs[i - 1] } : null;
|
|
const next = i < pageSlugs.length - 1 ? { slug: pageSlugs[i + 1] } : null;
|
|
const navRow = buildNavRow(prev, next);
|
|
|
|
const mdFile = path.join(scenarioDir, slug, `${slug}.md`);
|
|
let content;
|
|
try {
|
|
content = fs.readFileSync(mdFile, 'utf8');
|
|
} catch (error) {
|
|
process.stderr.write(` Error reading ${mdFile}: ${error.message}\n`);
|
|
continue;
|
|
}
|
|
|
|
const { content: newContent, navCount } = updateNavInContent(content, navRow);
|
|
|
|
if (navCount === 0) {
|
|
process.stderr.write(` Warning: No navigation rows found in ${slug}.md\n`);
|
|
}
|
|
|
|
try {
|
|
fs.writeFileSync(mdFile, newContent, 'utf8');
|
|
updated++;
|
|
} catch (error) {
|
|
process.stderr.write(` Error writing ${mdFile}: ${error.message}\n`);
|
|
}
|
|
}
|
|
|
|
process.stdout.write(`✓ Updated navigation for ${updated} pages in ${scenarioSlug}\n`);
|
|
return true;
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
|
|
if (args.help) {
|
|
printUsage();
|
|
process.exit(0);
|
|
}
|
|
|
|
if (!args.scenario && !args.all) {
|
|
process.stderr.write('Error: --scenario or --all is required.\n\n');
|
|
printUsage();
|
|
process.exit(1);
|
|
}
|
|
|
|
const outputBase = args.output || process.cwd();
|
|
const scenariosBase = path.join(outputBase, 'C-UX-Scenarios');
|
|
|
|
if (!fs.existsSync(scenariosBase)) {
|
|
process.stderr.write(`Error: C-UX-Scenarios directory not found at: ${scenariosBase}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (args.all) {
|
|
let entries;
|
|
try {
|
|
entries = fs.readdirSync(scenariosBase, { withFileTypes: true });
|
|
} catch (error) {
|
|
process.stderr.write(`Error reading C-UX-Scenarios: ${error.message}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const scenarios = entries
|
|
.filter((e) => e.isDirectory())
|
|
.map((e) => e.name)
|
|
.sort();
|
|
|
|
if (scenarios.length === 0) {
|
|
process.stdout.write('No scenarios found.\n');
|
|
process.exit(0);
|
|
}
|
|
|
|
let allOk = true;
|
|
for (const slug of scenarios) {
|
|
if (!processScenario(scenariosBase, slug)) allOk = false;
|
|
}
|
|
process.exit(allOk ? 0 : 1);
|
|
} else {
|
|
const scenarioSlug = toSlug(args.scenario);
|
|
const ok = processScenario(scenariosBase, scenarioSlug);
|
|
process.exit(ok ? 0 : 1);
|
|
}
|
|
}
|
|
|
|
main();
|