@timothyrusso/arch-rules 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 +66 -0
- package/dist/createArchRules.d.ts +39 -0
- package/dist/createArchRules.d.ts.map +1 -0
- package/dist/createArchRules.js +62 -0
- package/dist/createArchRules.js.map +1 -0
- package/dist/effectRules.d.ts +12 -0
- package/dist/effectRules.d.ts.map +1 -0
- package/dist/effectRules.js +50 -0
- package/dist/effectRules.js.map +1 -0
- package/dist/featureTiers.d.ts +20 -0
- package/dist/featureTiers.d.ts.map +1 -0
- package/dist/featureTiers.js +50 -0
- package/dist/featureTiers.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/indexBoundaryRules.d.ts +8 -0
- package/dist/indexBoundaryRules.d.ts.map +1 -0
- package/dist/indexBoundaryRules.js +21 -0
- package/dist/indexBoundaryRules.js.map +1 -0
- package/dist/layerRules.d.ts +12 -0
- package/dist/layerRules.d.ts.map +1 -0
- package/dist/layerRules.js +72 -0
- package/dist/layerRules.js.map +1 -0
- package/dist/paths.d.ts +27 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +26 -0
- package/dist/paths.js.map +1 -0
- package/dist/tierRules.d.ts +8 -0
- package/dist/tierRules.d.ts.map +1 -0
- package/dist/tierRules.js +35 -0
- package/dist/tierRules.js.map +1 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Timothy Russo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @timothyrusso/arch-rules
|
|
2
|
+
|
|
3
|
+
dependency-cruiser rules for the kit's feature architecture, generated from each feature's
|
|
4
|
+
`FEATURE_TIER` and the app's `kit.config.json`.
|
|
5
|
+
|
|
6
|
+
## Use
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install -D @timothyrusso/arch-rules @timothyrusso/config-presets dependency-cruiser
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`.dependency-cruiser.js` at the app root:
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import { createDependencyCruiserConfig } from '@timothyrusso/arch-rules';
|
|
16
|
+
import { loadKitConfig } from '@timothyrusso/config-presets';
|
|
17
|
+
|
|
18
|
+
export default createDependencyCruiserConfig(loadKitConfig());
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then `npx depcruise features app --config .dependency-cruiser.js` (use `src/features` if that is
|
|
22
|
+
the app's `featuresRoot`).
|
|
23
|
+
|
|
24
|
+
`createArchRules(kitConfig, options)` returns only the `forbidden` array, for apps that keep their
|
|
25
|
+
own resolver options. Both read `featuresRoot` and `appRoot` from the config; `options` takes:
|
|
26
|
+
|
|
27
|
+
| Option | Default | Meaning |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `rootDir` | `process.cwd()` | App root the two folders are relative to |
|
|
30
|
+
| `featureTiers` | scanned | Feature path to tier, instead of reading each `index.ts` |
|
|
31
|
+
| `tsxPublicApiExceptions` | `core/navigation`, `core/design-system` | Features whose public API a `.tsx` may import at runtime |
|
|
32
|
+
| `domainPublicApiExceptions` | `core/error` | Features whose public API `domain/` may import at runtime |
|
|
33
|
+
| `tsConfigFile` | `tsconfig.json` | `createDependencyCruiserConfig` only |
|
|
34
|
+
|
|
35
|
+
## Tiers
|
|
36
|
+
|
|
37
|
+
Every feature declares its tier in its `index.ts`:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
export const FEATURE_TIER: FeatureTier = 2;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Tiers are the integers 0 to 5. A feature imports only strictly lower tiers; Tier 0 (core) may
|
|
44
|
+
also import Tier 0. Features are found one and two levels under `featuresRoot`
|
|
45
|
+
(`features/<name>` and `features/core/<concern>`).
|
|
46
|
+
|
|
47
|
+
## Rules
|
|
48
|
+
|
|
49
|
+
| Rule | Forbids |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `no-tier-violation-<feature>` | Importing a same-tier peer or a higher tier (Tier 0 peers allowed) |
|
|
52
|
+
| `enforce-index-boundary-<feature>` | Reaching into a feature other than through `index.ts`, `pages.ts` or `assets/` |
|
|
53
|
+
| `tsx-no-direct-layer-import` | A `.tsx` importing facades, data, useCases, di, state, hooks, libraries or mappers |
|
|
54
|
+
| `tsx-no-runtime-domain-import` | A `.tsx` importing runtime values from `domain/` (`import type` is fine) |
|
|
55
|
+
| `tsx-no-cross-feature-public-api` | A `.tsx` outside `appRoot` importing runtime values from a feature `index.ts` |
|
|
56
|
+
| `domain-no-outer-layer-import` | `domain/` importing any outer layer or `design-system/` |
|
|
57
|
+
| `usecases-no-data-import` | `useCases/` importing `data/` |
|
|
58
|
+
| `domain-no-cross-feature-runtime-import` | `domain/` importing another feature's `index.ts` |
|
|
59
|
+
| `no-circular` | Circular dependencies |
|
|
60
|
+
| `effect-only-in-inner-layers` | `effect` or `@effect/*` outside `domain/`, `data/`, `useCases/`, `di/` and `core/runtime`, `core/error`, `core/testing` |
|
|
61
|
+
| `domain-pure-except-effect` | `domain/` importing anything from `node_modules` except `effect` |
|
|
62
|
+
| `facades-run-through-boundary` | `facades/` importing `core/runtime`, `effect/ManagedRuntime` or `effect/Runtime` |
|
|
63
|
+
|
|
64
|
+
dependency-cruiser sees modules, not imported names. A facade calling `Effect.runPromise` is
|
|
65
|
+
already caught by `effect-only-in-inner-layers` (facades import no `effect` at all); the
|
|
66
|
+
name-level check belongs to the ESLint plugin.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { IConfiguration } from 'dependency-cruiser';
|
|
2
|
+
import { type FeatureTiers } from './featureTiers.js';
|
|
3
|
+
import { type ArchKitConfig, type ArchRule } from './paths.js';
|
|
4
|
+
/** Feature folders whose public API a `.tsx` may import at runtime, relative to `featuresRoot`. */
|
|
5
|
+
export declare const DEFAULT_TSX_PUBLIC_API_EXCEPTIONS: readonly ["core/navigation", "core/design-system"];
|
|
6
|
+
/** Feature folders whose public API `domain/` may import at runtime, relative to `featuresRoot`. */
|
|
7
|
+
export declare const DEFAULT_DOMAIN_PUBLIC_API_EXCEPTIONS: readonly ["core/error"];
|
|
8
|
+
export interface ArchRulesOptions {
|
|
9
|
+
/** App root that `featuresRoot` and `appRoot` are relative to. Defaults to `process.cwd()`. */
|
|
10
|
+
readonly rootDir?: string;
|
|
11
|
+
/** Tiers to use instead of scanning each feature's `index.ts`. */
|
|
12
|
+
readonly featureTiers?: FeatureTiers;
|
|
13
|
+
/** Defaults to `DEFAULT_TSX_PUBLIC_API_EXCEPTIONS`. An empty list allows none. */
|
|
14
|
+
readonly tsxPublicApiExceptions?: readonly string[];
|
|
15
|
+
/** Defaults to `DEFAULT_DOMAIN_PUBLIC_API_EXCEPTIONS`. An empty list allows none. */
|
|
16
|
+
readonly domainPublicApiExceptions?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The dependency-cruiser `forbidden` array for an app: tier rules and index boundary rules per
|
|
20
|
+
* feature (from each feature's `FEATURE_TIER`), the layer rules, and the Effect placement rules.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createArchRules(config: ArchKitConfig, options?: ArchRulesOptions): ArchRule[];
|
|
23
|
+
export interface DependencyCruiserConfigOptions extends ArchRulesOptions {
|
|
24
|
+
/** tsconfig the resolver reads path aliases from. Defaults to `tsconfig.json`. */
|
|
25
|
+
readonly tsConfigFile?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A complete dependency-cruiser configuration: `createArchRules` plus the resolver and reporter
|
|
29
|
+
* options an Expo app needs. Use it as the whole `.dependency-cruiser.js`:
|
|
30
|
+
*
|
|
31
|
+
* ```js
|
|
32
|
+
* import { createDependencyCruiserConfig } from '@timothyrusso/arch-rules';
|
|
33
|
+
* import { loadKitConfig } from '@timothyrusso/config-presets';
|
|
34
|
+
*
|
|
35
|
+
* export default createDependencyCruiserConfig(loadKitConfig());
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function createDependencyCruiserConfig(config: ArchKitConfig, options?: DependencyCruiserConfigOptions): IConfiguration;
|
|
39
|
+
//# sourceMappingURL=createArchRules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createArchRules.d.ts","sourceRoot":"","sources":["../src/createArchRules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,KAAK,YAAY,EAAoB,MAAM,mBAAmB,CAAC;AAGxE,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAe,MAAM,YAAY,CAAC;AAG5E,mGAAmG;AACnG,eAAO,MAAM,iCAAiC,oDAAqD,CAAC;AAEpG,oGAAoG;AACpG,eAAO,MAAM,oCAAoC,yBAA0B,CAAC;AAE5E,MAAM,WAAW,gBAAgB;IAC/B,+FAA+F;IAC/F,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC;IACrC,kFAAkF;IAClF,QAAQ,CAAC,sBAAsB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpD,qFAAqF;IACrF,QAAQ,CAAC,yBAAyB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,gBAAqB,GAAG,QAAQ,EAAE,CAajG;AAED,MAAM,WAAW,8BAA+B,SAAQ,gBAAgB;IACtE,kFAAkF;IAClF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,aAAa,EACrB,OAAO,GAAE,8BAAmC,GAC3C,cAAc,CAqBhB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { generateEffectRules } from './effectRules.js';
|
|
2
|
+
import { findFeatureTiers } from './featureTiers.js';
|
|
3
|
+
import { generateIndexBoundaryRules } from './indexBoundaryRules.js';
|
|
4
|
+
import { generateLayerRules } from './layerRules.js';
|
|
5
|
+
import { escapeRegex } from './paths.js';
|
|
6
|
+
import { generateTierRules } from './tierRules.js';
|
|
7
|
+
/** Feature folders whose public API a `.tsx` may import at runtime, relative to `featuresRoot`. */
|
|
8
|
+
export const DEFAULT_TSX_PUBLIC_API_EXCEPTIONS = ['core/navigation', 'core/design-system'];
|
|
9
|
+
/** Feature folders whose public API `domain/` may import at runtime, relative to `featuresRoot`. */
|
|
10
|
+
export const DEFAULT_DOMAIN_PUBLIC_API_EXCEPTIONS = ['core/error'];
|
|
11
|
+
/**
|
|
12
|
+
* The dependency-cruiser `forbidden` array for an app: tier rules and index boundary rules per
|
|
13
|
+
* feature (from each feature's `FEATURE_TIER`), the layer rules, and the Effect placement rules.
|
|
14
|
+
*/
|
|
15
|
+
export function createArchRules(config, options = {}) {
|
|
16
|
+
const featureTiers = options.featureTiers ?? findFeatureTiers(options.rootDir ?? process.cwd(), config.featuresRoot);
|
|
17
|
+
return [
|
|
18
|
+
...generateTierRules(featureTiers),
|
|
19
|
+
...generateIndexBoundaryRules(featureTiers),
|
|
20
|
+
...generateLayerRules({
|
|
21
|
+
featuresRoot: config.featuresRoot,
|
|
22
|
+
appRoot: config.appRoot,
|
|
23
|
+
tsxPublicApiExceptions: options.tsxPublicApiExceptions ?? DEFAULT_TSX_PUBLIC_API_EXCEPTIONS,
|
|
24
|
+
domainPublicApiExceptions: options.domainPublicApiExceptions ?? DEFAULT_DOMAIN_PUBLIC_API_EXCEPTIONS,
|
|
25
|
+
}),
|
|
26
|
+
...generateEffectRules({ featuresRoot: config.featuresRoot, appRoot: config.appRoot }),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A complete dependency-cruiser configuration: `createArchRules` plus the resolver and reporter
|
|
31
|
+
* options an Expo app needs. Use it as the whole `.dependency-cruiser.js`:
|
|
32
|
+
*
|
|
33
|
+
* ```js
|
|
34
|
+
* import { createDependencyCruiserConfig } from '@timothyrusso/arch-rules';
|
|
35
|
+
* import { loadKitConfig } from '@timothyrusso/config-presets';
|
|
36
|
+
*
|
|
37
|
+
* export default createDependencyCruiserConfig(loadKitConfig());
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function createDependencyCruiserConfig(config, options = {}) {
|
|
41
|
+
const roots = `(?:${escapeRegex(config.featuresRoot)}|${escapeRegex(config.appRoot)})`;
|
|
42
|
+
return {
|
|
43
|
+
forbidden: createArchRules(config, options),
|
|
44
|
+
options: {
|
|
45
|
+
doNotFollow: { path: ['node_modules'] },
|
|
46
|
+
tsPreCompilationDeps: true,
|
|
47
|
+
tsConfig: { fileName: options.tsConfigFile ?? 'tsconfig.json' },
|
|
48
|
+
enhancedResolveOptions: {
|
|
49
|
+
exportsFields: ['exports'],
|
|
50
|
+
conditionNames: ['react-native', 'import', 'require', 'node', 'default', 'types'],
|
|
51
|
+
mainFields: ['main', 'types', 'typings'],
|
|
52
|
+
},
|
|
53
|
+
skipAnalysisNotInRules: true,
|
|
54
|
+
reporterOptions: {
|
|
55
|
+
dot: { collapsePattern: 'node_modules/(?:@[^/]+/[^/]+|[^/]+)' },
|
|
56
|
+
archi: { collapsePattern: `^${roots}/[^/]+|node_modules/(?:@[^/]+/[^/]+|[^/]+)` },
|
|
57
|
+
text: { highlightFocused: true },
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=createArchRules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createArchRules.js","sourceRoot":"","sources":["../src/createArchRules.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAqB,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,EAAqC,WAAW,EAAE,MAAM,YAAY,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEnD,mGAAmG;AACnG,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,iBAAiB,EAAE,oBAAoB,CAAU,CAAC;AAEpG,oGAAoG;AACpG,MAAM,CAAC,MAAM,oCAAoC,GAAG,CAAC,YAAY,CAAU,CAAC;AAa5E;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAqB,EAAE,UAA4B,EAAE;IACnF,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,gBAAgB,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IACrH,OAAO;QACL,GAAG,iBAAiB,CAAC,YAAY,CAAC;QAClC,GAAG,0BAA0B,CAAC,YAAY,CAAC;QAC3C,GAAG,kBAAkB,CAAC;YACpB,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,sBAAsB,EAAE,OAAO,CAAC,sBAAsB,IAAI,iCAAiC;YAC3F,yBAAyB,EAAE,OAAO,CAAC,yBAAyB,IAAI,oCAAoC;SACrG,CAAC;QACF,GAAG,mBAAmB,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC;KACvF,CAAC;AACJ,CAAC;AAOD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,6BAA6B,CAC3C,MAAqB,EACrB,UAA0C,EAAE;IAE5C,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC;IACvF,OAAO;QACL,SAAS,EAAE,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC;QAC3C,OAAO,EAAE;YACP,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC,cAAc,CAAC,EAAE;YACvC,oBAAoB,EAAE,IAAI;YAC1B,QAAQ,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,YAAY,IAAI,eAAe,EAAE;YAC/D,sBAAsB,EAAE;gBACtB,aAAa,EAAE,CAAC,SAAS,CAAC;gBAC1B,cAAc,EAAE,CAAC,cAAc,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC;gBACjF,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC;aACzC;YACD,sBAAsB,EAAE,IAAI;YAC5B,eAAe,EAAE;gBACf,GAAG,EAAE,EAAE,eAAe,EAAE,qCAAqC,EAAE;gBAC/D,KAAK,EAAE,EAAE,eAAe,EAAE,IAAI,KAAK,4CAA4C,EAAE;gBACjF,IAAI,EAAE,EAAE,gBAAgB,EAAE,IAAI,EAAE;aACjC;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ArchRule } from './paths.js';
|
|
2
|
+
/** Layers inside a feature that may use Effect. */
|
|
3
|
+
export declare const EFFECT_LAYERS: readonly ["domain", "data", "useCases", "di"];
|
|
4
|
+
/** Core concerns, relative to the features folder, that may use Effect outside those layers. */
|
|
5
|
+
export declare const EFFECT_CORE_FOLDERS: readonly ["core/runtime", "core/error", "core/testing"];
|
|
6
|
+
export interface EffectRulesInput {
|
|
7
|
+
readonly featuresRoot: string;
|
|
8
|
+
readonly appRoot: string;
|
|
9
|
+
}
|
|
10
|
+
/** Where Effect may live, what `domain/` may depend on, and how facades run Effects. */
|
|
11
|
+
export declare function generateEffectRules(input: EffectRulesInput): ArchRule[];
|
|
12
|
+
//# sourceMappingURL=effectRules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effectRules.d.ts","sourceRoot":"","sources":["../src/effectRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAA4B,MAAM,YAAY,CAAC;AAErE,mDAAmD;AACnD,eAAO,MAAM,aAAa,+CAAgD,CAAC;AAE3E,gGAAgG;AAChG,eAAO,MAAM,mBAAmB,yDAA0D,CAAC;AAkB3F,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wFAAwF;AACxF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,QAAQ,EAAE,CA+BvE"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { alternation, escapeRegex } from './paths.js';
|
|
2
|
+
/** Layers inside a feature that may use Effect. */
|
|
3
|
+
export const EFFECT_LAYERS = ['domain', 'data', 'useCases', 'di'];
|
|
4
|
+
/** Core concerns, relative to the features folder, that may use Effect outside those layers. */
|
|
5
|
+
export const EFFECT_CORE_FOLDERS = ['core/runtime', 'core/error', 'core/testing'];
|
|
6
|
+
/**
|
|
7
|
+
* `effect` or an `@effect/*` package, whether resolved into `node_modules` or left unresolved
|
|
8
|
+
* as a bare specifier.
|
|
9
|
+
*/
|
|
10
|
+
const EFFECT_MODULE = '(^|/)node_modules/(effect|@effect/[^/]+)/|^(effect|@effect/[^/]+)(/|$)';
|
|
11
|
+
/** The `effect` package itself, resolved or bare. */
|
|
12
|
+
const EFFECT_PACKAGE = '(^|/)node_modules/effect/|^effect(/|$)';
|
|
13
|
+
/**
|
|
14
|
+
* The modules that construct or run a runtime, `effect/ManagedRuntime` and `effect/Runtime`, bare or
|
|
15
|
+
* resolved to their JavaScript or declaration file.
|
|
16
|
+
*/
|
|
17
|
+
const EFFECT_RUNTIME_MODULE = '^effect/(ManagedRuntime|Runtime)$|(^|/)node_modules/effect/(ManagedRuntime|Runtime)(\\.d)?\\.[cm]?[jt]s$|(^|/)node_modules/effect/.*/(ManagedRuntime|Runtime)(\\.d)?\\.[cm]?[jt]s$';
|
|
18
|
+
/** Where Effect may live, what `domain/` may depend on, and how facades run Effects. */
|
|
19
|
+
export function generateEffectRules(input) {
|
|
20
|
+
const features = escapeRegex(input.featuresRoot);
|
|
21
|
+
const app = escapeRegex(input.appRoot);
|
|
22
|
+
const allowedCore = EFFECT_CORE_FOLDERS.map(folder => `${input.featuresRoot}/${folder}/`).join(', ');
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
name: 'effect-only-in-inner-layers',
|
|
26
|
+
comment: `effect and @effect/* may be imported only from domain/, data/, useCases/ and di/, plus ${allowedCore}. Never from ui/, facades/, hooks/, state/ or ${input.appRoot}/: facades hand Effects to useEffectQuery and useEffectMutation`,
|
|
27
|
+
severity: 'error',
|
|
28
|
+
from: {
|
|
29
|
+
path: `^(${features}|${app})/`,
|
|
30
|
+
pathNot: `/(${EFFECT_LAYERS.join('|')})/|^${features}/${alternation(EFFECT_CORE_FOLDERS)}/`,
|
|
31
|
+
},
|
|
32
|
+
to: { path: EFFECT_MODULE },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'domain-pure-except-effect',
|
|
36
|
+
comment: 'domain/ may import effect and nothing else from node_modules: it holds plain types, rules and errors',
|
|
37
|
+
severity: 'error',
|
|
38
|
+
from: { path: '/domain/' },
|
|
39
|
+
to: { path: '(^|/)node_modules/', pathNot: EFFECT_PACKAGE },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'facades-run-through-boundary',
|
|
43
|
+
comment: `facades/ never touch the runtime: no ${input.featuresRoot}/core/runtime/, no ManagedRuntime, no Effect.run*. They run Effects only through useEffectQuery and useEffectMutation from @timothyrusso/effect-core/react`,
|
|
44
|
+
severity: 'error',
|
|
45
|
+
from: { path: '/facades/' },
|
|
46
|
+
to: { path: `^${features}/core/runtime/|${EFFECT_RUNTIME_MODULE}` },
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=effectRules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effectRules.js","sourceRoot":"","sources":["../src/effectRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAErE,mDAAmD;AACnD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,CAAU,CAAC;AAE3E,gGAAgG;AAChG,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,cAAc,EAAE,YAAY,EAAE,cAAc,CAAU,CAAC;AAE3F;;;GAGG;AACH,MAAM,aAAa,GAAG,wEAAwE,CAAC;AAE/F,qDAAqD;AACrD,MAAM,cAAc,GAAG,wCAAwC,CAAC;AAEhE;;;GAGG;AACH,MAAM,qBAAqB,GACzB,oLAAoL,CAAC;AAOvL,wFAAwF;AACxF,MAAM,UAAU,mBAAmB,CAAC,KAAuB;IACzD,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,WAAW,GAAG,mBAAmB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,YAAY,IAAI,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAErG,OAAO;QACL;YACE,IAAI,EAAE,6BAA6B;YACnC,OAAO,EAAE,0FAA0F,WAAW,iDAAiD,KAAK,CAAC,OAAO,iEAAiE;YAC7O,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE;gBACJ,IAAI,EAAE,KAAK,QAAQ,IAAI,GAAG,IAAI;gBAC9B,OAAO,EAAE,KAAK,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,QAAQ,IAAI,WAAW,CAAC,mBAAmB,CAAC,GAAG;aAC5F;YACD,EAAE,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE;SAC5B;QACD;YACE,IAAI,EAAE,2BAA2B;YACjC,OAAO,EAAE,sGAAsG;YAC/G,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;YAC1B,EAAE,EAAE,EAAE,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,cAAc,EAAE;SAC5D;QACD;YACE,IAAI,EAAE,8BAA8B;YACpC,OAAO,EAAE,wCAAwC,KAAK,CAAC,YAAY,4JAA4J;YAC/N,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE;YAC3B,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,QAAQ,kBAAkB,qBAAqB,EAAE,EAAE;SACpE;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Every tier a feature may declare, lowest first. */
|
|
2
|
+
export declare const FEATURE_TIERS: readonly [0, 1, 2, 3, 4, 5];
|
|
3
|
+
/**
|
|
4
|
+
* A feature's position in the dependency order. A feature imports only strictly lower tiers;
|
|
5
|
+
* Tier 0 (core) may also import Tier 0.
|
|
6
|
+
*/
|
|
7
|
+
export type FeatureTier = (typeof FEATURE_TIERS)[number];
|
|
8
|
+
/** Feature folder (relative to the app root, e.g. `features/trips`) to its declared tier. */
|
|
9
|
+
export type FeatureTiers = Readonly<Record<string, FeatureTier>>;
|
|
10
|
+
/** Thrown when a feature's `index.ts` declares a tier outside `FEATURE_TIERS`. */
|
|
11
|
+
export declare class FeatureTierError extends Error {
|
|
12
|
+
readonly name = "FeatureTierError";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Reads the tier of every feature under `featuresRoot`: each folder, one or two levels deep
|
|
16
|
+
* (`features/<name>` and `features/core/<concern>`), whose `index.ts` declares `FEATURE_TIER`.
|
|
17
|
+
* Keys are relative to `rootDir` with forward slashes, sorted.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findFeatureTiers(rootDir: string, featuresRoot: string): FeatureTiers;
|
|
20
|
+
//# sourceMappingURL=featureTiers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"featureTiers.d.ts","sourceRoot":"","sources":["../src/featureTiers.ts"],"names":[],"mappings":"AAGA,sDAAsD;AACtD,eAAO,MAAM,aAAa,6BAA8B,CAAC;AAEzD;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,6FAA6F;AAC7F,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;AAEjE,kFAAkF;AAClF,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,SAAkB,IAAI,sBAAsB;CAC7C;AAQD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,YAAY,CAiCpF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve } from 'node:path';
|
|
3
|
+
/** Every tier a feature may declare, lowest first. */
|
|
4
|
+
export const FEATURE_TIERS = [0, 1, 2, 3, 4, 5];
|
|
5
|
+
/** Thrown when a feature's `index.ts` declares a tier outside `FEATURE_TIERS`. */
|
|
6
|
+
export class FeatureTierError extends Error {
|
|
7
|
+
name = 'FeatureTierError';
|
|
8
|
+
}
|
|
9
|
+
const TIER_REGEX = /export const FEATURE_TIER(?:\s*:\s*FeatureTier)?\s*=\s*(\d+)/;
|
|
10
|
+
function isFeatureTier(value) {
|
|
11
|
+
return FEATURE_TIERS.includes(value);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reads the tier of every feature under `featuresRoot`: each folder, one or two levels deep
|
|
15
|
+
* (`features/<name>` and `features/core/<concern>`), whose `index.ts` declares `FEATURE_TIER`.
|
|
16
|
+
* Keys are relative to `rootDir` with forward slashes, sorted.
|
|
17
|
+
*/
|
|
18
|
+
export function findFeatureTiers(rootDir, featuresRoot) {
|
|
19
|
+
const root = resolve(rootDir);
|
|
20
|
+
const results = {};
|
|
21
|
+
const walk = (dir, depth) => {
|
|
22
|
+
if (depth > 1)
|
|
23
|
+
return;
|
|
24
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
25
|
+
.filter(entry => entry.isDirectory())
|
|
26
|
+
.map(entry => entry.name)
|
|
27
|
+
.sort();
|
|
28
|
+
for (const name of entries) {
|
|
29
|
+
const fullPath = join(dir, name);
|
|
30
|
+
const indexPath = join(fullPath, 'index.ts');
|
|
31
|
+
if (existsSync(indexPath)) {
|
|
32
|
+
const match = readFileSync(indexPath, 'utf8').match(TIER_REGEX);
|
|
33
|
+
if (match?.[1] !== undefined) {
|
|
34
|
+
const tier = Number.parseInt(match[1], 10);
|
|
35
|
+
const featurePath = relative(root, fullPath).replace(/\\/g, '/');
|
|
36
|
+
if (!isFeatureTier(tier)) {
|
|
37
|
+
throw new FeatureTierError(`${featurePath}/index.ts declares FEATURE_TIER = ${tier}; tiers are ${FEATURE_TIERS.join(', ')}.`);
|
|
38
|
+
}
|
|
39
|
+
results[featurePath] = tier;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walk(fullPath, depth + 1);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const featuresDir = join(root, featuresRoot);
|
|
46
|
+
if (existsSync(featuresDir))
|
|
47
|
+
walk(featuresDir, 0);
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=featureTiers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"featureTiers.js","sourceRoot":"","sources":["../src/featureTiers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpD,sDAAsD;AACtD,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAU,CAAC;AAWzD,kFAAkF;AAClF,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IACvB,IAAI,GAAG,kBAAkB,CAAC;CAC7C;AAED,MAAM,UAAU,GAAG,8DAA8D,CAAC;AAElF,SAAS,aAAa,CAAC,KAAa;IAClC,OAAQ,aAAmC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC9D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,YAAoB;IACpE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAgC,EAAE,CAAC;IAEhD,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,KAAa,EAAQ,EAAE;QAChD,IAAI,KAAK,GAAG,CAAC;YAAE,OAAO;QACtB,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC;aACtD,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;aACpC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;aACxB,IAAI,EAAE,CAAC;QACV,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACjC,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YAC7C,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC1B,MAAM,KAAK,GAAG,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;gBAChE,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;oBAC7B,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;oBAC3C,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;oBACjE,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;wBACzB,MAAM,IAAI,gBAAgB,CACxB,GAAG,WAAW,qCAAqC,IAAI,eAAe,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAClG,CAAC;oBACJ,CAAC;oBACD,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;gBAC9B,CAAC;YACH,CAAC;YACD,IAAI,CAAC,QAAQ,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IAC7C,IAAI,UAAU,CAAC,WAAW,CAAC;QAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAClD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @timothyrusso/arch-rules. dependency-cruiser rule generators for feature tiers, layers and Effect placement.
|
|
3
|
+
*/
|
|
4
|
+
export { type ArchRulesOptions, createArchRules, createDependencyCruiserConfig, DEFAULT_DOMAIN_PUBLIC_API_EXCEPTIONS, DEFAULT_TSX_PUBLIC_API_EXCEPTIONS, type DependencyCruiserConfigOptions, } from './createArchRules.js';
|
|
5
|
+
export { EFFECT_CORE_FOLDERS, EFFECT_LAYERS, generateEffectRules } from './effectRules.js';
|
|
6
|
+
export { FEATURE_TIERS, type FeatureTier, FeatureTierError, type FeatureTiers, findFeatureTiers, } from './featureTiers.js';
|
|
7
|
+
export { generateIndexBoundaryRules } from './indexBoundaryRules.js';
|
|
8
|
+
export { generateLayerRules } from './layerRules.js';
|
|
9
|
+
export type { ArchKitConfig, ArchRule } from './paths.js';
|
|
10
|
+
export { generateTierRules } from './tierRules.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EACL,KAAK,gBAAgB,EACrB,eAAe,EACf,6BAA6B,EAC7B,oCAAoC,EACpC,iCAAiC,EACjC,KAAK,8BAA8B,GACpC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC3F,OAAO,EACL,aAAa,EACb,KAAK,WAAW,EAChB,gBAAgB,EAChB,KAAK,YAAY,EACjB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @timothyrusso/arch-rules. dependency-cruiser rule generators for feature tiers, layers and Effect placement.
|
|
3
|
+
*/
|
|
4
|
+
export { createArchRules, createDependencyCruiserConfig, DEFAULT_DOMAIN_PUBLIC_API_EXCEPTIONS, DEFAULT_TSX_PUBLIC_API_EXCEPTIONS, } from './createArchRules.js';
|
|
5
|
+
export { EFFECT_CORE_FOLDERS, EFFECT_LAYERS, generateEffectRules } from './effectRules.js';
|
|
6
|
+
export { FEATURE_TIERS, FeatureTierError, findFeatureTiers, } from './featureTiers.js';
|
|
7
|
+
export { generateIndexBoundaryRules } from './indexBoundaryRules.js';
|
|
8
|
+
export { generateLayerRules } from './layerRules.js';
|
|
9
|
+
export { generateTierRules } from './tierRules.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAEL,eAAe,EACf,6BAA6B,EAC7B,oCAAoC,EACpC,iCAAiC,GAElC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC3F,OAAO,EACL,aAAa,EAEb,gBAAgB,EAEhB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FeatureTiers } from './featureTiers.js';
|
|
2
|
+
import { type ArchRule } from './paths.js';
|
|
3
|
+
/**
|
|
4
|
+
* One `enforce-index-boundary-<feature>` rule per feature: code outside a feature reaches it only
|
|
5
|
+
* through its `index.ts` (public API) or `pages.ts` (router entry), plus its `assets/`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateIndexBoundaryRules(featureTiers: FeatureTiers): ArchRule[];
|
|
8
|
+
//# sourceMappingURL=indexBoundaryRules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexBoundaryRules.d.ts","sourceRoot":"","sources":["../src/indexBoundaryRules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,KAAK,QAAQ,EAAqB,MAAM,YAAY,CAAC;AAE9D;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,YAAY,GAAG,QAAQ,EAAE,CAcjF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { escapeRegex, slug } from './paths.js';
|
|
2
|
+
/**
|
|
3
|
+
* One `enforce-index-boundary-<feature>` rule per feature: code outside a feature reaches it only
|
|
4
|
+
* through its `index.ts` (public API) or `pages.ts` (router entry), plus its `assets/`.
|
|
5
|
+
*/
|
|
6
|
+
export function generateIndexBoundaryRules(featureTiers) {
|
|
7
|
+
return Object.keys(featureTiers).map(featurePath => {
|
|
8
|
+
const own = escapeRegex(featurePath);
|
|
9
|
+
return {
|
|
10
|
+
name: `enforce-index-boundary-${slug(featurePath)}`,
|
|
11
|
+
comment: `All imports into ${featurePath} from outside must go through its index.ts or pages.ts`,
|
|
12
|
+
severity: 'error',
|
|
13
|
+
from: { pathNot: `^${own}/` },
|
|
14
|
+
to: {
|
|
15
|
+
path: `^${own}/`,
|
|
16
|
+
pathNot: `^${own}/(index|pages)\\.ts$|^${own}/assets/`,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=indexBoundaryRules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexBoundaryRules.js","sourceRoot":"","sources":["../src/indexBoundaryRules.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,WAAW,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAE9D;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CAAC,YAA0B;IACnE,OAAO,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE;QACjD,MAAM,GAAG,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;QACrC,OAAO;YACL,IAAI,EAAE,0BAA0B,IAAI,CAAC,WAAW,CAAC,EAAE;YACnD,OAAO,EAAE,oBAAoB,WAAW,wDAAwD;YAChG,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,GAAG,GAAG,EAAE;YAC7B,EAAE,EAAE;gBACF,IAAI,EAAE,IAAI,GAAG,GAAG;gBAChB,OAAO,EAAE,IAAI,GAAG,yBAAyB,GAAG,UAAU;aACvD;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ArchRule } from './paths.js';
|
|
2
|
+
export interface LayerRulesInput {
|
|
3
|
+
readonly featuresRoot: string;
|
|
4
|
+
readonly appRoot: string;
|
|
5
|
+
/** Feature folders, relative to `featuresRoot`, whose public API a `.tsx` may import at runtime. */
|
|
6
|
+
readonly tsxPublicApiExceptions: readonly string[];
|
|
7
|
+
/** Feature folders, relative to `featuresRoot`, whose public API `domain/` may import at runtime. */
|
|
8
|
+
readonly domainPublicApiExceptions: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
/** The layer rules that do not depend on the feature list. */
|
|
11
|
+
export declare function generateLayerRules(input: LayerRulesInput): ArchRule[];
|
|
12
|
+
//# sourceMappingURL=layerRules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layerRules.d.ts","sourceRoot":"","sources":["../src/layerRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAA4B,MAAM,YAAY,CAAC;AAErE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oGAAoG;IACpG,QAAQ,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,CAAC;IACnD,qGAAqG;IACrG,QAAQ,CAAC,yBAAyB,EAAE,SAAS,MAAM,EAAE,CAAC;CACvD;AAMD,8DAA8D;AAC9D,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,eAAe,GAAG,QAAQ,EAAE,CAsErE"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { alternation, escapeRegex } from './paths.js';
|
|
2
|
+
function exceptionPath(features, folders) {
|
|
3
|
+
return folders.length === 0 ? {} : { pathNot: `^${features}/${alternation(folders)}/` };
|
|
4
|
+
}
|
|
5
|
+
/** The layer rules that do not depend on the feature list. */
|
|
6
|
+
export function generateLayerRules(input) {
|
|
7
|
+
const features = escapeRegex(input.featuresRoot);
|
|
8
|
+
const app = escapeRegex(input.appRoot);
|
|
9
|
+
const publicApi = `^${features}/.*/index\\.ts$`;
|
|
10
|
+
const tsxExceptions = input.tsxPublicApiExceptions.map(folder => `${input.featuresRoot}/${folder}`).join(', ');
|
|
11
|
+
const domainExceptions = input.domainPublicApiExceptions.map(folder => `${input.featuresRoot}/${folder}`).join(', ');
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
name: 'tsx-no-direct-layer-import',
|
|
15
|
+
comment: '.tsx files may only import the ViewModel (.logic.ts), UI components and styles, never facades, data, useCases, di, state, hooks, libraries or mappers directly. domain/ is allowed for import type only (see tsx-no-runtime-domain-import)',
|
|
16
|
+
severity: 'error',
|
|
17
|
+
from: { path: '\\.tsx$' },
|
|
18
|
+
to: { path: '/(facades|data|useCases|di|state|hooks|libraries|mappers)/' },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'tsx-no-runtime-domain-import',
|
|
22
|
+
comment: '.tsx files may use import type from domain/ for prop annotations, but must never import runtime values from domain/: all runtime logic flows through the ViewModel (.logic.ts)',
|
|
23
|
+
severity: 'error',
|
|
24
|
+
from: { path: '\\.tsx$' },
|
|
25
|
+
to: { path: '/domain/', dependencyTypesNot: ['type-only'] },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'tsx-no-cross-feature-public-api',
|
|
29
|
+
comment: `.tsx files must not import runtime values from a cross-feature public API (index.ts): cross-feature runtime values flow through the ViewModel (.logic.ts). import type from any feature index.ts is allowed for prop annotations. Route and layout files in ${input.appRoot}/ are thin entry points and may import public APIs directly.${tsxExceptions ? ` Exceptions: ${tsxExceptions}.` : ''}`,
|
|
30
|
+
severity: 'error',
|
|
31
|
+
from: { path: '\\.tsx$', pathNot: `^${app}/` },
|
|
32
|
+
to: {
|
|
33
|
+
path: publicApi,
|
|
34
|
+
...exceptionPath(features, input.tsxPublicApiExceptions),
|
|
35
|
+
dependencyTypesNot: ['type-only'],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'domain-no-outer-layer-import',
|
|
40
|
+
comment: 'domain/ is the innermost layer: it must not import from data/, useCases/, facades/, ui/, state/, hooks/, libraries/, di/ or mappers/, nor from the shared UI module design-system/',
|
|
41
|
+
severity: 'error',
|
|
42
|
+
from: { path: '/domain/' },
|
|
43
|
+
to: { path: '/(data|useCases|facades|ui|state|hooks|libraries|di|mappers|design-system)/' },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'usecases-no-data-import',
|
|
47
|
+
comment: 'useCases/ depend only on domain/ interfaces, never on data/ concrete implementations',
|
|
48
|
+
severity: 'error',
|
|
49
|
+
from: { path: '/useCases/' },
|
|
50
|
+
to: { path: '/data/' },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'domain-no-cross-feature-runtime-import',
|
|
54
|
+
comment: `domain/ files must not import runtime values from another feature's public API (index.ts). Use import type for cross-feature type references.${domainExceptions ? ` Exceptions: ${domainExceptions}.` : ''}`,
|
|
55
|
+
severity: 'error',
|
|
56
|
+
from: { path: '/domain/' },
|
|
57
|
+
to: {
|
|
58
|
+
path: publicApi,
|
|
59
|
+
...exceptionPath(features, input.domainPublicApiExceptions),
|
|
60
|
+
dependencyTypes: ['import'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'no-circular',
|
|
65
|
+
comment: 'Circular dependencies make code hard to reason about and test: resolve them with dependency inversion or by restructuring modules',
|
|
66
|
+
severity: 'error',
|
|
67
|
+
from: {},
|
|
68
|
+
to: { circular: true },
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=layerRules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layerRules.js","sourceRoot":"","sources":["../src/layerRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAWrE,SAAS,aAAa,CAAC,QAAgB,EAAE,OAA0B;IACjE,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,QAAQ,IAAI,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;AAC1F,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,kBAAkB,CAAC,KAAsB;IACvD,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,WAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,SAAS,GAAG,IAAI,QAAQ,iBAAiB,CAAC;IAChD,MAAM,aAAa,GAAG,KAAK,CAAC,sBAAsB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,YAAY,IAAI,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/G,MAAM,gBAAgB,GAAG,KAAK,CAAC,yBAAyB,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,YAAY,IAAI,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAErH,OAAO;QACL;YACE,IAAI,EAAE,4BAA4B;YAClC,OAAO,EACL,4OAA4O;YAC9O,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;YACzB,EAAE,EAAE,EAAE,IAAI,EAAE,4DAA4D,EAAE;SAC3E;QACD;YACE,IAAI,EAAE,8BAA8B;YACpC,OAAO,EACL,gLAAgL;YAClL,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;YACzB,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,kBAAkB,EAAE,CAAC,WAAW,CAAC,EAAE;SAC5D;QACD;YACE,IAAI,EAAE,iCAAiC;YACvC,OAAO,EAAE,+PAA+P,KAAK,CAAC,OAAO,+DAA+D,aAAa,CAAC,CAAC,CAAC,gBAAgB,aAAa,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YAC3Y,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,GAAG,EAAE;YAC9C,EAAE,EAAE;gBACF,IAAI,EAAE,SAAS;gBACf,GAAG,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAC,sBAAsB,CAAC;gBACxD,kBAAkB,EAAE,CAAC,WAAW,CAAC;aAClC;SACF;QACD;YACE,IAAI,EAAE,8BAA8B;YACpC,OAAO,EACL,oLAAoL;YACtL,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;YAC1B,EAAE,EAAE,EAAE,IAAI,EAAE,6EAA6E,EAAE;SAC5F;QACD;YACE,IAAI,EAAE,yBAAyB;YAC/B,OAAO,EAAE,sFAAsF;YAC/F,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE;YAC5B,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;SACvB;QACD;YACE,IAAI,EAAE,wCAAwC;YAC9C,OAAO,EAAE,gJAAgJ,gBAAgB,CAAC,CAAC,CAAC,gBAAgB,gBAAgB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACtN,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;YAC1B,EAAE,EAAE;gBACF,IAAI,EAAE,SAAS;gBACf,GAAG,aAAa,CAAC,QAAQ,EAAE,KAAK,CAAC,yBAAyB,CAAC;gBAC3D,eAAe,EAAE,CAAC,QAAQ,CAAC;aAC5B;SACF;QACD;YACE,IAAI,EAAE,aAAa;YACnB,OAAO,EACL,mIAAmI;YACrI,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,EAAE;YACR,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SACvB;KACF,CAAC;AACJ,CAAC"}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IRegularForbiddenRuleType } from 'dependency-cruiser';
|
|
2
|
+
/** A generated dependency-cruiser `forbidden` rule. Every rule has a name and an error severity. */
|
|
3
|
+
export type ArchRule = IRegularForbiddenRuleType & {
|
|
4
|
+
name: string;
|
|
5
|
+
comment: string;
|
|
6
|
+
severity: 'error';
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The fields of `kit.config.json` the generators read. A `KitConfig` from
|
|
10
|
+
* `@timothyrusso/config-presets` satisfies it.
|
|
11
|
+
*/
|
|
12
|
+
export interface ArchKitConfig {
|
|
13
|
+
/** Folder that holds the feature modules, relative to the app root. */
|
|
14
|
+
readonly featuresRoot: string;
|
|
15
|
+
/** expo-router app folder, relative to the app root. */
|
|
16
|
+
readonly appRoot: string;
|
|
17
|
+
}
|
|
18
|
+
/** Escapes a literal path for use inside a regular expression. Hyphens and slashes stay as they are. */
|
|
19
|
+
export declare function escapeRegex(path: string): string;
|
|
20
|
+
/** Turns a feature path such as `features/core/navigation` into a rule name suffix. */
|
|
21
|
+
export declare function slug(featurePath: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Builds one regex alternation for a list of paths relative to a folder, grouping siblings:
|
|
24
|
+
* `['core/navigation', 'core/design-system']` becomes `core/(navigation|design-system)`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function alternation(paths: readonly string[]): string;
|
|
27
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAEpE,oGAAoG;AACpG,MAAM,MAAM,QAAQ,GAAG,yBAAyB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAExG;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wGAAwG;AACxG,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,uFAAuF;AACvF,wBAAgB,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAc5D"}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Escapes a literal path for use inside a regular expression. Hyphens and slashes stay as they are. */
|
|
2
|
+
export function escapeRegex(path) {
|
|
3
|
+
return path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
4
|
+
}
|
|
5
|
+
/** Turns a feature path such as `features/core/navigation` into a rule name suffix. */
|
|
6
|
+
export function slug(featurePath) {
|
|
7
|
+
return featurePath.replace(/\//g, '-');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Builds one regex alternation for a list of paths relative to a folder, grouping siblings:
|
|
11
|
+
* `['core/navigation', 'core/design-system']` becomes `core/(navigation|design-system)`.
|
|
12
|
+
*/
|
|
13
|
+
export function alternation(paths) {
|
|
14
|
+
const groups = new Map();
|
|
15
|
+
for (const path of paths) {
|
|
16
|
+
const cut = path.lastIndexOf('/');
|
|
17
|
+
const parent = cut === -1 ? '' : path.slice(0, cut + 1);
|
|
18
|
+
const leaf = path.slice(cut + 1);
|
|
19
|
+
const leaves = groups.get(parent) ?? [];
|
|
20
|
+
leaves.push(escapeRegex(leaf));
|
|
21
|
+
groups.set(parent, leaves);
|
|
22
|
+
}
|
|
23
|
+
const parts = [...groups].map(([parent, leaves]) => leaves.length === 1 ? `${escapeRegex(parent)}${leaves[0]}` : `${escapeRegex(parent)}(${leaves.join('|')})`);
|
|
24
|
+
return parts.length === 1 ? (parts[0] ?? '') : `(${parts.join('|')})`;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAgBA,wGAAwG;AACxG,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACrD,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,IAAI,CAAC,WAAmB;IACtC,OAAO,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AACzC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAwB;IAClD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;QACxD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;QACjC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;QAC/B,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,EAAE,CACjD,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAC3G,CAAC;IACF,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FeatureTiers } from './featureTiers.js';
|
|
2
|
+
import { type ArchRule } from './paths.js';
|
|
3
|
+
/**
|
|
4
|
+
* One `no-tier-violation-<feature>` rule per feature that has something above it: a feature
|
|
5
|
+
* imports only strictly lower tiers, and Tier 0 may import Tier 0.
|
|
6
|
+
*/
|
|
7
|
+
export declare function generateTierRules(featureTiers: FeatureTiers): ArchRule[];
|
|
8
|
+
//# sourceMappingURL=tierRules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tierRules.d.ts","sourceRoot":"","sources":["../src/tierRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAE,KAAK,QAAQ,EAAqB,MAAM,YAAY,CAAC;AAM9D;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,YAAY,GAAG,QAAQ,EAAE,CA4BxE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { FEATURE_TIERS } from './featureTiers.js';
|
|
2
|
+
import { escapeRegex, slug } from './paths.js';
|
|
3
|
+
function forbiddenTiersFor(tier) {
|
|
4
|
+
return tier === 0 ? FEATURE_TIERS.filter(t => t > 0) : FEATURE_TIERS.filter(t => t >= tier);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* One `no-tier-violation-<feature>` rule per feature that has something above it: a feature
|
|
8
|
+
* imports only strictly lower tiers, and Tier 0 may import Tier 0.
|
|
9
|
+
*/
|
|
10
|
+
export function generateTierRules(featureTiers) {
|
|
11
|
+
const featuresByTier = new Map();
|
|
12
|
+
for (const [featurePath, tier] of Object.entries(featureTiers)) {
|
|
13
|
+
const peers = featuresByTier.get(tier) ?? [];
|
|
14
|
+
peers.push(featurePath);
|
|
15
|
+
featuresByTier.set(tier, peers);
|
|
16
|
+
}
|
|
17
|
+
return Object.entries(featureTiers).flatMap(([featurePath, tier]) => {
|
|
18
|
+
const forbiddenPaths = forbiddenTiersFor(tier).flatMap(t => (featuresByTier.get(t) ?? []).map(f => `^${escapeRegex(f)}/`));
|
|
19
|
+
if (forbiddenPaths.length === 0)
|
|
20
|
+
return [];
|
|
21
|
+
const own = `^${escapeRegex(featurePath)}/`;
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
name: `no-tier-violation-${slug(featurePath)}`,
|
|
25
|
+
comment: tier === 0
|
|
26
|
+
? 'Tier 0 (core): import only Tier 0 peers; never a feature of Tier 1 or above'
|
|
27
|
+
: `Tier ${tier}: import only strictly lower tiers; never a Tier ${tier} peer or a higher tier`,
|
|
28
|
+
severity: 'error',
|
|
29
|
+
from: { path: own },
|
|
30
|
+
to: { path: forbiddenPaths.join('|'), pathNot: own },
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=tierRules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tierRules.js","sourceRoot":"","sources":["../src/tierRules.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAuC,MAAM,mBAAmB,CAAC;AACvF,OAAO,EAAiB,WAAW,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAE9D,SAAS,iBAAiB,CAAC,IAAiB;IAC1C,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;AAC9F,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,YAA0B;IAC1D,MAAM,cAAc,GAAG,IAAI,GAAG,EAAyB,CAAC;IACxD,KAAK,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QAC/D,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QAC7C,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACxB,cAAc,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAClC,CAAC;IAED,OAAO,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,EAAc,EAAE;QAC9E,MAAM,cAAc,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CACzD,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAC9D,CAAC;QACF,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QAE3C,MAAM,GAAG,GAAG,IAAI,WAAW,CAAC,WAAW,CAAC,GAAG,CAAC;QAC5C,OAAO;YACL;gBACE,IAAI,EAAE,qBAAqB,IAAI,CAAC,WAAW,CAAC,EAAE;gBAC9C,OAAO,EACL,IAAI,KAAK,CAAC;oBACR,CAAC,CAAC,6EAA6E;oBAC/E,CAAC,CAAC,QAAQ,IAAI,oDAAoD,IAAI,wBAAwB;gBAClG,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;gBACnB,EAAE,EAAE,EAAE,IAAI,EAAE,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE;aACrD;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@timothyrusso/arch-rules",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "dependency-cruiser rule generators for feature tiers, layers and Effect placement.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/timothyrusso/agentic-kit.git",
|
|
10
|
+
"directory": "packages/arch-rules"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.build.json",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"test": "NODE_OPTIONS=\"--experimental-vm-modules --no-warnings=ExperimentalWarning\" jest"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"dependency-cruiser": "^18.4.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"dependency-cruiser": "^18.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|