@webpieces/nx-webpieces-rules 0.4.534 → 0.4.535
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 +6 -6
- package/src/executors/generate/executor.js +2 -0
- package/src/executors/generate/executor.js.map +1 -1
- package/src/executors/validate-runtime-architecture/executor.js +2 -0
- package/src/executors/validate-runtime-architecture/executor.js.map +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/graph-comparator.js +1 -0
- package/src/lib/graph-comparator.js.map +1 -1
- package/src/lib/graph-loader.js +8 -1
- package/src/lib/graph-loader.js.map +1 -1
- package/src/lib/graph-metadata.js +44 -23
- package/src/lib/graph-metadata.js.map +1 -1
- package/src/lib/graph-sorter.d.ts +11 -0
- package/src/lib/graph-sorter.js.map +1 -1
- package/src/lib/runtime-graph-decls.d.ts +52 -0
- package/src/lib/runtime-graph-decls.js +58 -0
- package/src/lib/runtime-graph-decls.js.map +1 -0
- package/src/lib/runtime-graph.d.ts +19 -1
- package/src/lib/runtime-graph.js +65 -31
- package/src/lib/runtime-graph.js.map +1 -1
- package/src/lib/runtime-participant-resolver.d.ts +61 -0
- package/src/lib/runtime-participant-resolver.js +108 -0
- package/src/lib/runtime-participant-resolver.js.map +1 -0
|
@@ -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 } 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 { 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 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 enrichClientNames(entry, info, workspaceRoot, projectName, serviceNames, problems);\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 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. Every\n // project.json project gets a design.json written, but plain libs get an\n // empty `{ designs: [] }` — those must NOT become clickable in the arch\n // viz (designHtmlHref 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 * 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). Plain\n * libs get a `{ designs: [] }` file written, which must read as \"no design\" so\n * the arch viz does not render them as clickable. A missing/unparseable file is\n * treated as \"no design\".\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. Every\n // project.json project gets a design.json written, but plain libs get an\n // empty `{ designs: [] }` — those must NOT become clickable in the arch\n // viz (designHtmlHref 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). Plain\n * libs get a `{ designs: [] }` file written, which must read as \"no design\" so\n * the arch viz does not render them as clickable. A missing/unparseable file is\n * treated as \"no design\".\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"]}
|
|
@@ -53,6 +53,17 @@ export interface GraphEntry {
|
|
|
53
53
|
responsibilitiesFile?: string;
|
|
54
54
|
designFile?: string;
|
|
55
55
|
apiRelations?: ProjectApiRelations;
|
|
56
|
+
/**
|
|
57
|
+
* The @webpieces runtime packages this project's OWN package.json declares — http-routing,
|
|
58
|
+
* http-server, http-client-node, http-client-browser, cloudtasks-client. Omitted when it
|
|
59
|
+
* declares none, so TOTAL absence across the file means the file predates this field and
|
|
60
|
+
* nothing is auto-hidden (see runtime-graph.ts). Core/util/logging packages are deliberately
|
|
61
|
+
* not markers; see runtime-participant-resolver.ts for why.
|
|
62
|
+
*
|
|
63
|
+
* The runtime graph ORs this over a node's library closure: a server whose whole webpieces
|
|
64
|
+
* stack comes from a shared bootstrap lib participates even though it declares nothing itself.
|
|
65
|
+
*/
|
|
66
|
+
webpiecesRuntime?: string[];
|
|
56
67
|
}
|
|
57
68
|
/**
|
|
58
69
|
* Enhanced graph format with level information
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-sorter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-sorter.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Sorter\n *\n * Performs topological sorting on the dependency graph to:\n * 1. Detect circular dependencies (fails if cycle found)\n * 2. Assign level numbers to each project (level 0 = no deps, level 1 = depends on level 0, etc.)\n * 3. Group projects into layers for deterministic ordering\n */\n\nimport type { ProjectApiRelations } from './api-usage/api-relations';\n\n/**\n * Graph entry with level metadata plus AI-oriented metadata filled in by\n * enrichGraph() (lib/graph-metadata.ts) before the graph is saved:\n * - framework: the project's libType — the SET of runtime environments it is\n * validated to run in, drawn from browser | react | angular | node | express\n * (e.g. [\"browser\",\"node\"]); from its `framework:` nx tags (source of truth)\n * or inferred from package.json deps\n * - shortDescription: summary extracted from the project's responsibilities.md\n * - responsibilitiesFile: repo-relative path to the FULL responsibilities doc\n * - designFile: repo-relative path to the generated DI design.json (only for\n * project.json projects)\n * - apiRelations: for each api-lib in `dependsOn`, WHY the edge exists — the\n * APIs this project implements (serves) and/or uses (calls), each with its\n * transport (rpc | pubsub). Derived by scanning source (see api-usage/).\n */\nexport interface GraphEntry {\n level: number;\n dependsOn: string[];\n framework?: string[];\n role?: string;\n /**\n * The name CLIENTS address this app by at runtime (`new ClientConfig('helper-fsdb')`) — its\n * deployed service name, DECLARED in project.json as metadata.webpieces.serviceName. Absent for\n * anything nothing calls by name (libraries, browser apps). See service-name-resolver.ts.\n */\n serviceName?: string;\n /**\n * The service(s) this project's clients call when the call site cannot carry a literal\n * `ClientConfig` — the symmetric half of `serviceName`. DECLARED in project.json as\n * metadata.webpieces.callsService, either a single service name (every untargeted `uses` aims\n * there) or an `{ apiClassName: serviceName }` map (a client that calls several). Absent for a\n * project that never calls anything, or one whose call sites all carry literals.\n * See service-name-resolver.ts.\n */\n callsService?: string | Record<string, string>;\n /**\n * When false, the project is hidden from the rendered architecture graphs\n * (its box AND every edge touching it are omitted from dependencies.html and\n * the runtime graph). It stays in this JSON so the data view is complete.\n * Absent means drawn (the default). From the project's `drawOnGraph:` nx tag.\n */\n drawOnGraph?: boolean;\n shortDescription?: string;\n responsibilitiesFile?: string;\n designFile?: string;\n apiRelations?: ProjectApiRelations;\n /**\n * The @webpieces runtime packages this project's OWN package.json declares — http-routing,\n * http-server, http-client-node, http-client-browser, cloudtasks-client. Omitted when it\n * declares none, so TOTAL absence across the file means the file predates this field and\n * nothing is auto-hidden (see runtime-graph.ts). Core/util/logging packages are deliberately\n * not markers; see runtime-participant-resolver.ts for why.\n *\n * The runtime graph ORs this over a node's library closure: a server whose whole webpieces\n * stack comes from a shared bootstrap lib participates even though it declares nothing itself.\n */\n webpiecesRuntime?: string[];\n}\n\n/**\n * Enhanced graph format with level information\n */\nexport type EnhancedGraph = Record<string, GraphEntry>;\n\n/**\n * Compute topological layers for dependency graph using Kahn's algorithm\n *\n * Projects are grouped into layers where each layer only depends on previous layers.\n * Throws an error if a circular dependency is detected.\n *\n * @param graph - Dependency graph { project: [deps] }\n * @returns Array of layers, each containing sorted project names\n */\nexport function computeTopologicalLayers(graph: Record<string, string[]>): string[][] {\n const layers: string[][] = [];\n const processed = new Set<string>();\n const allProjects = Object.keys(graph);\n\n while (processed.size < allProjects.length) {\n const currentLayer: string[] = [];\n\n for (const project of allProjects) {\n if (processed.has(project)) continue;\n\n const deps = graph[project] || [];\n // Check if all dependencies are in previous layers (already processed)\n const allDepsInPrevLayers = deps.every((dep) => processed.has(dep));\n\n if (allDepsInPrevLayers) {\n currentLayer.push(project);\n }\n }\n\n if (currentLayer.length === 0) {\n // No progress made = circular dependency detected\n const remaining = allProjects.filter((p) => !processed.has(p));\n\n // Try to identify the cycle\n const cycleInfo = findCycle(graph, remaining);\n\n throw new Error(\n `Circular dependency detected among: ${remaining.join(', ')}\\n` +\n (cycleInfo ? `Cycle: ${cycleInfo}\\n` : '') +\n 'Fix: Remove one of the dependencies to break the cycle.'\n );\n }\n\n // Sort alphabetically within layer for deterministic output\n currentLayer.sort();\n layers.push(currentLayer);\n\n // Mark as processed\n currentLayer.forEach((p) => processed.add(p));\n }\n\n return layers;\n}\n\n/**\n * Try to find and describe a cycle in the graph\n */\nfunction findCycle(graph: Record<string, string[]>, remaining: string[]): string | null {\n const visited = new Set<string>();\n const path: string[] = [];\n\n function dfs(node: string): string | null {\n if (path.includes(node)) {\n const cycleStart = path.indexOf(node);\n return [...path.slice(cycleStart), node].join(' -> ');\n }\n if (visited.has(node)) return null;\n\n visited.add(node);\n path.push(node);\n\n const deps = graph[node] || [];\n for (const dep of deps) {\n if (remaining.includes(dep)) {\n const result = dfs(dep);\n if (result) return result;\n }\n }\n\n path.pop();\n return null;\n }\n\n for (const node of remaining) {\n const cycle = dfs(node);\n if (cycle) return cycle;\n }\n\n return null;\n}\n\n/**\n * Sort graph in topological order with alphabetical sorting within layers\n * Returns enhanced format with level metadata\n *\n * @param graph - Unsorted dependency graph { project: [deps] }\n * @returns Sorted graph with level metadata { project: { level: number, dependsOn: [deps] } }\n */\nexport function sortGraphTopologically(graph: Record<string, string[]>): EnhancedGraph {\n const layers = computeTopologicalLayers(graph);\n const result: EnhancedGraph = {};\n\n // Add projects layer by layer (dependencies before dependents)\n layers.forEach((layer, levelIndex) => {\n for (const project of layer) {\n // Already sorted alphabetically within layer\n result[project] = {\n level: levelIndex,\n dependsOn: (graph[project] || []).sort(),\n };\n }\n });\n\n return result;\n}\n"]}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the dependsOn CLOSURE WALK accumulates for one runtime node, and the result it produces.
|
|
3
|
+
*
|
|
4
|
+
* A node's effective facts are never just its own: a library that registers routes or builds
|
|
5
|
+
* clients runs inside the server that embeds it, so both its api relations AND the webpieces
|
|
6
|
+
* runtime packages it declares belong to that server. RelationSink is the accumulator for one such
|
|
7
|
+
* walk (see RuntimeGraphDeriver.collectEffectiveRelations); ScanDecl is what it settles into.
|
|
8
|
+
*
|
|
9
|
+
* Split out of runtime-graph.ts so the deriver file holds the derivation and this one holds what
|
|
10
|
+
* the derivation collects.
|
|
11
|
+
*/
|
|
12
|
+
import type { ApiRef } from './api-usage/api-relations';
|
|
13
|
+
/** One project's implements/uses at api-CLASS granularity, from dependencies.json apiRelations. */
|
|
14
|
+
export interface ScanDecl {
|
|
15
|
+
name: string;
|
|
16
|
+
implementsApis: ApiRef[];
|
|
17
|
+
usesApis: ApiRef[];
|
|
18
|
+
/** apiClassName -> the embedded LIBRARY project that declared the implements (never the node itself). */
|
|
19
|
+
implementsVia: Map<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Marker package -> the project in this node's closure that declared it (the node itself, or a
|
|
22
|
+
* library it embeds). Empty means nothing in the closure speaks the webpieces runtime.
|
|
23
|
+
*/
|
|
24
|
+
markerVia: Map<string, string>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Accumulates one node's effective relations while its dependsOn closure is walked, keeping the
|
|
28
|
+
* PROVENANCE the walk would otherwise throw away: which library contributed an implements, and
|
|
29
|
+
* which api-lib owns each contract.
|
|
30
|
+
*/
|
|
31
|
+
export declare class RelationSink {
|
|
32
|
+
/** The runtime node these relations are attributed to. */
|
|
33
|
+
private readonly node;
|
|
34
|
+
readonly implementsApis: ApiRef[];
|
|
35
|
+
readonly usesApis: ApiRef[];
|
|
36
|
+
readonly implementsVia: Map<string, string>;
|
|
37
|
+
/** Marker package -> the project in the closure that declared it. See ScanDecl.markerVia. */
|
|
38
|
+
readonly markerVia: Map<string, string>;
|
|
39
|
+
constructor(
|
|
40
|
+
/** The runtime node these relations are attributed to. */
|
|
41
|
+
node: string);
|
|
42
|
+
/**
|
|
43
|
+
* Record the webpieces runtime packages `from` declares. Accumulated on the SAME walk as the
|
|
44
|
+
* relations because participation is the same transitive question: a server whose entire
|
|
45
|
+
* webpieces stack arrives through a shared bootstrap library speaks the runtime just as much
|
|
46
|
+
* as one that declares http-routing itself.
|
|
47
|
+
*/
|
|
48
|
+
addMarkers(markers: string[] | undefined, from: string): void;
|
|
49
|
+
/** `from` is the project whose apiRelations declared this — the node itself, or a lib it embeds. */
|
|
50
|
+
addImplements(ref: ApiRef, from: string): void;
|
|
51
|
+
addUses(ref: ApiRef): void;
|
|
52
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What the dependsOn CLOSURE WALK accumulates for one runtime node, and the result it produces.
|
|
4
|
+
*
|
|
5
|
+
* A node's effective facts are never just its own: a library that registers routes or builds
|
|
6
|
+
* clients runs inside the server that embeds it, so both its api relations AND the webpieces
|
|
7
|
+
* runtime packages it declares belong to that server. RelationSink is the accumulator for one such
|
|
8
|
+
* walk (see RuntimeGraphDeriver.collectEffectiveRelations); ScanDecl is what it settles into.
|
|
9
|
+
*
|
|
10
|
+
* Split out of runtime-graph.ts so the deriver file holds the derivation and this one holds what
|
|
11
|
+
* the derivation collects.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.RelationSink = void 0;
|
|
15
|
+
/**
|
|
16
|
+
* Accumulates one node's effective relations while its dependsOn closure is walked, keeping the
|
|
17
|
+
* PROVENANCE the walk would otherwise throw away: which library contributed an implements, and
|
|
18
|
+
* which api-lib owns each contract.
|
|
19
|
+
*/
|
|
20
|
+
class RelationSink {
|
|
21
|
+
node;
|
|
22
|
+
implementsApis = [];
|
|
23
|
+
usesApis = [];
|
|
24
|
+
implementsVia = new Map();
|
|
25
|
+
/** Marker package -> the project in the closure that declared it. See ScanDecl.markerVia. */
|
|
26
|
+
markerVia = new Map();
|
|
27
|
+
constructor(
|
|
28
|
+
/** The runtime node these relations are attributed to. */
|
|
29
|
+
node) {
|
|
30
|
+
this.node = node;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Record the webpieces runtime packages `from` declares. Accumulated on the SAME walk as the
|
|
34
|
+
* relations because participation is the same transitive question: a server whose entire
|
|
35
|
+
* webpieces stack arrives through a shared bootstrap library speaks the runtime just as much
|
|
36
|
+
* as one that declares http-routing itself.
|
|
37
|
+
*/
|
|
38
|
+
addMarkers(markers, from) {
|
|
39
|
+
if (markers === undefined)
|
|
40
|
+
return;
|
|
41
|
+
// First declarer wins, matching implementsVia's keep-the-first rule.
|
|
42
|
+
for (const marker of markers)
|
|
43
|
+
if (!this.markerVia.has(marker))
|
|
44
|
+
this.markerVia.set(marker, from);
|
|
45
|
+
}
|
|
46
|
+
/** `from` is the project whose apiRelations declared this — the node itself, or a lib it embeds. */
|
|
47
|
+
addImplements(ref, from) {
|
|
48
|
+
this.implementsApis.push(ref);
|
|
49
|
+
// First contributor wins, matching dedupApiRefs' keep-the-first rule on the ref list.
|
|
50
|
+
if (from !== this.node && !this.implementsVia.has(ref.api))
|
|
51
|
+
this.implementsVia.set(ref.api, from);
|
|
52
|
+
}
|
|
53
|
+
addUses(ref) {
|
|
54
|
+
this.usesApis.push(ref);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.RelationSink = RelationSink;
|
|
58
|
+
//# sourceMappingURL=runtime-graph-decls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-graph-decls.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-graph-decls.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAkBH;;;;GAIG;AACH,MAAa,YAAY;IASA;IARZ,cAAc,GAAa,EAAE,CAAC;IAC9B,QAAQ,GAAa,EAAE,CAAC;IACxB,aAAa,GAAG,IAAI,GAAG,EAAkB,CAAC;IACnD,6FAA6F;IACpF,SAAS,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE/C;IACI,0DAA0D;IACzC,IAAY;QAAZ,SAAI,GAAJ,IAAI,CAAQ;IAC9B,CAAC;IAEJ;;;;;OAKG;IACH,UAAU,CAAC,OAA6B,EAAE,IAAY;QAClD,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO;QAClC,qEAAqE;QACrE,KAAK,MAAM,MAAM,IAAI,OAAO;YACxB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC1E,CAAC;IAED,oGAAoG;IACpG,aAAa,CAAC,GAAW,EAAE,IAAY;QACnC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC9B,sFAAsF;QACtF,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC;YACtD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,CAAC,GAAW;QACf,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;CACJ;AApCD,oCAoCC","sourcesContent":["/**\n * What the dependsOn CLOSURE WALK accumulates for one runtime node, and the result it produces.\n *\n * A node's effective facts are never just its own: a library that registers routes or builds\n * clients runs inside the server that embeds it, so both its api relations AND the webpieces\n * runtime packages it declares belong to that server. RelationSink is the accumulator for one such\n * walk (see RuntimeGraphDeriver.collectEffectiveRelations); ScanDecl is what it settles into.\n *\n * Split out of runtime-graph.ts so the deriver file holds the derivation and this one holds what\n * the derivation collects.\n */\n\nimport type { ApiRef } from './api-usage/api-relations';\n\n/** One project's implements/uses at api-CLASS granularity, from dependencies.json apiRelations. */\nexport interface ScanDecl {\n name: string;\n implementsApis: ApiRef[];\n usesApis: ApiRef[];\n /** apiClassName -> the embedded LIBRARY project that declared the implements (never the node itself). */\n implementsVia: Map<string, string>;\n /**\n * Marker package -> the project in this node's closure that declared it (the node itself, or a\n * library it embeds). Empty means nothing in the closure speaks the webpieces runtime.\n */\n markerVia: Map<string, string>;\n}\n\n/**\n * Accumulates one node's effective relations while its dependsOn closure is walked, keeping the\n * PROVENANCE the walk would otherwise throw away: which library contributed an implements, and\n * which api-lib owns each contract.\n */\nexport class RelationSink {\n readonly implementsApis: ApiRef[] = [];\n readonly usesApis: ApiRef[] = [];\n readonly implementsVia = new Map<string, string>();\n /** Marker package -> the project in the closure that declared it. See ScanDecl.markerVia. */\n readonly markerVia = new Map<string, string>();\n\n constructor(\n /** The runtime node these relations are attributed to. */\n private readonly node: string,\n ) {}\n\n /**\n * Record the webpieces runtime packages `from` declares. Accumulated on the SAME walk as the\n * relations because participation is the same transitive question: a server whose entire\n * webpieces stack arrives through a shared bootstrap library speaks the runtime just as much\n * as one that declares http-routing itself.\n */\n addMarkers(markers: string[] | undefined, from: string): void {\n if (markers === undefined) return;\n // First declarer wins, matching implementsVia's keep-the-first rule.\n for (const marker of markers)\n if (!this.markerVia.has(marker)) this.markerVia.set(marker, from);\n }\n\n /** `from` is the project whose apiRelations declared this — the node itself, or a lib it embeds. */\n addImplements(ref: ApiRef, from: string): void {\n this.implementsApis.push(ref);\n // First contributor wins, matching dedupApiRefs' keep-the-first rule on the ref list.\n if (from !== this.node && !this.implementsVia.has(ref.api))\n this.implementsVia.set(ref.api, from);\n }\n\n addUses(ref: ApiRef): void {\n this.usesApis.push(ref);\n }\n}\n"]}
|
|
@@ -49,6 +49,15 @@ export declare class RuntimeGraphReport {
|
|
|
49
49
|
* contract, so it is `unresolvedUses`.)
|
|
50
50
|
*/
|
|
51
51
|
readonly problems: string[];
|
|
52
|
+
/**
|
|
53
|
+
* role:server nodes omitted from the DRAWING because they declare no webpieces runtime
|
|
54
|
+
* package anywhere in their library closure and serve/call nothing in-repo. Neither a
|
|
55
|
+
* warning nor a problem — this is intended behavior, and shouting on every clean run is how
|
|
56
|
+
* warnings stop being read. Deliberately NOT persisted: a graph file that records its own
|
|
57
|
+
* omissions gets committed and stops being read. Executors print it instead, so the
|
|
58
|
+
* omission is always visible and never silent.
|
|
59
|
+
*/
|
|
60
|
+
readonly autoHidden: string[];
|
|
52
61
|
constructor(graph: RuntimeGraph,
|
|
53
62
|
/** Human-readable lines naming every call site whose target the graph could not pin down. */
|
|
54
63
|
warnings: string[],
|
|
@@ -59,7 +68,16 @@ export declare class RuntimeGraphReport {
|
|
|
59
68
|
* (A call to a service outside the repo never reaches here — nothing in-repo implements its
|
|
60
69
|
* contract, so it is `unresolvedUses`.)
|
|
61
70
|
*/
|
|
62
|
-
problems?: string[]
|
|
71
|
+
problems?: string[],
|
|
72
|
+
/**
|
|
73
|
+
* role:server nodes omitted from the DRAWING because they declare no webpieces runtime
|
|
74
|
+
* package anywhere in their library closure and serve/call nothing in-repo. Neither a
|
|
75
|
+
* warning nor a problem — this is intended behavior, and shouting on every clean run is how
|
|
76
|
+
* warnings stop being read. Deliberately NOT persisted: a graph file that records its own
|
|
77
|
+
* omissions gets committed and stops being read. Executors print it instead, so the
|
|
78
|
+
* omission is always visible and never silent.
|
|
79
|
+
*/
|
|
80
|
+
autoHidden?: string[]);
|
|
63
81
|
}
|
|
64
82
|
/** Adjacency (service -> [targets]) from a loaded runtime graph. */
|
|
65
83
|
export declare function runtimeAdjacency(graph: RuntimeGraph): Record<string, string[]>;
|
package/src/lib/runtime-graph.js
CHANGED
|
@@ -35,6 +35,7 @@ exports.deriveRuntimeGraph = deriveRuntimeGraph;
|
|
|
35
35
|
exports.deriveRuntimeGraphReport = deriveRuntimeGraphReport;
|
|
36
36
|
const graph_sorter_1 = require("./graph-sorter");
|
|
37
37
|
const api_relations_1 = require("./api-usage/api-relations");
|
|
38
|
+
const runtime_graph_decls_1 = require("./runtime-graph-decls");
|
|
38
39
|
const runtime_graph_sorters_1 = require("./runtime-graph-sorters");
|
|
39
40
|
const external_systems_1 = require("./api-usage/external-systems");
|
|
40
41
|
const toError_1 = require("../toError");
|
|
@@ -54,6 +55,7 @@ class RuntimeGraphReport {
|
|
|
54
55
|
graph;
|
|
55
56
|
warnings;
|
|
56
57
|
problems;
|
|
58
|
+
autoHidden;
|
|
57
59
|
constructor(graph,
|
|
58
60
|
/** Human-readable lines naming every call site whose target the graph could not pin down. */
|
|
59
61
|
warnings,
|
|
@@ -64,10 +66,20 @@ class RuntimeGraphReport {
|
|
|
64
66
|
* (A call to a service outside the repo never reaches here — nothing in-repo implements its
|
|
65
67
|
* contract, so it is `unresolvedUses`.)
|
|
66
68
|
*/
|
|
67
|
-
problems = []
|
|
69
|
+
problems = [],
|
|
70
|
+
/**
|
|
71
|
+
* role:server nodes omitted from the DRAWING because they declare no webpieces runtime
|
|
72
|
+
* package anywhere in their library closure and serve/call nothing in-repo. Neither a
|
|
73
|
+
* warning nor a problem — this is intended behavior, and shouting on every clean run is how
|
|
74
|
+
* warnings stop being read. Deliberately NOT persisted: a graph file that records its own
|
|
75
|
+
* omissions gets committed and stops being read. Executors print it instead, so the
|
|
76
|
+
* omission is always visible and never silent.
|
|
77
|
+
*/
|
|
78
|
+
autoHidden = []) {
|
|
68
79
|
this.graph = graph;
|
|
69
80
|
this.warnings = warnings;
|
|
70
81
|
this.problems = problems;
|
|
82
|
+
this.autoHidden = autoHidden;
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
exports.RuntimeGraphReport = RuntimeGraphReport;
|
|
@@ -115,32 +127,6 @@ function assignLevels(adjacency) {
|
|
|
115
127
|
}
|
|
116
128
|
return levels;
|
|
117
129
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Accumulates one node's effective relations while its dependsOn closure is walked, keeping the
|
|
120
|
-
* PROVENANCE the walk would otherwise throw away: which library contributed an implements, and
|
|
121
|
-
* which api-lib owns each contract.
|
|
122
|
-
*/
|
|
123
|
-
class RelationSink {
|
|
124
|
-
node;
|
|
125
|
-
implementsApis = [];
|
|
126
|
-
usesApis = [];
|
|
127
|
-
implementsVia = new Map();
|
|
128
|
-
constructor(
|
|
129
|
-
/** The runtime node these relations are attributed to. */
|
|
130
|
-
node) {
|
|
131
|
-
this.node = node;
|
|
132
|
-
}
|
|
133
|
-
/** `from` is the project whose apiRelations declared this — the node itself, or a lib it embeds. */
|
|
134
|
-
addImplements(ref, from) {
|
|
135
|
-
this.implementsApis.push(ref);
|
|
136
|
-
// First contributor wins, matching dedupApiRefs' keep-the-first rule on the ref list.
|
|
137
|
-
if (from !== this.node && !this.implementsVia.has(ref.api))
|
|
138
|
-
this.implementsVia.set(ref.api, from);
|
|
139
|
-
}
|
|
140
|
-
addUses(ref) {
|
|
141
|
-
this.usesApis.push(ref);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
130
|
/**
|
|
145
131
|
* Derives the runtime microservice graph from architecture/dependencies.json `apiRelations`:
|
|
146
132
|
* implementers × users per API, split by transport. An rpc edge is a direct call; a pubsub edge flows
|
|
@@ -161,6 +147,16 @@ class RuntimeGraphDeriver {
|
|
|
161
147
|
nodeByServiceName = new Map();
|
|
162
148
|
warnings = [];
|
|
163
149
|
problems = [];
|
|
150
|
+
/** role:server nodes hidden by isNonParticipantServer, in the order buildServices met them. */
|
|
151
|
+
autoHidden = [];
|
|
152
|
+
/**
|
|
153
|
+
* False when NO project in the graph carries `webpiecesRuntime`, i.e. the file was written
|
|
154
|
+
* before the field existed. Auto-hiding is then off ENTIRELY, so an old dependencies.json
|
|
155
|
+
* renders exactly as it did — absence of the field must never be read as "declares none".
|
|
156
|
+
* Any repo regenerated after this change has at least one webpieces project, so the guard
|
|
157
|
+
* cannot wrongly suppress the feature.
|
|
158
|
+
*/
|
|
159
|
+
markersKnown;
|
|
164
160
|
constructor(projects,
|
|
165
161
|
/** Project names tagged drawOnGraph:false — kept in the JSON but flagged so the viz omits them. */
|
|
166
162
|
hiddenProjects,
|
|
@@ -179,6 +175,7 @@ class RuntimeGraphDeriver {
|
|
|
179
175
|
this.hiddenProjects = hiddenProjects;
|
|
180
176
|
this.apiContracts = apiContracts;
|
|
181
177
|
this.externalSystemDecls = externalSystemDecls;
|
|
178
|
+
this.markersKnown = Object.values(projects).some((entry) => entry.webpiecesRuntime !== undefined);
|
|
182
179
|
// A node ALWAYS answers to its own module name, so a repo whose deployed names match its
|
|
183
180
|
// project names needs no declaration at all — and no alias can ever redirect 'ai-chat' away
|
|
184
181
|
// from the ai-chat module. Module names are therefore claimed FIRST and are unshadowable.
|
|
@@ -220,7 +217,7 @@ class RuntimeGraphDeriver {
|
|
|
220
217
|
triggers: this.buildTriggers(decls),
|
|
221
218
|
};
|
|
222
219
|
(0, external_systems_1.attachExternalSystems)(graph, (0, external_systems_1.resolveExternalSystems)(this.externalSystemDecls, services));
|
|
223
|
-
return new RuntimeGraphReport(graph, this.warnings, this.problems);
|
|
220
|
+
return new RuntimeGraphReport(graph, this.warnings, this.problems, [...this.autoHidden].sort());
|
|
224
221
|
}
|
|
225
222
|
/**
|
|
226
223
|
* The clock- and outside-driven entry points: for every contract a node IMPLEMENTS, each method
|
|
@@ -276,7 +273,7 @@ class RuntimeGraphDeriver {
|
|
|
276
273
|
for (const name of Object.keys(this.projects).sort()) {
|
|
277
274
|
if (!this.isNode(name))
|
|
278
275
|
continue;
|
|
279
|
-
const sink = new RelationSink(name);
|
|
276
|
+
const sink = new runtime_graph_decls_1.RelationSink(name);
|
|
280
277
|
this.collectEffectiveRelations(name, sink, new Set([name]));
|
|
281
278
|
// EVERY node gets a decl, including one with zero relations. Gating this on "has at
|
|
282
279
|
// least one implements/uses" silently DELETED any deployed server whose work arrives
|
|
@@ -288,6 +285,7 @@ class RuntimeGraphDeriver {
|
|
|
288
285
|
implementsApis: (0, runtime_graph_sorters_1.dedupApiRefs)((0, api_relations_1.sortApiRefs)(sink.implementsApis)),
|
|
289
286
|
usesApis: (0, runtime_graph_sorters_1.dedupApiRefs)((0, api_relations_1.sortApiRefs)(sink.usesApis)),
|
|
290
287
|
implementsVia: sink.implementsVia,
|
|
288
|
+
markerVia: sink.markerVia,
|
|
291
289
|
});
|
|
292
290
|
}
|
|
293
291
|
return decls;
|
|
@@ -297,6 +295,35 @@ class RuntimeGraphDeriver {
|
|
|
297
295
|
const role = this.projects[name]?.role;
|
|
298
296
|
return role === 'server' || role === 'client';
|
|
299
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* True for a role:server that speaks NO webpieces runtime package anywhere in its library
|
|
300
|
+
* closure AND serves/calls no in-repo contract — a legacy Express/NestJS service sitting in the
|
|
301
|
+
* same monorepo. This graph is built entirely out of webpieces contracts, so such a service can
|
|
302
|
+
* only ever draw as a disconnected box: it has no edges by construction and never will.
|
|
303
|
+
*
|
|
304
|
+
* It stays a NODE and stays in runtime-dependencies.json (drawOnGraph:false), which is the whole
|
|
305
|
+
* difference between hiding it and the silent deletion #542 removed — the data view still shows
|
|
306
|
+
* a deployed role:server, only the picture drops it. Removing it from isNode() instead would
|
|
307
|
+
* also drop it from nodeByServiceName, so a targeted ClientConfig('orders-manager') from a drawn
|
|
308
|
+
* service would stop resolving and decay into an unresolved use: a graph that got quietly WORSE
|
|
309
|
+
* while looking cleaner. The COMPILE-TIME graph (dependencies.html) still draws these projects;
|
|
310
|
+
* they genuinely exist, and only the runtime drawing claims to show webpieces services.
|
|
311
|
+
*
|
|
312
|
+
* role:client is deliberately never auto-hidden: a browser app can legitimately declare no
|
|
313
|
+
* marker at all, and nothing checks clients for presence the way checkServersPresent checks
|
|
314
|
+
* servers, so there is no equivalent failure to prevent.
|
|
315
|
+
*/
|
|
316
|
+
isNonParticipantServer(decl) {
|
|
317
|
+
if (!this.markersKnown)
|
|
318
|
+
return false;
|
|
319
|
+
if (this.projects[decl.name]?.role !== 'server')
|
|
320
|
+
return false;
|
|
321
|
+
if (decl.markerVia.size > 0)
|
|
322
|
+
return false;
|
|
323
|
+
// Belt-and-braces: serving or calling an in-repo contract IS participation, whatever the
|
|
324
|
+
// package list says. This is what keeps a mechanism we did not anticipate from being blanked.
|
|
325
|
+
return decl.implementsApis.length === 0 && decl.usesApis.length === 0;
|
|
326
|
+
}
|
|
300
327
|
/**
|
|
301
328
|
* Accumulate `name`'s own api relations, then recurse through its LIBRARY deps
|
|
302
329
|
* (skipping other nodes, which own their own relations). `visited` guards against
|
|
@@ -306,6 +333,7 @@ class RuntimeGraphDeriver {
|
|
|
306
333
|
const entry = this.projects[name];
|
|
307
334
|
if (entry === undefined)
|
|
308
335
|
return;
|
|
336
|
+
sink.addMarkers(entry.webpiecesRuntime, name);
|
|
309
337
|
const relations = entry.apiRelations;
|
|
310
338
|
if (relations !== undefined) {
|
|
311
339
|
for (const owner of Object.keys(relations).sort()) {
|
|
@@ -559,8 +587,14 @@ class RuntimeGraphDeriver {
|
|
|
559
587
|
dependsOn,
|
|
560
588
|
};
|
|
561
589
|
services[decl.name] = service;
|
|
562
|
-
|
|
563
|
-
|
|
590
|
+
// The explicit tag wins and is NOT reported as auto-hidden — somebody asked for it.
|
|
591
|
+
if (this.hiddenProjects.has(decl.name)) {
|
|
592
|
+
service.drawOnGraph = false;
|
|
593
|
+
}
|
|
594
|
+
else if (this.isNonParticipantServer(decl)) {
|
|
595
|
+
service.drawOnGraph = false;
|
|
596
|
+
this.autoHidden.push(decl.name);
|
|
597
|
+
}
|
|
564
598
|
}
|
|
565
599
|
const levels = assignLevels(adjacencyFromEdges(Object.keys(services), edges));
|
|
566
600
|
for (const name of Object.keys(services))
|