@lifesavertech/archguard-rules-react 2.0.0 → 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 +1 -0
- package/dist/index.js +84 -10
- package/package.json +2 -2
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.
|
|
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.
|
|
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,
|
|
302
|
+
function detectLayerFromImport(importedModule, fromFile, architecture, projectRoot) {
|
|
229
303
|
if (importedModule.resolvedPath) {
|
|
230
|
-
return (0, archguard_core_1.detectLayerFromPath)(importedModule.resolvedPath,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
28
|
+
"@lifesavertech/archguard-core": "2.2.0",
|
|
29
29
|
"typescript": "^5.0.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|