@harness-engineering/eslint-plugin 0.1.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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/configs/index.d.ts +5 -0
- package/dist/configs/index.d.ts.map +1 -0
- package/dist/configs/index.js +8 -0
- package/dist/configs/index.js.map +1 -0
- package/dist/configs/recommended.d.ts +4 -0
- package/dist/configs/recommended.d.ts.map +1 -0
- package/dist/configs/recommended.js +16 -0
- package/dist/configs/recommended.js.map +1 -0
- package/dist/configs/strict.d.ts +4 -0
- package/dist/configs/strict.d.ts.map +1 -0
- package/dist/configs/strict.js +16 -0
- package/dist/configs/strict.js.map +1 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/enforce-doc-exports.d.ts +12 -0
- package/dist/rules/enforce-doc-exports.d.ts.map +1 -0
- package/dist/rules/enforce-doc-exports.js +78 -0
- package/dist/rules/enforce-doc-exports.js.map +1 -0
- package/dist/rules/index.d.ts +21 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +14 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/no-circular-deps.d.ts +20 -0
- package/dist/rules/no-circular-deps.d.ts.map +1 -0
- package/dist/rules/no-circular-deps.js +110 -0
- package/dist/rules/no-circular-deps.js.map +1 -0
- package/dist/rules/no-forbidden-imports.d.ts +6 -0
- package/dist/rules/no-forbidden-imports.d.ts.map +1 -0
- package/dist/rules/no-forbidden-imports.js +58 -0
- package/dist/rules/no-forbidden-imports.js.map +1 -0
- package/dist/rules/no-layer-violation.d.ts +6 -0
- package/dist/rules/no-layer-violation.d.ts.map +1 -0
- package/dist/rules/no-layer-violation.js +62 -0
- package/dist/rules/no-layer-violation.js.map +1 -0
- package/dist/rules/require-boundary-schema.d.ts +6 -0
- package/dist/rules/require-boundary-schema.d.ts.map +1 -0
- package/dist/rules/require-boundary-schema.js +53 -0
- package/dist/rules/require-boundary-schema.js.map +1 -0
- package/dist/utils/ast-helpers.d.ts +14 -0
- package/dist/utils/ast-helpers.d.ts.map +1 -0
- package/dist/utils/ast-helpers.js +94 -0
- package/dist/utils/ast-helpers.js.map +1 -0
- package/dist/utils/config-loader.d.ts +10 -0
- package/dist/utils/config-loader.d.ts.map +1 -0
- package/dist/utils/config-loader.js +56 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/path-utils.d.ts +24 -0
- package/dist/utils/path-utils.d.ts.map +1 -0
- package/dist/utils/path-utils.js +62 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/dist/utils/schema.d.ts +117 -0
- package/dist/utils/schema.d.ts.map +1 -0
- package/dist/utils/schema.js +34 -0
- package/dist/utils/schema.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/rules/no-circular-deps.ts
|
|
2
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`);
|
|
5
|
+
// Module-level import graph (persists per lint run)
|
|
6
|
+
const importGraph = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Clear the import graph (for testing)
|
|
9
|
+
*/
|
|
10
|
+
export function clearImportGraph() {
|
|
11
|
+
importGraph.clear();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Add an edge to the import graph (exported for testing)
|
|
15
|
+
*/
|
|
16
|
+
export function addEdge(from, to) {
|
|
17
|
+
if (!importGraph.has(from)) {
|
|
18
|
+
importGraph.set(from, new Set());
|
|
19
|
+
}
|
|
20
|
+
importGraph.get(from).add(to);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if adding edge from -> to creates a cycle
|
|
24
|
+
* Returns the cycle path if found, null otherwise
|
|
25
|
+
* Exported for testing
|
|
26
|
+
*/
|
|
27
|
+
export function detectCycle(from, to) {
|
|
28
|
+
// DFS from 'to' back to 'from'
|
|
29
|
+
const visited = new Set();
|
|
30
|
+
const cyclePath = [to];
|
|
31
|
+
function dfs(current) {
|
|
32
|
+
if (current === from) {
|
|
33
|
+
return true; // Found cycle
|
|
34
|
+
}
|
|
35
|
+
if (visited.has(current)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
visited.add(current);
|
|
39
|
+
const deps = importGraph.get(current);
|
|
40
|
+
if (deps) {
|
|
41
|
+
for (const dep of deps) {
|
|
42
|
+
cyclePath.push(dep);
|
|
43
|
+
if (dfs(dep)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
cyclePath.pop();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (dfs(to)) {
|
|
52
|
+
return [from, ...cyclePath];
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Normalize file path to project-relative
|
|
58
|
+
*/
|
|
59
|
+
function normalizePath(filePath) {
|
|
60
|
+
// Extract path from /project/src/... or similar
|
|
61
|
+
const srcIndex = filePath.indexOf('/src/');
|
|
62
|
+
if (srcIndex !== -1) {
|
|
63
|
+
return filePath.slice(srcIndex + 1);
|
|
64
|
+
}
|
|
65
|
+
return path.basename(filePath);
|
|
66
|
+
}
|
|
67
|
+
export default createRule({
|
|
68
|
+
name: 'no-circular-deps',
|
|
69
|
+
meta: {
|
|
70
|
+
type: 'problem',
|
|
71
|
+
docs: {
|
|
72
|
+
description: 'Detect circular import dependencies',
|
|
73
|
+
},
|
|
74
|
+
messages: {
|
|
75
|
+
circularDep: 'Circular dependency detected: {{cycle}}',
|
|
76
|
+
},
|
|
77
|
+
schema: [],
|
|
78
|
+
},
|
|
79
|
+
defaultOptions: [],
|
|
80
|
+
create(context) {
|
|
81
|
+
const currentFile = normalizePath(context.filename);
|
|
82
|
+
return {
|
|
83
|
+
ImportDeclaration(node) {
|
|
84
|
+
const importPath = node.source.value;
|
|
85
|
+
// Skip external imports
|
|
86
|
+
if (!importPath.startsWith('.')) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Resolve import to normalized path
|
|
90
|
+
const importingDir = path.dirname(context.filename);
|
|
91
|
+
const resolvedPath = path.resolve(importingDir, importPath);
|
|
92
|
+
const normalizedImport = normalizePath(resolvedPath);
|
|
93
|
+
// Check for cycle before adding edge
|
|
94
|
+
const cycle = detectCycle(currentFile, normalizedImport);
|
|
95
|
+
if (cycle) {
|
|
96
|
+
context.report({
|
|
97
|
+
node,
|
|
98
|
+
messageId: 'circularDep',
|
|
99
|
+
data: {
|
|
100
|
+
cycle: cycle.map((f) => path.basename(f)).join(' → '),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Add edge to graph
|
|
105
|
+
addEdge(currentFile, normalizedImport);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=no-circular-deps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-circular-deps.js","sourceRoot":"","sources":["../../src/rules/no-circular-deps.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-forbidden-imports.d.ts","sourceRoot":"","sources":["../../src/rules/no-forbidden-imports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;;;;AAUtE,wBA6DG"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/rules/no-forbidden-imports.ts
|
|
2
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
3
|
+
import { getConfig } from '../utils/config-loader';
|
|
4
|
+
import { matchesPattern, resolveImportPath, normalizePath } from '../utils/path-utils';
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`);
|
|
6
|
+
export default createRule({
|
|
7
|
+
name: 'no-forbidden-imports',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Block forbidden imports based on configurable patterns',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
forbiddenImport: '{{message}}',
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [],
|
|
19
|
+
create(context) {
|
|
20
|
+
const config = getConfig(context.filename);
|
|
21
|
+
if (!config?.forbiddenImports?.length) {
|
|
22
|
+
return {}; // No-op if no config
|
|
23
|
+
}
|
|
24
|
+
// Get file path relative to project
|
|
25
|
+
const filePath = normalizePath(context.filename);
|
|
26
|
+
// Find matching rules for this file
|
|
27
|
+
const applicableRules = config.forbiddenImports.filter((rule) => matchesPattern(filePath, rule.from));
|
|
28
|
+
if (applicableRules.length === 0) {
|
|
29
|
+
return {}; // No rules apply to this file
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
ImportDeclaration(node) {
|
|
33
|
+
const importPath = node.source.value;
|
|
34
|
+
const resolvedImport = resolveImportPath(importPath, context.filename);
|
|
35
|
+
for (const rule of applicableRules) {
|
|
36
|
+
for (const disallowed of rule.disallow) {
|
|
37
|
+
// Check if import matches disallow pattern
|
|
38
|
+
const isMatch = importPath === disallowed ||
|
|
39
|
+
matchesPattern(resolvedImport, disallowed) ||
|
|
40
|
+
matchesPattern(importPath, disallowed);
|
|
41
|
+
if (isMatch) {
|
|
42
|
+
context.report({
|
|
43
|
+
node,
|
|
44
|
+
messageId: 'forbiddenImport',
|
|
45
|
+
data: {
|
|
46
|
+
message: rule.message ||
|
|
47
|
+
`Import "${importPath}" is forbidden in files matching "${rule.from}"`,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return; // Report once per import
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=no-forbidden-imports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-forbidden-imports.js","sourceRoot":"","sources":["../../src/rules/no-forbidden-imports.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEvF,MAAM,UAAU,GAAG,WAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CAAC,6EAA6E,IAAI,KAAK,CACjG,CAAC;AAIF,eAAe,UAAU,CAAiB;IACxC,IAAI,EAAE,sBAAsB;IAC5B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,wDAAwD;SACtE;QACD,QAAQ,EAAE;YACR,eAAe,EAAE,aAAa;SAC/B;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,CAAC,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,CAAC;YACtC,OAAO,EAAE,CAAC,CAAC,qBAAqB;QAClC,CAAC;QAED,oCAAoC;QACpC,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAEjD,oCAAoC;QACpC,MAAM,eAAe,GAAG,MAAM,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAC9D,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CACpC,CAAC;QAEF,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,CAAC,CAAC,8BAA8B;QAC3C,CAAC;QAED,OAAO;YACL,iBAAiB,CAAC,IAAgC;gBAChD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gBACrC,MAAM,cAAc,GAAG,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAEvE,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC;oBACnC,KAAK,MAAM,UAAU,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;wBACvC,2CAA2C;wBAC3C,MAAM,OAAO,GACX,UAAU,KAAK,UAAU;4BACzB,cAAc,CAAC,cAAc,EAAE,UAAU,CAAC;4BAC1C,cAAc,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;wBAEzC,IAAI,OAAO,EAAE,CAAC;4BACZ,OAAO,CAAC,MAAM,CAAC;gCACb,IAAI;gCACJ,SAAS,EAAE,iBAAiB;gCAC5B,IAAI,EAAE;oCACJ,OAAO,EACL,IAAI,CAAC,OAAO;wCACZ,WAAW,UAAU,qCAAqC,IAAI,CAAC,IAAI,GAAG;iCACzE;6BACF,CAAC,CAAC;4BACH,OAAO,CAAC,yBAAyB;wBACnC,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-layer-violation.d.ts","sourceRoot":"","sources":["../../src/rules/no-layer-violation.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;;;;AAetE,wBAiEG"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/rules/no-layer-violation.ts
|
|
2
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
3
|
+
import { getConfig } from '../utils/config-loader';
|
|
4
|
+
import { resolveImportPath, getLayerForFile, getLayerByName, normalizePath, } from '../utils/path-utils';
|
|
5
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`);
|
|
6
|
+
export default createRule({
|
|
7
|
+
name: 'no-layer-violation',
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: {
|
|
11
|
+
description: 'Enforce layer boundary imports',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
layerViolation: 'Layer "{{fromLayer}}" cannot import from layer "{{toLayer}}"',
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [],
|
|
19
|
+
create(context) {
|
|
20
|
+
const config = getConfig(context.filename);
|
|
21
|
+
if (!config?.layers?.length) {
|
|
22
|
+
return {}; // No-op if no layers configured
|
|
23
|
+
}
|
|
24
|
+
const filePath = normalizePath(context.filename);
|
|
25
|
+
const currentLayer = getLayerForFile(filePath, config.layers);
|
|
26
|
+
if (!currentLayer) {
|
|
27
|
+
return {}; // File not in any layer
|
|
28
|
+
}
|
|
29
|
+
const currentLayerDef = getLayerByName(currentLayer, config.layers);
|
|
30
|
+
if (!currentLayerDef) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
ImportDeclaration(node) {
|
|
35
|
+
const importPath = node.source.value;
|
|
36
|
+
// Skip external imports
|
|
37
|
+
if (!importPath.startsWith('.')) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const resolvedImport = resolveImportPath(importPath, context.filename);
|
|
41
|
+
const importLayer = getLayerForFile(resolvedImport, config.layers);
|
|
42
|
+
// Skip if import is not in any layer
|
|
43
|
+
if (!importLayer) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Check if import is allowed
|
|
47
|
+
if (importLayer !== currentLayer &&
|
|
48
|
+
!currentLayerDef.allowedDependencies.includes(importLayer)) {
|
|
49
|
+
context.report({
|
|
50
|
+
node,
|
|
51
|
+
messageId: 'layerViolation',
|
|
52
|
+
data: {
|
|
53
|
+
fromLayer: currentLayer,
|
|
54
|
+
toLayer: importLayer,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=no-layer-violation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-layer-violation.js","sourceRoot":"","sources":["../../src/rules/no-layer-violation.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,aAAa,GACd,MAAM,qBAAqB,CAAC;AAE7B,MAAM,UAAU,GAAG,WAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CAAC,6EAA6E,IAAI,KAAK,CACjG,CAAC;AAIF,eAAe,UAAU,CAAiB;IACxC,IAAI,EAAE,oBAAoB;IAC1B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,gCAAgC;SAC9C;QACD,QAAQ,EAAE;YACR,cAAc,EAAE,8DAA8D;SAC/E;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;YAC5B,OAAO,EAAE,CAAC,CAAC,gCAAgC;QAC7C,CAAC;QAED,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,YAAY,GAAG,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QAE9D,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,OAAO,EAAE,CAAC,CAAC,wBAAwB;QACrC,CAAC;QAED,MAAM,eAAe,GAAG,cAAc,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACpE,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO;YACL,iBAAiB,CAAC,IAAgC;gBAChD,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;gBAErC,wBAAwB;gBACxB,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;oBAChC,OAAO;gBACT,CAAC;gBAED,MAAM,cAAc,GAAG,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACvE,MAAM,WAAW,GAAG,eAAe,CAAC,cAAc,EAAE,MAAM,CAAC,MAAO,CAAC,CAAC;gBAEpE,qCAAqC;gBACrC,IAAI,CAAC,WAAW,EAAE,CAAC;oBACjB,OAAO;gBACT,CAAC;gBAED,6BAA6B;gBAC7B,IACE,WAAW,KAAK,YAAY;oBAC5B,CAAC,eAAe,CAAC,mBAAmB,CAAC,QAAQ,CAAC,WAAW,CAAC,EAC1D,CAAC;oBACD,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI;wBACJ,SAAS,EAAE,gBAAgB;wBAC3B,IAAI,EAAE;4BACJ,SAAS,EAAE,YAAY;4BACvB,OAAO,EAAE,WAAW;yBACrB;qBACF,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-boundary-schema.d.ts","sourceRoot":"","sources":["../../src/rules/require-boundary-schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAiC,MAAM,0BAA0B,CAAC;;;;AAWtF,wBAsDG"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/rules/require-boundary-schema.ts
|
|
2
|
+
import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
import { getConfig } from '../utils/config-loader';
|
|
4
|
+
import { matchesPattern, normalizePath } from '../utils/path-utils';
|
|
5
|
+
import { hasZodValidation } from '../utils/ast-helpers';
|
|
6
|
+
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/harness-engineering/eslint-plugin/blob/main/docs/rules/${name}.md`);
|
|
7
|
+
export default createRule({
|
|
8
|
+
name: 'require-boundary-schema',
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Require Zod schema validation at API boundaries',
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
missingSchema: 'Exported function "{{name}}" at API boundary must validate input with Zod schema',
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
defaultOptions: [],
|
|
20
|
+
create(context) {
|
|
21
|
+
const config = getConfig(context.filename);
|
|
22
|
+
if (!config?.boundaries?.requireSchema?.length) {
|
|
23
|
+
return {}; // No-op if no boundaries configured
|
|
24
|
+
}
|
|
25
|
+
const filePath = normalizePath(context.filename);
|
|
26
|
+
// Check if file matches any boundary pattern
|
|
27
|
+
const isBoundaryFile = config.boundaries.requireSchema.some((pattern) => matchesPattern(filePath, pattern));
|
|
28
|
+
if (!isBoundaryFile) {
|
|
29
|
+
return {}; // Not a boundary file
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
ExportNamedDeclaration(node) {
|
|
33
|
+
const decl = node.declaration;
|
|
34
|
+
// Only check function declarations
|
|
35
|
+
if (!decl || decl.type !== AST_NODE_TYPES.FunctionDeclaration) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const fn = decl;
|
|
39
|
+
if (!fn.id || !fn.body)
|
|
40
|
+
return;
|
|
41
|
+
// Check if function has Zod validation
|
|
42
|
+
if (!hasZodValidation(fn.body)) {
|
|
43
|
+
context.report({
|
|
44
|
+
node: fn,
|
|
45
|
+
messageId: 'missingSchema',
|
|
46
|
+
data: { name: fn.id.name },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
//# sourceMappingURL=require-boundary-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-boundary-schema.js","sourceRoot":"","sources":["../../src/rules/require-boundary-schema.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,OAAO,EAAE,WAAW,EAAE,cAAc,EAAiB,MAAM,0BAA0B,CAAC;AACtF,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,MAAM,UAAU,GAAG,WAAW,CAAC,WAAW,CACxC,CAAC,IAAI,EAAE,EAAE,CAAC,6EAA6E,IAAI,KAAK,CACjG,CAAC;AAIF,eAAe,UAAU,CAAiB;IACxC,IAAI,EAAE,yBAAyB;IAC/B,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,iDAAiD;SAC/D;QACD,QAAQ,EAAE;YACR,aAAa,EACX,kFAAkF;SACrF;QACD,MAAM,EAAE,EAAE;KACX;IACD,cAAc,EAAE,EAAE;IAClB,MAAM,CAAC,OAAO;QACZ,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;YAC/C,OAAO,EAAE,CAAC,CAAC,oCAAoC;QACjD,CAAC;QAED,MAAM,QAAQ,GAAG,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QAEjD,6CAA6C;QAC7C,MAAM,cAAc,GAAG,MAAM,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CACtE,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,CAClC,CAAC;QAEF,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,OAAO,EAAE,CAAC,CAAC,sBAAsB;QACnC,CAAC;QAED,OAAO;YACL,sBAAsB,CAAC,IAAqC;gBAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC;gBAE9B,mCAAmC;gBACnC,IAAI,CAAC,IAAI,IAAK,IAAI,CAAC,IAAuB,KAAK,cAAc,CAAC,mBAAmB,EAAE,CAAC;oBAClF,OAAO;gBACT,CAAC;gBAED,MAAM,EAAE,GAAG,IAAoC,CAAC;gBAChD,IAAI,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI;oBAAE,OAAO;gBAE/B,uCAAuC;gBACvC,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC/B,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI,EAAE,EAAE;wBACR,SAAS,EAAE,eAAe;wBAC1B,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE;qBAC3B,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Check if a node has a preceding JSDoc comment
|
|
4
|
+
*/
|
|
5
|
+
export declare function hasJSDocComment(node: TSESTree.Node, sourceCode: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Check if a function body contains Zod validation (.parse or .safeParse)
|
|
8
|
+
*/
|
|
9
|
+
export declare function hasZodValidation(body: TSESTree.BlockStatement): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Check if a node is marked with @internal
|
|
12
|
+
*/
|
|
13
|
+
export declare function isMarkedInternal(node: TSESTree.Node, sourceCode: string): boolean;
|
|
14
|
+
//# sourceMappingURL=ast-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-helpers.d.ts","sourceRoot":"","sources":["../../src/utils/ast-helpers.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEzE;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CA+BhF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,cAAc,GAAG,OAAO,CA+CvE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAYjF"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// src/utils/ast-helpers.ts
|
|
2
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
|
+
/**
|
|
4
|
+
* Check if a node has a preceding JSDoc comment
|
|
5
|
+
*/
|
|
6
|
+
export function hasJSDocComment(node, sourceCode) {
|
|
7
|
+
if (!node.range)
|
|
8
|
+
return false;
|
|
9
|
+
// Get text before the node
|
|
10
|
+
const textBefore = sourceCode.slice(0, node.range[0]);
|
|
11
|
+
const lines = textBefore.split('\n');
|
|
12
|
+
// Look backwards for /** ... */
|
|
13
|
+
let foundJSDoc = false;
|
|
14
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
15
|
+
const line = lines[i]?.trim() ?? '';
|
|
16
|
+
// Empty line or whitespace - keep looking
|
|
17
|
+
if (line === '')
|
|
18
|
+
continue;
|
|
19
|
+
// Found JSDoc end
|
|
20
|
+
if (line.endsWith('*/')) {
|
|
21
|
+
// Check if it's JSDoc (starts with /**)
|
|
22
|
+
const startIdx = textBefore.lastIndexOf('/**');
|
|
23
|
+
const endIdx = textBefore.lastIndexOf('*/');
|
|
24
|
+
if (startIdx !== -1 && endIdx > startIdx) {
|
|
25
|
+
foundJSDoc = true;
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
// Found something else - no JSDoc
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
return foundJSDoc;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if a function body contains Zod validation (.parse or .safeParse)
|
|
36
|
+
*/
|
|
37
|
+
export function hasZodValidation(body) {
|
|
38
|
+
let found = false;
|
|
39
|
+
// Keys to skip to avoid circular references
|
|
40
|
+
const skipKeys = new Set(['parent', 'loc', 'range', 'tokens', 'comments']);
|
|
41
|
+
function visit(node) {
|
|
42
|
+
if (found)
|
|
43
|
+
return;
|
|
44
|
+
if (node.type === AST_NODE_TYPES.CallExpression &&
|
|
45
|
+
node.callee.type ===
|
|
46
|
+
AST_NODE_TYPES.MemberExpression) {
|
|
47
|
+
const prop = node.callee.property;
|
|
48
|
+
const propType = prop.type;
|
|
49
|
+
if (propType === AST_NODE_TYPES.Identifier &&
|
|
50
|
+
(prop.name === 'parse' ||
|
|
51
|
+
prop.name === 'safeParse')) {
|
|
52
|
+
found = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Recursively visit children
|
|
57
|
+
for (const key of Object.keys(node)) {
|
|
58
|
+
if (skipKeys.has(key))
|
|
59
|
+
continue;
|
|
60
|
+
const value = node[key];
|
|
61
|
+
if (value && typeof value === 'object') {
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
for (const item of value) {
|
|
64
|
+
if (item && typeof item === 'object' && 'type' in item) {
|
|
65
|
+
visit(item);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if ('type' in value) {
|
|
70
|
+
visit(value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
visit(body);
|
|
76
|
+
return found;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if a node is marked with @internal
|
|
80
|
+
*/
|
|
81
|
+
export function isMarkedInternal(node, sourceCode) {
|
|
82
|
+
if (!node.range)
|
|
83
|
+
return false;
|
|
84
|
+
const textBefore = sourceCode.slice(0, node.range[0]);
|
|
85
|
+
const lastComment = textBefore.lastIndexOf('/**');
|
|
86
|
+
if (lastComment === -1)
|
|
87
|
+
return false;
|
|
88
|
+
const commentEnd = textBefore.lastIndexOf('*/');
|
|
89
|
+
if (commentEnd === -1 || commentEnd < lastComment)
|
|
90
|
+
return false;
|
|
91
|
+
const comment = textBefore.slice(lastComment, commentEnd + 2);
|
|
92
|
+
return comment.includes('@internal');
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=ast-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast-helpers.js","sourceRoot":"","sources":["../../src/utils/ast-helpers.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,cAAc,EAAiB,MAAM,0BAA0B,CAAC;AAEzE;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,IAAmB,EAAE,UAAkB;IACrE,IAAI,CAAC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAE9B,2BAA2B;IAC3B,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAErC,gCAAgC;IAChC,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,KAAK,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAEpC,0CAA0C;QAC1C,IAAI,IAAI,KAAK,EAAE;YAAE,SAAS;QAE1B,kBAAkB;QAClB,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,wCAAwC;YACxC,MAAM,QAAQ,GAAG,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YAC5C,IAAI,QAAQ,KAAK,CAAC,CAAC,IAAI,MAAM,GAAG,QAAQ,EAAE,CAAC;gBACzC,UAAU,GAAG,IAAI,CAAC;YACpB,CAAC;YACD,MAAM;QACR,CAAC;QAED,kCAAkC;QAClC,MAAM;IACR,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAA6B;IAC5D,IAAI,KAAK,GAAG,KAAK,CAAC;IAElB,4CAA4C;IAC5C,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;IAE3E,SAAS,KAAK,CAAC,IAAmB;QAChC,IAAI,KAAK;YAAE,OAAO;QAElB,IACG,IAAI,CAAC,IAAuB,KAAK,cAAc,CAAC,cAAc;YAC7D,IAAgC,CAAC,MAAM,CAAC,IAAuB;gBAC/D,cAAc,CAAC,gBAAgB,EACjC,CAAC;YACD,MAAM,IAAI,GAAK,IAAgC,CAAC,MAAoC,CAAC,QAAQ,CAAC;YAC9F,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAsB,CAAC;YAC7C,IACE,QAAQ,KAAK,cAAc,CAAC,UAAU;gBACtC,CAAE,IAA4B,CAAC,IAAI,KAAK,OAAO;oBAC5C,IAA4B,CAAC,IAAI,KAAK,WAAW,CAAC,EACrD,CAAC;gBACD,KAAK,GAAG,IAAI,CAAC;gBACb,OAAO;YACT,CAAC;QACH,CAAC;QAED,6BAA6B;QAC7B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACpC,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS;YAEhC,MAAM,KAAK,GAAI,IAA2C,CAAC,GAAG,CAAC,CAAC;YAChE,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBACvC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;oBACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;wBACzB,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;4BACvD,KAAK,CAAC,IAAqB,CAAC,CAAC;wBAC/B,CAAC;oBACH,CAAC;gBACH,CAAC;qBAAM,IAAI,MAAM,IAAI,KAAK,EAAE,CAAC;oBAC3B,KAAK,CAAC,KAAsB,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,CAAC;IACZ,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAmB,EAAE,UAAkB;IACtE,IAAI,CAAC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IAE9B,MAAM,UAAU,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,WAAW,GAAG,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IAClD,IAAI,WAAW,KAAK,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAErC,MAAM,UAAU,GAAG,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,UAAU,KAAK,CAAC,CAAC,IAAI,UAAU,GAAG,WAAW;QAAE,OAAO,KAAK,CAAC;IAEhE,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,WAAW,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;IAC9D,OAAO,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type HarnessConfig } from './schema';
|
|
2
|
+
/**
|
|
3
|
+
* Load and validate config, with caching
|
|
4
|
+
*/
|
|
5
|
+
export declare function getConfig(filePath: string): HarnessConfig | null;
|
|
6
|
+
/**
|
|
7
|
+
* Clear the config cache (for testing)
|
|
8
|
+
*/
|
|
9
|
+
export declare function clearConfigCache(): void;
|
|
10
|
+
//# sourceMappingURL=config-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-loader.d.ts","sourceRoot":"","sources":["../../src/utils/config-loader.ts"],"names":[],"mappings":"AAGA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAwBnE;;GAEG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAuBhE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAGvC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/utils/config-loader.ts
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { HarnessConfigSchema } from './schema';
|
|
5
|
+
const CONFIG_FILENAME = 'harness.config.json';
|
|
6
|
+
let cachedConfig = null;
|
|
7
|
+
let cachedConfigPath = null;
|
|
8
|
+
/**
|
|
9
|
+
* Find harness.config.json by walking up from the given directory
|
|
10
|
+
*/
|
|
11
|
+
function findConfigFile(startDir) {
|
|
12
|
+
let currentDir = path.resolve(startDir);
|
|
13
|
+
const root = path.parse(currentDir).root;
|
|
14
|
+
while (currentDir !== root) {
|
|
15
|
+
const configPath = path.join(currentDir, CONFIG_FILENAME);
|
|
16
|
+
if (fs.existsSync(configPath)) {
|
|
17
|
+
return configPath;
|
|
18
|
+
}
|
|
19
|
+
currentDir = path.dirname(currentDir);
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Load and validate config, with caching
|
|
25
|
+
*/
|
|
26
|
+
export function getConfig(filePath) {
|
|
27
|
+
const configPath = findConfigFile(path.dirname(filePath));
|
|
28
|
+
if (!configPath) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
// Return cached config if same path
|
|
32
|
+
if (cachedConfigPath === configPath && cachedConfig) {
|
|
33
|
+
return cachedConfig;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
37
|
+
const parsed = HarnessConfigSchema.safeParse(JSON.parse(content));
|
|
38
|
+
if (!parsed.success) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
cachedConfig = parsed.data;
|
|
42
|
+
cachedConfigPath = configPath;
|
|
43
|
+
return cachedConfig;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Clear the config cache (for testing)
|
|
51
|
+
*/
|
|
52
|
+
export function clearConfigCache() {
|
|
53
|
+
cachedConfig = null;
|
|
54
|
+
cachedConfigPath = null;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=config-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-loader.js","sourceRoot":"","sources":["../../src/utils/config-loader.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAC7B,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAsB,MAAM,UAAU,CAAC;AAEnE,MAAM,eAAe,GAAG,qBAAqB,CAAC;AAE9C,IAAI,YAAY,GAAyB,IAAI,CAAC;AAC9C,IAAI,gBAAgB,GAAkB,IAAI,CAAC;AAE3C;;GAEG;AACH,SAAS,cAAc,CAAC,QAAgB;IACtC,IAAI,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;IAEzC,OAAO,UAAU,KAAK,IAAI,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;QAC1D,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC9B,OAAO,UAAU,CAAC;QACpB,CAAC;QACD,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,QAAgB;IACxC,MAAM,UAAU,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC1D,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,oCAAoC;IACpC,IAAI,gBAAgB,KAAK,UAAU,IAAI,YAAY,EAAE,CAAC;QACpD,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAClE,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC;QAC3B,gBAAgB,GAAG,UAAU,CAAC;QAC9B,OAAO,YAAY,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB;IAC9B,YAAY,GAAG,IAAI,CAAC;IACpB,gBAAgB,GAAG,IAAI,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AACA,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,qBAAqB;AACrB,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Layer } from './schema';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve an import path relative to the importing file
|
|
4
|
+
* Returns path relative to project root (assumes /project/ prefix)
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveImportPath(importPath: string, importingFile: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Check if a file path matches a glob pattern
|
|
9
|
+
*/
|
|
10
|
+
export declare function matchesPattern(filePath: string, pattern: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Find which layer a file belongs to
|
|
13
|
+
*/
|
|
14
|
+
export declare function getLayerForFile(filePath: string, layers: Layer[]): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Get layer definition by name
|
|
17
|
+
*/
|
|
18
|
+
export declare function getLayerByName(name: string, layers: Layer[]): Layer | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Normalize a file path to project-relative format
|
|
21
|
+
* Extracts path from /any/path/src/... to src/...
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizePath(filePath: string): string;
|
|
24
|
+
//# sourceMappingURL=path-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../src/utils/path-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEtC;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAmBnF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAMzE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,GAAG,IAAI,CAOhF;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,GAAG,SAAS,CAE/E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMtD"}
|