@webpieces/nx-webpieces-rules 0.4.656 → 0.4.658

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.656",
3
+ "version": "0.4.658",
4
4
  "description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -18,11 +18,11 @@
18
18
  "README.md"
19
19
  ],
20
20
  "dependencies": {
21
- "@webpieces/ai-hook-rules": "0.4.656",
22
- "@webpieces/code-rules": "0.4.656",
23
- "@webpieces/eslint-rules": "0.4.656",
24
- "@webpieces/pr-gate": "0.4.656",
25
- "@webpieces/rules-config": "0.4.656",
21
+ "@webpieces/ai-hook-rules": "0.4.658",
22
+ "@webpieces/code-rules": "0.4.658",
23
+ "@webpieces/eslint-rules": "0.4.658",
24
+ "@webpieces/pr-gate": "0.4.658",
25
+ "@webpieces/rules-config": "0.4.658",
26
26
  "madge": "8.0.0"
27
27
  },
28
28
  "peerDependencies": {
@@ -50,7 +50,7 @@ export declare function removeDesignFiles(projectRootAbs: string): string[];
50
50
  *
51
51
  * This is safe because nothing downstream distinguishes "empty file" from "no file", and that is by
52
52
  * design rather than luck: `graph-metadata.ts:hasGeneratedDesign()` documents that a MISSING or
53
- * unparseable design.json reads as "no design", so the architecture viz makes a box clickable only
53
+ * unparseable design.json reads as "no design", so a box's node menu gets a "View Design" item only
54
54
  * when `designs[]` is non-empty either way. `wp-design-visualize` likewise only fails when ZERO
55
55
  * design.json exist repo-wide.
56
56
  *
@@ -138,7 +138,7 @@ function removeDesignFiles(projectRootAbs) {
138
138
  *
139
139
  * This is safe because nothing downstream distinguishes "empty file" from "no file", and that is by
140
140
  * design rather than luck: `graph-metadata.ts:hasGeneratedDesign()` documents that a MISSING or
141
- * unparseable design.json reads as "no design", so the architecture viz makes a box clickable only
141
+ * unparseable design.json reads as "no design", so a box's node menu gets a "View Design" item only
142
142
  * when `designs[]` is non-empty either way. `wp-design-visualize` likewise only fails when ZERO
143
143
  * design.json exist repo-wide.
144
144
  *
@@ -1 +1 @@
1
- {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/di-graph-generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Graph Generate Executor\n *\n * Per-project: statically analyzes the project's Inversify dependency DAG\n * (constructor injection from controllers — or library top-of-DAG classes —\n * down to leaves) and writes three checked-in files at the project root:\n *\n * design.json — machine-readable graph (deterministic, sorted)\n * design.md — Mermaid diagram rendered by GitHub/IDEs in PRs\n * design.html — clickable viz.js page (linked from architecture/dependencies.html)\n *\n * A project with NO design root writes NO files, and any stale ones are removed — see\n * writeDesignFiles for why an empty `{ \"designs\": [] }` is noise rather than information, and why\n * nothing downstream can tell \"empty\" from \"absent\".\n *\n * Runs on every build (cache:false; `ci` dependsOn this target directly).\n * Unrecognized DI patterns become \"unresolved\" nodes rather than failing the build.\n *\n * That the regenerated files are actually COMMITTED is no longer checked here (the deleted\n * validate-di-graph-unchanged target); it is one repo-wide \"committed or staged\" check in\n * `wp-review-upsert-pr` — @webpieces/pr-gate BuildArtifactGate.\n *\n * Config (webpieces.config.json, rule key `di-graph`): mode RUN_EVERY_TIME | OFF. That key still\n * governs THIS executor, so it stays in webpieces.config.json unchanged.\n *\n * Usage: nx run <project>:di-graph-generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadAndValidate, ResolvedConfig } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n DiAnalyzer,\n frameworkTags,\n explicitRoleTag,\n FrameworkMarkers,\n selectAnalyzer,\n} from '../../lib/di-graph/analyzer-strategy';\nimport { createProjectProgram } from '../../lib/di-graph/program';\nimport { toDesignJson } from '../../lib/di-graph/serializer';\nimport { toDesignMarkdown } from '../../lib/di-graph/mermaid';\nimport { generateDesignHTML } from '../../lib/di-graph/design-visualizer';\nimport { DiDesign, DiGraph } from '../../lib/di-graph/model';\nimport { toError } from '../../toError';\n\nexport interface DiGraphGenerateOptions {\n // No options here — config comes from webpieces.config.json at runtime.\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst RULE_NAME = 'di-graph';\nconst MISSING_DESIGN_RULE_NAME = 'missing-design-annotation';\n\n// Cheap substring pre-scan: a project whose source never mentions any DI marker\n// gets an empty graph without paying for a ts.Program. Angular markers\n// (@Component/bootstrapApplication) are included so an Angular app that uses no\n// Inversify decorator isn't short-circuited to empty.\nconst DI_MARKERS = [\n '@DocumentDesign(',\n '@provideSingleton',\n '@provideFrameworkSingleton',\n '@provideTransient',\n '@injectable',\n 'new ContainerModule',\n '@inject(',\n '@Component(',\n 'bootstrapApplication',\n];\n\nconst ANGULAR_MARKERS = ['@Component(', 'bootstrapApplication'];\n// A non-Angular DI-design root. When no role tag is set, its presence steers the\n// marker-fallback toward the Inversify analyzer (server/controller mode).\nconst DESIGN_ROOT_MARKER = '@DocumentDesign(';\n\n/** Recursively read every project .ts file, folding each into `visit`. */\nfunction forEachSourceFile(dir: string, visit: (content: string) => void): void {\n if (!fs.existsSync(dir)) return;\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (entry.name === 'node_modules' || entry.name === 'dist') continue;\n forEachSourceFile(full, visit);\n } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {\n visit(fs.readFileSync(full, 'utf-8'));\n }\n }\n}\n\nfunction sourceHasDiMarkers(dir: string): boolean {\n let found = false;\n forEachSourceFile(dir, (content: string) => {\n if (!found && DI_MARKERS.some((marker: string) => content.includes(marker))) found = true;\n });\n return found;\n}\n\n/** Pre-scan a project's source for the framework markers used when no tag is set. */\nfunction detectFrameworkMarkers(dir: string): FrameworkMarkers {\n let angular = false;\n let controller = false;\n forEachSourceFile(dir, (content: string) => {\n if (!angular && ANGULAR_MARKERS.some((marker: string) => content.includes(marker))) angular = true;\n if (!controller && content.includes(DESIGN_ROOT_MARKER)) controller = true;\n });\n return new FrameworkMarkers(angular, controller);\n}\n\n/**\n * Repo-relative back link from a project's committed design.html up to\n * architecture/dependencies.html, so a reader who clicked a box in the\n * architecture graph can click back out. E.g. 'packages/http/http-api' →\n * '../../../architecture/dependencies.html'.\n */\nfunction architectureBackHref(projectRoot: string): string {\n return path.posix.relative(projectRoot.replace(/\\\\/g, '/'), 'architecture/dependencies.html');\n}\n\n/** The three checked-in artifacts, named once so writing and reaping can never cover different sets. */\nconst DESIGN_FILES = ['design.json', 'design.md', 'design.html'];\n\n/**\n * Delete this project's design artifacts, returning the names actually removed.\n *\n * Reaping rather than leaving them is the point: a project that USED to have a `@DocumentDesign` root\n * and lost it would otherwise keep serving a stale committed design that describes code no longer\n * there, which is worse than having none.\n */\n// webpieces-disable no-function-outside-class -- nx executor module: nx resolves a default-export function here, and every helper in this file is module-scope by that contract\nexport function removeDesignFiles(projectRootAbs: string): string[] {\n const removed: string[] = [];\n for (const name of DESIGN_FILES) {\n const file = path.join(projectRootAbs, name);\n if (!fs.existsSync(file)) continue;\n fs.rmSync(file);\n removed.push(name);\n }\n return removed;\n}\n\n/**\n * Write the design artifacts — or, when there is NO design, make sure none exist.\n *\n * WHY AN EMPTY GRAPH WRITES NOTHING: `{ \"designs\": [] }` carries no information, and every project\n * without an Inversify/Angular DI root produced one — legacy Express services, plain libs, api-libs,\n * bundles. That is most of a monorepo carrying three committed files apiece that say nothing, showing\n * up in diffs and PR file lists forever.\n *\n * This is safe because nothing downstream distinguishes \"empty file\" from \"no file\", and that is by\n * design rather than luck: `graph-metadata.ts:hasGeneratedDesign()` documents that a MISSING or\n * unparseable design.json reads as \"no design\", so the architecture viz makes a box clickable only\n * when `designs[]` is non-empty either way. `wp-design-visualize` likewise only fails when ZERO\n * design.json exist repo-wide.\n *\n * The ONE-TIME cost is a commit deleting the empty files a repo already carries — surfaced by the\n * usual \"build left the tree committed\" gate, exactly like any other regenerated artifact.\n */\n// webpieces-disable no-function-outside-class -- nx executor module: nx resolves a default-export function here, and every helper in this file is module-scope by that contract\nexport function writeDesignFiles(projectRootAbs: string, projectRoot: string, graph: DiGraph): void {\n if (graph.designs.length === 0) {\n const removed = removeDesignFiles(projectRootAbs);\n const detail = removed.length > 0 ? `removed stale ${removed.join(', ')}` : 'nothing to write or remove';\n console.log(` No DI design for this project — ${detail}`);\n return;\n }\n // toDesignJson sorts the graph in place, so design.md/design.html below all\n // see the same deterministic ordering (no git churn on re-run).\n fs.writeFileSync(path.join(projectRootAbs, 'design.json'), toDesignJson(graph));\n fs.writeFileSync(path.join(projectRootAbs, 'design.md'), toDesignMarkdown(graph));\n fs.writeFileSync(\n path.join(projectRootAbs, 'design.html'),\n generateDesignHTML(graph, architectureBackHref(projectRoot)),\n );\n}\n\n/** The analyzer chosen for a project plus the role tag that drove the choice. */\nclass AnalyzerChoice {\n constructor(\n public readonly analyzer: DiAnalyzer,\n public readonly role: string | null,\n ) {}\n}\n\n/**\n * Select the analyzer by role (server & designed-lib → @DocumentDesign,\n * client→angular design, lib→skip). The explicit `role:` nx tag is the source of\n * truth; when absent we fall back to the legacy `framework:` selection + marker\n * pre-scan so designs stay identical until a project is retagged.\n */\nfunction chooseAnalyzer(tags: string[], srcDir: string): AnalyzerChoice {\n const role = explicitRoleTag(tags);\n const frameworks = frameworkTags(tags);\n const analyzer = selectAnalyzer(role, frameworks, detectFrameworkMarkers(srcDir));\n console.log(\n ` Analyzer: ${analyzer.constructor.name} ` +\n `(role tag: ${role ?? 'none'}, framework tags: ${frameworks.length > 0 ? frameworks.join(', ') : 'none'})`,\n );\n return new AnalyzerChoice(analyzer, role);\n}\n\n/**\n * A server/designed-lib project that produced no design (zero @DocumentDesign\n * roots) fails the build with role-specific guidance. Enforced under the\n * `missing-design-annotation` rule.\n */\nfunction reportMissingDesignAnnotation(projectName: string, role: string): void {\n if (role === 'server' || role === 'app') {\n console.error(\n `❌ ${projectName} is tagged role:${role} but has no @DocumentDesign class.\\n` +\n ` A role:${role} project must expose one @DocumentDesign() root class (the App the ` +\n `container resolves via container.get(XxxApp)) so its design.json / design.html get\\n` +\n ` generated and linked from architecture/dependencies.html.`,\n );\n return;\n }\n console.error(\n `❌ ${projectName} is tagged role:designed-lib but has no @DocumentDesign class.\\n` +\n ` One or more classes you want a design printed for need @DocumentDesign() ` +\n `(from @webpieces/http-routing) added —\\n` +\n ` or retag the project role:lib if it has no design.`,\n );\n}\n\n/**\n * A server/designed-lib project MUST expose at least one @DocumentDesign root,\n * else its design is empty and the role is meaningless. Returns true (and reports)\n * when the build should fail. The `missing-design-annotation` rule gates it:\n * absent (an older published config) → enforce; OFF → skip.\n */\nfunction failsMissingDesignAnnotation(\n shared: ResolvedConfig,\n role: string | null,\n graph: DiGraph,\n projectName: string,\n): boolean {\n const missingRule = shared.rules.get(MISSING_DESIGN_RULE_NAME);\n const enforce = !missingRule || !missingRule.isOff;\n if (enforce && graph.designs.length === 0 && (role === 'server' || role === 'app' || role === 'designed-lib')) {\n reportMissingDesignAnnotation(projectName, role);\n return true;\n }\n return false;\n}\n\nexport default async function runExecutor(\n _options: DiGraphGenerateOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const shared = loadAndValidate(context.root).resolved;\n const rule = shared.rules.get(RULE_NAME);\n if (rule && rule.isOff) {\n console.log(`\\n⏭️ Skipping ${RULE_NAME} generation (mode: OFF)\\n`);\n return { success: true };\n }\n\n const projectName = context.projectName ?? 'project';\n const projectConfig = context.projectsConfigurations?.projects[projectName];\n const projectRoot = projectConfig?.root ?? '.';\n const projectRootAbs = path.join(context.root, projectRoot);\n const srcDir = path.join(projectRootAbs, 'src');\n\n console.log(`\\n🧬 Generating DI design graph for ${projectName}\\n`);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- chokepoint: a generator crash must produce an actionable failure, not a stack trace mid-build\n try {\n if (!sourceHasDiMarkers(srcDir)) {\n console.log(' No DI markers found');\n writeDesignFiles(projectRootAbs, projectRoot, new DiGraph(projectName));\n return { success: true };\n }\n\n const program = createProjectProgram(projectRootAbs);\n if (!program) {\n console.log(' No usable tsconfig/source');\n writeDesignFiles(projectRootAbs, projectRoot, new DiGraph(projectName));\n return { success: true };\n }\n\n const choice = chooseAnalyzer(projectConfig?.tags ?? [], srcDir);\n const graph = choice.analyzer.analyzeProject(program, context.root, projectRoot, projectName);\n\n if (failsMissingDesignAnnotation(shared, choice.role, graph, projectName)) {\n return { success: false };\n }\n\n writeDesignFiles(projectRootAbs, projectRoot, graph);\n\n if (graph.designs.length > 0) {\n const nodeCount = graph.designs.reduce((sum: number, d: DiDesign) => sum + d.nodes.length, 0);\n const edgeCount = graph.designs.reduce((sum: number, d: DiDesign) => sum + d.edges.length, 0);\n console.log(\n `✅ Wrote ${projectRoot}/design.json + design.md + design.html ` +\n `(${graph.designs.length} design(s), ${nodeCount} node(s), ${edgeCount} edge(s))`,\n );\n }\n const unresolved = [...new Set(graph.designs.flatMap((d: DiDesign) => d.unresolved))];\n if (unresolved.length > 0) {\n console.warn(`⚠️ ${unresolved.length} unresolved token(s)/type(s): ${unresolved.join(', ')}`);\n }\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error(`❌ DI graph generation failed for ${projectName}: ${error.message}`);\n console.error(` To unblock builds, set rules[\"${RULE_NAME}\"].mode=\"OFF\" in webpieces.config.json.`);\n return { success: false };\n }\n}\n"]}
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/di-graph-generate/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Graph Generate Executor\n *\n * Per-project: statically analyzes the project's Inversify dependency DAG\n * (constructor injection from controllers — or library top-of-DAG classes —\n * down to leaves) and writes three checked-in files at the project root:\n *\n * design.json — machine-readable graph (deterministic, sorted)\n * design.md — Mermaid diagram rendered by GitHub/IDEs in PRs\n * design.html — clickable viz.js page (linked from architecture/dependencies.html)\n *\n * A project with NO design root writes NO files, and any stale ones are removed — see\n * writeDesignFiles for why an empty `{ \"designs\": [] }` is noise rather than information, and why\n * nothing downstream can tell \"empty\" from \"absent\".\n *\n * Runs on every build (cache:false; `ci` dependsOn this target directly).\n * Unrecognized DI patterns become \"unresolved\" nodes rather than failing the build.\n *\n * That the regenerated files are actually COMMITTED is no longer checked here (the deleted\n * validate-di-graph-unchanged target); it is one repo-wide \"committed or staged\" check in\n * `wp-review-upsert-pr` — @webpieces/pr-gate BuildArtifactGate.\n *\n * Config (webpieces.config.json, rule key `di-graph`): mode RUN_EVERY_TIME | OFF. That key still\n * governs THIS executor, so it stays in webpieces.config.json unchanged.\n *\n * Usage: nx run <project>:di-graph-generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadAndValidate, ResolvedConfig } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n DiAnalyzer,\n frameworkTags,\n explicitRoleTag,\n FrameworkMarkers,\n selectAnalyzer,\n} from '../../lib/di-graph/analyzer-strategy';\nimport { createProjectProgram } from '../../lib/di-graph/program';\nimport { toDesignJson } from '../../lib/di-graph/serializer';\nimport { toDesignMarkdown } from '../../lib/di-graph/mermaid';\nimport { generateDesignHTML } from '../../lib/di-graph/design-visualizer';\nimport { DiDesign, DiGraph } from '../../lib/di-graph/model';\nimport { toError } from '../../toError';\n\nexport interface DiGraphGenerateOptions {\n // No options here — config comes from webpieces.config.json at runtime.\n}\n\nexport interface ExecutorResult {\n success: boolean;\n}\n\nconst RULE_NAME = 'di-graph';\nconst MISSING_DESIGN_RULE_NAME = 'missing-design-annotation';\n\n// Cheap substring pre-scan: a project whose source never mentions any DI marker\n// gets an empty graph without paying for a ts.Program. Angular markers\n// (@Component/bootstrapApplication) are included so an Angular app that uses no\n// Inversify decorator isn't short-circuited to empty.\nconst DI_MARKERS = [\n '@DocumentDesign(',\n '@provideSingleton',\n '@provideFrameworkSingleton',\n '@provideTransient',\n '@injectable',\n 'new ContainerModule',\n '@inject(',\n '@Component(',\n 'bootstrapApplication',\n];\n\nconst ANGULAR_MARKERS = ['@Component(', 'bootstrapApplication'];\n// A non-Angular DI-design root. When no role tag is set, its presence steers the\n// marker-fallback toward the Inversify analyzer (server/controller mode).\nconst DESIGN_ROOT_MARKER = '@DocumentDesign(';\n\n/** Recursively read every project .ts file, folding each into `visit`. */\nfunction forEachSourceFile(dir: string, visit: (content: string) => void): void {\n if (!fs.existsSync(dir)) return;\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (entry.name === 'node_modules' || entry.name === 'dist') continue;\n forEachSourceFile(full, visit);\n } else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {\n visit(fs.readFileSync(full, 'utf-8'));\n }\n }\n}\n\nfunction sourceHasDiMarkers(dir: string): boolean {\n let found = false;\n forEachSourceFile(dir, (content: string) => {\n if (!found && DI_MARKERS.some((marker: string) => content.includes(marker))) found = true;\n });\n return found;\n}\n\n/** Pre-scan a project's source for the framework markers used when no tag is set. */\nfunction detectFrameworkMarkers(dir: string): FrameworkMarkers {\n let angular = false;\n let controller = false;\n forEachSourceFile(dir, (content: string) => {\n if (!angular && ANGULAR_MARKERS.some((marker: string) => content.includes(marker))) angular = true;\n if (!controller && content.includes(DESIGN_ROOT_MARKER)) controller = true;\n });\n return new FrameworkMarkers(angular, controller);\n}\n\n/**\n * Repo-relative back link from a project's committed design.html up to\n * architecture/dependencies.html, so a reader who clicked a box in the\n * architecture graph can click back out. E.g. 'packages/http/http-api' →\n * '../../../architecture/dependencies.html'.\n */\nfunction architectureBackHref(projectRoot: string): string {\n return path.posix.relative(projectRoot.replace(/\\\\/g, '/'), 'architecture/dependencies.html');\n}\n\n/** The three checked-in artifacts, named once so writing and reaping can never cover different sets. */\nconst DESIGN_FILES = ['design.json', 'design.md', 'design.html'];\n\n/**\n * Delete this project's design artifacts, returning the names actually removed.\n *\n * Reaping rather than leaving them is the point: a project that USED to have a `@DocumentDesign` root\n * and lost it would otherwise keep serving a stale committed design that describes code no longer\n * there, which is worse than having none.\n */\n// webpieces-disable no-function-outside-class -- nx executor module: nx resolves a default-export function here, and every helper in this file is module-scope by that contract\nexport function removeDesignFiles(projectRootAbs: string): string[] {\n const removed: string[] = [];\n for (const name of DESIGN_FILES) {\n const file = path.join(projectRootAbs, name);\n if (!fs.existsSync(file)) continue;\n fs.rmSync(file);\n removed.push(name);\n }\n return removed;\n}\n\n/**\n * Write the design artifacts — or, when there is NO design, make sure none exist.\n *\n * WHY AN EMPTY GRAPH WRITES NOTHING: `{ \"designs\": [] }` carries no information, and every project\n * without an Inversify/Angular DI root produced one — legacy Express services, plain libs, api-libs,\n * bundles. That is most of a monorepo carrying three committed files apiece that say nothing, showing\n * up in diffs and PR file lists forever.\n *\n * This is safe because nothing downstream distinguishes \"empty file\" from \"no file\", and that is by\n * design rather than luck: `graph-metadata.ts:hasGeneratedDesign()` documents that a MISSING or\n * unparseable design.json reads as \"no design\", so a box's node menu gets a \"View Design\" item only\n * when `designs[]` is non-empty either way. `wp-design-visualize` likewise only fails when ZERO\n * design.json exist repo-wide.\n *\n * The ONE-TIME cost is a commit deleting the empty files a repo already carries — surfaced by the\n * usual \"build left the tree committed\" gate, exactly like any other regenerated artifact.\n */\n// webpieces-disable no-function-outside-class -- nx executor module: nx resolves a default-export function here, and every helper in this file is module-scope by that contract\nexport function writeDesignFiles(projectRootAbs: string, projectRoot: string, graph: DiGraph): void {\n if (graph.designs.length === 0) {\n const removed = removeDesignFiles(projectRootAbs);\n const detail = removed.length > 0 ? `removed stale ${removed.join(', ')}` : 'nothing to write or remove';\n console.log(` No DI design for this project — ${detail}`);\n return;\n }\n // toDesignJson sorts the graph in place, so design.md/design.html below all\n // see the same deterministic ordering (no git churn on re-run).\n fs.writeFileSync(path.join(projectRootAbs, 'design.json'), toDesignJson(graph));\n fs.writeFileSync(path.join(projectRootAbs, 'design.md'), toDesignMarkdown(graph));\n fs.writeFileSync(\n path.join(projectRootAbs, 'design.html'),\n generateDesignHTML(graph, architectureBackHref(projectRoot)),\n );\n}\n\n/** The analyzer chosen for a project plus the role tag that drove the choice. */\nclass AnalyzerChoice {\n constructor(\n public readonly analyzer: DiAnalyzer,\n public readonly role: string | null,\n ) {}\n}\n\n/**\n * Select the analyzer by role (server & designed-lib → @DocumentDesign,\n * client→angular design, lib→skip). The explicit `role:` nx tag is the source of\n * truth; when absent we fall back to the legacy `framework:` selection + marker\n * pre-scan so designs stay identical until a project is retagged.\n */\nfunction chooseAnalyzer(tags: string[], srcDir: string): AnalyzerChoice {\n const role = explicitRoleTag(tags);\n const frameworks = frameworkTags(tags);\n const analyzer = selectAnalyzer(role, frameworks, detectFrameworkMarkers(srcDir));\n console.log(\n ` Analyzer: ${analyzer.constructor.name} ` +\n `(role tag: ${role ?? 'none'}, framework tags: ${frameworks.length > 0 ? frameworks.join(', ') : 'none'})`,\n );\n return new AnalyzerChoice(analyzer, role);\n}\n\n/**\n * A server/designed-lib project that produced no design (zero @DocumentDesign\n * roots) fails the build with role-specific guidance. Enforced under the\n * `missing-design-annotation` rule.\n */\nfunction reportMissingDesignAnnotation(projectName: string, role: string): void {\n if (role === 'server' || role === 'app') {\n console.error(\n `❌ ${projectName} is tagged role:${role} but has no @DocumentDesign class.\\n` +\n ` A role:${role} project must expose one @DocumentDesign() root class (the App the ` +\n `container resolves via container.get(XxxApp)) so its design.json / design.html get\\n` +\n ` generated and linked from architecture/dependencies.html.`,\n );\n return;\n }\n console.error(\n `❌ ${projectName} is tagged role:designed-lib but has no @DocumentDesign class.\\n` +\n ` One or more classes you want a design printed for need @DocumentDesign() ` +\n `(from @webpieces/http-routing) added —\\n` +\n ` or retag the project role:lib if it has no design.`,\n );\n}\n\n/**\n * A server/designed-lib project MUST expose at least one @DocumentDesign root,\n * else its design is empty and the role is meaningless. Returns true (and reports)\n * when the build should fail. The `missing-design-annotation` rule gates it:\n * absent (an older published config) → enforce; OFF → skip.\n */\nfunction failsMissingDesignAnnotation(\n shared: ResolvedConfig,\n role: string | null,\n graph: DiGraph,\n projectName: string,\n): boolean {\n const missingRule = shared.rules.get(MISSING_DESIGN_RULE_NAME);\n const enforce = !missingRule || !missingRule.isOff;\n if (enforce && graph.designs.length === 0 && (role === 'server' || role === 'app' || role === 'designed-lib')) {\n reportMissingDesignAnnotation(projectName, role);\n return true;\n }\n return false;\n}\n\nexport default async function runExecutor(\n _options: DiGraphGenerateOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const shared = loadAndValidate(context.root).resolved;\n const rule = shared.rules.get(RULE_NAME);\n if (rule && rule.isOff) {\n console.log(`\\n⏭️ Skipping ${RULE_NAME} generation (mode: OFF)\\n`);\n return { success: true };\n }\n\n const projectName = context.projectName ?? 'project';\n const projectConfig = context.projectsConfigurations?.projects[projectName];\n const projectRoot = projectConfig?.root ?? '.';\n const projectRootAbs = path.join(context.root, projectRoot);\n const srcDir = path.join(projectRootAbs, 'src');\n\n console.log(`\\n🧬 Generating DI design graph for ${projectName}\\n`);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- chokepoint: a generator crash must produce an actionable failure, not a stack trace mid-build\n try {\n if (!sourceHasDiMarkers(srcDir)) {\n console.log(' No DI markers found');\n writeDesignFiles(projectRootAbs, projectRoot, new DiGraph(projectName));\n return { success: true };\n }\n\n const program = createProjectProgram(projectRootAbs);\n if (!program) {\n console.log(' No usable tsconfig/source');\n writeDesignFiles(projectRootAbs, projectRoot, new DiGraph(projectName));\n return { success: true };\n }\n\n const choice = chooseAnalyzer(projectConfig?.tags ?? [], srcDir);\n const graph = choice.analyzer.analyzeProject(program, context.root, projectRoot, projectName);\n\n if (failsMissingDesignAnnotation(shared, choice.role, graph, projectName)) {\n return { success: false };\n }\n\n writeDesignFiles(projectRootAbs, projectRoot, graph);\n\n if (graph.designs.length > 0) {\n const nodeCount = graph.designs.reduce((sum: number, d: DiDesign) => sum + d.nodes.length, 0);\n const edgeCount = graph.designs.reduce((sum: number, d: DiDesign) => sum + d.edges.length, 0);\n console.log(\n `✅ Wrote ${projectRoot}/design.json + design.md + design.html ` +\n `(${graph.designs.length} design(s), ${nodeCount} node(s), ${edgeCount} edge(s))`,\n );\n }\n const unresolved = [...new Set(graph.designs.flatMap((d: DiDesign) => d.unresolved))];\n if (unresolved.length > 0) {\n console.warn(`⚠️ ${unresolved.length} unresolved token(s)/type(s): ${unresolved.join(', ')}`);\n }\n return { success: true };\n } catch (err: unknown) {\n const error = toError(err);\n console.error(`❌ DI graph generation failed for ${projectName}: ${error.message}`);\n console.error(` To unblock builds, set rules[\"${RULE_NAME}\"].mode=\"OFF\" in webpieces.config.json.`);\n return { success: false };\n }\n}\n"]}
@@ -16,6 +16,9 @@ const fs = tslib_1.__importStar(require("fs"));
16
16
  const path = tslib_1.__importStar(require("path"));
17
17
  const serializer_1 = require("./serializer");
18
18
  const dot_1 = require("./dot");
19
+ const graph_node_menu_1 = require("../graph-node-menu");
20
+ /** The ONE floating-node-menu implementation, shared with architecture/dependencies.html. */
21
+ const NODE_MENU = new graph_node_menu_1.GraphNodeMenu();
19
22
  class DesignVisualizationPaths {
20
23
  dotPath;
21
24
  htmlPath;
@@ -52,6 +55,7 @@ function pageStyles() {
52
55
  .back { max-width: 95%; margin: 0 auto 8px; }
53
56
  .back a { color: #1565C0; text-decoration: none; }
54
57
  .back a:hover { text-decoration: underline; }
58
+ .hint { text-align: center; color: #555; margin: 0 0 16px; }
55
59
  h2 { color: #333; margin-bottom: 4px; }
56
60
  .meta { color: #777; font-family: monospace; font-size: 13px; margin-bottom: 10px; }
57
61
  .section { background: white; padding: 20px; border-radius: 8px;
@@ -62,6 +66,8 @@ function pageStyles() {
62
66
  .legend-item { margin: 8px 0; }
63
67
  .legend-box { display: inline-block; width: 20px; height: 20px;
64
68
  border: 1px solid #ccc; margin-right: 10px; vertical-align: middle; }
69
+ ${NODE_MENU.styles()}
70
+ ${NODE_MENU.dimStyles('.graph')}
65
71
  `;
66
72
  }
67
73
  function pageLegend() {
@@ -128,16 +134,39 @@ function stackScript() {
128
134
  }
129
135
  `;
130
136
  }
137
+ /**
138
+ * Wire the shared floating menu onto every box of one rendered design graph.
139
+ *
140
+ * A class box has no design page of its own (design.html is per PROJECT, and this IS one), so no
141
+ * "View Design" item is ever built here — the item is absent, never dead. The bottom item is the
142
+ * lock: this page has no dropdown and no responsibilities list, so locking dims every other box in
143
+ * this graph and lights the locked one. Each graph on the page locks independently.
144
+ */
145
+ // webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file
146
+ function menuWiringScript() {
147
+ return `
148
+ function wireNodeMenu(element) {
149
+ const lock = new WpNodeLock(element);
150
+ WpNodeMenu.wire(element, function (name, node) {
151
+ return [new WpNodeMenuItem(lock.isLocked(name) ? 'Unlock' : 'Lock', function () {
152
+ lock.toggle(name, node);
153
+ })];
154
+ });
155
+ }
156
+ `;
157
+ }
131
158
  function renderScript(entries) {
132
159
  return `
133
160
  const graphs = ${JSON.stringify(entries)};
134
161
  ${stackScript()}
162
+ ${menuWiringScript()}
135
163
  const viz = new Viz();
136
164
  for (const g of graphs) {
137
165
  viz.renderSVGElement(g.dot)
138
166
  .then(element => {
139
167
  paintStacks(element, g.stackedIds);
140
168
  document.getElementById(g.id).appendChild(element);
169
+ wireNodeMenu(element);
141
170
  })
142
171
  .catch(err => {
143
172
  console.error(err);
@@ -185,8 +214,10 @@ function generateDesignHTML(graph, backHref) {
185
214
  <body>
186
215
  ${backLink}
187
216
  <h1>${htmlEscape(title)}</h1>
217
+ <p class="hint">💡 <strong>Click any box</strong> for its menu — <strong>Lock</strong> dims every other box in that graph so one class stands alone; <strong>Unlock</strong> restores it.</p>
188
218
  ${pageLegend()}
189
219
  ${body}
220
+ <script>${NODE_MENU.script()}</script>
190
221
  <script>${renderScript(entries)}</script>
191
222
  </body>
192
223
  </html>`;
@@ -1 +1 @@
1
- {"version":3,"file":"design-visualizer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/design-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design HTML Visualizer\n *\n * Builds one HTML page per project showing EVERY controller/root design as\n * its own Graphviz graph (rendered client-side with viz.js, same pipeline as\n * the architecture visualization). Output goes to tmp/webpieces/ — a view,\n * never committed (the committed artifacts are design.json/design.md).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { DiDesign, DiGraph, DiNode } from './model';\nimport { sortGraph } from './serializer';\nimport { generateDesignDot, isStackedNode } from './dot';\n\nexport class DesignVisualizationPaths {\n constructor(\n public readonly dotPath: string,\n public readonly htmlPath: string\n ) {}\n}\n\nclass DesignGraphEntry {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n /** Node ids to paint as a stack of instances (transient). Sorted, so the HTML is stable. */\n public readonly stackedIds: string[]\n ) {}\n}\n\n/** The transient nodes of one design, in the design's (already sorted) node order. */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackedIdsOf(design: DiDesign): string[] {\n return design.nodes.filter((node: DiNode) => isStackedNode(node)).map((node: DiNode) => node.id);\n}\n\nfunction htmlEscape(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nfunction pageStyles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .back { max-width: 95%; margin: 0 auto 8px; }\n .back a { color: #1565C0; text-decoration: none; }\n .back a:hover { text-decoration: underline; }\n h2 { color: #333; margin-bottom: 4px; }\n .meta { color: #777; font-family: monospace; font-size: 13px; margin-bottom: 10px; }\n .section { background: white; padding: 20px; border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 20px auto; max-width: 95%; }\n .graph { text-align: center; overflow-x: auto; }\n .legend { margin: 20px auto; max-width: 700px; padding: 15px; background: white;\n border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }\n .legend-item { margin: 8px 0; }\n .legend-box { display: inline-block; width: 20px; height: 20px;\n border: 1px solid #ccc; margin-right: 10px; vertical-align: middle; }\n `;\n}\n\nfunction pageLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Design root</strong> — level 0, the @DocumentDesign entry class of the tree\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5;\"></span>\n <strong>Class</strong> — injectable class (constructor injection)</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Constant / dynamic</strong> — toConstantValue / toDynamicValue leaf</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FCE4EC; border-style: dashed;\"></span>\n <strong>Unresolved</strong> — token the analyzer could not resolve</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #EDE7F6; border: 3px double #5e35b1;\"></span>\n <strong>External</strong> — class from a published package outside this workspace; shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E1F5FE; border: 3px double #0277bd;\"></span>\n <strong>API client</strong> — generated <code>createApiClient</code> proxy (service/network boundary); shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5; margin-left: 12px; box-shadow: -5px -5px 0 -1px #fff, -5px -5px 0 0 #ccc, -10px -10px 0 -1px #fff, -10px -10px 0 0 #ccc;\"></span>\n <strong>Stack of boxes</strong> — a TRANSIENT class: every arrow into it resolves its OWN\n instance. A single box is a singleton, whose arrows all share one instance.</div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>One graph per controller/root; a shared dependency appears in each root's tree.\n Edge labels are injection tokens; unlabeled edges are inject-by-type.</em></div>\n </div>`;\n}\n\n/**\n * Paint the \"many instances\" glyph: for each transient node, clone its outline twice and offset\n * the copies up-and-left BEHIND the real box, so you see a stack of three whose back two show only\n * their top and left edges.\n *\n * Graphviz has no offset-stack primitive, so we do it on the SVG viz.js hands back before it is\n * attached. That costs nothing in review stability — the committed design.html holds only this\n * static script plus a sorted id list; the SVG itself is produced in the browser at view time.\n *\n * Nodes are matched by their <title>, which Graphviz always emits as the node id. (Its `class`\n * attribute arrived in Graphviz 2.40 and is not verified to survive viz.js@2.1.2.)\n */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackScript(): string {\n return `\n function paintStacks(element, stackedIds) {\n const nodes = element.querySelectorAll('g.node');\n const byTitle = new Map();\n nodes.forEach(n => {\n const title = n.querySelector('title');\n if (title) byTitle.set(title.textContent, n);\n });\n for (const id of stackedIds) {\n const node = byTitle.get(id);\n if (!node) continue;\n const outline = node.querySelector('polygon, polyline, path');\n if (!outline) continue;\n // We insertBefore(firstChild), so the LAST inserted paints furthest back. Insert\n // nearest (-5) first then farthest (-10): final paint order is -10, -5, original —\n // a clean 3-card stack where all three cards are visible. (The reverse order hid\n // the -5 card entirely behind the -10 one, so only 2 boxes showed.)\n for (const offset of [-5, -10]) {\n const ghost = outline.cloneNode(false);\n ghost.setAttribute('transform', 'translate(' + offset + ',' + offset + ')');\n ghost.setAttribute('fill', '#ffffff');\n node.insertBefore(ghost, node.firstChild);\n }\n }\n }\n `;\n}\n\nfunction renderScript(entries: DesignGraphEntry[]): string {\n return `\n const graphs = ${JSON.stringify(entries)};\n ${stackScript()}\n const viz = new Viz();\n for (const g of graphs) {\n viz.renderSVGElement(g.dot)\n .then(element => {\n paintStacks(element, g.stackedIds);\n document.getElementById(g.id).appendChild(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById(g.id).innerHTML = '<pre>' + err + '</pre>';\n });\n }\n `;\n}\n\n/**\n * Build the full HTML page for a project's DI designs — one section (heading\n * + meta + rendered graph) per controller/root design.\n *\n * `backHref`, when given, renders a \"back to architecture\" link at the top —\n * used by the committed per-project design.html so a reader who clicked in from\n * dependencies.html can click back out. Omitted for the tmp view.\n */\nexport function generateDesignHTML(graph: DiGraph, backHref?: string): string {\n const title = `DI Designs — ${graph.project}`;\n const entries: DesignGraphEntry[] = [];\n const sections: string[] = [];\n const backLink = backHref\n ? `<p class=\"back\"><a href=\"${htmlEscape(backHref)}\">← Back to architecture graph</a></p>`\n : '';\n\n graph.designs.forEach((design: DiDesign, index: number) => {\n const id = `graph-${index}`;\n entries.push(new DesignGraphEntry(id, generateDesignDot(design), stackedIdsOf(design)));\n sections.push(`<div class=\"section\">\n <h2>${htmlEscape(design.root)} — ${design.rootKind}, Level 0…${design.maxLevel}</h2>\n <div class=\"meta\">${htmlEscape(design.file)}</div>\n <div id=\"${id}\" class=\"graph\"></div>\n </div>`);\n });\n\n const body =\n sections.length > 0\n ? sections.join('\\n ')\n : '<div class=\"section\"><em>No DI-registered classes found in this project.</em></div>';\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>${htmlEscape(title)}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>${pageStyles()}</style>\n</head>\n<body>\n ${backLink}\n <h1>${htmlEscape(title)}</h1>\n ${pageLegend()}\n ${body}\n <script>${renderScript(entries)}</script>\n</body>\n</html>`;\n}\n\n/**\n * Write tmp/webpieces/design-<project>.html (+ .dot with all digraphs\n * concatenated, for debugging) and return the paths.\n */\nexport function writeDesignVisualization(\n graph: DiGraph,\n workspaceRoot: string\n): DesignVisualizationPaths {\n sortGraph(graph);\n\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const allDot = graph.designs.map((design: DiDesign) => generateDesignDot(design)).join('\\n');\n const dotPath = path.join(outputDir, `design-${graph.project}.dot`);\n fs.writeFileSync(dotPath, allDot, 'utf-8');\n\n const htmlPath = path.join(outputDir, `design-${graph.project}.html`);\n fs.writeFileSync(htmlPath, generateDesignHTML(graph), 'utf-8');\n\n return new DesignVisualizationPaths(dotPath, htmlPath);\n}\n"]}
1
+ {"version":3,"file":"design-visualizer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/design-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Design HTML Visualizer\n *\n * Builds one HTML page per project showing EVERY controller/root design as\n * its own Graphviz graph (rendered client-side with viz.js, same pipeline as\n * the architecture visualization). Output goes to tmp/webpieces/ — a view,\n * never committed (the committed artifacts are design.json/design.md).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { DiDesign, DiGraph, DiNode } from './model';\nimport { sortGraph } from './serializer';\nimport { generateDesignDot, isStackedNode } from './dot';\nimport { GraphNodeMenu } from '../graph-node-menu';\n\n/** The ONE floating-node-menu implementation, shared with architecture/dependencies.html. */\nconst NODE_MENU = new GraphNodeMenu();\n\nexport class DesignVisualizationPaths {\n constructor(\n public readonly dotPath: string,\n public readonly htmlPath: string\n ) {}\n}\n\nclass DesignGraphEntry {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n /** Node ids to paint as a stack of instances (transient). Sorted, so the HTML is stable. */\n public readonly stackedIds: string[]\n ) {}\n}\n\n/** The transient nodes of one design, in the design's (already sorted) node order. */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackedIdsOf(design: DiDesign): string[] {\n return design.nodes.filter((node: DiNode) => isStackedNode(node)).map((node: DiNode) => node.id);\n}\n\nfunction htmlEscape(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nfunction pageStyles(): string {\n return `\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n .back { max-width: 95%; margin: 0 auto 8px; }\n .back a { color: #1565C0; text-decoration: none; }\n .back a:hover { text-decoration: underline; }\n .hint { text-align: center; color: #555; margin: 0 0 16px; }\n h2 { color: #333; margin-bottom: 4px; }\n .meta { color: #777; font-family: monospace; font-size: 13px; margin-bottom: 10px; }\n .section { background: white; padding: 20px; border-radius: 8px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 20px auto; max-width: 95%; }\n .graph { text-align: center; overflow-x: auto; }\n .legend { margin: 20px auto; max-width: 700px; padding: 15px; background: white;\n border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }\n .legend-item { margin: 8px 0; }\n .legend-box { display: inline-block; width: 20px; height: 20px;\n border: 1px solid #ccc; margin-right: 10px; vertical-align: middle; }\n ${NODE_MENU.styles()}\n ${NODE_MENU.dimStyles('.graph')}\n `;\n}\n\nfunction pageLegend(): string {\n return `<div class=\"legend\">\n <h2>Legend</h2>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E3F2FD;\"></span>\n <strong>Design root</strong> — level 0, the @DocumentDesign entry class of the tree\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5;\"></span>\n <strong>Class</strong> — injectable class (constructor injection)</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FFF3E0;\"></span>\n <strong>Constant / dynamic</strong> — toConstantValue / toDynamicValue leaf</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #FCE4EC; border-style: dashed;\"></span>\n <strong>Unresolved</strong> — token the analyzer could not resolve</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #EDE7F6; border: 3px double #5e35b1;\"></span>\n <strong>External</strong> — class from a published package outside this workspace; shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #E1F5FE; border: 3px double #0277bd;\"></span>\n <strong>API client</strong> — generated <code>createApiClient</code> proxy (service/network boundary); shown as a boundary, not expanded</div>\n <div class=\"legend-item\"><span class=\"legend-box\" style=\"background: #F5F5F5; margin-left: 12px; box-shadow: -5px -5px 0 -1px #fff, -5px -5px 0 0 #ccc, -10px -10px 0 -1px #fff, -10px -10px 0 0 #ccc;\"></span>\n <strong>Stack of boxes</strong> — a TRANSIENT class: every arrow into it resolves its OWN\n instance. A single box is a singleton, whose arrows all share one instance.</div>\n <div class=\"legend-item\" style=\"margin-top: 15px;\">\n <em>One graph per controller/root; a shared dependency appears in each root's tree.\n Edge labels are injection tokens; unlabeled edges are inject-by-type.</em></div>\n </div>`;\n}\n\n/**\n * Paint the \"many instances\" glyph: for each transient node, clone its outline twice and offset\n * the copies up-and-left BEHIND the real box, so you see a stack of three whose back two show only\n * their top and left edges.\n *\n * Graphviz has no offset-stack primitive, so we do it on the SVG viz.js hands back before it is\n * attached. That costs nothing in review stability — the committed design.html holds only this\n * static script plus a sorted id list; the SVG itself is produced in the browser at view time.\n *\n * Nodes are matched by their <title>, which Graphviz always emits as the node id. (Its `class`\n * attribute arrived in Graphviz 2.40 and is not verified to survive viz.js@2.1.2.)\n */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction stackScript(): string {\n return `\n function paintStacks(element, stackedIds) {\n const nodes = element.querySelectorAll('g.node');\n const byTitle = new Map();\n nodes.forEach(n => {\n const title = n.querySelector('title');\n if (title) byTitle.set(title.textContent, n);\n });\n for (const id of stackedIds) {\n const node = byTitle.get(id);\n if (!node) continue;\n const outline = node.querySelector('polygon, polyline, path');\n if (!outline) continue;\n // We insertBefore(firstChild), so the LAST inserted paints furthest back. Insert\n // nearest (-5) first then farthest (-10): final paint order is -10, -5, original —\n // a clean 3-card stack where all three cards are visible. (The reverse order hid\n // the -5 card entirely behind the -10 one, so only 2 boxes showed.)\n for (const offset of [-5, -10]) {\n const ghost = outline.cloneNode(false);\n ghost.setAttribute('transform', 'translate(' + offset + ',' + offset + ')');\n ghost.setAttribute('fill', '#ffffff');\n node.insertBefore(ghost, node.firstChild);\n }\n }\n }\n `;\n}\n\n/**\n * Wire the shared floating menu onto every box of one rendered design graph.\n *\n * A class box has no design page of its own (design.html is per PROJECT, and this IS one), so no\n * \"View Design\" item is ever built here — the item is absent, never dead. The bottom item is the\n * lock: this page has no dropdown and no responsibilities list, so locking dims every other box in\n * this graph and lights the locked one. Each graph on the page locks independently.\n */\n// webpieces-disable no-function-outside-class -- pure emitter helper, matching every sibling in this file\nfunction menuWiringScript(): string {\n return `\n function wireNodeMenu(element) {\n const lock = new WpNodeLock(element);\n WpNodeMenu.wire(element, function (name, node) {\n return [new WpNodeMenuItem(lock.isLocked(name) ? 'Unlock' : 'Lock', function () {\n lock.toggle(name, node);\n })];\n });\n }\n `;\n}\n\nfunction renderScript(entries: DesignGraphEntry[]): string {\n return `\n const graphs = ${JSON.stringify(entries)};\n ${stackScript()}\n ${menuWiringScript()}\n const viz = new Viz();\n for (const g of graphs) {\n viz.renderSVGElement(g.dot)\n .then(element => {\n paintStacks(element, g.stackedIds);\n document.getElementById(g.id).appendChild(element);\n wireNodeMenu(element);\n })\n .catch(err => {\n console.error(err);\n document.getElementById(g.id).innerHTML = '<pre>' + err + '</pre>';\n });\n }\n `;\n}\n\n/**\n * Build the full HTML page for a project's DI designs — one section (heading\n * + meta + rendered graph) per controller/root design.\n *\n * `backHref`, when given, renders a \"back to architecture\" link at the top —\n * used by the committed per-project design.html so a reader who clicked in from\n * dependencies.html can click back out. Omitted for the tmp view.\n */\nexport function generateDesignHTML(graph: DiGraph, backHref?: string): string {\n const title = `DI Designs — ${graph.project}`;\n const entries: DesignGraphEntry[] = [];\n const sections: string[] = [];\n const backLink = backHref\n ? `<p class=\"back\"><a href=\"${htmlEscape(backHref)}\">← Back to architecture graph</a></p>`\n : '';\n\n graph.designs.forEach((design: DiDesign, index: number) => {\n const id = `graph-${index}`;\n entries.push(new DesignGraphEntry(id, generateDesignDot(design), stackedIdsOf(design)));\n sections.push(`<div class=\"section\">\n <h2>${htmlEscape(design.root)} — ${design.rootKind}, Level 0…${design.maxLevel}</h2>\n <div class=\"meta\">${htmlEscape(design.file)}</div>\n <div id=\"${id}\" class=\"graph\"></div>\n </div>`);\n });\n\n const body =\n sections.length > 0\n ? sections.join('\\n ')\n : '<div class=\"section\"><em>No DI-registered classes found in this project.</em></div>';\n\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <title>${htmlEscape(title)}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>${pageStyles()}</style>\n</head>\n<body>\n ${backLink}\n <h1>${htmlEscape(title)}</h1>\n <p class=\"hint\">💡 <strong>Click any box</strong> for its menu — <strong>Lock</strong> dims every other box in that graph so one class stands alone; <strong>Unlock</strong> restores it.</p>\n ${pageLegend()}\n ${body}\n <script>${NODE_MENU.script()}</script>\n <script>${renderScript(entries)}</script>\n</body>\n</html>`;\n}\n\n/**\n * Write tmp/webpieces/design-<project>.html (+ .dot with all digraphs\n * concatenated, for debugging) and return the paths.\n */\nexport function writeDesignVisualization(\n graph: DiGraph,\n workspaceRoot: string\n): DesignVisualizationPaths {\n sortGraph(graph);\n\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n\n const allDot = graph.designs.map((design: DiDesign) => generateDesignDot(design)).join('\\n');\n const dotPath = path.join(outputDir, `design-${graph.project}.dot`);\n fs.writeFileSync(dotPath, allDot, 'utf-8');\n\n const htmlPath = path.join(outputDir, `design-${graph.project}.html`);\n fs.writeFileSync(htmlPath, generateDesignHTML(graph), 'utf-8');\n\n return new DesignVisualizationPaths(dotPath, htmlPath);\n}\n"]}
@@ -83,8 +83,8 @@ function enrichGraph(graph, infos, workspaceRoot) {
83
83
  // without one has NO design.json at all (di-graph-generate stopped writing
84
84
  // empty ones and reaps stale ones), and legacy repos may still carry an
85
85
  // empty `{ designs: [] }` until their next build — both must read the same
86
- // way, and neither becomes clickable in the arch viz (designHtmlHref keys
87
- // off designFile). See graph-visualizer.ts.
86
+ // way, and neither gets a "View Design" item on its node menu in the arch
87
+ // viz (designLinks keys off designFile). See graph-visualizer.ts.
88
88
  if (hasGeneratedDesign(workspaceRoot, info.root)) {
89
89
  entry.designFile = toRepoRelative(info.root, 'design.json');
90
90
  }
@@ -289,8 +289,8 @@ function toRepoRelative(projectRoot, fileName) {
289
289
  * equivalence is load-bearing rather than incidental: di-graph-generate writes NO
290
290
  * file for a project without a design root (and reaps a stale one), so "absent"
291
291
  * is now the normal state for every plain lib and legacy service. Repos that have
292
- * not rebuilt since still carry `{ designs: [] }` files. Either way the arch viz
293
- * must not render the box as clickable. See design-file-emission.spec.ts.
292
+ * not rebuilt since still carry `{ designs: [] }` files. Either way the box's node
293
+ * menu must offer no "View Design" item. See design-file-emission.spec.ts.
294
294
  */
295
295
  function hasGeneratedDesign(workspaceRoot, projectRoot) {
296
296
  const designPath = path.join(workspaceRoot, projectRoot, 'design.json');
@@ -1 +1 @@
1
- {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * serviceName — from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveRuntimeParticipant } from './runtime-participant-resolver';\nimport { resolveCallsService, resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n enrichDeclarations(entry, info, workspaceRoot, problems);\n enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. A project\n // without one has NO design.json at all (di-graph-generate stopped writing\n // empty ones and reaps stale ones), and legacy repos may still carry an\n // empty `{ designs: [] }` until their next build — both must read the same\n // way, and neither becomes clickable in the arch viz (designHtmlHref keys\n // off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * The fields a project DECLARES about itself — its framework env set and role (nx tags), the\n * webpieces runtime packages it depends on (package.json), and whether it opts out of the drawings\n * (nx tag). Grouped because they share a shape: resolve, record the problem, or write the field.\n *\n * Distinct from enrichResponsibilities/designFile, which read generated or hand-written FILES.\n */\n// webpieces-disable no-function-outside-class -- enrichGraph step helper, matching enrichClientNames/enrichResponsibilities in this file\nfunction enrichDeclarations(\n entry: GraphEntry,\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n // Only persist when the project declares at least one — a repo full of\n // \"webpiecesRuntime\": [] lines would be noise, and absence already means \"declares none\".\n const participantResolution = resolveRuntimeParticipant(info, workspaceRoot);\n if (participantResolution.problem !== null) {\n problems.push(participantResolution.problem);\n } else if (participantResolution.markers !== null && participantResolution.markers.length > 0) {\n entry.webpiecesRuntime = participantResolution.markers;\n }\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n}\n\n/**\n * Roles that are terminal APPS — nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice — the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child → parent: react → browser, angular → browser, express → node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType — the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express). For a dependency edge Consumer C → Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] — the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angular→browser, express→node). Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | designed-lib | lib | client).\n * Apps are terminal — libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` — the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle → app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps — it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\n/**\n * Fill the two symmetric client-addressing fields — `serviceName` (the name clients address THIS app\n * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the\n * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).\n */\n// webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities\nfunction enrichClientNames(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n projectName: string,\n serviceNames: Map<string, string>,\n problems: string[]\n): void {\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site\n // literal and the fan-out fallback.\n const callsResolution = resolveCallsService(info, workspaceRoot);\n if (callsResolution.problem !== null) {\n problems.push(callsResolution.problem);\n } else if (callsResolution.callsService !== null) {\n entry.callsService = callsResolution.callsService;\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design — a committed design.json\n * whose `designs[]` is non-empty (i.e. it has ≥1 @DocumentDesign root).\n *\n * MISSING, empty-`designs[]`, and unparseable all read as \"no design\", and that\n * equivalence is load-bearing rather than incidental: di-graph-generate writes NO\n * file for a project without a design root (and reaps a stale one), so \"absent\"\n * is now the normal state for every plain lib and legacy service. Repos that have\n * not rebuilt since still carry `{ designs: [] }` files. Either way the arch viz\n * must not render the box as clickable. See design-file-emission.spec.ts.\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-metadata.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-metadata.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Metadata Enrichment\n *\n * Fills the AI-oriented fields on each architecture/dependencies.json entry:\n * framework — from `framework:<x>` nx tag or package.json inference\n * serviceName — from project.json metadata.webpieces.serviceName; the\n * name clients address this app by at runtime\n * shortDescription — first paragraph of the project's responsibilities.md\n * responsibilitiesFile — repo-relative path to the required responsibilities.md\n * designFile — repo-relative path to the generated DI design.json\n *\n * Validation is aggregated: ALL problems across ALL projects are collected and\n * thrown as one MetadataValidationError so a repo adopting this sees the full\n * seeding list in a single run. Callers must enrich BEFORE writing any file so\n * a failed run never clobbers dependencies.json.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectGraphAsync } from '@nx/devkit';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { ProjectInfo } from './project-info';\nimport { resolveFramework } from './framework-resolver';\nimport { resolveRole } from './role-resolver';\nimport { resolveDrawOnGraph } from './draw-on-graph-resolver';\nimport { resolveRuntimeParticipant } from './runtime-participant-resolver';\nimport { resolveCallsService, resolveServiceName, validateUniqueServiceNames } from './service-name-resolver';\nimport { extractShortDescription, validateShortDescription } from './responsibilities';\nimport { toError } from '../toError';\n\nexport const RESPONSIBILITIES_FILE_NAME = 'responsibilities.md';\n\n/**\n * Thrown when one or more projects fail metadata validation (missing/invalid\n * responsibilities.md, bad framework tags, ...). Executors catch this to point\n * AI at the webpieces.responsibilities.md instructions template.\n */\nexport class MetadataValidationError extends Error {\n constructor(public readonly problems: string[]) {\n super(\n `Architecture metadata validation failed (${problems.length} problem(s)):\\n` +\n problems.map((problem: string) => ` - ${problem}`).join('\\n')\n );\n this.name = 'MetadataValidationError';\n }\n}\n\n/**\n * Read per-project root + tags from nx's project graph.\n */\nexport async function collectProjectInfo(): Promise<Map<string, ProjectInfo>> {\n const projectGraph = await createProjectGraphAsync();\n const infos = new Map<string, ProjectInfo>();\n for (const [name, node] of Object.entries(projectGraph.nodes)) {\n infos.set(name, new ProjectInfo(name, node.data.root, node.data.tags ?? []));\n }\n return infos;\n}\n\n/**\n * Enrich every graph entry in place with framework, shortDescription,\n * responsibilitiesFile and designFile. Throws MetadataValidationError listing\n * every problem when any project fails validation.\n */\nexport function enrichGraph(\n graph: EnhancedGraph,\n infos: Map<string, ProjectInfo>,\n workspaceRoot: string\n): void {\n const problems: string[] = [];\n // project -> declared serviceName, collected so duplicates can be reported across the workspace.\n const serviceNames = new Map<string, string>();\n\n for (const [projectName, entry] of Object.entries(graph)) {\n const info = infos.get(projectName);\n if (!info) {\n problems.push(`${projectName}: not found in nx project graph`);\n continue;\n }\n\n enrichDeclarations(entry, info, workspaceRoot, problems);\n enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);\n enrichResponsibilities(entry, info, workspaceRoot, problems);\n\n // Set designFile ONLY when the project has a REAL generated design (a\n // non-empty `designs[]`), i.e. it has a @DocumentDesign root. A project\n // without one has NO design.json at all (di-graph-generate stopped writing\n // empty ones and reaps stale ones), and legacy repos may still carry an\n // empty `{ designs: [] }` until their next build — both must read the same\n // way, and neither gets a \"View Design\" item on its node menu in the arch\n // viz (designLinks keys off designFile). See graph-visualizer.ts.\n if (hasGeneratedDesign(workspaceRoot, info.root)) {\n entry.designFile = toRepoRelative(info.root, 'design.json');\n }\n }\n\n validateUniqueServiceNames(serviceNames, problems);\n validateLibraryTypesMatch(graph, problems);\n validateRoleDependencies(graph, problems);\n\n if (problems.length > 0) {\n throw new MetadataValidationError(problems);\n }\n}\n\n/**\n * The fields a project DECLARES about itself — its framework env set and role (nx tags), the\n * webpieces runtime packages it depends on (package.json), and whether it opts out of the drawings\n * (nx tag). Grouped because they share a shape: resolve, record the problem, or write the field.\n *\n * Distinct from enrichResponsibilities/designFile, which read generated or hand-written FILES.\n */\n// webpieces-disable no-function-outside-class -- enrichGraph step helper, matching enrichClientNames/enrichResponsibilities in this file\nfunction enrichDeclarations(\n entry: GraphEntry,\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const resolution = resolveFramework(info, workspaceRoot);\n if (resolution.problem !== null) {\n problems.push(resolution.problem);\n } else if (resolution.frameworks !== null) {\n entry.framework = resolution.frameworks;\n }\n\n const roleResolution = resolveRole(info);\n if (roleResolution.problem !== null) {\n problems.push(roleResolution.problem);\n } else if (roleResolution.role !== null) {\n entry.role = roleResolution.role;\n }\n\n // Only persist when the project declares at least one — a repo full of\n // \"webpiecesRuntime\": [] lines would be noise, and absence already means \"declares none\".\n const participantResolution = resolveRuntimeParticipant(info, workspaceRoot);\n if (participantResolution.problem !== null) {\n problems.push(participantResolution.problem);\n } else if (participantResolution.markers !== null && participantResolution.markers.length > 0) {\n entry.webpiecesRuntime = participantResolution.markers;\n }\n\n // Only persist the field when hidden (false); drawn projects (the\n // default) stay clean in dependencies.json with no drawOnGraph line.\n const drawResolution = resolveDrawOnGraph(info);\n if (drawResolution.problem !== null) {\n problems.push(drawResolution.problem);\n } else if (drawResolution.drawOnGraph === false) {\n entry.drawOnGraph = false;\n }\n}\n\n/**\n * Roles that are terminal APPS — nothing may depend on them. A server, a\n * non-HTTP `app`, or a client is a top-level runnable; being depended upon means\n * it is really a library and should be retagged `role:lib`/`role:designed-lib`.\n */\nexport const APP_ROLES: ReadonlyArray<string> = ['server', 'app', 'client'];\n\n/**\n * Compatibility lattice — the \"up-set\" of each atomic env is the env itself\n * PLUS every ancestor it can legally consume code from (specialization edges\n * child → parent: react → browser, angular → browser, express → node). A\n * consumer promising env `c` can be satisfied by any dependency env in `up(c)`.\n */\nexport const ENV_UP_SETS: Readonly<Record<string, ReadonlyArray<string>>> = {\n react: ['react', 'browser'],\n angular: ['angular', 'browser'],\n browser: ['browser'],\n express: ['express', 'node'],\n node: ['node'],\n};\n\n/** The up-set of an env (env itself + ancestors); unknown envs map to just themselves. */\nfunction upSet(env: string): ReadonlyArray<string> {\n return ENV_UP_SETS[env] ?? [env];\n}\n\n/**\n * `library-types-match-client` rule.\n *\n * A project's `framework` field is its libType — the SET of runtime\n * environments it is validated to run in (browser | react | angular | node |\n * express). For a dependency edge Consumer C → Library L, the edge is LEGAL iff\n * for EVERY env `c` in C's set, up(c) ∩ L's set ≠ ∅ — i.e. every environment\n * the consumer promises to run in can be satisfied by the dependency. This keeps\n * an express app from depending on a browser-only lib, and lets a `browser+node`\n * lib be consumed by both react and express projects. Every violation is\n * appended to `problems` so `arch:generate` fails with the full list.\n */\nexport function validateLibraryTypesMatch(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromSet = entry.framework;\n if (fromSet === undefined) continue; // framework resolution already flagged this project\n\n for (const dep of entry.dependsOn) {\n const depEntry = graph[dep];\n const toSet = depEntry?.framework;\n if (toSet === undefined) continue;\n\n const unsatisfied = fromSet.filter(\n (env: string) => !upSet(env).some((up: string) => toSet.includes(up))\n );\n if (unsatisfied.length === 0) continue;\n\n problems.push(\n `library-types-match-client: '${projectName}' [${fromSet.join(', ')}] must not depend on ` +\n `'${dep}' [${toSet.join(', ')}] — the consumer env(s) ${unsatisfied.join(', ')} cannot be ` +\n `satisfied by the dependency (each consumer env must resolve to itself or an ancestor it ` +\n `consumes from: react/angular→browser, express→node). Widen '${dep}' framework tags or ` +\n `remove the dependency.`\n );\n }\n }\n}\n\n/**\n * `role-dependency` rule.\n *\n * A project's `role` is its function (server | designed-lib | lib | client).\n * Apps are terminal — libraries and clients consume them, never the reverse:\n * - a `client` is fully terminal: NOTHING may depend on it.\n * - a `server` may only be depended upon by another `server` — the one\n * legitimate case is a server-side orchestrator/e2e harness that boots\n * other servers. A `lib`/`designed-lib`/`client` depending on a `server`\n * inverts the dependency direction and is a violation.\n * - a `bundle` is the one role permitted to depend on ANY app: it aggregates\n * several apps into one distributable (e.g. an nx plugin re-exposing multiple\n * tooling apps), so a `bundle → app` edge is legitimate, not inverted.\n */\nexport function validateRoleDependencies(graph: EnhancedGraph, problems: string[]): void {\n for (const [projectName, entry] of Object.entries(graph)) {\n const fromRole = entry.role;\n for (const dep of entry.dependsOn) {\n const toRole = graph[dep]?.role;\n if (toRole === undefined) continue; // role resolution already flagged this project\n if (!APP_ROLES.includes(toRole)) continue;\n // A server may orchestrate/boot other servers (e.g. an e2e harness).\n if (toRole === 'server' && fromRole === 'server') continue;\n // A bundle aggregates apps — it may depend on any app role.\n if (fromRole === 'bundle') continue;\n\n const why =\n toRole === 'client'\n ? `a 'client' app is terminal and may never be depended upon`\n : `a 'server' may only be depended upon by another 'server' (an orchestrator/e2e harness)`;\n problems.push(\n `role-dependency: '${projectName}' (role:${fromRole ?? 'none'}) must not depend on ` +\n `'${dep}' (role:${toRole}) — ${why}. Retag '${dep}' role:lib/role:designed-lib if it ` +\n `is actually a library, or remove the dependency.`\n );\n }\n }\n}\n\n/**\n * Fill the two symmetric client-addressing fields — `serviceName` (the name clients address THIS app\n * by) and `callsService` (the name THIS app's clients call when no literal ClientConfig sits at the\n * call site). Both are declared in project.json, never derived (see service-name-resolver.ts).\n */\n// webpieces-disable no-function-outside-class -- pure enrichment helper, mirrors enrichResponsibilities\nfunction enrichClientNames(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n projectName: string,\n serviceNames: Map<string, string>,\n problems: string[]\n): void {\n const serviceResolution = resolveServiceName(info, workspaceRoot);\n if (serviceResolution.problem !== null) {\n problems.push(serviceResolution.problem);\n } else if (serviceResolution.serviceName !== null) {\n entry.serviceName = serviceResolution.serviceName;\n serviceNames.set(projectName, serviceResolution.serviceName);\n }\n\n // Consumed by the runtime graph's target resolution (runtime-graph.ts), between the call-site\n // literal and the fan-out fallback.\n const callsResolution = resolveCallsService(info, workspaceRoot);\n if (callsResolution.problem !== null) {\n problems.push(callsResolution.problem);\n } else if (callsResolution.callsService !== null) {\n entry.callsService = callsResolution.callsService;\n }\n}\n\nfunction enrichResponsibilities(\n entry: EnhancedGraph[string],\n info: ProjectInfo,\n workspaceRoot: string,\n problems: string[]\n): void {\n const responsibilitiesFile = toRepoRelative(info.root, RESPONSIBILITIES_FILE_NAME);\n entry.responsibilitiesFile = responsibilitiesFile;\n\n const absolutePath = path.join(workspaceRoot, info.root, RESPONSIBILITIES_FILE_NAME);\n if (!fs.existsSync(absolutePath)) {\n problems.push(\n `${info.name}: missing required ${responsibilitiesFile} — create it with a heading, ` +\n `one short summary paragraph, then the full responsibilities of the module`\n );\n return;\n }\n\n const summary = extractShortDescription(fs.readFileSync(absolutePath, 'utf-8'));\n const summaryProblem = validateShortDescription(summary, responsibilitiesFile);\n if (summaryProblem !== null) {\n problems.push(`${info.name}: ${summaryProblem}`);\n return;\n }\n entry.shortDescription = summary;\n}\n\n/**\n * Repo-relative path with forward slashes (stable across platforms in the\n * committed JSON).\n */\nfunction toRepoRelative(projectRoot: string, fileName: string): string {\n return [projectRoot.replace(/\\\\/g, '/').replace(/\\/+$/, ''), fileName].join('/');\n}\n\n/**\n * True when the project has a REAL generated DI design — a committed design.json\n * whose `designs[]` is non-empty (i.e. it has ≥1 @DocumentDesign root).\n *\n * MISSING, empty-`designs[]`, and unparseable all read as \"no design\", and that\n * equivalence is load-bearing rather than incidental: di-graph-generate writes NO\n * file for a project without a design root (and reaps a stale one), so \"absent\"\n * is now the normal state for every plain lib and legacy service. Repos that have\n * not rebuilt since still carry `{ designs: [] }` files. Either way the box's node\n * menu must offer no \"View Design\" item. See design-file-emission.spec.ts.\n */\nfunction hasGeneratedDesign(workspaceRoot: string, projectRoot: string): boolean {\n const designPath = path.join(workspaceRoot, projectRoot, 'design.json');\n if (!fs.existsSync(designPath)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(designPath, 'utf-8'));\n return Array.isArray(parsed.designs) && parsed.designs.length > 0;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${designPath}: ${error.message}`);\n return false;\n }\n}\n"]}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The floating per-node menu, shared by BOTH generated graphs.
3
+ *
4
+ * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the
5
+ * click wiring that opens it — and both `architecture/dependencies.html` (graph-visualizer.ts) and
6
+ * every project's `design.html` (di-graph/design-visualizer.ts) inline these same bytes. Duplicating
7
+ * it into the two emitters is what this module exists to prevent.
8
+ *
9
+ * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`
10
+ * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the
11
+ * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a
12
+ * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one
13
+ * copy reachable by both emitters with no build-order coupling.
14
+ *
15
+ * What the menu does NOT own is the LOCK BEHAVIOUR, because the two pages genuinely differ:
16
+ * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain
17
+ * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;
18
+ * - a design page has neither dropdown nor responsibilities, so it uses `WpNodeLock` below —
19
+ * dim every other box in that graph, light the locked one.
20
+ * Both spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.
21
+ */
22
+ export declare class GraphNodeMenu {
23
+ /**
24
+ * Menu chrome + the "this box is clickable" affordance. Every node is clickable now (the menu
25
+ * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with
26
+ * a design page.
27
+ */
28
+ styles(): string;
29
+ /**
30
+ * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered
31
+ * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).
32
+ *
33
+ * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat
34
+ * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.
35
+ */
36
+ dimStyles(scope: string): string;
37
+ /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */
38
+ script(): string;
39
+ /**
40
+ * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,
41
+ * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole
42
+ * menu. Opening a menu closes any other, so at most one is ever on screen.
43
+ */
44
+ private menuScript;
45
+ /**
46
+ * The lock a DESIGN page uses: no dropdown and no responsibilities exist there, so locking a class
47
+ * box dims every other box in that design's graph and lights the locked one.
48
+ */
49
+ private lockScript;
50
+ /** Outside click and Escape both dismiss — wired once, on the document. */
51
+ private dismissScript;
52
+ }