@lifesavertech/archguard-rules-react 2.0.1 → 2.2.0

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/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /** React-specific rule implementations. */
2
2
  import { Rule } from "@lifesavertech/archguard-core";
3
3
  export declare function createLayerImportBoundaryRule(): Rule;
4
+ export declare function createBarrelBoundaryBypassRule(): Rule;
4
5
  export declare function createBusinessLogicInComponentsRule(): Rule;
5
6
  export declare function createDataFetchingInUIRule(): Rule;
6
7
  export declare function createCircularDependencyRule(): Rule;
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  exports.createLayerImportBoundaryRule = createLayerImportBoundaryRule;
38
+ exports.createBarrelBoundaryBypassRule = createBarrelBoundaryBypassRule;
38
39
  exports.createBusinessLogicInComponentsRule = createBusinessLogicInComponentsRule;
39
40
  exports.createDataFetchingInUIRule = createDataFetchingInUIRule;
40
41
  exports.createCircularDependencyRule = createCircularDependencyRule;
@@ -61,7 +62,7 @@ function createLayerImportBoundaryRule() {
61
62
  return { violations: [], passed: true };
62
63
  }
63
64
  const filePath = context.filePath;
64
- const layerName = (0, archguard_core_1.detectLayerFromPath)(filePath, context.architecture.layers);
65
+ const layerName = (0, archguard_core_1.detectLayerFromPath)(filePath, context.architecture, context.projectRoot);
65
66
  if (!layerName) {
66
67
  return { violations: [], passed: true };
67
68
  }
@@ -74,7 +75,7 @@ function createLayerImportBoundaryRule() {
74
75
  if (imp.isTypeOnly) {
75
76
  continue;
76
77
  }
77
- const importedLayer = detectLayerFromImport(imp, filePath, context.architecture.layers);
78
+ const importedLayer = detectLayerFromImport(imp, filePath, context.architecture, context.projectRoot);
78
79
  if (importedLayer && importedLayer !== layerName) {
79
80
  if (!allowedLayers.has(importedLayer)) {
80
81
  const sourceFile = (0, archguard_core_1.parseTypeScript)(context.sourceCode, context.filePath);
@@ -90,6 +91,12 @@ function createLayerImportBoundaryRule() {
90
91
  column: character + 1,
91
92
  message: `Layer '${layerName}' cannot import from '${importedLayer}'. Import from allowed layers only: ${allowedList}. Move the imported code to an allowed layer or restructure dependencies.`,
92
93
  rule: "layer-import-boundary",
94
+ fix: `Move the imported symbol to an allowed layer (${allowedList}) or add '${importedLayer}' to ${layerName}.allowedImports if intentional.`,
95
+ context: {
96
+ sourceLayer: layerName,
97
+ targetLayer: importedLayer,
98
+ importPath: imp.from,
99
+ },
93
100
  });
94
101
  }
95
102
  }
@@ -102,6 +109,73 @@ function createLayerImportBoundaryRule() {
102
109
  },
103
110
  };
104
111
  }
