@lifesavertech/archguard-core 2.0.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/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @lifesavertech/archguard-core
2
+
3
+ Core engine for ArchGuard: configuration loading, TypeScript parsing, import resolution, workspace discovery, and shared layer helpers.
4
+
5
+ Most users should install `@lifesavertech/archguard-cli` rather than depending on this package directly.
6
+
7
+ Documentation: [github.com/lindseystead/ArchGuard](https://github.com/lindseystead/ArchGuard)
@@ -0,0 +1,5 @@
1
+ /** Configuration loading and validation. */
2
+ import { ArchitectureConfig } from "./index";
3
+ export declare function loadArchitectureConfig(configPath: string): ArchitectureConfig;
4
+ export declare function validateArchitectureConfig(config: unknown): ArchitectureConfig;
5
+ export declare function findArchitectureConfig(startPath: string): string | null;
package/dist/config.js ADDED
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ /** Configuration loading and validation. */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.loadArchitectureConfig = loadArchitectureConfig;
38
+ exports.validateArchitectureConfig = validateArchitectureConfig;
39
+ exports.findArchitectureConfig = findArchitectureConfig;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const yaml_1 = require("yaml");
43
+ function loadArchitectureConfig(configPath) {
44
+ if (!fs.existsSync(configPath)) {
45
+ throw new Error(`Architecture config not found: ${configPath}`);
46
+ }
47
+ const content = fs.readFileSync(configPath, "utf-8");
48
+ const parsed = (0, yaml_1.parse)(content);
49
+ return validateArchitectureConfig(parsed);
50
+ }
51
+ function validateArchitectureConfig(config) {
52
+ if (!config || typeof config !== "object") {
53
+ throw new Error("Invalid architecture config: must be an object");
54
+ }
55
+ const rawConfig = config;
56
+ if (!rawConfig.layers || typeof rawConfig.layers !== "object") {
57
+ throw new Error("Invalid architecture config: 'layers' must be an object");
58
+ }
59
+ const layers = {};
60
+ for (const [layerName, layerConfig] of Object.entries(rawConfig.layers)) {
61
+ if (!layerConfig || typeof layerConfig !== "object") {
62
+ throw new Error(`Invalid layer config for '${layerName}': must be an object`);
63
+ }
64
+ const layer = layerConfig;
65
+ if (!Array.isArray(layer.allowedImports)) {
66
+ throw new Error(`Invalid layer config for '${layerName}': 'allowedImports' must be an array`);
67
+ }
68
+ if (!layer.allowedImports.every((entry) => typeof entry === "string")) {
69
+ throw new Error(`Invalid layer config for '${layerName}': 'allowedImports' must contain strings`);
70
+ }
71
+ layers[layerName] = {
72
+ allowedImports: layer.allowedImports,
73
+ };
74
+ }
75
+ if (Object.keys(layers).length === 0) {
76
+ throw new Error("Invalid architecture config: at least one layer is required");
77
+ }
78
+ const layerNames = new Set(Object.keys(layers));
79
+ for (const [layerName, layerConfig] of Object.entries(layers)) {
80
+ for (const allowedLayer of layerConfig.allowedImports) {
81
+ if (!layerNames.has(allowedLayer)) {
82
+ throw new Error(`Invalid architecture config: layer '${layerName}' allows imports from undefined layer '${allowedLayer}'`);
83
+ }
84
+ }
85
+ }
86
+ let uiLayers;
87
+ if (rawConfig.uiLayers !== undefined) {
88
+ if (!Array.isArray(rawConfig.uiLayers)) {
89
+ throw new Error("Invalid architecture config: 'uiLayers' must be an array of layer names");
90
+ }
91
+ uiLayers = rawConfig.uiLayers.map((entry, index) => {
92
+ if (typeof entry !== "string" || entry.trim().length === 0) {
93
+ throw new Error(`Invalid architecture config: 'uiLayers[${index}]' must be a non-empty string`);
94
+ }
95
+ if (!layerNames.has(entry)) {
96
+ throw new Error(`Invalid architecture config: uiLayers references undefined layer '${entry}'`);
97
+ }
98
+ return entry;
99
+ });
100
+ }
101
+ const rules = (rawConfig.rules ?? {});
102
+ const unknownRuleKeys = Object.keys(rules).filter((key) => ![
103
+ "noBusinessLogicInComponents",
104
+ "noDataFetchingInUI",
105
+ "enforceLayerBoundaries",
106
+ "enforceFeatureBoundaries",
107
+ "noCircularLayerDeps",
108
+ ].includes(key));
109
+ if (unknownRuleKeys.length > 0) {
110
+ throw new Error(`Invalid architecture config: unknown rule(s): ${unknownRuleKeys.join(", ")}`);
111
+ }
112
+ const enforceLayerBoundaries = Boolean(rules.enforceLayerBoundaries ?? rules.enforceFeatureBoundaries);
113
+ return {
114
+ layers,
115
+ uiLayers,
116
+ rules: {
117
+ noBusinessLogicInComponents: Boolean(rules.noBusinessLogicInComponents),
118
+ noDataFetchingInUI: Boolean(rules.noDataFetchingInUI),
119
+ enforceLayerBoundaries,
120
+ enforceFeatureBoundaries: Boolean(rules.enforceFeatureBoundaries),
121
+ noCircularLayerDeps: Boolean(rules.noCircularLayerDeps),
122
+ },
123
+ };
124
+ }
125
+ function findArchitectureConfig(startPath) {
126
+ let currentPath = path.resolve(startPath);
127
+ while (true) {
128
+ const configPath = path.join(currentPath, "architecture.yaml");
129
+ if (fs.existsSync(configPath)) {
130
+ return configPath;
131
+ }
132
+ const parentPath = path.dirname(currentPath);
133
+ if (currentPath === parentPath) {
134
+ return null;
135
+ }
136
+ currentPath = parentPath;
137
+ }
138
+ }
@@ -0,0 +1,65 @@
1
+ /** Core rule engine types and abstractions. */
2
+ export interface Violation {
3
+ file: string;
4
+ line: number;
5
+ column: number;
6
+ message: string;
7
+ rule: string;
8
+ }
9
+ export interface ImportGraph {
10
+ [filePath: string]: string[];
11
+ }
12
+ export interface RuleContext {
13
+ filePath: string;
14
+ sourceCode: string;
15
+ ast: any;
16
+ imports: ImportInfo[];
17
+ architecture: ArchitectureConfig;
18
+ importGraph?: ImportGraph;
19
+ projectRoot?: string;
20
+ }
21
+ export interface ImportInfo {
22
+ from: string;
23
+ specifiers: string[];
24
+ isTypeOnly: boolean;
25
+ resolvedPath?: string | null;
26
+ }
27
+ export interface RuleResult {
28
+ violations: Violation[];
29
+ passed: boolean;
30
+ }
31
+ export interface Rule {
32
+ name: string;
33
+ description: string;
34
+ execute(context: RuleContext): RuleResult;
35
+ }
36
+ export interface ArchitectureConfig {
37
+ layers: Record<string, LayerConfig>;
38
+ /** Layer folder names that receive React UI behavior rules. Defaults to `ui` when present. */
39
+ uiLayers?: string[];
40
+ rules: {
41
+ noBusinessLogicInComponents?: boolean;
42
+ noDataFetchingInUI?: boolean;
43
+ /** Preferred name for layer import boundary enforcement. */
44
+ enforceLayerBoundaries?: boolean;
45
+ /** @deprecated Use `enforceLayerBoundaries`. Kept for backward compatibility. */
46
+ enforceFeatureBoundaries?: boolean;
47
+ noCircularLayerDeps?: boolean;
48
+ };
49
+ }
50
+ export interface LayerConfig {
51
+ allowedImports: string[];
52
+ }
53
+ export declare class RuleEngine {
54
+ private rules;
55
+ registerRule(rule: Rule): void;
56
+ executeRules(context: RuleContext): RuleResult;
57
+ getRegisteredRules(): Rule[];
58
+ }
59
+ export { parseTypeScript, extractImports } from "./parser";
60
+ export { loadArchitectureConfig, validateArchitectureConfig, findArchitectureConfig } from "./config";
61
+ export { createImportResolver } from "./resolver";
62
+ export type { ImportResolver } from "./resolver";
63
+ export { detectLayerFromPath, fileBelongsToUiLayer, isLayerBoundaryEnforcementEnabled, resolveUiLayers, } from "./layers";
64
+ export { discoverWorkspacePackages, resolveWorkspacePackageEntry } from "./workspace";
65
+ export type { WorkspacePackage } from "./workspace";
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /** Core rule engine types and abstractions. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.resolveWorkspacePackageEntry = exports.discoverWorkspacePackages = exports.resolveUiLayers = exports.isLayerBoundaryEnforcementEnabled = exports.fileBelongsToUiLayer = exports.detectLayerFromPath = exports.createImportResolver = exports.findArchitectureConfig = exports.validateArchitectureConfig = exports.loadArchitectureConfig = exports.extractImports = exports.parseTypeScript = exports.RuleEngine = void 0;
5
+ class RuleEngine {
6
+ constructor() {
7
+ this.rules = [];
8
+ }
9
+ registerRule(rule) {
10
+ this.rules.push(rule);
11
+ }
12
+ executeRules(context) {
13
+ const allViolations = [];
14
+ for (const rule of this.rules) {
15
+ const result = rule.execute(context);
16
+ allViolations.push(...result.violations);
17
+ }
18
+ return {
19
+ violations: allViolations,
20
+ passed: allViolations.length === 0,
21
+ };
22
+ }
23
+ getRegisteredRules() {
24
+ return [...this.rules];
25
+ }
26
+ }
27
+ exports.RuleEngine = RuleEngine;
28
+ var parser_1 = require("./parser");
29
+ Object.defineProperty(exports, "parseTypeScript", { enumerable: true, get: function () { return parser_1.parseTypeScript; } });
30
+ Object.defineProperty(exports, "extractImports", { enumerable: true, get: function () { return parser_1.extractImports; } });
31
+ var config_1 = require("./config");
32
+ Object.defineProperty(exports, "loadArchitectureConfig", { enumerable: true, get: function () { return config_1.loadArchitectureConfig; } });
33
+ Object.defineProperty(exports, "validateArchitectureConfig", { enumerable: true, get: function () { return config_1.validateArchitectureConfig; } });
34
+ Object.defineProperty(exports, "findArchitectureConfig", { enumerable: true, get: function () { return config_1.findArchitectureConfig; } });
35
+ var resolver_1 = require("./resolver");
36
+ Object.defineProperty(exports, "createImportResolver", { enumerable: true, get: function () { return resolver_1.createImportResolver; } });
37
+ var layers_1 = require("./layers");
38
+ Object.defineProperty(exports, "detectLayerFromPath", { enumerable: true, get: function () { return layers_1.detectLayerFromPath; } });
39
+ Object.defineProperty(exports, "fileBelongsToUiLayer", { enumerable: true, get: function () { return layers_1.fileBelongsToUiLayer; } });
40
+ Object.defineProperty(exports, "isLayerBoundaryEnforcementEnabled", { enumerable: true, get: function () { return layers_1.isLayerBoundaryEnforcementEnabled; } });
41
+ Object.defineProperty(exports, "resolveUiLayers", { enumerable: true, get: function () { return layers_1.resolveUiLayers; } });
42
+ var workspace_1 = require("./workspace");
43
+ Object.defineProperty(exports, "discoverWorkspacePackages", { enumerable: true, get: function () { return workspace_1.discoverWorkspacePackages; } });
44
+ Object.defineProperty(exports, "resolveWorkspacePackageEntry", { enumerable: true, get: function () { return workspace_1.resolveWorkspacePackageEntry; } });
@@ -0,0 +1,6 @@
1
+ /** Layer detection and UI targeting helpers shared across rule packs. */
2
+ import { ArchitectureConfig, LayerConfig } from "./index";
3
+ export declare function detectLayerFromPath(filePath: string, layers: Record<string, LayerConfig>): string | null;
4
+ export declare function resolveUiLayers(config: ArchitectureConfig): Set<string>;
5
+ export declare function fileBelongsToUiLayer(filePath: string, config: ArchitectureConfig): boolean;
6
+ export declare function isLayerBoundaryEnforcementEnabled(rules: ArchitectureConfig["rules"]): boolean;
package/dist/layers.js ADDED
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ /** Layer detection and UI targeting helpers shared across rule packs. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.detectLayerFromPath = detectLayerFromPath;
5
+ exports.resolveUiLayers = resolveUiLayers;
6
+ exports.fileBelongsToUiLayer = fileBelongsToUiLayer;
7
+ exports.isLayerBoundaryEnforcementEnabled = isLayerBoundaryEnforcementEnabled;
8
+ const DEFAULT_UI_LAYER = "ui";
9
+ function detectLayerFromPath(filePath, layers) {
10
+ const normalizedPath = filePath.replace(/\\/g, "/");
11
+ const segments = normalizedPath.split("/").filter(Boolean);
12
+ const layerNames = new Set(Object.keys(layers));
13
+ for (const segment of segments) {
14
+ if (layerNames.has(segment)) {
15
+ return segment;
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+ function resolveUiLayers(config) {
21
+ if (config.uiLayers && config.uiLayers.length > 0) {
22
+ return new Set(config.uiLayers);
23
+ }
24
+ if (config.layers[DEFAULT_UI_LAYER]) {
25
+ return new Set([DEFAULT_UI_LAYER]);
26
+ }
27
+ return new Set();
28
+ }
29
+ function fileBelongsToUiLayer(filePath, config) {
30
+ const uiLayers = resolveUiLayers(config);
31
+ if (uiLayers.size === 0) {
32
+ return false;
33
+ }
34
+ const layerName = detectLayerFromPath(filePath, config.layers);
35
+ return layerName !== null && uiLayers.has(layerName);
36
+ }
37
+ function isLayerBoundaryEnforcementEnabled(rules) {
38
+ return Boolean(rules.enforceLayerBoundaries || rules.enforceFeatureBoundaries);
39
+ }
@@ -0,0 +1,5 @@
1
+ /** TypeScript parsing helpers. */
2
+ import * as ts from "typescript";
3
+ import { ImportInfo } from "./index";
4
+ export declare function parseTypeScript(sourceCode: string, fileName: string): ts.SourceFile;
5
+ export declare function extractImports(sourceFile: ts.SourceFile): ImportInfo[];
package/dist/parser.js ADDED
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /** TypeScript parsing helpers. */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.parseTypeScript = parseTypeScript;
38
+ exports.extractImports = extractImports;
39
+ const ts = __importStar(require("typescript"));
40
+ function parseTypeScript(sourceCode, fileName) {
41
+ return ts.createSourceFile(fileName, sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
42
+ }
43
+ function extractImports(sourceFile) {
44
+ const imports = [];
45
+ function visit(node) {
46
+ if (ts.isImportDeclaration(node)) {
47
+ const moduleSpecifier = node.moduleSpecifier;
48
+ if (ts.isStringLiteral(moduleSpecifier)) {
49
+ const from = moduleSpecifier.text;
50
+ const specifiers = [];
51
+ let isTypeOnly = false;
52
+ if (node.importClause) {
53
+ isTypeOnly = node.importClause.isTypeOnly || false;
54
+ if (node.importClause.namedBindings) {
55
+ if (ts.isNamespaceImport(node.importClause.namedBindings)) {
56
+ specifiers.push(node.importClause.namedBindings.name.text);
57
+ }
58
+ else if (ts.isNamedImports(node.importClause.namedBindings)) {
59
+ for (const element of node.importClause.namedBindings.elements) {
60
+ specifiers.push(element.name.text);
61
+ }
62
+ }
63
+ }
64
+ if (node.importClause.name) {
65
+ specifiers.push(node.importClause.name.text);
66
+ }
67
+ }
68
+ imports.push({
69
+ from,
70
+ specifiers,
71
+ isTypeOnly,
72
+ });
73
+ }
74
+ }
75
+ ts.forEachChild(node, visit);
76
+ }
77
+ visit(sourceFile);
78
+ return imports;
79
+ }
@@ -0,0 +1,6 @@
1
+ export interface ImportResolver {
2
+ projectRoot: string;
3
+ tsConfigPath: string | null;
4
+ resolve(importPath: string, fromFile: string): string | null;
5
+ }
6
+ export declare function createImportResolver(projectRoot: string): ImportResolver;
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createImportResolver = createImportResolver;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const ts = __importStar(require("typescript"));
40
+ const workspace_1 = require("./workspace");
41
+ function createImportResolver(projectRoot) {
42
+ const normalizedProjectRoot = path.resolve(projectRoot);
43
+ const tsConfigPath = findTypeScriptConfig(normalizedProjectRoot);
44
+ const compilerOptions = loadCompilerOptions(tsConfigPath);
45
+ const referencedProjects = collectReferencedProjects(tsConfigPath);
46
+ const workspacePackages = (0, workspace_1.discoverWorkspacePackages)(normalizedProjectRoot);
47
+ const moduleResolutionCache = ts.createModuleResolutionCache(normalizedProjectRoot, (value) => path.normalize(value), compilerOptions ?? {});
48
+ return {
49
+ projectRoot: normalizedProjectRoot,
50
+ tsConfigPath,
51
+ resolve(importPath, fromFile) {
52
+ if (isRelativeOrAbsoluteImport(importPath)) {
53
+ return resolveRelativeImport(importPath, fromFile, normalizedProjectRoot);
54
+ }
55
+ const workspaceMatch = resolveWorkspaceImport(importPath, workspacePackages, normalizedProjectRoot);
56
+ if (workspaceMatch) {
57
+ return workspaceMatch;
58
+ }
59
+ if (compilerOptions) {
60
+ const resolution = ts.resolveModuleName(importPath, fromFile, compilerOptions, ts.sys, moduleResolutionCache).resolvedModule;
61
+ if (resolution) {
62
+ const resolved = normalizeResolvedPath(resolution.resolvedFileName, normalizedProjectRoot);
63
+ if (resolved) {
64
+ return resolved;
65
+ }
66
+ }
67
+ }
68
+ for (const referencedProject of referencedProjects) {
69
+ const resolution = ts.resolveModuleName(importPath, fromFile, referencedProject.options, ts.sys, referencedProject.cache).resolvedModule;
70
+ if (resolution) {
71
+ const resolved = normalizeResolvedPath(resolution.resolvedFileName, normalizedProjectRoot, true);
72
+ if (resolved) {
73
+ return resolved;
74
+ }
75
+ }
76
+ }
77
+ return null;
78
+ },
79
+ };
80
+ }
81
+ function resolveWorkspaceImport(importPath, workspacePackages, projectRoot) {
82
+ const packageNames = Array.from(workspacePackages.keys()).sort((left, right) => right.length - left.length);
83
+ for (const packageName of packageNames) {
84
+ if (importPath === packageName || importPath.startsWith(`${packageName}/`)) {
85
+ const packageRoot = workspacePackages.get(packageName);
86
+ if (!packageRoot) {
87
+ continue;
88
+ }
89
+ return (0, workspace_1.resolveWorkspacePackageEntry)(packageRoot, importPath, projectRoot);
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ function collectReferencedProjects(tsConfigPath, visited = new Set()) {
95
+ if (!tsConfigPath || visited.has(tsConfigPath)) {
96
+ return [];
97
+ }
98
+ visited.add(tsConfigPath);
99
+ const readResult = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
100
+ if (readResult.error) {
101
+ return [];
102
+ }
103
+ const config = readResult.config;
104
+ if (!Array.isArray(config.references)) {
105
+ return [];
106
+ }
107
+ const projects = [];
108
+ const tsConfigDir = path.dirname(tsConfigPath);
109
+ for (const reference of config.references) {
110
+ if (!reference?.path) {
111
+ continue;
112
+ }
113
+ const referenceRoot = path.resolve(tsConfigDir, reference.path);
114
+ const referenceConfigPath = fs.existsSync(path.join(referenceRoot, "tsconfig.json"))
115
+ ? path.join(referenceRoot, "tsconfig.json")
116
+ : referenceRoot.endsWith(".json")
117
+ ? referenceRoot
118
+ : null;
119
+ if (!referenceConfigPath) {
120
+ continue;
121
+ }
122
+ const options = loadCompilerOptions(referenceConfigPath);
123
+ if (!options) {
124
+ continue;
125
+ }
126
+ projects.push({
127
+ rootDir: path.dirname(referenceConfigPath),
128
+ options,
129
+ cache: ts.createModuleResolutionCache(path.dirname(referenceConfigPath), (value) => path.normalize(value), options),
130
+ });
131
+ projects.push(...collectReferencedProjects(referenceConfigPath, visited));
132
+ }
133
+ return projects;
134
+ }
135
+ function findTypeScriptConfig(startPath) {
136
+ return ts.findConfigFile(startPath, ts.sys.fileExists, "tsconfig.json") ?? null;
137
+ }
138
+ function loadCompilerOptions(tsConfigPath) {
139
+ if (!tsConfigPath) {
140
+ return null;
141
+ }
142
+ const readResult = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
143
+ if (readResult.error) {
144
+ throw new Error(`Failed to read tsconfig: ${formatDiagnostic(readResult.error)}`);
145
+ }
146
+ const parsed = ts.parseJsonConfigFileContent(readResult.config, ts.sys, path.dirname(tsConfigPath), undefined, tsConfigPath);
147
+ const blockingDiagnostic = parsed.errors.find((diagnostic) => !isIgnorableConfigDiagnostic(diagnostic));
148
+ if (blockingDiagnostic) {
149
+ throw new Error(`Failed to parse tsconfig: ${formatDiagnostic(blockingDiagnostic)}`);
150
+ }
151
+ return parsed.options;
152
+ }
153
+ function isIgnorableConfigDiagnostic(diagnostic) {
154
+ return diagnostic.code === 18003;
155
+ }
156
+ function formatDiagnostic(diagnostic) {
157
+ return ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
158
+ }
159
+ function isRelativeOrAbsoluteImport(importPath) {
160
+ return importPath.startsWith(".") || importPath.startsWith("/");
161
+ }
162
+ function resolveRelativeImport(importPath, fromFile, projectRoot) {
163
+ return resolveFileCandidate(path.resolve(path.dirname(fromFile), importPath), projectRoot);
164
+ }
165
+ function resolveFileCandidate(candidatePath, projectRoot) {
166
+ const normalizedCandidate = path.resolve(candidatePath);
167
+ if (fs.existsSync(normalizedCandidate) && fs.statSync(normalizedCandidate).isFile()) {
168
+ return normalizeResolvedPath(normalizedCandidate, projectRoot);
169
+ }
170
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".d.ts"];
171
+ for (const extension of extensions) {
172
+ const candidateWithExtension = normalizedCandidate + extension;
173
+ if (fs.existsSync(candidateWithExtension)) {
174
+ return normalizeResolvedPath(candidateWithExtension, projectRoot);
175
+ }
176
+ }
177
+ const indexExtensions = [
178
+ "/index.ts",
179
+ "/index.tsx",
180
+ "/index.js",
181
+ "/index.jsx",
182
+ "/index.d.ts",
183
+ ];
184
+ for (const extension of indexExtensions) {
185
+ const candidateWithIndex = normalizedCandidate + extension;
186
+ if (fs.existsSync(candidateWithIndex)) {
187
+ return normalizeResolvedPath(candidateWithIndex, projectRoot);
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+ function normalizeResolvedPath(resolvedPath, projectRoot, allowOutsideProjectRoot = false) {
193
+ const normalizedPath = path.resolve(resolvedPath);
194
+ if (!allowOutsideProjectRoot && !isPathInsideRoot(normalizedPath, projectRoot)) {
195
+ return null;
196
+ }
197
+ if (normalizedPath.includes(`${path.sep}node_modules${path.sep}`)) {
198
+ return null;
199
+ }
200
+ return normalizedPath;
201
+ }
202
+ function isPathInsideRoot(targetPath, rootPath) {
203
+ const relativePath = path.relative(rootPath, targetPath);
204
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
205
+ }
@@ -0,0 +1,7 @@
1
+ /** Workspace package discovery for monorepo import resolution. */
2
+ export interface WorkspacePackage {
3
+ name: string;
4
+ rootDir: string;
5
+ }
6
+ export declare function discoverWorkspacePackages(projectRoot: string): Map<string, string>;
7
+ export declare function resolveWorkspacePackageEntry(packageRoot: string, importPath: string, projectRoot: string): string | null;
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ /** Workspace package discovery for monorepo import resolution. */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.discoverWorkspacePackages = discoverWorkspacePackages;
38
+ exports.resolveWorkspacePackageEntry = resolveWorkspacePackageEntry;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ function discoverWorkspacePackages(projectRoot) {
42
+ const packages = new Map();
43
+ const workspaceRoots = findWorkspaceRoots(projectRoot);
44
+ for (const workspaceRoot of workspaceRoots) {
45
+ for (const packageInfo of listWorkspacePackages(workspaceRoot)) {
46
+ packages.set(packageInfo.name, packageInfo.rootDir);
47
+ }
48
+ }
49
+ return packages;
50
+ }
51
+ function findWorkspaceRoots(startPath) {
52
+ const roots = [];
53
+ let currentPath = path.resolve(startPath);
54
+ while (true) {
55
+ const packageJsonPath = path.join(currentPath, "package.json");
56
+ if (fs.existsSync(packageJsonPath)) {
57
+ try {
58
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
59
+ if (packageJson.workspaces) {
60
+ roots.push(currentPath);
61
+ }
62
+ }
63
+ catch {
64
+ // Ignore malformed package.json files while walking parents.
65
+ }
66
+ }
67
+ const parentPath = path.dirname(currentPath);
68
+ if (parentPath === currentPath) {
69
+ break;
70
+ }
71
+ currentPath = parentPath;
72
+ }
73
+ return roots;
74
+ }
75
+ function listWorkspacePackages(workspaceRoot) {
76
+ const packageJsonPath = path.join(workspaceRoot, "package.json");
77
+ if (!fs.existsSync(packageJsonPath)) {
78
+ return [];
79
+ }
80
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
81
+ const patterns = normalizeWorkspacePatterns(packageJson.workspaces);
82
+ const discovered = new Map();
83
+ for (const pattern of patterns) {
84
+ for (const packageDir of expandWorkspacePattern(workspaceRoot, pattern)) {
85
+ const childPackageJsonPath = path.join(packageDir, "package.json");
86
+ if (!fs.existsSync(childPackageJsonPath)) {
87
+ continue;
88
+ }
89
+ try {
90
+ const childPackageJson = JSON.parse(fs.readFileSync(childPackageJsonPath, "utf-8"));
91
+ if (typeof childPackageJson.name === "string" && childPackageJson.name.length > 0) {
92
+ discovered.set(childPackageJson.name, {
93
+ name: childPackageJson.name,
94
+ rootDir: packageDir,
95
+ });
96
+ }
97
+ }
98
+ catch {
99
+ // Skip unreadable workspace packages.
100
+ }
101
+ }
102
+ }
103
+ return Array.from(discovered.values());
104
+ }
105
+ function normalizeWorkspacePatterns(workspaces) {
106
+ if (!workspaces) {
107
+ return [];
108
+ }
109
+ if (Array.isArray(workspaces)) {
110
+ return workspaces;
111
+ }
112
+ return workspaces.packages ?? [];
113
+ }
114
+ function expandWorkspacePattern(workspaceRoot, pattern) {
115
+ if (!pattern.includes("*")) {
116
+ const candidate = path.resolve(workspaceRoot, pattern);
117
+ return fs.existsSync(candidate) ? [candidate] : [];
118
+ }
119
+ const normalizedPattern = pattern.replace(/\\/g, "/");
120
+ const wildcardIndex = normalizedPattern.indexOf("*");
121
+ const prefix = normalizedPattern.slice(0, wildcardIndex);
122
+ const suffix = normalizedPattern.slice(wildcardIndex + 1);
123
+ const searchRoot = path.resolve(workspaceRoot, prefix);
124
+ if (!fs.existsSync(searchRoot)) {
125
+ return [];
126
+ }
127
+ const results = [];
128
+ walkForWorkspaceMatches(searchRoot, suffix, results);
129
+ return results;
130
+ }
131
+ function walkForWorkspaceMatches(currentDir, suffix, results) {
132
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
133
+ for (const entry of entries) {
134
+ if (!entry.isDirectory() || entry.name === "node_modules" || entry.name.startsWith(".")) {
135
+ continue;
136
+ }
137
+ const fullPath = path.join(currentDir, entry.name);
138
+ const relativeSuffix = suffix.replace(/^\//, "");
139
+ if (relativeSuffix.length === 0) {
140
+ results.push(fullPath);
141
+ continue;
142
+ }
143
+ if (relativeSuffix.startsWith("*/")) {
144
+ walkForWorkspaceMatches(fullPath, relativeSuffix.slice(2), results);
145
+ continue;
146
+ }
147
+ if (relativeSuffix === "*") {
148
+ results.push(fullPath);
149
+ continue;
150
+ }
151
+ const nextSegment = relativeSuffix.split("/")[0];
152
+ if (entry.name === nextSegment) {
153
+ const remainingSuffix = relativeSuffix.slice(nextSegment.length);
154
+ if (remainingSuffix.length === 0 || remainingSuffix.startsWith("/")) {
155
+ walkForWorkspaceMatches(fullPath, remainingSuffix.replace(/^\//, ""), results);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ function resolveWorkspacePackageEntry(packageRoot, importPath, projectRoot) {
161
+ const packageJsonPath = path.join(packageRoot, "package.json");
162
+ if (!fs.existsSync(packageJsonPath)) {
163
+ return null;
164
+ }
165
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
166
+ const packageName = packageJson.name;
167
+ if (!packageName) {
168
+ return null;
169
+ }
170
+ let subpath;
171
+ if (importPath === packageName) {
172
+ subpath = undefined;
173
+ }
174
+ else if (importPath.startsWith(`${packageName}/`)) {
175
+ subpath = importPath.slice(packageName.length + 1);
176
+ }
177
+ else {
178
+ return null;
179
+ }
180
+ const entryPath = resolvePackageExportTarget(packageJson, subpath);
181
+ if (!entryPath) {
182
+ return null;
183
+ }
184
+ const resolvedCandidate = path.resolve(packageRoot, entryPath);
185
+ return resolveSourceFileCandidate(resolvedCandidate, projectRoot);
186
+ }
187
+ function resolvePackageExportTarget(packageJson, subpath) {
188
+ if (subpath && packageJson.exports && typeof packageJson.exports === "object") {
189
+ const exportKey = `./${subpath}`;
190
+ const exportValue = packageJson.exports[exportKey] ?? packageJson.exports[subpath];
191
+ const resolvedExport = pickExportTarget(exportValue);
192
+ if (resolvedExport) {
193
+ return resolvedExport;
194
+ }
195
+ }
196
+ if (!subpath) {
197
+ const exportRoot = pickExportTarget(packageJson.exports);
198
+ if (exportRoot) {
199
+ return exportRoot;
200
+ }
201
+ return packageJson.types ?? packageJson.module ?? packageJson.main ?? "index.ts";
202
+ }
203
+ return path.join(subpath, "index.ts");
204
+ }
205
+ function pickExportTarget(value) {
206
+ if (typeof value === "string") {
207
+ return value;
208
+ }
209
+ if (!value || typeof value !== "object") {
210
+ return null;
211
+ }
212
+ const record = value;
213
+ const preferredKeys = ["types", "import", "default", "require"];
214
+ for (const key of preferredKeys) {
215
+ const candidate = record[key];
216
+ if (typeof candidate === "string") {
217
+ return candidate;
218
+ }
219
+ }
220
+ return null;
221
+ }
222
+ function resolveSourceFileCandidate(candidatePath, projectRoot) {
223
+ const normalizedCandidate = path.resolve(candidatePath);
224
+ if (fs.existsSync(normalizedCandidate) && fs.statSync(normalizedCandidate).isFile()) {
225
+ return normalizePathInsideProject(normalizedCandidate, projectRoot);
226
+ }
227
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".d.ts"];
228
+ for (const extension of extensions) {
229
+ const candidateWithExtension = normalizedCandidate + extension;
230
+ if (fs.existsSync(candidateWithExtension)) {
231
+ return normalizePathInsideProject(candidateWithExtension, projectRoot);
232
+ }
233
+ }
234
+ for (const extension of ["/index.ts", "/index.tsx", "/index.js", "/index.jsx"]) {
235
+ const candidateWithIndex = normalizedCandidate + extension;
236
+ if (fs.existsSync(candidateWithIndex)) {
237
+ return normalizePathInsideProject(candidateWithIndex, projectRoot);
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+ function normalizePathInsideProject(targetPath, projectRoot) {
243
+ const normalizedPath = path.resolve(targetPath);
244
+ const relativePath = path.relative(projectRoot, normalizedPath);
245
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
246
+ return normalizedPath;
247
+ }
248
+ if (normalizedPath.includes(`${path.sep}node_modules${path.sep}`)) {
249
+ return null;
250
+ }
251
+ return normalizedPath;
252
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@lifesavertech/archguard-core",
3
+ "version": "2.0.0",
4
+ "description": "Core rule engine for ArchGuard",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "engines": {
11
+ "node": ">=20"
12
+ },
13
+ "keywords": [
14
+ "archguard",
15
+ "architecture",
16
+ "typescript"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -b --force",
23
+ "prepack": "npm run build",
24
+ "build:test": "node -e \"require('fs').rmSync('dist-test',{recursive:true,force:true})\" && tsc -p tsconfig.test.json",
25
+ "test": "npm run build:test && node --test dist-test/index.test.js"
26
+ },
27
+ "dependencies": {
28
+ "typescript": "^5.0.0",
29
+ "yaml": "^2.3.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.0.0"
33
+ },
34
+ "author": "Lindsey Stead",
35
+ "homepage": "https://github.com/lindseystead/ArchGuard#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/lindseystead/ArchGuard/issues"
38
+ },
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/lindseystead/ArchGuard.git",
43
+ "directory": "packages/core"
44
+ }
45
+ }