@webpieces/nx-webpieces-rules 0.3.261 → 0.3.263

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.3.261",
3
+ "version": "0.3.263",
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",
@@ -21,11 +21,11 @@
21
21
  "README.md"
22
22
  ],
23
23
  "dependencies": {
24
- "@webpieces/ai-hook-rules": "0.3.261",
25
- "@webpieces/code-rules": "0.3.261",
26
- "@webpieces/eslint-rules": "0.3.261",
27
- "@webpieces/pr-gate": "0.3.261",
28
- "@webpieces/rules-config": "0.3.261",
24
+ "@webpieces/ai-hook-rules": "0.3.263",
25
+ "@webpieces/code-rules": "0.3.263",
26
+ "@webpieces/eslint-rules": "0.3.263",
27
+ "@webpieces/pr-gate": "0.3.263",
28
+ "@webpieces/rules-config": "0.3.263",
29
29
  "madge": "8.0.0"
30
30
  },
31
31
  "peerDependencies": {
@@ -118,10 +118,10 @@ class AnalyzerChoice {
118
118
  */
119
119
  function chooseAnalyzer(tags, srcDir) {
120
120
  const role = (0, analyzer_strategy_1.explicitRoleTag)(tags);
121
- const framework = (0, analyzer_strategy_1.explicitFrameworkTag)(tags);
122
- const analyzer = (0, analyzer_strategy_1.selectAnalyzer)(role, framework, detectFrameworkMarkers(srcDir));
121
+ const frameworks = (0, analyzer_strategy_1.frameworkTags)(tags);
122
+ const analyzer = (0, analyzer_strategy_1.selectAnalyzer)(role, frameworks, detectFrameworkMarkers(srcDir));
123
123
  console.log(` Analyzer: ${analyzer.constructor.name} ` +
124
- `(role tag: ${role ?? 'none'}, framework tag: ${framework ?? 'none'})`);
124
+ `(role tag: ${role ?? 'none'}, framework tags: ${frameworks.length > 0 ? frameworks.join(', ') : 'none'})`);
125
125
  return new AnalyzerChoice(analyzer, role);
126
126
  }
127
127
  function reportDesignedLibMissingRoot(projectName) {
@@ -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 * Runs on every build (cache:false; the build gates on\n * validate-di-graph-unchanged which dependsOn this target). Unrecognized DI\n * patterns become \"unresolved\" nodes rather than failing the build.\n *\n * Config (webpieces.config.json, rule key `di-graph`): mode RUN_EVERY_TIME | OFF.\n *\n * Usage: nx run <project>:di-graph-generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadAndValidate } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n DiAnalyzer,\n explicitFrameworkTag,\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';\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 '@Controller(',\n '@ApiImplementation(',\n '@provideSingleton',\n '@provideTransient',\n '@injectable',\n 'new ContainerModule',\n '@inject(',\n '@Component(',\n 'bootstrapApplication',\n];\n\nconst ANGULAR_MARKERS = ['@Component(', 'bootstrapApplication'];\nconst CONTROLLER_MARKER = '@Controller(';\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(CONTROLLER_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\nfunction writeDesignFiles(projectRootAbs: string, projectRoot: string, graph: DiGraph): void {\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→@Controller, designed-lib→@ApiImplementation,\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 framework = explicitFrameworkTag(tags);\n const analyzer = selectAnalyzer(role, framework, detectFrameworkMarkers(srcDir));\n console.log(\n ` Analyzer: ${analyzer.constructor.name} ` +\n `(role tag: ${role ?? 'none'}, framework tag: ${framework ?? 'none'})`,\n );\n return new AnalyzerChoice(analyzer, role);\n}\n\nfunction reportDesignedLibMissingRoot(projectName: string): void {\n console.error(\n `❌ ${projectName} is tagged role:designed-lib but has no @ApiImplementation class.\\n` +\n ` Annotate the library's top implementation class with @ApiImplementation ` +\n `(from @webpieces/http-routing), or retag the project role:lib if it has no design.`,\n );\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 — writing empty design graph');\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 — writing empty design graph');\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 // A designed-lib MUST expose at least one @ApiImplementation root, else its\n // design would be empty and the tag is meaningless — fail loudly.\n if (choice.role === 'designed-lib' && graph.designs.length === 0) {\n reportDesignedLibMissingRoot(projectName);\n return { success: false };\n }\n\n writeDesignFiles(projectRootAbs, projectRoot, graph);\n\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 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 * Runs on every build (cache:false; the build gates on\n * validate-di-graph-unchanged which dependsOn this target). Unrecognized DI\n * patterns become \"unresolved\" nodes rather than failing the build.\n *\n * Config (webpieces.config.json, rule key `di-graph`): mode RUN_EVERY_TIME | OFF.\n *\n * Usage: nx run <project>:di-graph-generate\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadAndValidate } 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';\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 '@Controller(',\n '@ApiImplementation(',\n '@provideSingleton',\n '@provideTransient',\n '@injectable',\n 'new ContainerModule',\n '@inject(',\n '@Component(',\n 'bootstrapApplication',\n];\n\nconst ANGULAR_MARKERS = ['@Component(', 'bootstrapApplication'];\nconst CONTROLLER_MARKER = '@Controller(';\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(CONTROLLER_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\nfunction writeDesignFiles(projectRootAbs: string, projectRoot: string, graph: DiGraph): void {\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→@Controller, designed-lib→@ApiImplementation,\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\nfunction reportDesignedLibMissingRoot(projectName: string): void {\n console.error(\n `❌ ${projectName} is tagged role:designed-lib but has no @ApiImplementation class.\\n` +\n ` Annotate the library's top implementation class with @ApiImplementation ` +\n `(from @webpieces/http-routing), or retag the project role:lib if it has no design.`,\n );\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 — writing empty design graph');\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 — writing empty design graph');\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 // A designed-lib MUST expose at least one @ApiImplementation root, else its\n // design would be empty and the tag is meaningless — fail loudly.\n if (choice.role === 'designed-lib' && graph.designs.length === 0) {\n reportDesignedLibMissingRoot(projectName);\n return { success: false };\n }\n\n writeDesignFiles(projectRootAbs, projectRoot, graph);\n\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 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,16 +1,16 @@
1
1
  /**
2
2
  * DI Analyzer Strategy
3
3
  *
4
- * Picks which DI analyzer generates a project's design graph, driven by the
5
- * project's `framework` nx tag (see the framework/libType tagging PR):
4
+ * Picks which DI analyzer generates a project's design graph. The primary
5
+ * driver is the project's `role` nx tag; the `framework` env set (its libType)
6
+ * only distinguishes the client runtime by set membership:
6
7
  *
7
- * - `express` → {@link InversifyAnalyzer} (roots on `@Controller`)
8
- * - `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)
9
- * - anything else (`react`, `all`, ...) → {@link EmptyAnalyzer} (skip; the
10
- * controller-less library top-of-DAG behavior is deferred for v1)
8
+ * - env set includes `express` → {@link InversifyAnalyzer} (roots on `@Controller`)
9
+ * - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)
10
+ * - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)
11
11
  *
12
- * The explicit tag is the source of truth. When it is ABSENT (e.g. before the
13
- * tagging PR lands), a cheap marker pre-scan corroborates: `@Component(` /
12
+ * The explicit tags are the source of truth. When the role tag is ABSENT (e.g.
13
+ * before retag), a cheap marker pre-scan corroborates: `@Component(` /
14
14
  * `bootstrapApplication` → angular; `@Controller(` → express. This makes the
15
15
  * committed design.* identical whether selection is tag- or marker-driven.
16
16
  */
@@ -45,15 +45,18 @@ export declare class AngularAnalyzer implements DiAnalyzer {
45
45
  export declare class EmptyAnalyzer implements DiAnalyzer {
46
46
  analyzeProject(_program: ts.Program, _workspaceRoot: string, _projectRoot: string, projectName: string): DiGraph;
47
47
  }
48
- /** Extract the explicit `framework:<value>` nx tag, or null when the project has none. */
48
+ /** Extract the FIRST explicit `framework:<value>` nx tag, or null when the project has none. */
49
49
  export declare function explicitFrameworkTag(tags: readonly string[]): string | null;
50
+ /** Extract every explicit `framework:<value>` nx tag as an env set (the project's libType). */
51
+ export declare function frameworkTags(tags: readonly string[]): string[];
50
52
  /** Extract the explicit `role:<value>` nx tag, or null when the project has none. */
51
53
  export declare function explicitRoleTag(tags: readonly string[]): string | null;
52
54
  /**
53
55
  * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is
54
- * the source of truth for WHAT to root on; `framework` distinguishes the client
55
- * runtime (angular vs other); `markers` corroborate only when the role tag is
56
- * ABSENT (rollout fallback keeps pre-retag designs identical).
56
+ * the source of truth for WHAT to root on; `frameworks` is the project's env set
57
+ * (its libType) and distinguishes the client runtime (angular vs other) by set
58
+ * membership; `markers` corroborate only when the role tag is ABSENT (rollout
59
+ * fallback keeps pre-retag designs identical).
57
60
  *
58
61
  * - `server` → Inversify, roots on `@Controller`
59
62
  * - `designed-lib` → Inversify, roots on `@ApiImplementation`
@@ -61,4 +64,4 @@ export declare function explicitRoleTag(tags: readonly string[]): string | null;
61
64
  * - `lib` → skip (plain libraries get no design)
62
65
  * - role absent → legacy framework/marker selection
63
66
  */
64
- export declare function selectAnalyzer(role: string | null, framework: string | null, markers: FrameworkMarkers): DiAnalyzer;
67
+ export declare function selectAnalyzer(role: string | null, frameworks: string[], markers: FrameworkMarkers): DiAnalyzer;
@@ -2,22 +2,23 @@
2
2
  /**
3
3
  * DI Analyzer Strategy
4
4
  *
5
- * Picks which DI analyzer generates a project's design graph, driven by the
6
- * project's `framework` nx tag (see the framework/libType tagging PR):
5
+ * Picks which DI analyzer generates a project's design graph. The primary
6
+ * driver is the project's `role` nx tag; the `framework` env set (its libType)
7
+ * only distinguishes the client runtime by set membership:
7
8
  *
8
- * - `express` → {@link InversifyAnalyzer} (roots on `@Controller`)
9
- * - `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)
10
- * - anything else (`react`, `all`, ...) → {@link EmptyAnalyzer} (skip; the
11
- * controller-less library top-of-DAG behavior is deferred for v1)
9
+ * - env set includes `express` → {@link InversifyAnalyzer} (roots on `@Controller`)
10
+ * - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)
11
+ * - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)
12
12
  *
13
- * The explicit tag is the source of truth. When it is ABSENT (e.g. before the
14
- * tagging PR lands), a cheap marker pre-scan corroborates: `@Component(` /
13
+ * The explicit tags are the source of truth. When the role tag is ABSENT (e.g.
14
+ * before retag), a cheap marker pre-scan corroborates: `@Component(` /
15
15
  * `bootstrapApplication` → angular; `@Controller(` → express. This makes the
16
16
  * committed design.* identical whether selection is tag- or marker-driven.
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.EmptyAnalyzer = exports.AngularAnalyzer = exports.InversifyAnalyzer = exports.FrameworkMarkers = void 0;
20
20
  exports.explicitFrameworkTag = explicitFrameworkTag;
21
+ exports.frameworkTags = frameworkTags;
21
22
  exports.explicitRoleTag = explicitRoleTag;
22
23
  exports.selectAnalyzer = selectAnalyzer;
23
24
  const model_1 = require("./model");
@@ -75,19 +76,32 @@ function explicitTag(tags, prefix) {
75
76
  }
76
77
  return null;
77
78
  }
78
- /** Extract the explicit `framework:<value>` nx tag, or null when the project has none. */
79
+ /** Extract the FIRST explicit `framework:<value>` nx tag, or null when the project has none. */
79
80
  function explicitFrameworkTag(tags) {
80
81
  return explicitTag(tags, FRAMEWORK_TAG_PREFIX);
81
82
  }
83
+ /** Extract every explicit `framework:<value>` nx tag as an env set (the project's libType). */
84
+ function frameworkTags(tags) {
85
+ const values = [];
86
+ for (const tag of tags) {
87
+ if (tag.startsWith(FRAMEWORK_TAG_PREFIX)) {
88
+ const value = tag.slice(FRAMEWORK_TAG_PREFIX.length).trim();
89
+ if (value.length > 0)
90
+ values.push(value);
91
+ }
92
+ }
93
+ return values;
94
+ }
82
95
  /** Extract the explicit `role:<value>` nx tag, or null when the project has none. */
83
96
  function explicitRoleTag(tags) {
84
97
  return explicitTag(tags, ROLE_TAG_PREFIX);
85
98
  }
86
99
  /**
87
100
  * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is
88
- * the source of truth for WHAT to root on; `framework` distinguishes the client
89
- * runtime (angular vs other); `markers` corroborate only when the role tag is
90
- * ABSENT (rollout fallback keeps pre-retag designs identical).
101
+ * the source of truth for WHAT to root on; `frameworks` is the project's env set
102
+ * (its libType) and distinguishes the client runtime (angular vs other) by set
103
+ * membership; `markers` corroborate only when the role tag is ABSENT (rollout
104
+ * fallback keeps pre-retag designs identical).
91
105
  *
92
106
  * - `server` → Inversify, roots on `@Controller`
93
107
  * - `designed-lib` → Inversify, roots on `@ApiImplementation`
@@ -95,22 +109,22 @@ function explicitRoleTag(tags) {
95
109
  * - `lib` → skip (plain libraries get no design)
96
110
  * - role absent → legacy framework/marker selection
97
111
  */
98
- function selectAnalyzer(role, framework, markers) {
112
+ function selectAnalyzer(role, frameworks, markers) {
99
113
  if (role === 'server')
100
114
  return new InversifyAnalyzer('controller');
101
115
  if (role === 'designed-lib')
102
116
  return new InversifyAnalyzer('apiImplementation');
103
117
  if (role === 'client')
104
- return framework === 'angular' ? new AngularAnalyzer() : new EmptyAnalyzer();
118
+ return frameworks.includes('angular') ? new AngularAnalyzer() : new EmptyAnalyzer();
105
119
  if (role === 'lib')
106
120
  return new EmptyAnalyzer();
107
121
  // Role tag absent — fall back to the legacy framework/marker selection so
108
122
  // designs stay identical until a project is retagged.
109
- if (framework === 'express')
123
+ if (frameworks.includes('express'))
110
124
  return new InversifyAnalyzer('controller');
111
- if (framework === 'angular')
125
+ if (frameworks.includes('angular'))
112
126
  return new AngularAnalyzer();
113
- if (framework !== null)
127
+ if (frameworks.length > 0)
114
128
  return new EmptyAnalyzer();
115
129
  if (markers.angular)
116
130
  return new AngularAnalyzer();
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer-strategy.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/analyzer-strategy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Analyzer Strategy\n *\n * Picks which DI analyzer generates a project's design graph, driven by the\n * project's `framework` nx tag (see the framework/libType tagging PR):\n *\n * - `express` → {@link InversifyAnalyzer} (roots on `@Controller`)\n * - `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)\n * - anything else (`react`, `all`, ...) → {@link EmptyAnalyzer} (skip; the\n * controller-less library top-of-DAG behavior is deferred for v1)\n *\n * The explicit tag is the source of truth. When it is ABSENT (e.g. before the\n * tagging PR lands), a cheap marker pre-scan corroborates: `@Component(` /\n * `bootstrapApplication` → angular; `@Controller(` → express. This makes the\n * committed design.* identical whether selection is tag- or marker-driven.\n */\n\nimport type * as ts from 'typescript';\nimport { DiGraph } from './model';\nimport { buildDiGraph, DiRootMode } from './analyzer';\nimport { buildAngularDiGraph } from './angular-analyzer';\n\nconst FRAMEWORK_TAG_PREFIX = 'framework:';\nconst ROLE_TAG_PREFIX = 'role:';\n\n/** Which framework a source tree's decorators point at (marker pre-scan fallback). */\nexport class FrameworkMarkers {\n angular: boolean;\n controller: boolean;\n\n constructor(angular: boolean, controller: boolean) {\n this.angular = angular;\n this.controller = controller;\n }\n}\n\n/** Statically analyzes one project's DI dependency DAG into a `DiGraph`. */\nexport interface DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;\n}\n\n/**\n * Inversify analyzer: one design per root decorator.\n * - `'controller'` (server projects) roots on `@Controller`.\n * - `'apiImplementation'` (role:designed-lib) roots on `@ApiImplementation`.\n */\nexport class InversifyAnalyzer implements DiAnalyzer {\n private readonly rootMode: DiRootMode;\n\n constructor(rootMode: DiRootMode = 'controller') {\n this.rootMode = rootMode;\n }\n\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildDiGraph(program, workspaceRoot, projectRoot, projectName, false, this.rootMode);\n }\n}\n\n/** Angular: one design per entry component (bootstrap + routed). */\nexport class AngularAnalyzer implements DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildAngularDiGraph(program, workspaceRoot, projectRoot, projectName);\n }\n}\n\n/** Non-angular/non-express projects: an empty graph (skip). */\nexport class EmptyAnalyzer implements DiAnalyzer {\n analyzeProject(\n _program: ts.Program,\n _workspaceRoot: string,\n _projectRoot: string,\n projectName: string,\n ): DiGraph {\n return new DiGraph(projectName);\n }\n}\n\n/** Extract the explicit `<prefix><value>` nx tag, or null when the project has none. */\nfunction explicitTag(tags: readonly string[], prefix: string): string | null {\n for (const tag of tags) {\n if (tag.startsWith(prefix)) {\n const value = tag.slice(prefix.length).trim();\n if (value.length > 0) return value;\n }\n }\n return null;\n}\n\n/** Extract the explicit `framework:<value>` nx tag, or null when the project has none. */\nexport function explicitFrameworkTag(tags: readonly string[]): string | null {\n return explicitTag(tags, FRAMEWORK_TAG_PREFIX);\n}\n\n/** Extract the explicit `role:<value>` nx tag, or null when the project has none. */\nexport function explicitRoleTag(tags: readonly string[]): string | null {\n return explicitTag(tags, ROLE_TAG_PREFIX);\n}\n\n/**\n * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is\n * the source of truth for WHAT to root on; `framework` distinguishes the client\n * runtime (angular vs other); `markers` corroborate only when the role tag is\n * ABSENT (rollout fallback keeps pre-retag designs identical).\n *\n * - `server` → Inversify, roots on `@Controller`\n * - `designed-lib` → Inversify, roots on `@ApiImplementation`\n * - `client` → Angular design for angular apps; otherwise skip\n * - `lib` → skip (plain libraries get no design)\n * - role absent → legacy framework/marker selection\n */\nexport function selectAnalyzer(\n role: string | null,\n framework: string | null,\n markers: FrameworkMarkers,\n): DiAnalyzer {\n if (role === 'server') return new InversifyAnalyzer('controller');\n if (role === 'designed-lib') return new InversifyAnalyzer('apiImplementation');\n if (role === 'client') return framework === 'angular' ? new AngularAnalyzer() : new EmptyAnalyzer();\n if (role === 'lib') return new EmptyAnalyzer();\n\n // Role tag absent — fall back to the legacy framework/marker selection so\n // designs stay identical until a project is retagged.\n if (framework === 'express') return new InversifyAnalyzer('controller');\n if (framework === 'angular') return new AngularAnalyzer();\n if (framework !== null) return new EmptyAnalyzer();\n if (markers.angular) return new AngularAnalyzer();\n if (markers.controller) return new InversifyAnalyzer('controller');\n return new EmptyAnalyzer();\n}\n"]}
1
+ {"version":3,"file":"analyzer-strategy.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/lib/di-graph/analyzer-strategy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DI Analyzer Strategy\n *\n * Picks which DI analyzer generates a project's design graph. The primary\n * driver is the project's `role` nx tag; the `framework` env set (its libType)\n * only distinguishes the client runtime by set membership:\n *\n * - env set includes `express` → {@link InversifyAnalyzer} (roots on `@Controller`)\n * - env set includes `angular` → {@link AngularAnalyzer} (roots on the bootstrap/route components)\n * - anything else (`react`, `browser`, `node`, ...) → {@link EmptyAnalyzer} (skip)\n *\n * The explicit tags are the source of truth. When the role tag is ABSENT (e.g.\n * before retag), a cheap marker pre-scan corroborates: `@Component(` /\n * `bootstrapApplication` → angular; `@Controller(` → express. This makes the\n * committed design.* identical whether selection is tag- or marker-driven.\n */\n\nimport type * as ts from 'typescript';\nimport { DiGraph } from './model';\nimport { buildDiGraph, DiRootMode } from './analyzer';\nimport { buildAngularDiGraph } from './angular-analyzer';\n\nconst FRAMEWORK_TAG_PREFIX = 'framework:';\nconst ROLE_TAG_PREFIX = 'role:';\n\n/** Which framework a source tree's decorators point at (marker pre-scan fallback). */\nexport class FrameworkMarkers {\n angular: boolean;\n controller: boolean;\n\n constructor(angular: boolean, controller: boolean) {\n this.angular = angular;\n this.controller = controller;\n }\n}\n\n/** Statically analyzes one project's DI dependency DAG into a `DiGraph`. */\nexport interface DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph;\n}\n\n/**\n * Inversify analyzer: one design per root decorator.\n * - `'controller'` (server projects) roots on `@Controller`.\n * - `'apiImplementation'` (role:designed-lib) roots on `@ApiImplementation`.\n */\nexport class InversifyAnalyzer implements DiAnalyzer {\n private readonly rootMode: DiRootMode;\n\n constructor(rootMode: DiRootMode = 'controller') {\n this.rootMode = rootMode;\n }\n\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildDiGraph(program, workspaceRoot, projectRoot, projectName, false, this.rootMode);\n }\n}\n\n/** Angular: one design per entry component (bootstrap + routed). */\nexport class AngularAnalyzer implements DiAnalyzer {\n analyzeProject(program: ts.Program, workspaceRoot: string, projectRoot: string, projectName: string): DiGraph {\n return buildAngularDiGraph(program, workspaceRoot, projectRoot, projectName);\n }\n}\n\n/** Non-angular/non-express projects: an empty graph (skip). */\nexport class EmptyAnalyzer implements DiAnalyzer {\n analyzeProject(\n _program: ts.Program,\n _workspaceRoot: string,\n _projectRoot: string,\n projectName: string,\n ): DiGraph {\n return new DiGraph(projectName);\n }\n}\n\n/** Extract the explicit `<prefix><value>` nx tag, or null when the project has none. */\nfunction explicitTag(tags: readonly string[], prefix: string): string | null {\n for (const tag of tags) {\n if (tag.startsWith(prefix)) {\n const value = tag.slice(prefix.length).trim();\n if (value.length > 0) return value;\n }\n }\n return null;\n}\n\n/** Extract the FIRST explicit `framework:<value>` nx tag, or null when the project has none. */\nexport function explicitFrameworkTag(tags: readonly string[]): string | null {\n return explicitTag(tags, FRAMEWORK_TAG_PREFIX);\n}\n\n/** Extract every explicit `framework:<value>` nx tag as an env set (the project's libType). */\nexport function frameworkTags(tags: readonly string[]): string[] {\n const values: string[] = [];\n for (const tag of tags) {\n if (tag.startsWith(FRAMEWORK_TAG_PREFIX)) {\n const value = tag.slice(FRAMEWORK_TAG_PREFIX.length).trim();\n if (value.length > 0) values.push(value);\n }\n }\n return values;\n}\n\n/** Extract the explicit `role:<value>` nx tag, or null when the project has none. */\nexport function explicitRoleTag(tags: readonly string[]): string | null {\n return explicitTag(tags, ROLE_TAG_PREFIX);\n}\n\n/**\n * Choose the analyzer for a project. `role` (server|designed-lib|lib|client) is\n * the source of truth for WHAT to root on; `frameworks` is the project's env set\n * (its libType) and distinguishes the client runtime (angular vs other) by set\n * membership; `markers` corroborate only when the role tag is ABSENT (rollout\n * fallback keeps pre-retag designs identical).\n *\n * - `server` → Inversify, roots on `@Controller`\n * - `designed-lib` → Inversify, roots on `@ApiImplementation`\n * - `client` → Angular design for angular apps; otherwise skip\n * - `lib` → skip (plain libraries get no design)\n * - role absent → legacy framework/marker selection\n */\nexport function selectAnalyzer(\n role: string | null,\n frameworks: string[],\n markers: FrameworkMarkers,\n): DiAnalyzer {\n if (role === 'server') return new InversifyAnalyzer('controller');\n if (role === 'designed-lib') return new InversifyAnalyzer('apiImplementation');\n if (role === 'client') return frameworks.includes('angular') ? new AngularAnalyzer() : new EmptyAnalyzer();\n if (role === 'lib') return new EmptyAnalyzer();\n\n // Role tag absent — fall back to the legacy framework/marker selection so\n // designs stay identical until a project is retagged.\n if (frameworks.includes('express')) return new InversifyAnalyzer('controller');\n if (frameworks.includes('angular')) return new AngularAnalyzer();\n if (frameworks.length > 0) return new EmptyAnalyzer();\n if (markers.angular) return new AngularAnalyzer();\n if (markers.controller) return new InversifyAnalyzer('controller');\n return new EmptyAnalyzer();\n}\n"]}
@@ -1,29 +1,39 @@
1
1
  /**
2
2
  * Framework Resolver
3
3
  *
4
- * Determines the `framework` field (a project's "libType" — which client side
5
- * it targets) written per project into architecture/dependencies.json. Known
6
- * values: angular | react | express | all ("all" = a library usable by any
7
- * side). This is the field the `library-types-match-client` rule reads to keep
8
- * an express project from depending on an angular-only lib (and vice-versa).
4
+ * Determines the `framework` field (a project's "libType" — the SET of runtime
5
+ * environments it is validated to run in) written per project into
6
+ * architecture/dependencies.json. A project carries a MULTI-VALUE env set drawn
7
+ * from the atomic values:
8
+ *
9
+ * browser · react · angular · node · express
10
+ *
11
+ * A project lists every environment it promises to run in, e.g.
12
+ * `framework:browser` + `framework:node`. This is the field the
13
+ * `library-types-match-client` rule reads (via the up-set lattice) to keep an
14
+ * express project from depending on a browser-only lib (and vice-versa). The
15
+ * legacy single-value `all` "usable by any side" bucket is REMOVED — a project
16
+ * must declare its actual env set.
9
17
  *
10
18
  * Resolution order:
11
- * 1. Explicit nx tag `framework:<value>` on the project (project.json tags) —
12
- * any value is allowed so new frameworks need no code change here. This is
13
- * the source of truth; every project should carry one.
14
- * 2. Inference from the project's package.json dependencies.
15
- * 3. Fallback: 'all' (plain TypeScript library usable by any side).
19
+ * 1. Explicit nx tags `framework:<value>` on the project (project.json tags) —
20
+ * any value is allowed so new frameworks need no code change here, and MANY
21
+ * are allowed (the env set). This is the source of truth; every project
22
+ * should carry at least one.
23
+ * 2. Inference from the project's package.json dependencies (a single-element
24
+ * set fallback).
25
+ * 3. FAILURE: no tag and nothing inferable is a problem, never a silent 'all'.
16
26
  */
17
27
  import { ProjectInfo } from './project-info';
18
28
  export declare const FRAMEWORK_TAG_PREFIX = "framework:";
19
29
  export declare class FrameworkResolution {
20
- /** Resolved framework name, or null when resolution failed */
21
- readonly framework: string | null;
30
+ /** Resolved env set (never empty), or null when resolution failed. */
31
+ readonly frameworks: string[] | null;
22
32
  /** Problem description when resolution failed, otherwise null */
23
33
  readonly problem: string | null;
24
34
  constructor(
25
- /** Resolved framework name, or null when resolution failed */
26
- framework: string | null,
35
+ /** Resolved env set (never empty), or null when resolution failed. */
36
+ frameworks: string[] | null,
27
37
  /** Problem description when resolution failed, otherwise null */
28
38
  problem: string | null);
29
39
  }
@@ -2,18 +2,28 @@
2
2
  /**
3
3
  * Framework Resolver
4
4
  *
5
- * Determines the `framework` field (a project's "libType" — which client side
6
- * it targets) written per project into architecture/dependencies.json. Known
7
- * values: angular | react | express | all ("all" = a library usable by any
8
- * side). This is the field the `library-types-match-client` rule reads to keep
9
- * an express project from depending on an angular-only lib (and vice-versa).
5
+ * Determines the `framework` field (a project's "libType" — the SET of runtime
6
+ * environments it is validated to run in) written per project into
7
+ * architecture/dependencies.json. A project carries a MULTI-VALUE env set drawn
8
+ * from the atomic values:
9
+ *
10
+ * browser · react · angular · node · express
11
+ *
12
+ * A project lists every environment it promises to run in, e.g.
13
+ * `framework:browser` + `framework:node`. This is the field the
14
+ * `library-types-match-client` rule reads (via the up-set lattice) to keep an
15
+ * express project from depending on a browser-only lib (and vice-versa). The
16
+ * legacy single-value `all` "usable by any side" bucket is REMOVED — a project
17
+ * must declare its actual env set.
10
18
  *
11
19
  * Resolution order:
12
- * 1. Explicit nx tag `framework:<value>` on the project (project.json tags) —
13
- * any value is allowed so new frameworks need no code change here. This is
14
- * the source of truth; every project should carry one.
15
- * 2. Inference from the project's package.json dependencies.
16
- * 3. Fallback: 'all' (plain TypeScript library usable by any side).
20
+ * 1. Explicit nx tags `framework:<value>` on the project (project.json tags) —
21
+ * any value is allowed so new frameworks need no code change here, and MANY
22
+ * are allowed (the env set). This is the source of truth; every project
23
+ * should carry at least one.
24
+ * 2. Inference from the project's package.json dependencies (a single-element
25
+ * set fallback).
26
+ * 3. FAILURE: no tag and nothing inferable is a problem, never a silent 'all'.
17
27
  */
18
28
  Object.defineProperty(exports, "__esModule", { value: true });
19
29
  exports.FrameworkResolution = exports.FRAMEWORK_TAG_PREFIX = void 0;
@@ -43,14 +53,14 @@ const FRAMEWORK_DEPENDENCY_MARKERS = [
43
53
  new FrameworkMarker('express', 'express'),
44
54
  ];
45
55
  class FrameworkResolution {
46
- framework;
56
+ frameworks;
47
57
  problem;
48
58
  constructor(
49
- /** Resolved framework name, or null when resolution failed */
50
- framework,
59
+ /** Resolved env set (never empty), or null when resolution failed. */
60
+ frameworks,
51
61
  /** Problem description when resolution failed, otherwise null */
52
62
  problem) {
53
- this.framework = framework;
63
+ this.frameworks = frameworks;
54
64
  this.problem = problem;
55
65
  }
56
66
  }
@@ -59,21 +69,24 @@ function resolveFramework(info, workspaceRoot) {
59
69
  const tagValues = info.tags
60
70
  .filter((tag) => tag.startsWith(exports.FRAMEWORK_TAG_PREFIX))
61
71
  .map((tag) => tag.slice(exports.FRAMEWORK_TAG_PREFIX.length).trim());
62
- if (tagValues.length > 1) {
63
- return new FrameworkResolution(null, `${info.name}: has ${tagValues.length} 'framework:' tags (${tagValues.join(', ')}) — a project must have at most one`);
64
- }
65
- if (tagValues.length === 1) {
66
- if (tagValues[0].length === 0) {
72
+ if (tagValues.length >= 1) {
73
+ if (tagValues.some((value) => value.length === 0)) {
67
74
  return new FrameworkResolution(null, `${info.name}: 'framework:' tag has an empty value`);
68
75
  }
69
- return new FrameworkResolution(tagValues[0], null);
76
+ // Preserve declaration order, de-duplicating repeated envs.
77
+ return new FrameworkResolution(Array.from(new Set(tagValues)), null);
78
+ }
79
+ const inferred = inferFromPackageJson(info, workspaceRoot);
80
+ if (inferred === null) {
81
+ return new FrameworkResolution(null, `${info.name}: no 'framework:' tag and no framework inferable from package.json — declare the ` +
82
+ `env set this project runs in, e.g. framework:browser + framework:node`);
70
83
  }
71
- return new FrameworkResolution(inferFromPackageJson(info, workspaceRoot), null);
84
+ return new FrameworkResolution([inferred], null);
72
85
  }
73
86
  function inferFromPackageJson(info, workspaceRoot) {
74
87
  const pkgJsonPath = path.join(workspaceRoot, info.root, 'package.json');
75
88
  if (!fs.existsSync(pkgJsonPath)) {
76
- return 'all';
89
+ return null;
77
90
  }
78
91
  let allDeps;
79
92
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
@@ -96,6 +109,6 @@ function inferFromPackageJson(info, workspaceRoot) {
96
109
  return marker.framework;
97
110
  }
98
111
  }
99
- return 'all';
112
+ return null;
100
113
  }
101
114
  //# sourceMappingURL=framework-resolver.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"framework-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/framework-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Framework Resolver\n *\n * Determines the `framework` field (a project's \"libType\" — which client side\n * it targets) written per project into architecture/dependencies.json. Known\n * values: angular | react | express | all (\"all\" = a library usable by any\n * side). This is the field the `library-types-match-client` rule reads to keep\n * an express project from depending on an angular-only lib (and vice-versa).\n *\n * Resolution order:\n * 1. Explicit nx tag `framework:<value>` on the project (project.json tags) —\n * any value is allowed so new frameworks need no code change here. This is\n * the source of truth; every project should carry one.\n * 2. Inference from the project's package.json dependencies.\n * 3. Fallback: 'all' (plain TypeScript library usable by any side).\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\nexport const FRAMEWORK_TAG_PREFIX = 'framework:';\n\n/**\n * A package.json dependency name that identifies a framework.\n */\nclass FrameworkMarker {\n constructor(\n public readonly dependency: string,\n public readonly framework: string\n ) {}\n}\n\n/**\n * Dependency name → framework, checked in order (first match wins).\n */\nconst FRAMEWORK_DEPENDENCY_MARKERS: ReadonlyArray<FrameworkMarker> = [\n new FrameworkMarker('@angular/core', 'angular'),\n new FrameworkMarker('react', 'react'),\n new FrameworkMarker('express', 'express'),\n];\n\nexport class FrameworkResolution {\n constructor(\n /** Resolved framework name, or null when resolution failed */\n public readonly framework: string | null,\n /** Problem description when resolution failed, otherwise null */\n public readonly problem: string | null\n ) {}\n}\n\nexport function resolveFramework(info: ProjectInfo, workspaceRoot: string): FrameworkResolution {\n const tagValues = info.tags\n .filter((tag: string) => tag.startsWith(FRAMEWORK_TAG_PREFIX))\n .map((tag: string) => tag.slice(FRAMEWORK_TAG_PREFIX.length).trim());\n\n if (tagValues.length > 1) {\n return new FrameworkResolution(\n null,\n `${info.name}: has ${tagValues.length} 'framework:' tags (${tagValues.join(', ')}) a project must have at most one`\n );\n }\n if (tagValues.length === 1) {\n if (tagValues[0].length === 0) {\n return new FrameworkResolution(null, `${info.name}: 'framework:' tag has an empty value`);\n }\n return new FrameworkResolution(tagValues[0], null);\n }\n\n return new FrameworkResolution(inferFromPackageJson(info, workspaceRoot), null);\n}\n\nfunction inferFromPackageJson(info: ProjectInfo, workspaceRoot: string): string {\n const pkgJsonPath = path.join(workspaceRoot, info.root, 'package.json');\n if (!fs.existsSync(pkgJsonPath)) {\n return 'all';\n }\n\n let allDeps: Record<string, string>;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n allDeps = {\n ...(pkgJson.dependencies ?? {}),\n ...(pkgJson.devDependencies ?? {}),\n ...(pkgJson.peerDependencies ?? {}),\n };\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to parse ${pkgJsonPath} while inferring framework for ${info.name}`, {\n cause: error,\n });\n }\n\n for (const marker of FRAMEWORK_DEPENDENCY_MARKERS) {\n if (marker.dependency in allDeps) {\n return marker.framework;\n }\n }\n return 'all';\n}\n"]}
1
+ {"version":3,"file":"framework-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/framework-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Framework Resolver\n *\n * Determines the `framework` field (a project's \"libType\" — the SET of runtime\n * environments it is validated to run in) written per project into\n * architecture/dependencies.json. A project carries a MULTI-VALUE env set drawn\n * from the atomic values:\n *\n * browser · react · angular · node · express\n *\n * A project lists every environment it promises to run in, e.g.\n * `framework:browser` + `framework:node`. This is the field the\n * `library-types-match-client` rule reads (via the up-set lattice) to keep an\n * express project from depending on a browser-only lib (and vice-versa). The\n * legacy single-value `all` \"usable by any side\" bucket is REMOVED — a project\n * must declare its actual env set.\n *\n * Resolution order:\n * 1. Explicit nx tags `framework:<value>` on the project (project.json tags) —\n * any value is allowed so new frameworks need no code change here, and MANY\n * are allowed (the env set). This is the source of truth; every project\n * should carry at least one.\n * 2. Inference from the project's package.json dependencies (a single-element\n * set fallback).\n * 3. FAILURE: no tag and nothing inferable is a problem, never a silent 'all'.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\nexport const FRAMEWORK_TAG_PREFIX = 'framework:';\n\n/**\n * A package.json dependency name that identifies a framework.\n */\nclass FrameworkMarker {\n constructor(\n public readonly dependency: string,\n public readonly framework: string\n ) {}\n}\n\n/**\n * Dependency name → framework, checked in order (first match wins).\n */\nconst FRAMEWORK_DEPENDENCY_MARKERS: ReadonlyArray<FrameworkMarker> = [\n new FrameworkMarker('@angular/core', 'angular'),\n new FrameworkMarker('react', 'react'),\n new FrameworkMarker('express', 'express'),\n];\n\nexport class FrameworkResolution {\n constructor(\n /** Resolved env set (never empty), or null when resolution failed. */\n public readonly frameworks: string[] | null,\n /** Problem description when resolution failed, otherwise null */\n public readonly problem: string | null\n ) {}\n}\n\nexport function resolveFramework(info: ProjectInfo, workspaceRoot: string): FrameworkResolution {\n const tagValues = info.tags\n .filter((tag: string) => tag.startsWith(FRAMEWORK_TAG_PREFIX))\n .map((tag: string) => tag.slice(FRAMEWORK_TAG_PREFIX.length).trim());\n\n if (tagValues.length >= 1) {\n if (tagValues.some((value: string) => value.length === 0)) {\n return new FrameworkResolution(null, `${info.name}: 'framework:' tag has an empty value`);\n }\n // Preserve declaration order, de-duplicating repeated envs.\n return new FrameworkResolution(Array.from(new Set(tagValues)), null);\n }\n\n const inferred = inferFromPackageJson(info, workspaceRoot);\n if (inferred === null) {\n return new FrameworkResolution(\n null,\n `${info.name}: no 'framework:' tag and no framework inferable from package.json — declare the ` +\n `env set this project runs in, e.g. framework:browser + framework:node`\n );\n }\n return new FrameworkResolution([inferred], null);\n}\n\nfunction inferFromPackageJson(info: ProjectInfo, workspaceRoot: string): string | null {\n const pkgJsonPath = path.join(workspaceRoot, info.root, 'package.json');\n if (!fs.existsSync(pkgJsonPath)) {\n return null;\n }\n\n let allDeps: Record<string, string>;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));\n allDeps = {\n ...(pkgJson.dependencies ?? {}),\n ...(pkgJson.devDependencies ?? {}),\n ...(pkgJson.peerDependencies ?? {}),\n };\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`Failed to parse ${pkgJsonPath} while inferring framework for ${info.name}`, {\n cause: error,\n });\n }\n\n for (const marker of FRAMEWORK_DEPENDENCY_MARKERS) {\n if (marker.dependency in allDeps) {\n return marker.framework;\n }\n }\n return null;\n}\n"]}
@@ -88,11 +88,21 @@ function findModifiedProjects(current, saved, currentProjects, savedProjects, di
88
88
  }
89
89
  }
90
90
  }
91
+ /**
92
+ * Normalize a metadata field value to a comparable/displayable string. The
93
+ * `framework` field is a string[] env set (compared by value, joined for
94
+ * display); every other field is already a plain string.
95
+ */
96
+ function normalizeFieldValue(value) {
97
+ if (value === undefined)
98
+ return undefined;
99
+ return Array.isArray(value) ? value.join(', ') : value;
100
+ }
91
101
  function findChangedFields(currentEntry, savedEntry) {
92
102
  const changes = [];
93
103
  for (const field of METADATA_FIELDS) {
94
- const from = savedEntry[field];
95
- const to = currentEntry[field];
104
+ const from = normalizeFieldValue(savedEntry[field]);
105
+ const to = normalizeFieldValue(currentEntry[field]);
96
106
  if (from !== to) {
97
107
  changes.push({ field, from, to });
98
108
  }
@@ -1 +1 @@
1
- {"version":3,"file":"graph-comparator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-comparator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Comparator\n *\n * Compares the current generated graph with the saved (blessed) graph.\n * Used in validate mode to ensure developers have updated the graph file.\n */\n\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\n\n/**\n * A changed metadata field on a project (framework, shortDescription, ...)\n */\nexport interface FieldChange {\n field: string;\n from: string | undefined;\n to: string | undefined;\n}\n\n/**\n * Difference between two graphs\n */\nexport interface GraphDiff {\n added: string[];\n removed: string[];\n modified: {\n project: string;\n addedDeps: string[];\n removedDeps: string[];\n levelChanged: { from: number; to: number } | null;\n changedFields: FieldChange[];\n }[];\n}\n\n/**\n * Metadata fields compared per project (beyond level + dependsOn)\n */\nconst METADATA_FIELDS: ReadonlyArray<keyof GraphEntry & string> = [\n 'framework',\n 'shortDescription',\n 'responsibilitiesFile',\n 'designFile',\n];\n\n/**\n * Comparison result\n */\nexport interface ComparisonResult {\n identical: boolean;\n diff: GraphDiff;\n summary: string;\n}\n\n/**\n * Compare two graphs and return the differences\n *\n * @param current - Currently generated graph\n * @param saved - Previously saved (blessed) graph\n * @returns Comparison result with detailed diff\n */\nexport function compareGraphs(current: EnhancedGraph, saved: EnhancedGraph): ComparisonResult {\n const currentProjects = new Set(Object.keys(current));\n const savedProjects = new Set(Object.keys(saved));\n\n const diff: GraphDiff = {\n added: [],\n removed: [],\n modified: [],\n };\n\n // Find added projects\n for (const project of currentProjects) {\n if (!savedProjects.has(project)) {\n diff.added.push(project);\n }\n }\n\n // Find removed projects\n for (const project of savedProjects) {\n if (!currentProjects.has(project)) {\n diff.removed.push(project);\n }\n }\n\n // Find modified projects\n findModifiedProjects(current, saved, currentProjects, savedProjects, diff);\n\n const identical =\n diff.added.length === 0 && diff.removed.length === 0 && diff.modified.length === 0;\n\n const summary = identical ? 'Graphs are identical' : buildSummary(diff);\n\n return {\n identical,\n diff,\n summary,\n };\n}\n\nfunction findModifiedProjects(\n current: EnhancedGraph,\n saved: EnhancedGraph,\n currentProjects: Set<string>,\n savedProjects: Set<string>,\n diff: GraphDiff\n): void {\n for (const project of currentProjects) {\n if (!savedProjects.has(project)) continue;\n\n const currentEntry = current[project];\n const savedEntry = saved[project];\n\n const currentDeps = new Set(currentEntry.dependsOn);\n const savedDeps = new Set(savedEntry.dependsOn);\n\n const addedDeps: string[] = [];\n const removedDeps: string[] = [];\n\n for (const dep of currentDeps) {\n if (!savedDeps.has(dep)) {\n addedDeps.push(dep);\n }\n }\n\n for (const dep of savedDeps) {\n if (!currentDeps.has(dep)) {\n removedDeps.push(dep);\n }\n }\n\n const levelChanged =\n currentEntry.level !== savedEntry.level\n ? { from: savedEntry.level, to: currentEntry.level }\n : null;\n\n const changedFields = findChangedFields(currentEntry, savedEntry);\n\n if (addedDeps.length > 0 || removedDeps.length > 0 || levelChanged || changedFields.length > 0) {\n diff.modified.push({\n project,\n addedDeps,\n removedDeps,\n levelChanged,\n changedFields,\n });\n }\n }\n}\n\nfunction findChangedFields(currentEntry: GraphEntry, savedEntry: GraphEntry): FieldChange[] {\n const changes: FieldChange[] = [];\n for (const field of METADATA_FIELDS) {\n const from = savedEntry[field] as string | undefined;\n const to = currentEntry[field] as string | undefined;\n if (from !== to) {\n changes.push({ field, from, to });\n }\n }\n return changes;\n}\n\nfunction formatFieldValue(value: string | undefined): string {\n if (value === undefined) return '(none)';\n const MAX_SUMMARY_VALUE_CHARS = 60;\n return value.length > MAX_SUMMARY_VALUE_CHARS\n ? `\"${value.slice(0, MAX_SUMMARY_VALUE_CHARS)}...\"`\n : `\"${value}\"`;\n}\n\nfunction buildSummary(diff: GraphDiff): string {\n const summaryParts: string[] = [];\n\n if (diff.added.length > 0) {\n summaryParts.push(`Added projects: ${diff.added.join(', ')}`);\n }\n\n if (diff.removed.length > 0) {\n summaryParts.push(`Removed projects: ${diff.removed.join(', ')}`);\n }\n\n for (const mod of diff.modified) {\n const parts: string[] = [];\n if (mod.addedDeps.length > 0) {\n parts.push(`+deps: ${mod.addedDeps.join(', ')}`);\n }\n if (mod.removedDeps.length > 0) {\n parts.push(`-deps: ${mod.removedDeps.join(', ')}`);\n }\n if (mod.levelChanged) {\n parts.push(`level: ${mod.levelChanged.from} -> ${mod.levelChanged.to}`);\n }\n for (const change of mod.changedFields) {\n parts.push(\n `${change.field}: ${formatFieldValue(change.from)} -> ${formatFieldValue(change.to)}`\n );\n }\n if (parts.length > 0) {\n summaryParts.push(`${mod.project}: ${parts.join('; ')}`);\n }\n }\n\n return summaryParts.join('\\n');\n}\n"]}
1
+ {"version":3,"file":"graph-comparator.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-comparator.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Graph Comparator\n *\n * Compares the current generated graph with the saved (blessed) graph.\n * Used in validate mode to ensure developers have updated the graph file.\n */\n\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\n\n/**\n * A changed metadata field on a project (framework, shortDescription, ...)\n */\nexport interface FieldChange {\n field: string;\n from: string | undefined;\n to: string | undefined;\n}\n\n/**\n * Difference between two graphs\n */\nexport interface GraphDiff {\n added: string[];\n removed: string[];\n modified: {\n project: string;\n addedDeps: string[];\n removedDeps: string[];\n levelChanged: { from: number; to: number } | null;\n changedFields: FieldChange[];\n }[];\n}\n\n/**\n * Metadata fields compared per project (beyond level + dependsOn)\n */\nconst METADATA_FIELDS: ReadonlyArray<keyof GraphEntry & string> = [\n 'framework',\n 'shortDescription',\n 'responsibilitiesFile',\n 'designFile',\n];\n\n/**\n * Comparison result\n */\nexport interface ComparisonResult {\n identical: boolean;\n diff: GraphDiff;\n summary: string;\n}\n\n/**\n * Compare two graphs and return the differences\n *\n * @param current - Currently generated graph\n * @param saved - Previously saved (blessed) graph\n * @returns Comparison result with detailed diff\n */\nexport function compareGraphs(current: EnhancedGraph, saved: EnhancedGraph): ComparisonResult {\n const currentProjects = new Set(Object.keys(current));\n const savedProjects = new Set(Object.keys(saved));\n\n const diff: GraphDiff = {\n added: [],\n removed: [],\n modified: [],\n };\n\n // Find added projects\n for (const project of currentProjects) {\n if (!savedProjects.has(project)) {\n diff.added.push(project);\n }\n }\n\n // Find removed projects\n for (const project of savedProjects) {\n if (!currentProjects.has(project)) {\n diff.removed.push(project);\n }\n }\n\n // Find modified projects\n findModifiedProjects(current, saved, currentProjects, savedProjects, diff);\n\n const identical =\n diff.added.length === 0 && diff.removed.length === 0 && diff.modified.length === 0;\n\n const summary = identical ? 'Graphs are identical' : buildSummary(diff);\n\n return {\n identical,\n diff,\n summary,\n };\n}\n\nfunction findModifiedProjects(\n current: EnhancedGraph,\n saved: EnhancedGraph,\n currentProjects: Set<string>,\n savedProjects: Set<string>,\n diff: GraphDiff\n): void {\n for (const project of currentProjects) {\n if (!savedProjects.has(project)) continue;\n\n const currentEntry = current[project];\n const savedEntry = saved[project];\n\n const currentDeps = new Set(currentEntry.dependsOn);\n const savedDeps = new Set(savedEntry.dependsOn);\n\n const addedDeps: string[] = [];\n const removedDeps: string[] = [];\n\n for (const dep of currentDeps) {\n if (!savedDeps.has(dep)) {\n addedDeps.push(dep);\n }\n }\n\n for (const dep of savedDeps) {\n if (!currentDeps.has(dep)) {\n removedDeps.push(dep);\n }\n }\n\n const levelChanged =\n currentEntry.level !== savedEntry.level\n ? { from: savedEntry.level, to: currentEntry.level }\n : null;\n\n const changedFields = findChangedFields(currentEntry, savedEntry);\n\n if (addedDeps.length > 0 || removedDeps.length > 0 || levelChanged || changedFields.length > 0) {\n diff.modified.push({\n project,\n addedDeps,\n removedDeps,\n levelChanged,\n changedFields,\n });\n }\n }\n}\n\n/**\n * Normalize a metadata field value to a comparable/displayable string. The\n * `framework` field is a string[] env set (compared by value, joined for\n * display); every other field is already a plain string.\n */\nfunction normalizeFieldValue(value: string | string[] | undefined): string | undefined {\n if (value === undefined) return undefined;\n return Array.isArray(value) ? value.join(', ') : value;\n}\n\nfunction findChangedFields(currentEntry: GraphEntry, savedEntry: GraphEntry): FieldChange[] {\n const changes: FieldChange[] = [];\n for (const field of METADATA_FIELDS) {\n const from = normalizeFieldValue(savedEntry[field] as string | string[] | undefined);\n const to = normalizeFieldValue(currentEntry[field] as string | string[] | undefined);\n if (from !== to) {\n changes.push({ field, from, to });\n }\n }\n return changes;\n}\n\nfunction formatFieldValue(value: string | undefined): string {\n if (value === undefined) return '(none)';\n const MAX_SUMMARY_VALUE_CHARS = 60;\n return value.length > MAX_SUMMARY_VALUE_CHARS\n ? `\"${value.slice(0, MAX_SUMMARY_VALUE_CHARS)}...\"`\n : `\"${value}\"`;\n}\n\nfunction buildSummary(diff: GraphDiff): string {\n const summaryParts: string[] = [];\n\n if (diff.added.length > 0) {\n summaryParts.push(`Added projects: ${diff.added.join(', ')}`);\n }\n\n if (diff.removed.length > 0) {\n summaryParts.push(`Removed projects: ${diff.removed.join(', ')}`);\n }\n\n for (const mod of diff.modified) {\n const parts: string[] = [];\n if (mod.addedDeps.length > 0) {\n parts.push(`+deps: ${mod.addedDeps.join(', ')}`);\n }\n if (mod.removedDeps.length > 0) {\n parts.push(`-deps: ${mod.removedDeps.join(', ')}`);\n }\n if (mod.levelChanged) {\n parts.push(`level: ${mod.levelChanged.from} -> ${mod.levelChanged.to}`);\n }\n for (const change of mod.changedFields) {\n parts.push(\n `${change.field}: ${formatFieldValue(change.from)} -> ${formatFieldValue(change.to)}`\n );\n }\n if (parts.length > 0) {\n summaryParts.push(`${mod.project}: ${parts.join('; ')}`);\n }\n }\n\n return summaryParts.join('\\n');\n}\n"]}
@@ -13,10 +13,10 @@
13
13
  * }
14
14
  * }
15
15
  *
16
- * `framework` is the project's libType — which client side it targets:
17
- * angular | react | express | all ("all" = usable by any side). It comes from
18
- * the project's `framework:` nx tag and is enforced across edges by the
19
- * `library-types-match-client` rule.
16
+ * `framework` is the project's libType — the SET of runtime environments it is
17
+ * validated to run in, drawn from browser | react | angular | node | express
18
+ * (e.g. ["browser","node"]). It comes from the project's `framework:` nx tags
19
+ * and is enforced across edges by the `library-types-match-client` rule.
20
20
  *
21
21
  * The legacy format (flat { "<project>": { level, dependsOn } } map) is still
22
22
  * readable so validation against a pre-upgrade file produces a clean