112
+ function createBarrelBoundaryBypassRule() {
113
+ return {
114
+ name: "no-barrel-boundary-bypass",
115
+ description: "Detects layer boundary bypass through barrel re-exports",
116
+ execute(context) {
117
+ const violations = [];
118
+ if (!(0, archguard_core_1.isBarrelBoundaryBypassEnabled)(context.architecture.rules)) {
119
+ return { violations: [], passed: true };
120
+ }
121
+ const layerName = (0, archguard_core_1.detectLayerFromPath)(context.filePath, context.architecture, context.projectRoot);
122
+ if (!layerName) {
123
+ return { violations: [], passed: true };
124
+ }
125
+ const layerConfig = context.architecture.layers[layerName];
126
+ if (!layerConfig) {
127
+ return { violations: [], passed: true };
128
+ }
129
+ const allowedLayers = new Set(layerConfig.allowedImports);
130
+ const projectRoot = context.projectRoot ?? process.cwd();
131
+ const resolver = (0, archguard_core_1.createImportResolver)(projectRoot);
132
+ for (const imp of context.imports) {
133
+ const resolvedPath = imp.resolvedPath ?? resolver.resolve(imp.from, context.filePath);
134
+ if (!resolvedPath) {
135
+ continue;
136
+ }
137
+ const leaks = (0, archguard_core_1.collectBarrelLeaks)({
138
+ barrelPath: resolvedPath,
139
+ importerLayer: layerName,
140
+ allowedLayers,
141
+ architecture: context.architecture,
142
+ projectRoot,
143
+ resolver,
144
+ });
145
+ if (leaks.length === 0) {
146
+ continue;
147
+ }
148
+ const sourceFile = (0, archguard_core_1.parseTypeScript)(context.sourceCode, context.filePath);
149
+ const importNode = findImportNode(sourceFile, imp.from);
150
+ if (!importNode) {
151
+ continue;
152
+ }
153
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(importNode.getStart());
154
+ for (const leak of leaks) {
155
+ const chain = (0, archguard_core_1.formatBarrelChain)(leak.chain, projectRoot);
156
+ violations.push({
157
+ file: context.filePath,
158
+ line: line + 1,
159
+ column: character + 1,
160
+ message: `Layer '${layerName}' accesses '${leak.leakedLayer}' through a barrel re-export (${chain}). Import from allowed layers only or remove the re-export.`,
161
+ rule: "no-barrel-boundary-bypass",
162
+ fix: `Stop re-exporting '${leak.leakedLayer}' through this barrel, or expose a facade in an allowed layer (${[...allowedLayers].join(", ") || "none"}).`,
163
+ context: {
164
+ sourceLayer: layerName,
165
+ targetLayer: leak.leakedLayer,
166
+ importPath: imp.from,
167
+ reexportChain: chain,
168
+ },
169
+ });
170
+ }
171
+ }
172
+ return {
173
+ violations,
174
+ passed: violations.length === 0,
175
+ };
176
+ },
177
+ };
178
+ }
105
179
  function createBusinessLogicInComponentsRule() {
106
180
  return {
107
181
  name: "no-business-logic-in-components",
@@ -111,7 +185,7 @@ function createBusinessLogicInComponentsRule() {
111
185
  if (!context.architecture.rules.noBusinessLogicInComponents) {
112
186
  return { violations: [], passed: true };
113
187
  }
114
- if (!(0, archguard_core_1.fileBelongsToUiLayer)(context.filePath, context.architecture)) {
188
+ if (!(0, archguard_core_1.fileBelongsToUiLayer)(context.filePath, context.architecture, context.projectRoot)) {
115
189
  return { violations: [], passed: true };
116
190
  }
117
191
  const sourceFile = (0, archguard_core_1.parseTypeScript)(context.sourceCode, context.filePath);
@@ -138,7 +212,7 @@ function createDataFetchingInUIRule() {
138
212
  if (!context.architecture.rules.noDataFetchingInUI) {
139
213
  return { violations: [], passed: true };
140
214
  }
141
- if (!(0, archguard_core_1.fileBelongsToUiLayer)(context.filePath, context.architecture)) {
215
+ if (!(0, archguard_core_1.fileBelongsToUiLayer)(context.filePath, context.architecture, context.projectRoot)) {
142
216
  return { violations: [], passed: true };
143
217
  }
144
218
  const sourceFile = (0, archguard_core_1.parseTypeScript)(context.sourceCode, context.filePath);
@@ -225,12 +299,12 @@ function detectCycles(graph) {
225
299
  }
226
300
  return cycles;
227
301
  }
228
- function detectLayerFromImport(importedModule, fromFile, layers) {
302
+ function detectLayerFromImport(importedModule, fromFile, architecture, projectRoot) {
229
303
  if (importedModule.resolvedPath) {
230
- return (0, archguard_core_1.detectLayerFromPath)(importedModule.resolvedPath, layers);
304
+ return (0, archguard_core_1.detectLayerFromPath)(importedModule.resolvedPath, architecture, projectRoot);
231
305
  }
232
306
  if (isRelativeImport(importedModule.from)) {
233
- return detectLayerFromImportPath(importedModule.from, fromFile, layers);
307
+ return detectLayerFromImportPath(importedModule.from, fromFile, architecture, projectRoot);
234
308
  }
235
309
  return null;
236
310
  }
@@ -244,9 +318,9 @@ function formatProjectRelativePath(filePath, projectRoot) {
244
318
  }
245
319
  return filePath.replace(/\\/g, "/");
246
320
  }
247
- function detectLayerFromImportPath(importPath, fromFile, layers) {
321
+ function detectLayerFromImportPath(importPath, fromFile, architecture, projectRoot) {
248
322
  const resolvedPath = path.resolve(path.dirname(fromFile), importPath);
249
- return (0, archguard_core_1.detectLayerFromPath)(resolvedPath, layers);
323
+ return (0, archguard_core_1.detectLayerFromPath)(resolvedPath, architecture, projectRoot);
250
324
  }
251
325
  function isRelativeImport(importPath) {
252
326
  return importPath.startsWith(".") || importPath.startsWith("/");
@@ -358,7 +432,7 @@ function buildImportedSymbolLayers(context) {
358
432
  if (imp.isTypeOnly) {
359
433
  continue;
360
434
  }
361
- const importedLayer = detectLayerFromImport(imp, context.filePath, context.architecture.layers);
435
+ const importedLayer = detectLayerFromImport(imp, context.filePath, context.architecture, context.projectRoot);
362
436
  for (const specifier of imp.specifiers) {
363
437
  importedSymbols.set(specifier, importedLayer);
364
438
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifesavertech/archguard-rules-react",
3
- "version": "2.0.1",
3
+ "version": "2.2.0",
4
4
  "description": "React-specific rules for ArchGuard",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,7 +25,7 @@
25
25
  "test": "npm run build:test && node --test dist-test/index.test.js"
26
26
  },
27
27
  "dependencies": {
28
- "@lifesavertech/archguard-core": "2.0.1",
28
+ "@lifesavertech/archguard-core": "2.2.0",
29
29
  "typescript": "^5.0.0"
30
30
  },
31
31
  "devDependencies": {