@webpieces/nx-webpieces-rules 0.4.464 → 0.4.466

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.
@@ -24,8 +24,9 @@
24
24
  * client call, and the runtime graph says so out loud rather than guessing.
25
25
  */
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.ServiceNameResolution = exports.SERVICE_NAME_METADATA_PATH = void 0;
27
+ exports.CallsServiceResolution = exports.ServiceNameResolution = exports.CALLS_SERVICE_METADATA_PATH = exports.SERVICE_NAME_METADATA_PATH = void 0;
28
28
  exports.resolveServiceName = resolveServiceName;
29
+ exports.resolveCallsService = resolveCallsService;
29
30
  exports.validateUniqueServiceNames = validateUniqueServiceNames;
30
31
  const tslib_1 = require("tslib");
31
32
  const fs = tslib_1.__importStar(require("fs"));
@@ -33,6 +34,8 @@ const path = tslib_1.__importStar(require("path"));
33
34
  const toError_1 = require("../toError");
34
35
  /** The project.json key path holding the declared name: metadata.webpieces.serviceName. */
35
36
  exports.SERVICE_NAME_METADATA_PATH = 'metadata.webpieces.serviceName';
37
+ /** The project.json key path holding a client's declared target: metadata.webpieces.callsService. */
38
+ exports.CALLS_SERVICE_METADATA_PATH = 'metadata.webpieces.callsService';
36
39
  class ServiceNameResolution {
37
40
  serviceName;
38
41
  problem;
@@ -53,7 +56,7 @@ exports.ServiceNameResolution = ServiceNameResolution;
53
56
  */
54
57
  // webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveRole/resolveDrawOnGraph
55
58
  function resolveServiceName(info, workspaceRoot) {
56
- const raw = readServiceNameField(path.join(workspaceRoot, info.root, 'project.json'));
59
+ const raw = readWebpiecesField(path.join(workspaceRoot, info.root, 'project.json'), 'serviceName');
57
60
  if (raw === undefined)
58
61
  return new ServiceNameResolution(null, null);
59
62
  if (typeof raw !== 'string' || raw.trim().length === 0) {
@@ -61,6 +64,72 @@ function resolveServiceName(info, workspaceRoot) {
61
64
  }
62
65
  return new ServiceNameResolution(raw.trim(), null);
63
66
  }
67
+ /**
68
+ * A client's declared target — the service it addresses when the call site cannot carry a literal
69
+ * `ClientConfig` (the client is built in a SHARED library from a config field, so the literal lives
70
+ * in the app, not the lib, one indirection away). Symmetric to `serviceName`: the IMPLEMENTING side
71
+ * declares the name it answers to, the CALLING side declares the name it calls.
72
+ *
73
+ * Two shapes:
74
+ * - a single string — every untargeted `uses` in this project aims at that one service (the common
75
+ * case: most clients talk to exactly one server);
76
+ * - an `{ apiClassName: serviceName }` map — the mixed case, a client that genuinely calls several.
77
+ */
78
+ class CallsServiceResolution {
79
+ callsService;
80
+ problem;
81
+ constructor(
82
+ /** Declared target(s), or null when none is declared (or resolution failed). */
83
+ callsService,
84
+ /** Problem description when the declaration is present but unusable, otherwise null. */
85
+ problem) {
86
+ this.callsService = callsService;
87
+ this.problem = problem;
88
+ }
89
+ }
90
+ exports.CallsServiceResolution = CallsServiceResolution;
91
+ /**
92
+ * Read `metadata.webpieces.callsService` from the project's project.json. A missing file, a missing
93
+ * key, or an unparseable file all resolve to "not declared". A PRESENT value must be either a
94
+ * non-empty string or a non-empty object of non-empty string values — anything else is a typo the
95
+ * author wants told about.
96
+ */
97
+ // webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveServiceName
98
+ function resolveCallsService(info, workspaceRoot) {
99
+ const raw = readWebpiecesField(path.join(workspaceRoot, info.root, 'project.json'), 'callsService');
100
+ if (raw === undefined)
101
+ return new CallsServiceResolution(null, null);
102
+ if (typeof raw === 'string') {
103
+ if (raw.trim().length === 0) {
104
+ return new CallsServiceResolution(null, `${info.name}: ${exports.CALLS_SERVICE_METADATA_PATH} in project.json must be a non-empty string`);
105
+ }
106
+ return new CallsServiceResolution(raw.trim(), null);
107
+ }
108
+ if (isPlainRecord(raw)) {
109
+ const apis = Object.keys(raw);
110
+ if (apis.length === 0) {
111
+ return new CallsServiceResolution(null, `${info.name}: ${exports.CALLS_SERVICE_METADATA_PATH} map in project.json must have at least one entry`);
112
+ }
113
+ const map = {};
114
+ for (const api of apis) {
115
+ const target = raw[api];
116
+ if (typeof target !== 'string' || target.trim().length === 0) {
117
+ return new CallsServiceResolution(null, `${info.name}: ${exports.CALLS_SERVICE_METADATA_PATH}['${api}'] in project.json must be a ` +
118
+ `non-empty string (an api-class -> serviceName map)`);
119
+ }
120
+ map[api] = target.trim();
121
+ }
122
+ return new CallsServiceResolution(map, null);
123
+ }
124
+ return new CallsServiceResolution(null, `${info.name}: ${exports.CALLS_SERVICE_METADATA_PATH} in project.json must be a non-empty string, or an ` +
125
+ `{ apiClassName: serviceName } map for a client that calls several services`);
126
+ }
127
+ /** True when a value is a plain object (a map), not an array or null. */
128
+ // webpieces-disable no-any-unknown -- narrowing opaque project.json JSON
129
+ // webpieces-disable no-function-outside-class -- pure type guard for the resolver above
130
+ function isPlainRecord(value) {
131
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
132
+ }
64
133
  /**
65
134
  * Two projects claiming the same service name make every targeted client edge ambiguous, so the
66
135
  * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.
@@ -83,16 +152,16 @@ function validateUniqueServiceNames(names, problems) {
83
152
  `Give each project a distinct ${exports.SERVICE_NAME_METADATA_PATH}.`);
84
153
  }
85
154
  }
86
- /** The raw `metadata.webpieces.serviceName` value, or undefined when not present at all. */
155
+ /** The raw `metadata.webpieces.<field>` value, or undefined when not present at all. */
87
156
  // webpieces-disable no-any-unknown -- project.json is opaque consumer JSON until narrowed here
88
157
  // webpieces-disable no-function-outside-class -- pure file accessor, matches the sibling resolvers
89
- function readServiceNameField(projectJsonPath) {
158
+ function readWebpiecesField(projectJsonPath, field) {
90
159
  if (!fs.existsSync(projectJsonPath))
91
160
  return undefined;
92
161
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
93
162
  try {
94
163
  const parsed = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));
95
- return parsed?.metadata?.webpieces?.serviceName;
164
+ return parsed?.metadata?.webpieces?.[field];
96
165
  }
97
166
  catch (err) {
98
167
  const error = (0, toError_1.toError)(err);
@@ -1 +1 @@
1
- {"version":3,"file":"service-name-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/service-name-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Service Name Resolver\n *\n * Resolves the `serviceName` field written per project into\n * architecture/dependencies.json — the name a CLIENT uses to address this app at\n * runtime (`new ClientConfig('helper-fsdb')`), i.e. its deployed Cloud Run\n * service name, NOT its nx project name.\n *\n * It is DECLARED, never derived. The three naming spaces have no mechanical\n * relationship, and any strip-the-suffix rule gets it wrong somewhere:\n *\n * nx project | serviceName | Cloud Run\n * helper-svr | helper-portal | helper-portal\n * helper-fsdb-svr | helper-fsdb | helper-fsdb\n * lang-server | lang | lang\n *\n * Declared in the project's own project.json, next to the code it names:\n *\n * { \"metadata\": { \"webpieces\": { \"serviceName\": \"helper-fsdb\" } } }\n *\n * Absent is legal and common — a library or a browser app is never addressed by\n * name. A node WITHOUT one simply cannot be the resolved target of a targeted\n * client call, and the runtime graph says so out loud rather than guessing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\n/** The project.json key path holding the declared name: metadata.webpieces.serviceName. */\nexport const SERVICE_NAME_METADATA_PATH = 'metadata.webpieces.serviceName';\n\nexport class ServiceNameResolution {\n constructor(\n /** Declared service name, or null when none is declared (or resolution failed). */\n public readonly serviceName: string | null,\n /** Problem description when the declaration is present but unusable, otherwise null. */\n public readonly problem: string | null,\n ) {}\n}\n\n/**\n * Read `metadata.webpieces.serviceName` from the project's project.json. A missing file, a missing\n * key, or an unparseable file all resolve to \"not declared\" — only a PRESENT-but-wrong value (empty\n * or not a string) is a problem, because that is a typo the author wants told about.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveRole/resolveDrawOnGraph\nexport function resolveServiceName(info: ProjectInfo, workspaceRoot: string): ServiceNameResolution {\n const raw = readServiceNameField(path.join(workspaceRoot, info.root, 'project.json'));\n if (raw === undefined) return new ServiceNameResolution(null, null);\n if (typeof raw !== 'string' || raw.trim().length === 0) {\n return new ServiceNameResolution(\n null,\n `${info.name}: ${SERVICE_NAME_METADATA_PATH} in project.json must be a non-empty string`,\n );\n }\n return new ServiceNameResolution(raw.trim(), null);\n}\n\n/**\n * Two projects claiming the same service name make every targeted client edge ambiguous, so the\n * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.\n */\n// webpieces-disable no-function-outside-class -- pure validator, mirrors validateRoleDependencies\nexport function validateUniqueServiceNames(names: Map<string, string>, problems: string[]): void {\n const projectsByName = new Map<string, string[]>();\n for (const project of [...names.keys()].sort()) {\n const serviceName = names.get(project)!;\n const claimants = projectsByName.get(serviceName) ?? [];\n claimants.push(project);\n projectsByName.set(serviceName, claimants);\n }\n for (const serviceName of [...projectsByName.keys()].sort()) {\n const claimants = projectsByName.get(serviceName)!;\n if (claimants.length < 2) continue;\n problems.push(\n `serviceName '${serviceName}' is declared by ${claimants.length} projects ` +\n `(${claimants.join(', ')}) — a client naming it could not be routed to one of them. ` +\n `Give each project a distinct ${SERVICE_NAME_METADATA_PATH}.`,\n );\n }\n}\n\n/** The raw `metadata.webpieces.serviceName` value, or undefined when not present at all. */\n// webpieces-disable no-any-unknown -- project.json is opaque consumer JSON until narrowed here\n// webpieces-disable no-function-outside-class -- pure file accessor, matches the sibling resolvers\nfunction readServiceNameField(projectJsonPath: string): unknown {\n if (!fs.existsSync(projectJsonPath)) return undefined;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));\n return parsed?.metadata?.webpieces?.serviceName;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${projectJsonPath}: ${error.message}`);\n return undefined;\n }\n}\n"]}
1
+ {"version":3,"file":"service-name-resolver.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/service-name-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Service Name Resolver\n *\n * Resolves the `serviceName` field written per project into\n * architecture/dependencies.json — the name a CLIENT uses to address this app at\n * runtime (`new ClientConfig('helper-fsdb')`), i.e. its deployed Cloud Run\n * service name, NOT its nx project name.\n *\n * It is DECLARED, never derived. The three naming spaces have no mechanical\n * relationship, and any strip-the-suffix rule gets it wrong somewhere:\n *\n * nx project | serviceName | Cloud Run\n * helper-svr | helper-portal | helper-portal\n * helper-fsdb-svr | helper-fsdb | helper-fsdb\n * lang-server | lang | lang\n *\n * Declared in the project's own project.json, next to the code it names:\n *\n * { \"metadata\": { \"webpieces\": { \"serviceName\": \"helper-fsdb\" } } }\n *\n * Absent is legal and common — a library or a browser app is never addressed by\n * name. A node WITHOUT one simply cannot be the resolved target of a targeted\n * client call, and the runtime graph says so out loud rather than guessing.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { ProjectInfo } from './project-info';\nimport { toError } from '../toError';\n\n/** The project.json key path holding the declared name: metadata.webpieces.serviceName. */\nexport const SERVICE_NAME_METADATA_PATH = 'metadata.webpieces.serviceName';\n\n/** The project.json key path holding a client's declared target: metadata.webpieces.callsService. */\nexport const CALLS_SERVICE_METADATA_PATH = 'metadata.webpieces.callsService';\n\nexport class ServiceNameResolution {\n constructor(\n /** Declared service name, or null when none is declared (or resolution failed). */\n public readonly serviceName: string | null,\n /** Problem description when the declaration is present but unusable, otherwise null. */\n public readonly problem: string | null,\n ) {}\n}\n\n/**\n * Read `metadata.webpieces.serviceName` from the project's project.json. A missing file, a missing\n * key, or an unparseable file all resolve to \"not declared\" — only a PRESENT-but-wrong value (empty\n * or not a string) is a problem, because that is a typo the author wants told about.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveRole/resolveDrawOnGraph\nexport function resolveServiceName(info: ProjectInfo, workspaceRoot: string): ServiceNameResolution {\n const raw = readWebpiecesField(path.join(workspaceRoot, info.root, 'project.json'), 'serviceName');\n if (raw === undefined) return new ServiceNameResolution(null, null);\n if (typeof raw !== 'string' || raw.trim().length === 0) {\n return new ServiceNameResolution(\n null,\n `${info.name}: ${SERVICE_NAME_METADATA_PATH} in project.json must be a non-empty string`,\n );\n }\n return new ServiceNameResolution(raw.trim(), null);\n}\n\n/**\n * A client's declared target — the service it addresses when the call site cannot carry a literal\n * `ClientConfig` (the client is built in a SHARED library from a config field, so the literal lives\n * in the app, not the lib, one indirection away). Symmetric to `serviceName`: the IMPLEMENTING side\n * declares the name it answers to, the CALLING side declares the name it calls.\n *\n * Two shapes:\n * - a single string — every untargeted `uses` in this project aims at that one service (the common\n * case: most clients talk to exactly one server);\n * - an `{ apiClassName: serviceName }` map — the mixed case, a client that genuinely calls several.\n */\nexport class CallsServiceResolution {\n constructor(\n /** Declared target(s), or null when none is declared (or resolution failed). */\n public readonly callsService: string | Record<string, string> | null,\n /** Problem description when the declaration is present but unusable, otherwise null. */\n public readonly problem: string | null,\n ) {}\n}\n\n/**\n * Read `metadata.webpieces.callsService` from the project's project.json. A missing file, a missing\n * key, or an unparseable file all resolve to \"not declared\". A PRESENT value must be either a\n * non-empty string or a non-empty object of non-empty string values — anything else is a typo the\n * author wants told about.\n */\n// webpieces-disable no-function-outside-class -- pure resolver, mirrors resolveServiceName\nexport function resolveCallsService(info: ProjectInfo, workspaceRoot: string): CallsServiceResolution {\n const raw = readWebpiecesField(path.join(workspaceRoot, info.root, 'project.json'), 'callsService');\n if (raw === undefined) return new CallsServiceResolution(null, null);\n\n if (typeof raw === 'string') {\n if (raw.trim().length === 0) {\n return new CallsServiceResolution(\n null,\n `${info.name}: ${CALLS_SERVICE_METADATA_PATH} in project.json must be a non-empty string`,\n );\n }\n return new CallsServiceResolution(raw.trim(), null);\n }\n\n if (isPlainRecord(raw)) {\n const apis = Object.keys(raw);\n if (apis.length === 0) {\n return new CallsServiceResolution(\n null,\n `${info.name}: ${CALLS_SERVICE_METADATA_PATH} map in project.json must have at least one entry`,\n );\n }\n const map: Record<string, string> = {};\n for (const api of apis) {\n const target = raw[api];\n if (typeof target !== 'string' || target.trim().length === 0) {\n return new CallsServiceResolution(\n null,\n `${info.name}: ${CALLS_SERVICE_METADATA_PATH}['${api}'] in project.json must be a ` +\n `non-empty string (an api-class -> serviceName map)`,\n );\n }\n map[api] = target.trim();\n }\n return new CallsServiceResolution(map, null);\n }\n\n return new CallsServiceResolution(\n null,\n `${info.name}: ${CALLS_SERVICE_METADATA_PATH} in project.json must be a non-empty string, or an ` +\n `{ apiClassName: serviceName } map for a client that calls several services`,\n );\n}\n\n/** True when a value is a plain object (a map), not an array or null. */\n// webpieces-disable no-any-unknown -- narrowing opaque project.json JSON\n// webpieces-disable no-function-outside-class -- pure type guard for the resolver above\nfunction isPlainRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/**\n * Two projects claiming the same service name make every targeted client edge ambiguous, so the\n * graph would have to guess. Reported as a metadata problem instead. Appends to `problems`.\n */\n// webpieces-disable no-function-outside-class -- pure validator, mirrors validateRoleDependencies\nexport function validateUniqueServiceNames(names: Map<string, string>, problems: string[]): void {\n const projectsByName = new Map<string, string[]>();\n for (const project of [...names.keys()].sort()) {\n const serviceName = names.get(project)!;\n const claimants = projectsByName.get(serviceName) ?? [];\n claimants.push(project);\n projectsByName.set(serviceName, claimants);\n }\n for (const serviceName of [...projectsByName.keys()].sort()) {\n const claimants = projectsByName.get(serviceName)!;\n if (claimants.length < 2) continue;\n problems.push(\n `serviceName '${serviceName}' is declared by ${claimants.length} projects ` +\n `(${claimants.join(', ')}) — a client naming it could not be routed to one of them. ` +\n `Give each project a distinct ${SERVICE_NAME_METADATA_PATH}.`,\n );\n }\n}\n\n/** The raw `metadata.webpieces.<field>` value, or undefined when not present at all. */\n// webpieces-disable no-any-unknown -- project.json is opaque consumer JSON until narrowed here\n// webpieces-disable no-function-outside-class -- pure file accessor, matches the sibling resolvers\nfunction readWebpiecesField(projectJsonPath: string, field: string): unknown {\n if (!fs.existsSync(projectJsonPath)) return undefined;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const parsed = JSON.parse(fs.readFileSync(projectJsonPath, 'utf-8'));\n return parsed?.metadata?.webpieces?.[field];\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Skipping unparseable ${projectJsonPath}: ${error.message}`);\n return undefined;\n }\n}\n"]}