@brignano/driftwood 0.0.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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * A tiny name -> implementation registry, shared by providers and renderers.
3
+ *
4
+ * Extension points are the whole point of this file: adding a provider or a
5
+ * renderer must never require editing core code. Built-ins register themselves
6
+ * at startup; third parties call `register()` with the same API, so a plugin
7
+ * is not a second-class citizen.
8
+ */
9
+ export class Registry {
10
+ label;
11
+ items = new Map();
12
+ constructor(label) {
13
+ this.label = label;
14
+ }
15
+ register(item) {
16
+ if (this.items.has(item.name)) {
17
+ throw new Error(`${this.label} '${item.name}' is already registered`);
18
+ }
19
+ this.items.set(item.name, item);
20
+ return this;
21
+ }
22
+ /** Replaces an existing entry. Used by tests and by deliberate overrides. */
23
+ override(item) {
24
+ this.items.set(item.name, item);
25
+ return this;
26
+ }
27
+ get(name) {
28
+ const item = this.items.get(name);
29
+ if (!item) {
30
+ throw new Error(`unknown ${this.label} '${name}' (available: ${this.names().join(', ') || 'none'})`);
31
+ }
32
+ return item;
33
+ }
34
+ has(name) {
35
+ return this.items.has(name);
36
+ }
37
+ names() {
38
+ return [...this.items.keys()].sort();
39
+ }
40
+ all() {
41
+ return this.names().map((n) => this.items.get(n));
42
+ }
43
+ }
@@ -0,0 +1,4 @@
1
+ import type { Model } from '../model/schema.js';
2
+ import type { RenderContext } from './types.js';
3
+ export declare function renderDot(model: Model, ctx?: RenderContext): string;
4
+ export declare const dotRenderer: import("./types.js").Renderer;
@@ -0,0 +1,92 @@
1
+ import { defineRenderer } from './types.js';
2
+ import { selectEntities } from './select.js';
3
+ /**
4
+ * Graphviz DOT source.
5
+ *
6
+ * Emitting DOT needs no Graphviz installed — it's just text. That distinction
7
+ * matters: `dot` (this renderer) always works, while `graphviz` (rendering DOT
8
+ * to SVG) needs an engine. So even a locked-down machine can produce DOT for
9
+ * someone else to render, and nothing is lost by not having the binary.
10
+ */
11
+ function quote(text) {
12
+ return text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
13
+ }
14
+ const SHAPES = [
15
+ [/(s3|bucket|rds|dynamodb|database|efs|volume|storage)/i, 'cylinder'],
16
+ [/(sqs|sns|queue|topic|kinesis|kafka|eventbridge)/i, 'parallelogram'],
17
+ [/(route53|dns|cloudfront|vpc|subnet|lb|gateway|cdn|zone|record)/i, 'ellipse'],
18
+ [/(iam|role|policy|secret|kms|cert|acm|auth)/i, 'hexagon'],
19
+ ];
20
+ function shapeFor(kind) {
21
+ for (const [pattern, shape] of SHAPES)
22
+ if (pattern.test(kind))
23
+ return shape;
24
+ return 'box';
25
+ }
26
+ const HEALTH_COLORS = {
27
+ healthy: '#0f9d58',
28
+ degraded: '#f4b400',
29
+ down: '#db4437',
30
+ };
31
+ export function renderDot(model, ctx = {}) {
32
+ const view = ctx.view ? model.views.find((v) => v.id === ctx.view) : undefined;
33
+ if (ctx.view && !view) {
34
+ throw new Error(`unknown view: ${ctx.view} (have: ${model.views.map((v) => v.id).join(', ') || 'none'})`);
35
+ }
36
+ const entities = selectEntities(model, view);
37
+ const visible = new Set(entities.map((e) => e.id));
38
+ const lines = [
39
+ `digraph "${quote(model.name)}" {`,
40
+ ` rankdir=${ctx.direction === 'TD' ? 'TB' : 'LR'};`,
41
+ ' node [fontname="Helvetica" style=filled fillcolor="#ffffff"];',
42
+ ' edge [fontname="Helvetica" color="#666666"];',
43
+ ];
44
+ const grouped = new Map();
45
+ const ungrouped = [];
46
+ for (const e of entities) {
47
+ if (e.group) {
48
+ const list = grouped.get(e.group) ?? [];
49
+ list.push(e);
50
+ grouped.set(e.group, list);
51
+ }
52
+ else
53
+ ungrouped.push(e);
54
+ }
55
+ const nodeLine = (e, indent) => {
56
+ const label = `${e.name ?? e.id}\\n${e.kind}`;
57
+ const health = ctx.health?.[e.id];
58
+ const color = health ? ` fillcolor="${HEALTH_COLORS[health]}" fontcolor="#ffffff"` : '';
59
+ return `${indent}"${quote(e.id)}" [label="${quote(label)}" shape=${shapeFor(e.kind)}${color}];`;
60
+ };
61
+ let clusterIndex = 0;
62
+ for (const [group, members] of [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b))) {
63
+ lines.push(` subgraph cluster_${clusterIndex++} {`);
64
+ lines.push(` label="${quote(group)}";`);
65
+ lines.push(' style=rounded; color="#999999";');
66
+ for (const e of members)
67
+ lines.push(nodeLine(e, ' '));
68
+ lines.push(' }');
69
+ }
70
+ for (const e of ungrouped)
71
+ lines.push(nodeLine(e, ' '));
72
+ for (const edge of model.edges) {
73
+ if (!visible.has(edge.from) || !visible.has(edge.to))
74
+ continue;
75
+ const label = edge.label ? ` [label="${quote(edge.label)}"]` : '';
76
+ lines.push(` "${quote(edge.from)}" -> "${quote(edge.to)}"${label};`);
77
+ }
78
+ lines.push('}');
79
+ return lines.join('\n') + '\n';
80
+ }
81
+ export const dotRenderer = defineRenderer({
82
+ name: 'dot',
83
+ description: 'Graphviz DOT source (no Graphviz installation required)',
84
+ extension: 'dot',
85
+ priority: 10,
86
+ async probe() {
87
+ return { available: true, via: 'built-in' };
88
+ },
89
+ async render(model, ctx) {
90
+ return renderDot(model, ctx);
91
+ },
92
+ });
@@ -0,0 +1,47 @@
1
+ import type { Model } from '../model/schema.js';
2
+ import type { RenderContext } from './types.js';
3
+ /**
4
+ * Graphviz rendering to SVG. Graphviz is **bundled, not required**.
5
+ *
6
+ * The problem this project came from: `mingrammer/diagrams` requires the
7
+ * Graphviz *system binary*, which cannot clear corporate software approval.
8
+ * The answer isn't to drop Graphviz, nor to make it an optional extra the user
9
+ * has to go and find — it's to ship it in a form that needs no system package.
10
+ *
11
+ * `@hpcc-js/wasm-graphviz` is real Graphviz compiled to WebAssembly: same DOT
12
+ * semantics, same layouts, zero transitive dependencies, and the WASM is
13
+ * inlined into the JS so there is no separate binary to locate. It is a
14
+ * regular dependency, so `npm install` yields working Graphviz on any machine.
15
+ *
16
+ * Three tiers remain, in preference order:
17
+ *
18
+ * 1. native — the `dot` binary is on PATH. Preferred when present: faster on
19
+ * very large graphs, and it honours a site's own Graphviz build/plugins.
20
+ * 2. wasm — the bundled WASM build. The default. Works anywhere
21
+ * WebAssembly does, with no admin rights and no system package.
22
+ * 3. none — WebAssembly is switched off (a hardened or `--jitless`
23
+ * runtime), so `--engine auto` falls back to Mermaid.
24
+ *
25
+ * Tier 3 is rare but real, which is why the fallback is not vestigial.
26
+ */
27
+ export type GraphvizTier = 'native' | 'wasm' | 'none';
28
+ /** Runs `dot -V` to see whether a usable native Graphviz is on PATH. */
29
+ export declare function probeNativeDot(command?: string): Promise<boolean>;
30
+ interface WasmGraphviz {
31
+ layout(source: string, format: string, engine: string): string;
32
+ }
33
+ /**
34
+ * Loads the bundled WASM build.
35
+ *
36
+ * Imported lazily rather than at module load: it is ~800KB, and a Mermaid-only
37
+ * render should never pay for it. Still wrapped in try/catch because
38
+ * WebAssembly can be disabled at runtime, and that must degrade to the Mermaid
39
+ * fallback rather than crash.
40
+ */
41
+ export declare function loadWasmGraphviz(): Promise<WasmGraphviz | undefined>;
42
+ /** Test seam: forget any cached WASM instance. */
43
+ export declare function resetWasmCache(): void;
44
+ export declare function detectTier(): Promise<GraphvizTier>;
45
+ export declare function renderGraphvizSvg(model: Model, ctx?: RenderContext): Promise<string>;
46
+ export declare const graphvizRenderer: import("./types.js").Renderer;
47
+ export {};
@@ -0,0 +1,121 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { renderDot } from './dot.js';
3
+ import { defineRenderer } from './types.js';
4
+ /** Runs `dot -V` to see whether a usable native Graphviz is on PATH. */
5
+ export function probeNativeDot(command = 'dot') {
6
+ return new Promise((resolve) => {
7
+ let settled = false;
8
+ const done = (v) => {
9
+ if (!settled) {
10
+ settled = true;
11
+ resolve(v);
12
+ }
13
+ };
14
+ try {
15
+ const child = spawn(command, ['-V'], { stdio: 'ignore' });
16
+ child.on('error', () => done(false));
17
+ child.on('close', (code) => done(code === 0));
18
+ // A hung probe must never block a render; treat slow as absent.
19
+ setTimeout(() => {
20
+ child.kill();
21
+ done(false);
22
+ }, 2000).unref?.();
23
+ }
24
+ catch {
25
+ done(false);
26
+ }
27
+ });
28
+ }
29
+ let wasmCache;
30
+ let wasmAttempted = false;
31
+ /**
32
+ * Loads the bundled WASM build.
33
+ *
34
+ * Imported lazily rather than at module load: it is ~800KB, and a Mermaid-only
35
+ * render should never pay for it. Still wrapped in try/catch because
36
+ * WebAssembly can be disabled at runtime, and that must degrade to the Mermaid
37
+ * fallback rather than crash.
38
+ */
39
+ export async function loadWasmGraphviz() {
40
+ if (wasmAttempted)
41
+ return wasmCache;
42
+ wasmAttempted = true;
43
+ try {
44
+ // `lib` is ES2022, which has no WebAssembly types; probe it off globalThis.
45
+ if (typeof globalThis.WebAssembly === 'undefined')
46
+ return undefined;
47
+ const { Graphviz } = await import('@hpcc-js/wasm-graphviz');
48
+ wasmCache = (await Graphviz.load());
49
+ }
50
+ catch {
51
+ wasmCache = undefined;
52
+ }
53
+ return wasmCache;
54
+ }
55
+ /** Test seam: forget any cached WASM instance. */
56
+ export function resetWasmCache() {
57
+ wasmCache = undefined;
58
+ wasmAttempted = false;
59
+ }
60
+ export async function detectTier() {
61
+ if (await probeNativeDot())
62
+ return 'native';
63
+ if (await loadWasmGraphviz())
64
+ return 'wasm';
65
+ return 'none';
66
+ }
67
+ function runNativeDot(source, engine, format) {
68
+ return new Promise((resolve, reject) => {
69
+ const child = spawn(engine, [`-T${format}`]);
70
+ let out = '';
71
+ let err = '';
72
+ child.stdout.on('data', (d) => (out += d));
73
+ child.stderr.on('data', (d) => (err += d));
74
+ child.on('error', reject);
75
+ child.on('close', (code) => {
76
+ if (code === 0)
77
+ resolve(out);
78
+ else
79
+ reject(new Error(`${engine} exited ${code}: ${err.trim()}`));
80
+ });
81
+ child.stdin.write(source);
82
+ child.stdin.end();
83
+ });
84
+ }
85
+ export async function renderGraphvizSvg(model, ctx = {}) {
86
+ const source = renderDot(model, ctx);
87
+ const tier = await detectTier();
88
+ if (tier === 'native')
89
+ return runNativeDot(source, 'dot', 'svg');
90
+ if (tier === 'wasm') {
91
+ const gv = await loadWasmGraphviz();
92
+ if (gv)
93
+ return gv.layout(source, 'svg', 'dot');
94
+ }
95
+ throw new Error('Graphviz is unavailable because WebAssembly is disabled in this runtime ' +
96
+ '(for example `node --jitless`). Install the `dot` binary, or use ' +
97
+ '--engine mermaid, which needs neither.');
98
+ }
99
+ export const graphvizRenderer = defineRenderer({
100
+ name: 'graphviz',
101
+ description: 'Graphviz-rendered SVG (bundled WASM build, or a native `dot` binary)',
102
+ extension: 'svg',
103
+ // Highest priority: Graphviz layout beats Mermaid's, and it is available by
104
+ // default. `auto` only drops past it where WebAssembly is switched off.
105
+ priority: 100,
106
+ async probe() {
107
+ const tier = await detectTier();
108
+ if (tier === 'native')
109
+ return { available: true, via: 'native dot binary' };
110
+ if (tier === 'wasm')
111
+ return { available: true, via: 'bundled @hpcc-js/wasm-graphviz' };
112
+ return {
113
+ available: false,
114
+ reason: 'WebAssembly is disabled in this runtime, so the bundled Graphviz cannot load — ' +
115
+ 'install the `dot` binary or use --engine mermaid',
116
+ };
117
+ },
118
+ async render(model, ctx) {
119
+ return renderGraphvizSvg(model, ctx);
120
+ },
121
+ });
@@ -0,0 +1,28 @@
1
+ import { Registry } from '../registry.js';
2
+ import type { Model } from '../model/schema.js';
3
+ import type { Renderer, RenderContext } from './types.js';
4
+ export declare const renderers: Registry<Renderer>;
5
+ export interface Resolved {
6
+ renderer: Renderer;
7
+ via: string;
8
+ /** Set when `auto` wanted a higher-priority engine but it wasn't usable. */
9
+ fellBackFrom?: string;
10
+ }
11
+ /**
12
+ * Picks a renderer. `auto` walks engines by priority and takes the first that
13
+ * reports itself usable, so a machine with Graphviz gets Graphviz and a locked
14
+ * down one silently gets Mermaid — same command, same config, no failure.
15
+ */
16
+ export declare function resolveRenderer(engine?: string): Promise<Resolved>;
17
+ export declare function render(model: Model, ctx?: RenderContext, engine?: string): Promise<{
18
+ output: string;
19
+ renderer: Renderer;
20
+ via: string;
21
+ /** Set when `auto` wanted a higher-priority engine but it wasn't usable. */
22
+ fellBackFrom?: string;
23
+ }>;
24
+ export { renderMermaid } from './mermaid.js';
25
+ export { renderDot } from './dot.js';
26
+ export { renderGraphvizSvg, detectTier, probeNativeDot } from './graphviz.js';
27
+ export { selectEntities } from './select.js';
28
+ export type { Renderer, RenderContext, Availability } from './types.js';
@@ -0,0 +1,42 @@
1
+ import { Registry } from '../registry.js';
2
+ import { mermaidRenderer } from './mermaid.js';
3
+ import { dotRenderer } from './dot.js';
4
+ import { graphvizRenderer } from './graphviz.js';
5
+ export const renderers = new Registry('renderer');
6
+ renderers.register(graphvizRenderer);
7
+ renderers.register(mermaidRenderer);
8
+ renderers.register(dotRenderer);
9
+ /**
10
+ * Picks a renderer. `auto` walks engines by priority and takes the first that
11
+ * reports itself usable, so a machine with Graphviz gets Graphviz and a locked
12
+ * down one silently gets Mermaid — same command, same config, no failure.
13
+ */
14
+ export async function resolveRenderer(engine = 'auto') {
15
+ if (engine !== 'auto') {
16
+ const renderer = renderers.get(engine);
17
+ const probe = await renderer.probe();
18
+ if (!probe.available) {
19
+ // An explicit choice fails loudly; only `auto` is allowed to substitute.
20
+ throw new Error(`renderer '${engine}' is not available: ${probe.reason ?? 'unknown reason'}`);
21
+ }
22
+ return { renderer, via: probe.via ?? 'unknown' };
23
+ }
24
+ const ordered = [...renderers.all()].sort((a, b) => b.priority - a.priority);
25
+ let skipped;
26
+ for (const renderer of ordered) {
27
+ const probe = await renderer.probe();
28
+ if (probe.available) {
29
+ return { renderer, via: probe.via ?? 'unknown', fellBackFrom: skipped };
30
+ }
31
+ skipped ??= renderer.name;
32
+ }
33
+ throw new Error('no renderer is available');
34
+ }
35
+ export async function render(model, ctx = {}, engine = 'auto') {
36
+ const resolved = await resolveRenderer(engine);
37
+ return { ...resolved, output: await resolved.renderer.render(model, ctx) };
38
+ }
39
+ export { renderMermaid } from './mermaid.js';
40
+ export { renderDot } from './dot.js';
41
+ export { renderGraphvizSvg, detectTier, probeNativeDot } from './graphviz.js';
42
+ export { selectEntities } from './select.js';
@@ -0,0 +1,5 @@
1
+ import type { Model } from '../model/schema.js';
2
+ import type { RenderContext } from './types.js';
3
+ export type RenderOptions = RenderContext;
4
+ export declare function renderMermaid(model: Model, opts?: RenderOptions): string;
5
+ export declare const mermaidRenderer: import("./types.js").Renderer;
@@ -0,0 +1,107 @@
1
+ import { selectEntities } from './select.js';
2
+ import { defineRenderer } from './types.js';
3
+ /**
4
+ * Mermaid is the default renderer for one reason: it renders natively in
5
+ * GitHub and GitLab markdown, so the reader installs nothing. No Graphviz
6
+ * binary, no system package, no software-center ticket.
7
+ */
8
+ /** Mermaid node ids can't contain dots, brackets, or quotes. */
9
+ function nodeId(id) {
10
+ return 'n_' + id.replace(/[^a-zA-Z0-9_]/g, '_');
11
+ }
12
+ function escapeLabel(text) {
13
+ // Mermaid renders <br/> inside a quoted label; a raw newline breaks parsing.
14
+ return text.replace(/"/g, '&quot;').replace(/\n/g, '<br/>');
15
+ }
16
+ const KIND_PATTERNS = [
17
+ [/(s3|bucket|rds|dynamodb|db|database|efs|volume|storage)/i, 'storage'],
18
+ [/(sqs|sns|queue|topic|kinesis|kafka|eventbridge)/i, 'messaging'],
19
+ [/(route53|dns|cloudfront|vpc|subnet|lb|gateway|cdn|zone|record)/i, 'network'],
20
+ [/(iam|role|policy|secret|kms|cert|acm|auth)/i, 'identity'],
21
+ [/(lambda|ec2|ecs|function|instance|service|container|app)/i, 'compute'],
22
+ ];
23
+ function shapeFor(kind) {
24
+ for (const [pattern, family] of KIND_PATTERNS) {
25
+ if (pattern.test(kind))
26
+ return family;
27
+ }
28
+ return 'compute';
29
+ }
30
+ function renderNode(e) {
31
+ const label = escapeLabel(`${e.name ?? e.id}\n${e.kind}`);
32
+ const id = nodeId(e.id);
33
+ switch (shapeFor(e.kind)) {
34
+ case 'storage':
35
+ return `${id}[("${label}")]`;
36
+ case 'messaging':
37
+ return `${id}[/"${label}"/]`;
38
+ case 'network':
39
+ return `${id}("${label}")`;
40
+ case 'identity':
41
+ return `${id}{{"${label}"}}`;
42
+ default:
43
+ return `${id}["${label}"]`;
44
+ }
45
+ }
46
+ export function renderMermaid(model, opts = {}) {
47
+ const view = opts.view ? model.views.find((v) => v.id === opts.view) : undefined;
48
+ if (opts.view && !view) {
49
+ throw new Error(`unknown view: ${opts.view} (have: ${model.views.map((v) => v.id).join(', ') || 'none'})`);
50
+ }
51
+ const entities = selectEntities(model, view);
52
+ const visible = new Set(entities.map((e) => e.id));
53
+ const lines = [`flowchart ${opts.direction ?? 'LR'}`];
54
+ // Group into subgraphs; ungrouped entities are emitted at the top level.
55
+ const grouped = new Map();
56
+ const ungrouped = [];
57
+ for (const e of entities) {
58
+ if (e.group) {
59
+ const list = grouped.get(e.group) ?? [];
60
+ list.push(e);
61
+ grouped.set(e.group, list);
62
+ }
63
+ else {
64
+ ungrouped.push(e);
65
+ }
66
+ }
67
+ for (const [group, members] of [...grouped.entries()].sort(([a], [b]) => a.localeCompare(b))) {
68
+ lines.push(` subgraph ${nodeId(group)}["${escapeLabel(group)}"]`);
69
+ for (const e of members)
70
+ lines.push(` ${renderNode(e)}`);
71
+ lines.push(' end');
72
+ }
73
+ for (const e of ungrouped)
74
+ lines.push(` ${renderNode(e)}`);
75
+ // An edge is only drawn when both ends survived the view filter.
76
+ for (const edge of model.edges) {
77
+ if (!visible.has(edge.from) || !visible.has(edge.to))
78
+ continue;
79
+ const label = edge.label ? `|"${escapeLabel(edge.label)}"|` : '';
80
+ lines.push(` ${nodeId(edge.from)} -->${label} ${nodeId(edge.to)}`);
81
+ }
82
+ if (opts.health) {
83
+ lines.push('');
84
+ for (const [id, status] of Object.entries(opts.health)) {
85
+ if (!visible.has(id))
86
+ continue;
87
+ lines.push(` class ${nodeId(id)} ${status};`);
88
+ }
89
+ lines.push(' classDef healthy fill:#0b6b3a,stroke:#0f9d58,color:#fff;');
90
+ lines.push(' classDef degraded fill:#7a5b00,stroke:#f4b400,color:#fff;');
91
+ lines.push(' classDef down fill:#7a1c1c,stroke:#db4437,color:#fff;');
92
+ }
93
+ return lines.join('\n') + '\n';
94
+ }
95
+ export const mermaidRenderer = defineRenderer({
96
+ name: 'mermaid',
97
+ description: 'Mermaid flowchart — renders natively in GitHub, zero install',
98
+ extension: 'mmd',
99
+ priority: 50,
100
+ async probe() {
101
+ // Always available: it is string generation with no engine behind it.
102
+ return { available: true, via: 'built-in' };
103
+ },
104
+ async render(model, ctx) {
105
+ return renderMermaid(model, ctx);
106
+ },
107
+ });
@@ -0,0 +1,7 @@
1
+ import type { Entity, Model, View } from '../model/schema.js';
2
+ /**
3
+ * A view is a scoped slice of the model, matching entity ids or groups with an
4
+ * optional trailing `*`. Shared by every renderer so scoping behaves
5
+ * identically no matter which engine draws the picture.
6
+ */
7
+ export declare function selectEntities(model: Model, view?: View): Entity[];
@@ -0,0 +1,17 @@
1
+ import { matches } from '../model/validate.js';
2
+ /**
3
+ * A view is a scoped slice of the model, matching entity ids or groups with an
4
+ * optional trailing `*`. Shared by every renderer so scoping behaves
5
+ * identically no matter which engine draws the picture.
6
+ */
7
+ export function selectEntities(model, view) {
8
+ if (!view)
9
+ return model.entities;
10
+ return model.entities.filter((e) => {
11
+ const target = [e.id, e.group ?? ''];
12
+ const included = view.include.length === 0 ||
13
+ view.include.some((p) => target.some((t) => t !== '' && matches(p, t)));
14
+ const excluded = view.exclude.some((p) => target.some((t) => t !== '' && matches(p, t)));
15
+ return included && !excluded;
16
+ });
17
+ }
@@ -0,0 +1,33 @@
1
+ import type { Model } from '../model/schema.js';
2
+ export interface RenderContext {
3
+ view?: string;
4
+ direction?: 'LR' | 'TD';
5
+ /** Runtime health, applied at render time. Never committed to the model. */
6
+ health?: Record<string, 'healthy' | 'degraded' | 'down'>;
7
+ }
8
+ export interface Availability {
9
+ available: boolean;
10
+ /** How it will render, e.g. 'native dot binary', 'wasm', 'built-in'. */
11
+ via?: string;
12
+ /** Why it isn't available, and what to do about it. */
13
+ reason?: string;
14
+ }
15
+ /**
16
+ * The renderer extension point.
17
+ *
18
+ * `probe()` is what makes graceful degradation possible: a renderer reports
19
+ * whether it can actually run in this environment, so `--engine auto` can pick
20
+ * the best available one instead of failing. That is the whole answer to
21
+ * "Graphviz if we can install it, Mermaid if we can't".
22
+ */
23
+ export interface Renderer {
24
+ name: string;
25
+ description: string;
26
+ /** File extension for the output, without a dot. */
27
+ extension: string;
28
+ /** Higher wins under `--engine auto`. */
29
+ priority: number;
30
+ probe(): Promise<Availability>;
31
+ render(model: Model, ctx: RenderContext): Promise<string>;
32
+ }
33
+ export declare function defineRenderer(r: Renderer): Renderer;
@@ -0,0 +1,3 @@
1
+ export function defineRenderer(r) {
2
+ return r;
3
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@brignano/driftwood",
3
+ "version": "0.0.0",
4
+ "description": "Architecture as code, reconciled with live infrastructure.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/brignano/driftwood.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/brignano/driftwood/issues"
13
+ },
14
+ "homepage": "https://github.com/brignano/driftwood#readme",
15
+ "keywords": [
16
+ "architecture",
17
+ "architecture-as-code",
18
+ "diagram",
19
+ "graphviz",
20
+ "mermaid",
21
+ "terraform",
22
+ "drift",
23
+ "infrastructure",
24
+ "cli"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "bin": {
30
+ "driftwood": "./dist/cli.js"
31
+ },
32
+ "main": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "engines": {
38
+ "node": ">=20"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "prepublishOnly": "npm run build",
43
+ "dev": "tsx src/cli.ts",
44
+ "test": "vitest run",
45
+ "typecheck": "tsc -p tsconfig.json --noEmit"
46
+ },
47
+ "dependencies": {
48
+ "@hpcc-js/wasm-graphviz": "^1.28.0",
49
+ "commander": "^12.1.0",
50
+ "yaml": "^2.5.1",
51
+ "zod": "^3.23.8"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^22.7.4",
55
+ "tsx": "^4.19.1",
56
+ "typescript": "^5.6.2",
57
+ "vitest": "^2.1.2"
58
+ }
59
+ }