@webpieces/nx-webpieces-rules 0.4.462 → 0.4.464
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/validate-no-file-import-cycles/executor.d.ts +1 -1
- package/src/executors/validate-no-file-import-cycles/executor.js +54 -3
- package/src/executors/validate-no-file-import-cycles/executor.js.map +1 -1
- package/src/lib/dot-syntax.d.ts +36 -0
- package/src/lib/dot-syntax.js +149 -0
- package/src/lib/dot-syntax.js.map +1 -0
- package/src/lib/runtime-visualizer.js +18 -11
- package/src/lib/runtime-visualizer.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/nx-webpieces-rules",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.464",
|
|
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.4.
|
|
25
|
-
"@webpieces/code-rules": "0.4.
|
|
26
|
-
"@webpieces/eslint-rules": "0.4.
|
|
27
|
-
"@webpieces/pr-gate": "0.4.
|
|
28
|
-
"@webpieces/rules-config": "0.4.
|
|
24
|
+
"@webpieces/ai-hook-rules": "0.4.464",
|
|
25
|
+
"@webpieces/code-rules": "0.4.464",
|
|
26
|
+
"@webpieces/eslint-rules": "0.4.464",
|
|
27
|
+
"@webpieces/pr-gate": "0.4.464",
|
|
28
|
+
"@webpieces/rules-config": "0.4.464",
|
|
29
29
|
"madge": "8.0.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -41,6 +41,6 @@ interface MadgeOptions {
|
|
|
41
41
|
excludeRegExp?: string[];
|
|
42
42
|
detectiveOptions?: Record<string, unknown>;
|
|
43
43
|
}
|
|
44
|
-
export declare function buildMadgeOptions(ignoreTypeOnly: boolean, excludePackages: string[], workspaceRoot: string, projectRoot: string): MadgeOptions;
|
|
44
|
+
export declare function buildMadgeOptions(ignoreTypeOnly: boolean, excludePackages: string[], workspaceRoot: string, projectRoot: string, userExcludeRegExp?: string[]): MadgeOptions;
|
|
45
45
|
export default function runExecutor(_options: ValidateNoFileImportCyclesOptions, context: ExecutorContext): Promise<ExecutorResult>;
|
|
46
46
|
export {};
|
|
@@ -207,8 +207,12 @@ function buildExcludePattern(dir, base, pkgName) {
|
|
|
207
207
|
return `^${escapeRegex(rel)}(/|$)`;
|
|
208
208
|
}
|
|
209
209
|
// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)
|
|
210
|
-
function buildMadgeOptions(ignoreTypeOnly, excludePackages, workspaceRoot, projectRoot
|
|
211
|
-
|
|
210
|
+
function buildMadgeOptions(ignoreTypeOnly, excludePackages, workspaceRoot, projectRoot,
|
|
211
|
+
// Raw patterns from config, handed to madge verbatim. Matched against ids RELATIVE TO
|
|
212
|
+
// projectRoot (see NoFileImportCyclesConfig.excludeRegExp) — the executor warns on any that
|
|
213
|
+
// match nothing so a mis-anchored (workspace-rooted / absolute) pattern isn't a silent no-op.
|
|
214
|
+
userExcludeRegExp = []) {
|
|
215
|
+
const excludeRegExp = [EXCLUDE_BUILD_DIRS, EXCLUDE_DECLARATION_FILES, ...userExcludeRegExp];
|
|
212
216
|
// madge is invoked with projectRoot as its base and emits ids relative to it;
|
|
213
217
|
// realpath it to match resolvePackageDir's realpath'd result under pnpm symlinks.
|
|
214
218
|
const base = realpathOrSelf(projectRoot);
|
|
@@ -230,12 +234,52 @@ function buildMadgeOptions(ignoreTypeOnly, excludePackages, workspaceRoot, proje
|
|
|
230
234
|
}
|
|
231
235
|
return options;
|
|
232
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* The relative-id trap: madge matches excludeRegExp against ids RELATIVE TO
|
|
239
|
+
* projectRoot (e.g. 'src/generated/api.ts'), so a workspace-anchored pattern
|
|
240
|
+
* ('^libraries/apis/foo/src/...') or an absolute one ('^/abs/...') silently
|
|
241
|
+
* matches nothing and the exclusion is a no-op. To make that visible, run madge
|
|
242
|
+
* WITHOUT the user patterns to get the id universe and warn for any pattern that
|
|
243
|
+
* matches none of it (or is not a valid regex). Same "resolves but matches
|
|
244
|
+
* nothing" idea buildExcludePattern uses for excludePackages. Only invoked when
|
|
245
|
+
* the consumer actually configured excludeRegExp, so the extra traversal is paid
|
|
246
|
+
* only in the rare case that opts in.
|
|
247
|
+
*/
|
|
248
|
+
// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)
|
|
249
|
+
async function warnOnUnmatchedExcludeRegExp(madge, projectRoot, baseOptions, userExcludeRegExp) {
|
|
250
|
+
if (userExcludeRegExp.length === 0)
|
|
251
|
+
return;
|
|
252
|
+
const universe = Object.keys((await madge(projectRoot, baseOptions)).obj());
|
|
253
|
+
for (const pattern of userExcludeRegExp) {
|
|
254
|
+
let re;
|
|
255
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- invalid user regex is reported, not thrown
|
|
256
|
+
try {
|
|
257
|
+
re = new RegExp(pattern);
|
|
258
|
+
// webpieces-disable catch-error-pattern -- surface the bad pattern to the consumer, then move on
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
const error = (0, toError_1.toError)(err);
|
|
262
|
+
console.warn(`⚠️ no-file-import-cycles: excludeRegExp entry "${pattern}" is not a valid regex` +
|
|
263
|
+
` (${error.message}) — skipping.`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (!universe.some((id) => re.test(id))) {
|
|
267
|
+
console.warn(`⚠️ no-file-import-cycles: excludeRegExp entry "${pattern}" matched none of the` +
|
|
268
|
+
` ${universe.length} traversed file(s) — the exclusion does nothing.` +
|
|
269
|
+
`\n Patterns match paths RELATIVE TO THE PROJECT (e.g. "^src/generated/"),` +
|
|
270
|
+
` not workspace-rooted or absolute.`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
233
274
|
function reportCycles(projectName, cycles) {
|
|
234
275
|
console.error(`\n❌ Found ${cycles.length} circular import cycle(s) in ${projectName}:\n`);
|
|
235
276
|
cycles.forEach((cycle, i) => {
|
|
236
277
|
console.error(` ${i + 1}. ${cycle.join(' → ')} → ${cycle[0]}`);
|
|
237
278
|
});
|
|
238
279
|
console.error('\nTo fix, break the cycle (extract a shared module, or use an interface).');
|
|
280
|
+
console.error('To exempt a path (generated code, a deliberate bidirectional model), add a pattern to');
|
|
281
|
+
console.error(`"${RULE_NAME}".excludeRegExp in webpieces.config.json. Patterns match paths RELATIVE`);
|
|
282
|
+
console.error('TO THE PROJECT, e.g. "^src/generated/" or "^src/modules/(item|category)/".');
|
|
239
283
|
console.error('To time-box a known cycle, a human can set "ignoreModifiedUntilEpoch"');
|
|
240
284
|
console.error(`(epoch seconds) on the "${RULE_NAME}" rule in webpieces.config.json.`);
|
|
241
285
|
console.error(`To turn the gate off entirely, set "${RULE_NAME}".mode to "OFF".\n`);
|
|
@@ -254,10 +298,17 @@ async function runExecutor(_options, context) {
|
|
|
254
298
|
const branch = rule?.options['ignoreRuleWhileOnBranch'];
|
|
255
299
|
const ignoreTypeOnly = rule?.options['ignoreTypeOnly'] ?? false;
|
|
256
300
|
const excludePackages = rule?.options['excludePackages'] ?? [];
|
|
301
|
+
const userExcludeRegExp = rule?.options['excludeRegExp'] ?? [];
|
|
257
302
|
console.log(`\n🔁 Checking import cycles in ${projectName} (madge)\n`);
|
|
258
303
|
const madge = loadMadge();
|
|
259
|
-
|
|
304
|
+
// baseOptions carries the built-in + excludePackages regexes only; the real run appends the
|
|
305
|
+
// consumer's excludeRegExp, and the warning pass re-uses baseOptions to see what those patterns
|
|
306
|
+
// *would* have to match against (madge's un-user-filtered id universe).
|
|
307
|
+
const baseOptions = buildMadgeOptions(ignoreTypeOnly, excludePackages, context.root, projectRoot);
|
|
308
|
+
const options = buildMadgeOptions(ignoreTypeOnly, excludePackages, context.root, projectRoot, userExcludeRegExp);
|
|
309
|
+
const result = await madge(projectRoot, options);
|
|
260
310
|
const cycles = result.circular();
|
|
311
|
+
await warnOnUnmatchedExcludeRegExp(madge, projectRoot, baseOptions, userExcludeRegExp);
|
|
261
312
|
if (cycles.length === 0) {
|
|
262
313
|
console.log('✅ No circular import cycles found\n');
|
|
263
314
|
return { success: true };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-no-file-import-cycles/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate No File Import Cycles Executor\n *\n * Per-project circular-dependency gate. Runs `madge` over the project's\n * TypeScript sources and fails when an import cycle is found.\n *\n * Unlike the old `nx:run-commands` target (which shelled out to a runtime\n * `npx madge` fetch — see NEEDED_CHANGES.md #1), this executor:\n * - invokes the madge it bundles as a dependency (deterministic, no network),\n * - is driven by webpieces.config.json like every other webpieces rule, so it\n * supports an on/off `mode` and a time-boxed `ignoreModifiedUntilEpoch`.\n *\n * Config (webpieces.config.json, rule key `no-file-import-cycles`):\n * \"no-file-import-cycles\": {\n * \"mode\": \"RUN_EVERY_TIME\", // \"OFF\" disables the gate everywhere\n * \"ignoreModifiedUntilEpoch\": 1771931925, // epoch SECONDS; while now < epoch,\n * // cycles are reported but the gate PASSES\n * // (warn, don't fail). After it, fails again.\n * \"ignoreTypeOnly\": true, // ignore `import type` re-export cycles\n * // (erased at compile time, harmless at runtime)\n * \"excludePackages\": [\"@kami/entities\"] // npm package names whose source trees madge\n * // should NOT traverse (stops foreign cycles\n * // from leaking into this project's report)\n * }\n *\n * Mirrors the dated-disable model already used for the method/file-size rules:\n * the epoch is a grace window so a strict gate can be turned on against an\n * existing codebase without an open-ended \"off everywhere\" escape hatch.\n *\n * Usage: nx run <project>:validate-no-file-import-cycles\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadAndValidate, shouldSkipRule } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../../toError';\n\nexport type ValidateNoFileImportCyclesMode = 'RUN_EVERY_TIME' | 'OFF';\n\nexport interface ValidateNoFileImportCyclesOptions {\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 = 'no-file-import-cycles';\n\n// madge ships no type declarations; describe the slice of its API we use.\n// webpieces-disable no-any-unknown -- minimal hand-typed surface for an untyped dependency\ninterface MadgeOptions {\n fileExtensions: string[];\n excludeRegExp?: string[];\n detectiveOptions?: Record<string, unknown>;\n}\ninterface MadgeInstance {\n circular(): string[][];\n}\ntype MadgeFn = (target: string, options: MadgeOptions) => Promise<MadgeInstance>;\n\n// madge's CJS export is the callable itself; some bundlers wrap it under `.default`.\ninterface MadgeModuleExtras {\n default?: MadgeFn;\n}\ntype MadgeModule = MadgeFn & MadgeModuleExtras;\n\nfunction loadMadge(): MadgeFn {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const mod: MadgeModule = require('madge');\n return mod.default ?? mod;\n}\n\n/**\n * Decide whether the gate should still FAIL on cycles (true) or only warn\n * (false), considering the universal escape hatches: the ignoreModifiedUntilEpoch\n * grace window and ignoreRuleWhileOnBranch. Logs a one-line explanation when a\n * hatch is active.\n */\nfunction isFailingActive(epoch: number | undefined, branch: string | undefined): boolean {\n const skip = shouldSkipRule(epoch, branch);\n if (skip.skip) {\n console.log(\n `\\n⏳ no-file-import-cycles: ${skip.reason}.` +\n '\\n Cycles will be reported but NOT fail the build.\\n',\n );\n return false;\n }\n return true;\n}\n\n// Never scan build output or declaration files. A project that compiles into a\n// local `dist/` (or build/out/coverage) would otherwise report cycles among the\n// emitted `*.d.ts` files instead of — or in addition to — the real source\n// cycles, so the gate would flag compiled-output noise and could diverge from a\n// plain `madge src` run. Excluding these makes the gate scan source only.\nconst EXCLUDE_BUILD_DIRS = '(^|/)(node_modules|dist|build|out|coverage|\\\\.nx|\\\\.next)(/|$)';\nconst EXCLUDE_DECLARATION_FILES = '\\\\.d\\\\.ts$';\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[/\\-\\\\^$*+?.()|[\\]{}]/g, '\\\\$&');\n}\n\nclass TsconfigCompilerOptions {\n paths?: Record<string, string[]>;\n}\nclass TsconfigBase {\n compilerOptions?: TsconfigCompilerOptions;\n}\n\n/** Read tsconfig.base.json compilerOptions.paths from the workspace root, or null on failure. */\nfunction readTsconfigPaths(workspaceRoot: string): Record<string, string[]> | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort read; null on any failure\n try {\n const tsconfigPath = path.join(workspaceRoot, 'tsconfig.base.json');\n const content = fs.readFileSync(tsconfigPath, 'utf8');\n const tsconfig = JSON.parse(content) as TsconfigBase;\n return tsconfig?.compilerOptions?.paths ?? null;\n // webpieces-disable catch-error-pattern -- file missing or malformed JSON; caller handles null\n } catch (err: unknown) {\n //const error = toError(err);\n return null;\n }\n}\n\n/**\n * Walk up from startPath to find the nearest ancestor directory that contains\n * a package.json. Returns that directory path, or null if none found.\n */\nfunction findPackageRoot(startPath: string): string | null {\n let dir = fs.statSync(startPath).isDirectory() ? startPath : path.dirname(startPath);\n const fsRoot = path.parse(dir).root;\n while (dir !== fsRoot) {\n if (fs.existsSync(path.join(dir, 'package.json'))) return dir;\n dir = path.dirname(dir);\n }\n return null;\n}\n\nfunction resolvePackageDir(pkgName: string, workspaceRoot: string): string | null {\n // First try require.resolve (works for installed / symlinked packages).\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- resolution failure is expected; fall through to tsconfig path lookup\n try {\n const pkgJson = require.resolve(`${pkgName}/package.json`, { paths: [workspaceRoot] });\n return fs.realpathSync(path.dirname(pkgJson));\n // webpieces-disable catch-error-pattern -- expected for non-installed packages; fall through to tsconfig path lookup\n } catch (err: unknown) {\n //const error = toError(err);\n // Fall through to tsconfig path resolution for pnpm workspace packages.\n }\n\n // Fallback: resolve via tsconfig.base.json compilerOptions.paths.\n // pnpm workspace packages are not in node_modules, so require.resolve fails above;\n // tsconfig.base.json maps e.g. \"@mealco-internal/kami\" → [\"libraries/kami/index.ts\"].\n const tsconfigPaths = readTsconfigPaths(workspaceRoot);\n const entries = tsconfigPaths?.[pkgName];\n if (!entries || entries.length === 0) {\n console.warn(\n `⚠️ no-file-import-cycles: could not resolve excludePackages entry \"${pkgName}\"` +\n ` — not found in node_modules or tsconfig.base.json paths. Skipping.`,\n );\n return null;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort fallback; warn on any failure\n try {\n const resolved = path.resolve(workspaceRoot, entries[0]);\n const pkgRoot = findPackageRoot(resolved);\n if (!pkgRoot) {\n console.warn(\n `⚠️ no-file-import-cycles: resolved \"${pkgName}\" → \"${resolved}\"` +\n ` but found no package.json in parent directories — skipping.`,\n );\n return null;\n }\n return pkgRoot;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(\n `⚠️ no-file-import-cycles: could not resolve excludePackages entry \"${pkgName}\"` +\n ` via tsconfig paths (${error.message}) — skipping.`,\n );\n return null;\n }\n}\n\n/**\n * Realpath a directory, tolerating a non-existent path (returns the input).\n * madge traverses through pnpm symlinks to real paths, and resolvePackageDir\n * already realpaths its result, so projectRoot must be realpath'd too or the\n * computed relative path won't line up with the ids madge emits.\n */\n// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)\nfunction realpathOrSelf(dir: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort; fall back to the raw path\n try {\n return fs.realpathSync(dir);\n // webpieces-disable catch-error-pattern -- path may not exist yet; use it as-is\n } catch (err: unknown) {\n //const error = toError(err);\n return dir;\n }\n}\n\n/** True if dir contains at least one .ts/.tsx source file anywhere beneath it. */\n// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)\nfunction hasSourceFiles(dir: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort scan; false on any read failure\n try {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'build') continue;\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (hasSourceFiles(full)) return true;\n } else if (/\\.tsx?$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {\n return true;\n }\n }\n return false;\n // webpieces-disable catch-error-pattern -- unreadable dir; treat as no source\n } catch (err: unknown) {\n //const error = toError(err);\n return false;\n }\n}\n\n/**\n * Build the exclude pattern for one resolved package dir, RELATIVE to the base\n * madge is invoked with (projectRoot). madge matches excludeRegExp against ids\n * relative to that base — e.g. '../../libraries/kami/src/x.ts' — so an absolute\n * `^/abs/...` anchor can never match and silently excludes nothing. Returns the\n * relative-anchored pattern. Warns (but still returns the pattern) when the dir\n * holds no source madge would traverse, closing the resolves-but-matches-nothing\n * silent-failure gap.\n */\n// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)\nfunction buildExcludePattern(dir: string, base: string, pkgName: string): string {\n // madge ids always use forward slashes; normalise path.sep so this holds on Windows.\n const rel = path.relative(base, dir).split(path.sep).join('/');\n if (!hasSourceFiles(dir)) {\n console.warn(\n `⚠️ no-file-import-cycles: excludePackages entry \"${pkgName}\" resolved to \"${dir}\"` +\n ` but that directory contains no .ts/.tsx source — the exclusion will match nothing.`,\n );\n }\n return `^${escapeRegex(rel)}(/|$)`;\n}\n\n// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)\nexport function buildMadgeOptions(\n ignoreTypeOnly: boolean,\n excludePackages: string[],\n workspaceRoot: string,\n projectRoot: string,\n): MadgeOptions {\n const excludeRegExp = [EXCLUDE_BUILD_DIRS, EXCLUDE_DECLARATION_FILES];\n // madge is invoked with projectRoot as its base and emits ids relative to it;\n // realpath it to match resolvePackageDir's realpath'd result under pnpm symlinks.\n const base = realpathOrSelf(projectRoot);\n for (const pkg of excludePackages) {\n const dir = resolvePackageDir(pkg, workspaceRoot);\n if (dir) excludeRegExp.push(buildExcludePattern(dir, base, pkg));\n }\n const options: MadgeOptions = {\n fileExtensions: ['ts', 'tsx'],\n excludeRegExp,\n };\n if (ignoreTypeOnly) {\n // dependency-tree's TS detective drops `import type {...}` edges with this flag.\n options.detectiveOptions = {\n ts: { skipTypeImports: true },\n tsx: { skipTypeImports: true },\n };\n }\n return options;\n}\n\nfunction reportCycles(projectName: string, cycles: string[][]): void {\n console.error(`\\n❌ Found ${cycles.length} circular import cycle(s) in ${projectName}:\\n`);\n cycles.forEach((cycle: string[], i: number) => {\n console.error(` ${i + 1}. ${cycle.join(' → ')} → ${cycle[0]}`);\n });\n console.error('\\nTo fix, break the cycle (extract a shared module, or use an interface).');\n console.error('To time-box a known cycle, a human can set \"ignoreModifiedUntilEpoch\"');\n console.error(`(epoch seconds) on the \"${RULE_NAME}\" rule in webpieces.config.json.`);\n console.error(`To turn the gate off entirely, set \"${RULE_NAME}\".mode to \"OFF\".\\n`);\n}\n\nexport default async function runExecutor(\n _options: ValidateNoFileImportCyclesOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const shared = loadAndValidate(context.root).resolved;\n const rule = shared.rules.get(RULE_NAME);\n\n if (rule && rule.isOff) {\n console.log(`\\n⏭️ Skipping ${RULE_NAME} (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 ? path.join(context.root, projectConfig.root) : context.root;\n\n const epoch = rule?.options['ignoreModifiedUntilEpoch'] as number | undefined;\n const branch = rule?.options['ignoreRuleWhileOnBranch'] as string | undefined;\n const ignoreTypeOnly = (rule?.options['ignoreTypeOnly'] as boolean | undefined) ?? false;\n const excludePackages = (rule?.options['excludePackages'] as string[] | undefined) ?? [];\n\n console.log(`\\n🔁 Checking import cycles in ${projectName} (madge)\\n`);\n\n const madge = loadMadge();\n const result = await madge(projectRoot, buildMadgeOptions(ignoreTypeOnly, excludePackages, context.root, projectRoot));\n const cycles = result.circular();\n\n if (cycles.length === 0) {\n console.log('✅ No circular import cycles found\\n');\n return { success: true };\n }\n\n reportCycles(projectName, cycles);\n\n // Grace window or branch hatch active → report but pass; otherwise fail.\n return { success: !isFailingActive(epoch, branch) };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/nx-webpieces-rules/src/executors/validate-no-file-import-cycles/executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Validate No File Import Cycles Executor\n *\n * Per-project circular-dependency gate. Runs `madge` over the project's\n * TypeScript sources and fails when an import cycle is found.\n *\n * Unlike the old `nx:run-commands` target (which shelled out to a runtime\n * `npx madge` fetch — see NEEDED_CHANGES.md #1), this executor:\n * - invokes the madge it bundles as a dependency (deterministic, no network),\n * - is driven by webpieces.config.json like every other webpieces rule, so it\n * supports an on/off `mode` and a time-boxed `ignoreModifiedUntilEpoch`.\n *\n * Config (webpieces.config.json, rule key `no-file-import-cycles`):\n * \"no-file-import-cycles\": {\n * \"mode\": \"RUN_EVERY_TIME\", // \"OFF\" disables the gate everywhere\n * \"ignoreModifiedUntilEpoch\": 1771931925, // epoch SECONDS; while now < epoch,\n * // cycles are reported but the gate PASSES\n * // (warn, don't fail). After it, fails again.\n * \"ignoreTypeOnly\": true, // ignore `import type` re-export cycles\n * // (erased at compile time, harmless at runtime)\n * \"excludePackages\": [\"@kami/entities\"] // npm package names whose source trees madge\n * // should NOT traverse (stops foreign cycles\n * // from leaking into this project's report)\n * }\n *\n * Mirrors the dated-disable model already used for the method/file-size rules:\n * the epoch is a grace window so a strict gate can be turned on against an\n * existing codebase without an open-ended \"off everywhere\" escape hatch.\n *\n * Usage: nx run <project>:validate-no-file-import-cycles\n */\n\nimport type { ExecutorContext } from '@nx/devkit';\nimport { loadAndValidate, shouldSkipRule } from '@webpieces/rules-config';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { toError } from '../../toError';\n\nexport type ValidateNoFileImportCyclesMode = 'RUN_EVERY_TIME' | 'OFF';\n\nexport interface ValidateNoFileImportCyclesOptions {\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 = 'no-file-import-cycles';\n\n// madge ships no type declarations; describe the slice of its API we use.\n// webpieces-disable no-any-unknown -- minimal hand-typed surface for an untyped dependency\ninterface MadgeOptions {\n fileExtensions: string[];\n excludeRegExp?: string[];\n detectiveOptions?: Record<string, unknown>;\n}\ninterface MadgeInstance {\n circular(): string[][];\n // The full dependency graph: id → its dependency ids. Keys are the ids madge\n // traversed (relative to the base it was invoked with) — used to detect\n // excludeRegExp patterns that match nothing.\n obj(): Record<string, string[]>;\n}\ntype MadgeFn = (target: string, options: MadgeOptions) => Promise<MadgeInstance>;\n\n// madge's CJS export is the callable itself; some bundlers wrap it under `.default`.\ninterface MadgeModuleExtras {\n default?: MadgeFn;\n}\ntype MadgeModule = MadgeFn & MadgeModuleExtras;\n\nfunction loadMadge(): MadgeFn {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const mod: MadgeModule = require('madge');\n return mod.default ?? mod;\n}\n\n/**\n * Decide whether the gate should still FAIL on cycles (true) or only warn\n * (false), considering the universal escape hatches: the ignoreModifiedUntilEpoch\n * grace window and ignoreRuleWhileOnBranch. Logs a one-line explanation when a\n * hatch is active.\n */\nfunction isFailingActive(epoch: number | undefined, branch: string | undefined): boolean {\n const skip = shouldSkipRule(epoch, branch);\n if (skip.skip) {\n console.log(\n `\\n⏳ no-file-import-cycles: ${skip.reason}.` +\n '\\n Cycles will be reported but NOT fail the build.\\n',\n );\n return false;\n }\n return true;\n}\n\n// Never scan build output or declaration files. A project that compiles into a\n// local `dist/` (or build/out/coverage) would otherwise report cycles among the\n// emitted `*.d.ts` files instead of — or in addition to — the real source\n// cycles, so the gate would flag compiled-output noise and could diverge from a\n// plain `madge src` run. Excluding these makes the gate scan source only.\nconst EXCLUDE_BUILD_DIRS = '(^|/)(node_modules|dist|build|out|coverage|\\\\.nx|\\\\.next)(/|$)';\nconst EXCLUDE_DECLARATION_FILES = '\\\\.d\\\\.ts$';\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[/\\-\\\\^$*+?.()|[\\]{}]/g, '\\\\$&');\n}\n\nclass TsconfigCompilerOptions {\n paths?: Record<string, string[]>;\n}\nclass TsconfigBase {\n compilerOptions?: TsconfigCompilerOptions;\n}\n\n/** Read tsconfig.base.json compilerOptions.paths from the workspace root, or null on failure. */\nfunction readTsconfigPaths(workspaceRoot: string): Record<string, string[]> | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort read; null on any failure\n try {\n const tsconfigPath = path.join(workspaceRoot, 'tsconfig.base.json');\n const content = fs.readFileSync(tsconfigPath, 'utf8');\n const tsconfig = JSON.parse(content) as TsconfigBase;\n return tsconfig?.compilerOptions?.paths ?? null;\n // webpieces-disable catch-error-pattern -- file missing or malformed JSON; caller handles null\n } catch (err: unknown) {\n //const error = toError(err);\n return null;\n }\n}\n\n/**\n * Walk up from startPath to find the nearest ancestor directory that contains\n * a package.json. Returns that directory path, or null if none found.\n */\nfunction findPackageRoot(startPath: string): string | null {\n let dir = fs.statSync(startPath).isDirectory() ? startPath : path.dirname(startPath);\n const fsRoot = path.parse(dir).root;\n while (dir !== fsRoot) {\n if (fs.existsSync(path.join(dir, 'package.json'))) return dir;\n dir = path.dirname(dir);\n }\n return null;\n}\n\nfunction resolvePackageDir(pkgName: string, workspaceRoot: string): string | null {\n // First try require.resolve (works for installed / symlinked packages).\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- resolution failure is expected; fall through to tsconfig path lookup\n try {\n const pkgJson = require.resolve(`${pkgName}/package.json`, { paths: [workspaceRoot] });\n return fs.realpathSync(path.dirname(pkgJson));\n // webpieces-disable catch-error-pattern -- expected for non-installed packages; fall through to tsconfig path lookup\n } catch (err: unknown) {\n //const error = toError(err);\n // Fall through to tsconfig path resolution for pnpm workspace packages.\n }\n\n // Fallback: resolve via tsconfig.base.json compilerOptions.paths.\n // pnpm workspace packages are not in node_modules, so require.resolve fails above;\n // tsconfig.base.json maps e.g. \"@mealco-internal/kami\" → [\"libraries/kami/index.ts\"].\n const tsconfigPaths = readTsconfigPaths(workspaceRoot);\n const entries = tsconfigPaths?.[pkgName];\n if (!entries || entries.length === 0) {\n console.warn(\n `⚠️ no-file-import-cycles: could not resolve excludePackages entry \"${pkgName}\"` +\n ` — not found in node_modules or tsconfig.base.json paths. Skipping.`,\n );\n return null;\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort fallback; warn on any failure\n try {\n const resolved = path.resolve(workspaceRoot, entries[0]);\n const pkgRoot = findPackageRoot(resolved);\n if (!pkgRoot) {\n console.warn(\n `⚠️ no-file-import-cycles: resolved \"${pkgName}\" → \"${resolved}\"` +\n ` but found no package.json in parent directories — skipping.`,\n );\n return null;\n }\n return pkgRoot;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(\n `⚠️ no-file-import-cycles: could not resolve excludePackages entry \"${pkgName}\"` +\n ` via tsconfig paths (${error.message}) — skipping.`,\n );\n return null;\n }\n}\n\n/**\n * Realpath a directory, tolerating a non-existent path (returns the input).\n * madge traverses through pnpm symlinks to real paths, and resolvePackageDir\n * already realpaths its result, so projectRoot must be realpath'd too or the\n * computed relative path won't line up with the ids madge emits.\n */\n// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)\nfunction realpathOrSelf(dir: string): string {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort; fall back to the raw path\n try {\n return fs.realpathSync(dir);\n // webpieces-disable catch-error-pattern -- path may not exist yet; use it as-is\n } catch (err: unknown) {\n //const error = toError(err);\n return dir;\n }\n}\n\n/** True if dir contains at least one .ts/.tsx source file anywhere beneath it. */\n// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)\nfunction hasSourceFiles(dir: string): boolean {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- best-effort scan; false on any read failure\n try {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'build') continue;\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (hasSourceFiles(full)) return true;\n } else if (/\\.tsx?$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {\n return true;\n }\n }\n return false;\n // webpieces-disable catch-error-pattern -- unreadable dir; treat as no source\n } catch (err: unknown) {\n //const error = toError(err);\n return false;\n }\n}\n\n/**\n * Build the exclude pattern for one resolved package dir, RELATIVE to the base\n * madge is invoked with (projectRoot). madge matches excludeRegExp against ids\n * relative to that base — e.g. '../../libraries/kami/src/x.ts' — so an absolute\n * `^/abs/...` anchor can never match and silently excludes nothing. Returns the\n * relative-anchored pattern. Warns (but still returns the pattern) when the dir\n * holds no source madge would traverse, closing the resolves-but-matches-nothing\n * silent-failure gap.\n */\n// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)\nfunction buildExcludePattern(dir: string, base: string, pkgName: string): string {\n // madge ids always use forward slashes; normalise path.sep so this holds on Windows.\n const rel = path.relative(base, dir).split(path.sep).join('/');\n if (!hasSourceFiles(dir)) {\n console.warn(\n `⚠️ no-file-import-cycles: excludePackages entry \"${pkgName}\" resolved to \"${dir}\"` +\n ` but that directory contains no .ts/.tsx source — the exclusion will match nothing.`,\n );\n }\n return `^${escapeRegex(rel)}(/|$)`;\n}\n\n// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)\nexport function buildMadgeOptions(\n ignoreTypeOnly: boolean,\n excludePackages: string[],\n workspaceRoot: string,\n projectRoot: string,\n // Raw patterns from config, handed to madge verbatim. Matched against ids RELATIVE TO\n // projectRoot (see NoFileImportCyclesConfig.excludeRegExp) — the executor warns on any that\n // match nothing so a mis-anchored (workspace-rooted / absolute) pattern isn't a silent no-op.\n userExcludeRegExp: string[] = [],\n): MadgeOptions {\n const excludeRegExp = [EXCLUDE_BUILD_DIRS, EXCLUDE_DECLARATION_FILES, ...userExcludeRegExp];\n // madge is invoked with projectRoot as its base and emits ids relative to it;\n // realpath it to match resolvePackageDir's realpath'd result under pnpm symlinks.\n const base = realpathOrSelf(projectRoot);\n for (const pkg of excludePackages) {\n const dir = resolvePackageDir(pkg, workspaceRoot);\n if (dir) excludeRegExp.push(buildExcludePattern(dir, base, pkg));\n }\n const options: MadgeOptions = {\n fileExtensions: ['ts', 'tsx'],\n excludeRegExp,\n };\n if (ignoreTypeOnly) {\n // dependency-tree's TS detective drops `import type {...}` edges with this flag.\n options.detectiveOptions = {\n ts: { skipTypeImports: true },\n tsx: { skipTypeImports: true },\n };\n }\n return options;\n}\n\n/**\n * The relative-id trap: madge matches excludeRegExp against ids RELATIVE TO\n * projectRoot (e.g. 'src/generated/api.ts'), so a workspace-anchored pattern\n * ('^libraries/apis/foo/src/...') or an absolute one ('^/abs/...') silently\n * matches nothing and the exclusion is a no-op. To make that visible, run madge\n * WITHOUT the user patterns to get the id universe and warn for any pattern that\n * matches none of it (or is not a valid regex). Same \"resolves but matches\n * nothing\" idea buildExcludePattern uses for excludePackages. Only invoked when\n * the consumer actually configured excludeRegExp, so the extra traversal is paid\n * only in the rare case that opts in.\n */\n// webpieces-disable no-function-outside-class -- nx executor module; file is functional by convention (see resolvePackageDir/reportCycles above)\nasync function warnOnUnmatchedExcludeRegExp(\n madge: MadgeFn,\n projectRoot: string,\n baseOptions: MadgeOptions,\n userExcludeRegExp: string[],\n): Promise<void> {\n if (userExcludeRegExp.length === 0) return;\n const universe = Object.keys((await madge(projectRoot, baseOptions)).obj());\n for (const pattern of userExcludeRegExp) {\n let re: RegExp;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- invalid user regex is reported, not thrown\n try {\n re = new RegExp(pattern);\n // webpieces-disable catch-error-pattern -- surface the bad pattern to the consumer, then move on\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(\n `⚠️ no-file-import-cycles: excludeRegExp entry \"${pattern}\" is not a valid regex` +\n ` (${error.message}) — skipping.`,\n );\n continue;\n }\n if (!universe.some((id: string) => re.test(id))) {\n console.warn(\n `⚠️ no-file-import-cycles: excludeRegExp entry \"${pattern}\" matched none of the` +\n ` ${universe.length} traversed file(s) — the exclusion does nothing.` +\n `\\n Patterns match paths RELATIVE TO THE PROJECT (e.g. \"^src/generated/\"),` +\n ` not workspace-rooted or absolute.`,\n );\n }\n }\n}\n\nfunction reportCycles(projectName: string, cycles: string[][]): void {\n console.error(`\\n❌ Found ${cycles.length} circular import cycle(s) in ${projectName}:\\n`);\n cycles.forEach((cycle: string[], i: number) => {\n console.error(` ${i + 1}. ${cycle.join(' → ')} → ${cycle[0]}`);\n });\n console.error('\\nTo fix, break the cycle (extract a shared module, or use an interface).');\n console.error('To exempt a path (generated code, a deliberate bidirectional model), add a pattern to');\n console.error(`\"${RULE_NAME}\".excludeRegExp in webpieces.config.json. Patterns match paths RELATIVE`);\n console.error('TO THE PROJECT, e.g. \"^src/generated/\" or \"^src/modules/(item|category)/\".');\n console.error('To time-box a known cycle, a human can set \"ignoreModifiedUntilEpoch\"');\n console.error(`(epoch seconds) on the \"${RULE_NAME}\" rule in webpieces.config.json.`);\n console.error(`To turn the gate off entirely, set \"${RULE_NAME}\".mode to \"OFF\".\\n`);\n}\n\nexport default async function runExecutor(\n _options: ValidateNoFileImportCyclesOptions,\n context: ExecutorContext,\n): Promise<ExecutorResult> {\n const shared = loadAndValidate(context.root).resolved;\n const rule = shared.rules.get(RULE_NAME);\n\n if (rule && rule.isOff) {\n console.log(`\\n⏭️ Skipping ${RULE_NAME} (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 ? path.join(context.root, projectConfig.root) : context.root;\n\n const epoch = rule?.options['ignoreModifiedUntilEpoch'] as number | undefined;\n const branch = rule?.options['ignoreRuleWhileOnBranch'] as string | undefined;\n const ignoreTypeOnly = (rule?.options['ignoreTypeOnly'] as boolean | undefined) ?? false;\n const excludePackages = (rule?.options['excludePackages'] as string[] | undefined) ?? [];\n const userExcludeRegExp = (rule?.options['excludeRegExp'] as string[] | undefined) ?? [];\n\n console.log(`\\n🔁 Checking import cycles in ${projectName} (madge)\\n`);\n\n const madge = loadMadge();\n // baseOptions carries the built-in + excludePackages regexes only; the real run appends the\n // consumer's excludeRegExp, and the warning pass re-uses baseOptions to see what those patterns\n // *would* have to match against (madge's un-user-filtered id universe).\n const baseOptions = buildMadgeOptions(ignoreTypeOnly, excludePackages, context.root, projectRoot);\n const options = buildMadgeOptions(ignoreTypeOnly, excludePackages, context.root, projectRoot, userExcludeRegExp);\n const result = await madge(projectRoot, options);\n const cycles = result.circular();\n\n await warnOnUnmatchedExcludeRegExp(madge, projectRoot, baseOptions, userExcludeRegExp);\n\n if (cycles.length === 0) {\n console.log('✅ No circular import cycles found\\n');\n return { success: true };\n }\n\n reportCycles(projectName, cycles);\n\n // Grace window or branch hatch active → report but pass; otherwise fail.\n return { success: !isFailingActive(epoch, branch) };\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOT syntax helpers
|
|
3
|
+
*
|
|
4
|
+
* Two small pieces that exist because a generated DOT that nobody parses is a DOT that WILL break:
|
|
5
|
+
*
|
|
6
|
+
* 1. `dotValue()` — the ONE place a runtime value (service name, api name, project name, title)
|
|
7
|
+
* becomes safe to interpolate into a quoted DOT string. Inlining values at each call site is how
|
|
8
|
+
* an unescaped `"` shipped and took the whole diagram down: in DOT a bare `"` TERMINATES the
|
|
9
|
+
* string it appears in, so one bad node line makes the entire graph fail to parse.
|
|
10
|
+
* 2. `assertValidDot()` — a structural check on the emitted DOT that turns exactly that class of
|
|
11
|
+
* mistake into a thrown error at generation time, instead of a blank page with a Graphviz
|
|
12
|
+
* "syntax error in line N" that only a human opening the HTML ever sees.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Escape a runtime value for use INSIDE a quoted DOT string.
|
|
16
|
+
*
|
|
17
|
+
* Only `\` and `"` matter: everything else (parens, spaces, `-`, `#`, unicode) is ordinary text once
|
|
18
|
+
* it is inside quotes. Note this deliberately escapes `\` FIRST, so a value containing a backslash
|
|
19
|
+
* cannot smuggle an escape sequence in. Callers compose label lines with a literal `\\n` AFTER
|
|
20
|
+
* escaping their values — the separator is ours, the value is theirs.
|
|
21
|
+
*/
|
|
22
|
+
export declare function dotValue(value: string): string;
|
|
23
|
+
/** Thrown when the generator produces DOT that Graphviz could not parse. */
|
|
24
|
+
export declare class InvalidDotError extends Error {
|
|
25
|
+
constructor(message: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Fail loudly on structurally broken DOT.
|
|
29
|
+
*
|
|
30
|
+
* This is not a full Graphviz parser — it is the check that catches the failure mode a string
|
|
31
|
+
* builder actually has: a quote that ends a string early (or never ends it). It scans the quoted
|
|
32
|
+
* strings honouring `\"` escapes and asserts each one is terminated and is bounded by DOT
|
|
33
|
+
* punctuation rather than by bare text. An unescaped `"` inside a label always violates that: the
|
|
34
|
+
* string ends mid-label, and the remaining label text becomes stray tokens.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertValidDot(dot: string, source: string): void;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* DOT syntax helpers
|
|
4
|
+
*
|
|
5
|
+
* Two small pieces that exist because a generated DOT that nobody parses is a DOT that WILL break:
|
|
6
|
+
*
|
|
7
|
+
* 1. `dotValue()` — the ONE place a runtime value (service name, api name, project name, title)
|
|
8
|
+
* becomes safe to interpolate into a quoted DOT string. Inlining values at each call site is how
|
|
9
|
+
* an unescaped `"` shipped and took the whole diagram down: in DOT a bare `"` TERMINATES the
|
|
10
|
+
* string it appears in, so one bad node line makes the entire graph fail to parse.
|
|
11
|
+
* 2. `assertValidDot()` — a structural check on the emitted DOT that turns exactly that class of
|
|
12
|
+
* mistake into a thrown error at generation time, instead of a blank page with a Graphviz
|
|
13
|
+
* "syntax error in line N" that only a human opening the HTML ever sees.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.InvalidDotError = void 0;
|
|
17
|
+
exports.dotValue = dotValue;
|
|
18
|
+
exports.assertValidDot = assertValidDot;
|
|
19
|
+
/** Chars a quoted string may legally sit directly after, ignoring whitespace. */
|
|
20
|
+
const LEGAL_BEFORE_STRING = new Set(['=', '[', ',', ';', '{', '}', '>', '-']);
|
|
21
|
+
/** Chars a quoted string may legally be followed by, ignoring whitespace. */
|
|
22
|
+
const LEGAL_AFTER_STRING = new Set(['=', '[', ']', ',', ';', '{', '}', '-', '>']);
|
|
23
|
+
/**
|
|
24
|
+
* Escape a runtime value for use INSIDE a quoted DOT string.
|
|
25
|
+
*
|
|
26
|
+
* Only `\` and `"` matter: everything else (parens, spaces, `-`, `#`, unicode) is ordinary text once
|
|
27
|
+
* it is inside quotes. Note this deliberately escapes `\` FIRST, so a value containing a backslash
|
|
28
|
+
* cannot smuggle an escape sequence in. Callers compose label lines with a literal `\\n` AFTER
|
|
29
|
+
* escaping their values — the separator is ours, the value is theirs.
|
|
30
|
+
*/
|
|
31
|
+
// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
|
|
32
|
+
function dotValue(value) {
|
|
33
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
34
|
+
}
|
|
35
|
+
/** Thrown when the generator produces DOT that Graphviz could not parse. */
|
|
36
|
+
class InvalidDotError extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'InvalidDotError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.InvalidDotError = InvalidDotError;
|
|
43
|
+
/**
|
|
44
|
+
* Fail loudly on structurally broken DOT.
|
|
45
|
+
*
|
|
46
|
+
* This is not a full Graphviz parser — it is the check that catches the failure mode a string
|
|
47
|
+
* builder actually has: a quote that ends a string early (or never ends it). It scans the quoted
|
|
48
|
+
* strings honouring `\"` escapes and asserts each one is terminated and is bounded by DOT
|
|
49
|
+
* punctuation rather than by bare text. An unescaped `"` inside a label always violates that: the
|
|
50
|
+
* string ends mid-label, and the remaining label text becomes stray tokens.
|
|
51
|
+
*/
|
|
52
|
+
// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
|
|
53
|
+
function assertValidDot(dot, source) {
|
|
54
|
+
// Comment text is not code: a `"` or a word in it must never be read as a DOT token. Blanking
|
|
55
|
+
// it (offsets preserved) keeps every reported line number the one Graphviz would report.
|
|
56
|
+
const code = blankComments(dot);
|
|
57
|
+
let index = 0;
|
|
58
|
+
while (index < code.length) {
|
|
59
|
+
if (code[index] !== '"') {
|
|
60
|
+
index++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const start = index;
|
|
64
|
+
index++;
|
|
65
|
+
while (index < code.length && code[index] !== '"') {
|
|
66
|
+
index += code[index] === '\\' ? 2 : 1;
|
|
67
|
+
}
|
|
68
|
+
if (index >= code.length) {
|
|
69
|
+
throw new InvalidDotError(`${source}: unterminated string starting at ${describe(dot, start)}`);
|
|
70
|
+
}
|
|
71
|
+
const end = index;
|
|
72
|
+
index++;
|
|
73
|
+
checkNeighbor(code, dot, start, end, source);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Replace `//`, `#` and `/* *\/` comment bodies with spaces, preserving length and newlines. */
|
|
77
|
+
// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
|
|
78
|
+
function blankComments(dot) {
|
|
79
|
+
const out = dot.split('');
|
|
80
|
+
let index = 0;
|
|
81
|
+
let inString = false;
|
|
82
|
+
while (index < out.length) {
|
|
83
|
+
const two = dot.slice(index, index + 2);
|
|
84
|
+
if (inString) {
|
|
85
|
+
if (dot[index] === '\\')
|
|
86
|
+
index++;
|
|
87
|
+
else if (dot[index] === '"')
|
|
88
|
+
inString = false;
|
|
89
|
+
index++;
|
|
90
|
+
}
|
|
91
|
+
else if (dot[index] === '"') {
|
|
92
|
+
inString = true;
|
|
93
|
+
index++;
|
|
94
|
+
}
|
|
95
|
+
else if (two === '//' || dot[index] === '#') {
|
|
96
|
+
while (index < out.length && out[index] !== '\n')
|
|
97
|
+
out[index++] = ' ';
|
|
98
|
+
}
|
|
99
|
+
else if (two === '/*') {
|
|
100
|
+
while (index < out.length && dot.slice(index, index + 2) !== '*/') {
|
|
101
|
+
if (out[index] !== '\n')
|
|
102
|
+
out[index] = ' ';
|
|
103
|
+
index++;
|
|
104
|
+
}
|
|
105
|
+
if (index < out.length) {
|
|
106
|
+
out[index] = ' ';
|
|
107
|
+
out[index + 1] = ' ';
|
|
108
|
+
index += 2;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
index++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return out.join('');
|
|
116
|
+
}
|
|
117
|
+
/** Verify the non-whitespace chars bracketing a quoted string are DOT punctuation, not stray text. */
|
|
118
|
+
// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
|
|
119
|
+
function checkNeighbor(code, dot, start, end, source) {
|
|
120
|
+
const before = nonSpaceChar(code, start - 1, -1);
|
|
121
|
+
if (before !== undefined && !LEGAL_BEFORE_STRING.has(before)) {
|
|
122
|
+
throw new InvalidDotError(`${source}: a quoted string starts right after '${before}' at ${describe(dot, start)} — ` +
|
|
123
|
+
`an unescaped '"' in an interpolated value almost certainly ended the previous string early. ` +
|
|
124
|
+
`Interpolate values through dotValue().`);
|
|
125
|
+
}
|
|
126
|
+
const after = nonSpaceChar(code, end + 1, 1);
|
|
127
|
+
if (after !== undefined && !LEGAL_AFTER_STRING.has(after)) {
|
|
128
|
+
throw new InvalidDotError(`${source}: a quoted string is followed by '${after}' at ${describe(dot, end)} — ` +
|
|
129
|
+
`an unescaped '"' in an interpolated value almost certainly ended this string early. ` +
|
|
130
|
+
`Interpolate values through dotValue().`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** The first non-whitespace char walking `step` from `from`, or undefined at either end. */
|
|
134
|
+
// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
|
|
135
|
+
function nonSpaceChar(dot, from, step) {
|
|
136
|
+
for (let i = from; i >= 0 && i < dot.length; i += step) {
|
|
137
|
+
if (!/\s/.test(dot[i]))
|
|
138
|
+
return dot[i];
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
/** `line N: <the line>` for the offset, so the error names the same line Graphviz would. */
|
|
143
|
+
// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
|
|
144
|
+
function describe(dot, offset) {
|
|
145
|
+
const lineNumber = dot.slice(0, offset).split('\n').length;
|
|
146
|
+
const line = dot.split('\n')[lineNumber - 1];
|
|
147
|
+
return `line ${lineNumber}: ${line.trim()}`;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=dot-syntax.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dot-syntax.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/dot-syntax.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DOT syntax helpers\n *\n * Two small pieces that exist because a generated DOT that nobody parses is a DOT that WILL break:\n *\n * 1. `dotValue()` — the ONE place a runtime value (service name, api name, project name, title)\n * becomes safe to interpolate into a quoted DOT string. Inlining values at each call site is how\n * an unescaped `\"` shipped and took the whole diagram down: in DOT a bare `\"` TERMINATES the\n * string it appears in, so one bad node line makes the entire graph fail to parse.\n * 2. `assertValidDot()` — a structural check on the emitted DOT that turns exactly that class of\n * mistake into a thrown error at generation time, instead of a blank page with a Graphviz\n * \"syntax error in line N\" that only a human opening the HTML ever sees.\n */\n\n/** Chars a quoted string may legally sit directly after, ignoring whitespace. */\nconst LEGAL_BEFORE_STRING = new Set(['=', '[', ',', ';', '{', '}', '>', '-']);\n\n/** Chars a quoted string may legally be followed by, ignoring whitespace. */\nconst LEGAL_AFTER_STRING = new Set(['=', '[', ']', ',', ';', '{', '}', '-', '>']);\n\n/**\n * Escape a runtime value for use INSIDE a quoted DOT string.\n *\n * Only `\\` and `\"` matter: everything else (parens, spaces, `-`, `#`, unicode) is ordinary text once\n * it is inside quotes. Note this deliberately escapes `\\` FIRST, so a value containing a backslash\n * cannot smuggle an escape sequence in. Callers compose label lines with a literal `\\\\n` AFTER\n * escaping their values — the separator is ours, the value is theirs.\n */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nexport function dotValue(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\n/** Thrown when the generator produces DOT that Graphviz could not parse. */\nexport class InvalidDotError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidDotError';\n }\n}\n\n/**\n * Fail loudly on structurally broken DOT.\n *\n * This is not a full Graphviz parser — it is the check that catches the failure mode a string\n * builder actually has: a quote that ends a string early (or never ends it). It scans the quoted\n * strings honouring `\\\"` escapes and asserts each one is terminated and is bounded by DOT\n * punctuation rather than by bare text. An unescaped `\"` inside a label always violates that: the\n * string ends mid-label, and the remaining label text becomes stray tokens.\n */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nexport function assertValidDot(dot: string, source: string): void {\n // Comment text is not code: a `\"` or a word in it must never be read as a DOT token. Blanking\n // it (offsets preserved) keeps every reported line number the one Graphviz would report.\n const code = blankComments(dot);\n let index = 0;\n while (index < code.length) {\n if (code[index] !== '\"') {\n index++;\n continue;\n }\n const start = index;\n index++;\n while (index < code.length && code[index] !== '\"') {\n index += code[index] === '\\\\' ? 2 : 1;\n }\n if (index >= code.length) {\n throw new InvalidDotError(`${source}: unterminated string starting at ${describe(dot, start)}`);\n }\n const end = index;\n index++;\n checkNeighbor(code, dot, start, end, source);\n }\n}\n\n/** Replace `//`, `#` and `/* *\\/` comment bodies with spaces, preserving length and newlines. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction blankComments(dot: string): string {\n const out = dot.split('');\n let index = 0;\n let inString = false;\n while (index < out.length) {\n const two = dot.slice(index, index + 2);\n if (inString) {\n if (dot[index] === '\\\\') index++;\n else if (dot[index] === '\"') inString = false;\n index++;\n } else if (dot[index] === '\"') {\n inString = true;\n index++;\n } else if (two === '//' || dot[index] === '#') {\n while (index < out.length && out[index] !== '\\n') out[index++] = ' ';\n } else if (two === '/*') {\n while (index < out.length && dot.slice(index, index + 2) !== '*/') {\n if (out[index] !== '\\n') out[index] = ' ';\n index++;\n }\n if (index < out.length) {\n out[index] = ' ';\n out[index + 1] = ' ';\n index += 2;\n }\n } else {\n index++;\n }\n }\n return out.join('');\n}\n\n/** Verify the non-whitespace chars bracketing a quoted string are DOT punctuation, not stray text. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction checkNeighbor(code: string, dot: string, start: number, end: number, source: string): void {\n const before = nonSpaceChar(code, start - 1, -1);\n if (before !== undefined && !LEGAL_BEFORE_STRING.has(before)) {\n throw new InvalidDotError(\n `${source}: a quoted string starts right after '${before}' at ${describe(dot, start)} — ` +\n `an unescaped '\"' in an interpolated value almost certainly ended the previous string early. ` +\n `Interpolate values through dotValue().`,\n );\n }\n const after = nonSpaceChar(code, end + 1, 1);\n if (after !== undefined && !LEGAL_AFTER_STRING.has(after)) {\n throw new InvalidDotError(\n `${source}: a quoted string is followed by '${after}' at ${describe(dot, end)} — ` +\n `an unescaped '\"' in an interpolated value almost certainly ended this string early. ` +\n `Interpolate values through dotValue().`,\n );\n }\n}\n\n/** The first non-whitespace char walking `step` from `from`, or undefined at either end. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction nonSpaceChar(dot: string, from: number, step: number): string | undefined {\n for (let i = from; i >= 0 && i < dot.length; i += step) {\n if (!/\\s/.test(dot[i])) return dot[i];\n }\n return undefined;\n}\n\n/** `line N: <the line>` for the offset, so the error names the same line Graphviz would. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction describe(dot: string, offset: number): string {\n const lineNumber = dot.slice(0, offset).split('\\n').length;\n const line = dot.split('\\n')[lineNumber - 1];\n return `line ${lineNumber}: ${line.trim()}`;\n}\n"]}
|
|
@@ -24,6 +24,7 @@ exports.writeRuntimeVisualization = writeRuntimeVisualization;
|
|
|
24
24
|
const tslib_1 = require("tslib");
|
|
25
25
|
const fs = tslib_1.__importStar(require("fs"));
|
|
26
26
|
const path = tslib_1.__importStar(require("path"));
|
|
27
|
+
const dot_syntax_1 = require("./dot-syntax");
|
|
27
28
|
const LEVEL_COLORS = {
|
|
28
29
|
0: '#E8F5E9',
|
|
29
30
|
1: '#E3F2FD',
|
|
@@ -59,8 +60,9 @@ function getShortName(name) {
|
|
|
59
60
|
// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
|
|
60
61
|
function labelList(entries) {
|
|
61
62
|
const lines = [];
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const safe = entries.map((entry) => (0, dot_syntax_1.dotValue)(entry));
|
|
64
|
+
for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {
|
|
65
|
+
lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));
|
|
64
66
|
}
|
|
65
67
|
return lines.join('\\n');
|
|
66
68
|
}
|
|
@@ -83,8 +85,10 @@ function implementsEntries(svc) {
|
|
|
83
85
|
// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
|
|
84
86
|
function nodeLabel(name, svc) {
|
|
85
87
|
const role = svc.implements.length > 0 ? 'server' : 'client';
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
// The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end
|
|
89
|
+
// the label string and the whole graph stops parsing.
|
|
90
|
+
const declared = svc.serviceName === undefined ? '' : `, \\"${(0, dot_syntax_1.dotValue)(svc.serviceName)}\\"`;
|
|
91
|
+
let label = `${(0, dot_syntax_1.dotValue)(getShortName(name))}\\n(${role}, L${svc.level}${declared})`;
|
|
88
92
|
if (svc.implements.length > 0)
|
|
89
93
|
label += `\\nimplements: ${labelList(implementsEntries(svc))}`;
|
|
90
94
|
if (svc.uses.length > 0)
|
|
@@ -98,9 +102,9 @@ function nodeLabel(name, svc) {
|
|
|
98
102
|
*/
|
|
99
103
|
// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
|
|
100
104
|
function edgeDot(edge) {
|
|
101
|
-
const from = getShortName(edge.from);
|
|
102
|
-
const to = getShortName(edge.to);
|
|
103
|
-
const via = edge.via.map((v) => getShortName(v)).join(', ');
|
|
105
|
+
const from = (0, dot_syntax_1.dotValue)(getShortName(edge.from));
|
|
106
|
+
const to = (0, dot_syntax_1.dotValue)(getShortName(edge.to));
|
|
107
|
+
const via = edge.via.map((v) => (0, dot_syntax_1.dotValue)(getShortName(v))).join(', ');
|
|
104
108
|
if (edge.type !== 'pubsub') {
|
|
105
109
|
return ` "${from}" -> "${to}" [label="${via}"];\n`;
|
|
106
110
|
}
|
|
@@ -125,7 +129,7 @@ function externalDot(graph, hidden) {
|
|
|
125
129
|
for (const use of graph.unresolvedUses) {
|
|
126
130
|
if (hidden.has(use.service))
|
|
127
131
|
continue;
|
|
128
|
-
const external = getShortName(graph.apis[use.api]?.owner ?? use.api);
|
|
132
|
+
const external = (0, dot_syntax_1.dotValue)(getShortName(graph.apis[use.api]?.owner ?? use.api));
|
|
129
133
|
const key = `${use.service}${PAIR_SEP}${external}`;
|
|
130
134
|
if (!apisByPair.has(key))
|
|
131
135
|
apisByPair.set(key, []);
|
|
@@ -148,7 +152,7 @@ function externalDot(graph, hidden) {
|
|
|
148
152
|
const external = parts[1];
|
|
149
153
|
const via = labelList(apisByPair.get(key).sort());
|
|
150
154
|
dot +=
|
|
151
|
-
` "${getShortName(service)}" -> "external__${external}" ` +
|
|
155
|
+
` "${(0, dot_syntax_1.dotValue)(getShortName(service))}" -> "external__${external}" ` +
|
|
152
156
|
`[label="${via}", style=dashed, color="${EXTERNAL_BORDER}"];\n`;
|
|
153
157
|
}
|
|
154
158
|
return dot;
|
|
@@ -168,7 +172,7 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
|
|
|
168
172
|
continue;
|
|
169
173
|
const svc = graph.services[name];
|
|
170
174
|
const color = LEVEL_COLORS[svc.level] || '#F5F5F5';
|
|
171
|
-
dot += ` "${getShortName(name)}" [fillcolor="${color}", label="${nodeLabel(name, svc)}"];\n`;
|
|
175
|
+
dot += ` "${(0, dot_syntax_1.dotValue)(getShortName(name))}" [fillcolor="${color}", label="${nodeLabel(name, svc)}"];\n`;
|
|
172
176
|
}
|
|
173
177
|
dot += '\n';
|
|
174
178
|
for (const edge of graph.runtimeEdges) {
|
|
@@ -179,9 +183,12 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
|
|
|
179
183
|
if (options.showExternalNodes)
|
|
180
184
|
dot += externalDot(graph, hidden);
|
|
181
185
|
dot += '\n labelloc="t";\n';
|
|
182
|
-
dot += ` label="${title}\\n(from architecture/runtime-dependencies.json)";\n`;
|
|
186
|
+
dot += ` label="${(0, dot_syntax_1.dotValue)(title)}\\n(from architecture/runtime-dependencies.json)";\n`;
|
|
183
187
|
dot += ' fontsize=20;\n';
|
|
184
188
|
dot += '}\n';
|
|
189
|
+
// Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked
|
|
190
|
+
// HERE — a graph that cannot render is a generation failure, not a blank page to discover later.
|
|
191
|
+
(0, dot_syntax_1.assertValidDot)(dot, 'runtime-architecture.dot');
|
|
185
192
|
return dot;
|
|
186
193
|
}
|
|
187
194
|
function generateRuntimeHtml(dot, title) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS and USES. That list is the\n * single most important fact in a microservice architecture, and it used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as dashed terminal nodes, so the vendor systems that\n * actually page you at 3am stop being missing from the picture. They are\n * RENDER-ONLY: derivation, levels and cycle detection never see them.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge, RuntimeService } from './runtime-graph';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nconst EXTERNAL_FILL = '#FAFAFA';\nconst EXTERNAL_BORDER = '#9E9E9E';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n for (let i = 0; i < entries.length; i += APIS_PER_LABEL_LINE) {\n lines.push(entries.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it serves and\n * the contracts it calls. A node with neither reads exactly as before.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n const role = svc.implements.length > 0 ? 'server' : 'client';\n const declared = svc.serviceName === undefined ? '' : `, \"${svc.serviceName}\"`;\n let label = `${getShortName(name)}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n if (svc.uses.length > 0) label += `\\\\nuses: ${labelList(svc.uses)}`;\n return label;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled arrow (producer calls consumer). pubsub → the\n * producer enqueues and the consumer is delivered later, so we draw producer → QUEUE → consumer\n * with a cylinder queue node and dashed enqueue/deliver arrows.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = getShortName(edge.from);\n const to = getShortName(edge.to);\n const via = edge.via.map((v: string) => getShortName(v)).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${via}\"];\\n`;\n }\n const queueId = `queue__${from}__${to}`;\n return (\n ` \"${queueId}\" [shape=cylinder, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", label=\"${via}\\\\nqueue\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n const external = getShortName(graph.apis[use.api]?.owner ?? use.api);\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service of the same\n // short name; only the label carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${external}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${external}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n dot +=\n ` \"${getShortName(service)}\" -> \"external__${external}\" ` +\n `[label=\"${via}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${getShortName(name)}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge);\n }\n\n if (options.showExternalNodes) dot += externalDot(graph, hidden);\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n return dot;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n const script = `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n viz.renderSVGElement(dot)\n .then(el => document.getElementById('graph').appendChild(el))\n .catch(err => { document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>'; });\n `;\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; }\n .note { max-width: 700px; margin: 12px auto; color: #555; text-align: center; }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div class=\"note\">Runtime calls between services. <strong>rpc</strong> = a direct arrow (synchronous call, labeled with the api). <strong>pubsub</strong> = producer → <em>queue</em> (cylinder) → consumer: the producer enqueues a Cloud Task and the consumer is delivered it later.</div>\n <div class=\"note\">Each box lists the contracts it <strong>implements</strong> (serves) and <strong>uses</strong> (calls) — so an api a service serves is visible even when nothing in this repo calls it. <em>(via <lib>)</em> means the service serves that contract through an embedded library rather than its own source. A <strong>dashed box</strong> is a system OUTSIDE this repo (firestore, gmail, ...): a contract this repo calls and nothing here implements.</div>\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS and USES. That list is the\n * single most important fact in a microservice architecture, and it used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as dashed terminal nodes, so the vendor systems that\n * actually page you at 3am stop being missing from the picture. They are\n * RENDER-ONLY: derivation, levels and cycle detection never see them.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge, RuntimeService } from './runtime-graph';\nimport { dotValue, assertValidDot } from './dot-syntax';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nconst EXTERNAL_FILL = '#FAFAFA';\nconst EXTERNAL_BORDER = '#9E9E9E';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n const safe = entries.map((entry: string) => dotValue(entry));\n for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {\n lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it serves and\n * the contracts it calls. A node with neither reads exactly as before.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n const role = svc.implements.length > 0 ? 'server' : 'client';\n // The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end\n // the label string and the whole graph stops parsing.\n const declared = svc.serviceName === undefined ? '' : `, \\\\\"${dotValue(svc.serviceName)}\\\\\"`;\n let label = `${dotValue(getShortName(name))}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n if (svc.uses.length > 0) label += `\\\\nuses: ${labelList(svc.uses)}`;\n return label;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled arrow (producer calls consumer). pubsub → the\n * producer enqueues and the consumer is delivered later, so we draw producer → QUEUE → consumer\n * with a cylinder queue node and dashed enqueue/deliver arrows.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = dotValue(getShortName(edge.from));\n const to = dotValue(getShortName(edge.to));\n const via = edge.via.map((v: string) => dotValue(getShortName(v))).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${via}\"];\\n`;\n }\n const queueId = `queue__${from}__${to}`;\n return (\n ` \"${queueId}\" [shape=cylinder, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", label=\"${via}\\\\nqueue\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n const external = dotValue(getShortName(graph.apis[use.api]?.owner ?? use.api));\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service of the same\n // short name; only the label carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${external}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${external}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n dot +=\n ` \"${dotValue(getShortName(service))}\" -> \"external__${external}\" ` +\n `[label=\"${via}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${dotValue(getShortName(name))}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge);\n }\n\n if (options.showExternalNodes) dot += externalDot(graph, hidden);\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n // Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked\n // HERE — a graph that cannot render is a generation failure, not a blank page to discover later.\n assertValidDot(dot, 'runtime-architecture.dot');\n return dot;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n const script = `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n viz.renderSVGElement(dot)\n .then(el => document.getElementById('graph').appendChild(el))\n .catch(err => { document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>'; });\n `;\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; }\n .note { max-width: 700px; margin: 12px auto; color: #555; text-align: center; }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div class=\"note\">Runtime calls between services. <strong>rpc</strong> = a direct arrow (synchronous call, labeled with the api). <strong>pubsub</strong> = producer → <em>queue</em> (cylinder) → consumer: the producer enqueues a Cloud Task and the consumer is delivered it later.</div>\n <div class=\"note\">Each box lists the contracts it <strong>implements</strong> (serves) and <strong>uses</strong> (calls) — so an api a service serves is visible even when nothing in this repo calls it. <em>(via <lib>)</em> means the service serves that contract through an embedded library rather than its own source. A <strong>dashed box</strong> is a system OUTSIDE this repo (firestore, gmail, ...): a contract this repo calls and nothing here implements.</div>\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
|