Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/FEATURE_MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,3 +617,11 @@ find apps/e2e/src/tests -name '*.test.ts' | wc -l

A new feature adds a catalog row, a tree node and, if it stores anything, its
tables in the schema diagram.

The interactive page, [feature-map.html](feature-map.html), is generated. Edit
its inventory in `docs/feature-map/data.js` (its layout in `template.html`, its
script in `app.js`; the diagrams come from this page), then rebuild it:

```bash
npm run docs:feature-map
```
674 changes: 674 additions & 0 deletions docs/feature-map/app.js

Large diffs are not rendered by default.

264 changes: 264 additions & 0 deletions docs/feature-map/data.js

Large diffs are not rendered by default.

374 changes: 374 additions & 0 deletions docs/feature-map/template.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"lint": "eslint .",
"lint:security": "eslint . --config eslint.security.config.js --max-warnings 0",
"test-ids": "node scripts/test-ids/extract-test-ids.mjs --write",
"docs:feature-map": "node scripts/feature-map/build.mjs",
"feature:new": "node scripts/feature/new-feature.mjs",
"bundle:first-load": "node scripts/bundle/first-load.mjs apps/web/dist --budget-gzip-kb=170",
"audit:security": "npm audit --audit-level=high --omit=dev",
Expand Down
32 changes: 32 additions & 0 deletions packages/shared/src/feature-map-build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* The feature-map generator (scripts/feature-map/build.mjs): docs/feature-map.html
* is built from docs/feature-map/ and the diagrams in docs/FEATURE_MAP.md.
*/
import { describe, expect, it } from 'vitest';
import { buildBoardPage, buildStaticPage } from '../../../scripts/feature-map/build.mjs';

const featureMap = '# Map\n\n```mermaid\nflowchart LR\n a --> b\n```\n\n```mermaid\nflowchart LR\n c[x < y & z]\n```\n';
const template = '<title>T</title>\n<header class="top"></header>\n<pre class="mermaid">__M1__</pre>\n<script>\n/*__DATA__*/\n/*__APP__*/\n</script>';

describe('feature map build', () => {
it('places the named diagram, escaped, and the data and script as written', () => {
const page = buildBoardPage({ template, data: "const A = 'cost: $& and $1';", app: 'run();', featureMap });
expect(page).toContain('<pre class="mermaid">flowchart LR\n c[x &lt; y &amp; z]</pre>');
// A `$` pattern in the sources is text, not a replacement pattern.
expect(page).toContain("const A = 'cost: $& and $1';");
expect(page).toContain('run();');
});

it('refuses a template that places a diagram the doc does not have', () => {
expect(() =>
buildBoardPage({ template: template.replace('__M1__', '__M2__'), data: '', app: '', featureMap })
).toThrow(/diagram 2; FEATURE_MAP.md has only 2/);
});

it('wraps the repo copy in a whole document with the board off', () => {
const page = buildStaticPage(buildBoardPage({ template, data: '', app: '', featureMap }));
expect(page.startsWith('<!doctype html>')).toBe(true);
expect(page).toContain('<title>T</title>\n</head>\n<body>\n<script>window.FOX_MAP_STATIC = true;</script>\n<header class="top">');
expect(page).toContain('mermaid@11.4.1');
});
});
10 changes: 10 additions & 0 deletions scripts/feature-map/build.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** Types for build.mjs, for the tests that import it. */
export interface FeatureMapSources {
template: string;
data: string;
app: string;
featureMap: string;
}
export function buildBoardPage(sources: FeatureMapSources): string;
export function buildStaticPage(boardPage: string): string;
export function readSources(root?: string): FeatureMapSources;
94 changes: 94 additions & 0 deletions scripts/feature-map/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Fox Schema (foxschema)
* Copyright 2024-2026 Huy Phan <huyplb@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*
* Builds docs/feature-map.html, the interactive feature map, from:
*
* docs/feature-map/template.html page layout and styles; __M<n>__ marks where diagram n goes
* docs/feature-map/data.js the inventory: features, routes, tables, engines, tests
* docs/feature-map/app.js the page's script
* docs/FEATURE_MAP.md the Mermaid diagrams, in the order they appear there
*
* Usage:
* node scripts/feature-map/build.mjs write docs/feature-map.html
* node scripts/feature-map/build.mjs --board <file> also write the planning-board page
*
* The repo copy is a whole document with no shared board, and it loads Mermaid
* from a pinned CDN. The board page is the same page with the board turned on.
* It is published as an Artifact, which adds the document skeleton itself.
*/
import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
const SRC = path.join(ROOT, 'docs/feature-map');
const read = (p) => readFileSync(p, 'utf8');

/** HTML text escaping for a <pre>, leaving quotes alone. */
const escapeText = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');

/** The page with the board: every placeholder filled. */
export function buildBoardPage({ template, data, app, featureMap }) {
const blocks = [...featureMap.matchAll(/```mermaid\n([\s\S]*?)```/g)].map((m) => m[1].trim());
// The page shows most of the doc's diagrams, not all: a placeholder past the
// doc's last diagram means one was removed or the template drifted.
for (const [, n] of template.matchAll(/__M(\d+)__/g)) {
if (Number(n) >= blocks.length) {
throw new Error(`template.html places diagram ${n}; FEATURE_MAP.md has only ${blocks.length}`);
}
}
// Functions as replacements, so a `$` in the source is never read as a pattern.
let page = template.replace(/__M(\d+)__/g, (_, n) => escapeText(blocks[Number(n)]));
page = page.replace('/*__DATA__*/', () => data);
page = page.replace('/*__APP__*/', () => app);
return page;
}

/** The copy kept in the repo: a whole document, no shared board, Mermaid from a pinned CDN. */
export function buildStaticPage(boardPage) {
const cut = boardPage.indexOf('<header class="top">');
if (cut < 0) throw new Error('template.html has no <header class="top">');
const head = boardPage.slice(0, cut);
const body = boardPage.slice(cut);
return (
'<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n' +
'<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">\n' +
'<!-- Generated from docs/FEATURE_MAP.md and the feature inventory. The live planning board is a shared page;\n' +
' this copy shows the map, workflows, schema, engines and coverage. Open it in a browser. -->\n' +
'<style>body{margin:0}[hidden]{display:none!important}</style>\n' +
head +
'</head>\n<body>\n<script>window.FOX_MAP_STATIC = true;</script>\n' +
body +
'\n<script src="https://cdn.jsdelivr.net/npm/mermaid@11.4.1/dist/mermaid.min.js"></script>\n' +
'<script>\n if (window.mermaid) {\n' +
" mermaid.initialize({ startOnLoad: false, theme: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default' });\n" +
" mermaid.run({ querySelector: 'pre.mermaid' });\n }\n</script>\n</body>\n</html>\n"
);
}

export function readSources(root = ROOT) {
const src = path.join(root, 'docs/feature-map');
return {
template: read(path.join(src, 'template.html')),
data: read(path.join(src, 'data.js')),
app: read(path.join(src, 'app.js')),
featureMap: read(path.join(root, 'docs/FEATURE_MAP.md')),
};
}

const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
const board = buildBoardPage(readSources());
writeFileSync(path.join(ROOT, 'docs/feature-map.html'), buildStaticPage(board));
console.log(`wrote docs/feature-map.html (from ${path.relative(ROOT, SRC)}/)`);
const at = process.argv.indexOf('--board');
if (at > 0) {
const out = process.argv[at + 1];
if (!out) throw new Error('--board needs a file path');
writeFileSync(out, board);
console.log(`wrote ${out}`);
}
}
Loading