@webpieces/nx-webpieces-rules 0.4.462 → 0.4.463

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.462",
3
+ "version": "0.4.463",
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.462",
25
- "@webpieces/code-rules": "0.4.462",
26
- "@webpieces/eslint-rules": "0.4.462",
27
- "@webpieces/pr-gate": "0.4.462",
28
- "@webpieces/rules-config": "0.4.462",
24
+ "@webpieces/ai-hook-rules": "0.4.463",
25
+ "@webpieces/code-rules": "0.4.463",
26
+ "@webpieces/eslint-rules": "0.4.463",
27
+ "@webpieces/pr-gate": "0.4.463",
28
+ "@webpieces/rules-config": "0.4.463",
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
- const excludeRegExp = [EXCLUDE_BUILD_DIRS, EXCLUDE_DECLARATION_FILES];
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
- const result = await madge(projectRoot, buildMadgeOptions(ignoreTypeOnly, excludePackages, context.root, projectRoot));
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"]}