@fantastic.dev/repo-gates 0.2.1-bootstrap.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 +499 -0
- package/dist/bin/repo-gates.d.ts +1 -0
- package/dist/bin/repo-gates.js +2318 -0
- package/dist/config.d.ts +147 -0
- package/dist/config.js +178 -0
- package/dist/design-system.d.ts +17 -0
- package/dist/design-system.js +14 -0
- package/dist/eslint-boundaries.d.ts +49 -0
- package/dist/eslint-boundaries.js +40 -0
- package/dist/index.d.ts +796 -0
- package/dist/index.js +1995 -0
- package/package.json +78 -0
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-repo policy for @fantastic.dev/repo-gates.
|
|
3
|
+
*
|
|
4
|
+
* The engine is repo-agnostic; everything that varies between repos lives
|
|
5
|
+
* here. A consumer repo drops a partial `repo-gates.config.json` at its
|
|
6
|
+
* root; anything it omits falls back to {@link DEFAULT_CONFIG} (tuned for
|
|
7
|
+
* a pnpm + turbo + vitest monorepo). The loader always resolves policy
|
|
8
|
+
* relative to the *consumer* repo root (default `process.cwd()`), never
|
|
9
|
+
* the package's own location — that is the whole point of extracting this
|
|
10
|
+
* into a package.
|
|
11
|
+
*/
|
|
12
|
+
type GateSpec = {
|
|
13
|
+
/** The package.json script name, e.g. "typecheck" or "check:size". */
|
|
14
|
+
name: string;
|
|
15
|
+
/** Conditional gates run only when the consumer's package.json defines
|
|
16
|
+
* them; non-conditional gates in the selected manifest run even when the
|
|
17
|
+
* consumer does not define a matching script, so a missing required gate
|
|
18
|
+
* fails loudly. Consumers that replace the default manifest own that
|
|
19
|
+
* policy (for example, a coverage-backed test gate may replace `test`). */
|
|
20
|
+
conditional: boolean;
|
|
21
|
+
};
|
|
22
|
+
type RepoGatesConfig = {
|
|
23
|
+
/** How a gate script is invoked, e.g. "pnpm run" or "bun run". */
|
|
24
|
+
runner: string;
|
|
25
|
+
/** Ordered gate manifest — cheap gates first, the CI-parity meta-gate last. */
|
|
26
|
+
gates: GateSpec[];
|
|
27
|
+
/** Roots the file-size and debt-marker guards walk. */
|
|
28
|
+
scanRoots: string[];
|
|
29
|
+
/** Directory names pruned during scans (matched as a path segment). */
|
|
30
|
+
excludeDirSegments: string[];
|
|
31
|
+
/** Repo-relative posix path prefixes excluded from scans. */
|
|
32
|
+
excludePathPrefixes: string[];
|
|
33
|
+
/** Extensions the size/debt guards treat as source. */
|
|
34
|
+
sourceExtensions: string[];
|
|
35
|
+
fileSize: {
|
|
36
|
+
/** Default per-file line ceiling for files without an explicit budget. */
|
|
37
|
+
threshold: number;
|
|
38
|
+
/** Repo-relative path to the grandfathered per-file budgets JSON. */
|
|
39
|
+
budgetsPath: string;
|
|
40
|
+
};
|
|
41
|
+
debt: {
|
|
42
|
+
/** Repo-relative path to the grandfathered untracked-marker allowlist. */
|
|
43
|
+
allowlistPath: string;
|
|
44
|
+
/** Marker tokens that require a tracker reference on the same line. */
|
|
45
|
+
markerTokens: string[];
|
|
46
|
+
/** Source strings for the RegExps that count as a "tracker reference". */
|
|
47
|
+
trackerPatterns: string[];
|
|
48
|
+
};
|
|
49
|
+
circular: {
|
|
50
|
+
/** Repo-relative path to the grandfathered circular-import allowlist. */
|
|
51
|
+
allowlistPath: string;
|
|
52
|
+
};
|
|
53
|
+
secrets: {
|
|
54
|
+
/** Repo-relative path to the grandfathered secret-finding allowlist. */
|
|
55
|
+
allowlistPath: string;
|
|
56
|
+
/** Source strings for the RegExps matched against every git-tracked
|
|
57
|
+
* line (well-known credential shapes — AWS/GitHub/Slack/Stripe/npm/
|
|
58
|
+
* Google API keys, PEM private-key headers). Findings are reported as
|
|
59
|
+
* a redacted fingerprint, never the matched text. */
|
|
60
|
+
patterns: string[];
|
|
61
|
+
/** File extensions (with the dot) skipped as binary/non-text. */
|
|
62
|
+
binaryExtensions: string[];
|
|
63
|
+
};
|
|
64
|
+
coverage: {
|
|
65
|
+
/** Repo-relative path to the coverage floor JSON. */
|
|
66
|
+
budgetsPath: string;
|
|
67
|
+
/** Globs (repo-relative) matching each package's vitest
|
|
68
|
+
* `coverage-summary.json`. */
|
|
69
|
+
summaryGlobs: string[];
|
|
70
|
+
};
|
|
71
|
+
ciParity: {
|
|
72
|
+
/** Repo-relative path to the parity escape-hatch config. */
|
|
73
|
+
configPath: string;
|
|
74
|
+
/** The root gate every CI invocation must be reachable from. */
|
|
75
|
+
rootGate: string;
|
|
76
|
+
/** Entry points seeded into the reachability walk alongside the gates. */
|
|
77
|
+
entryGates: string[];
|
|
78
|
+
/** Only workflow files whose basename starts with this are gate workflows. */
|
|
79
|
+
workflowPrefix: string;
|
|
80
|
+
};
|
|
81
|
+
bundleSize: {
|
|
82
|
+
/** Repo-relative path to the bundle-size budgets JSON. */
|
|
83
|
+
budgetsPath: string;
|
|
84
|
+
/** Build+measure targets. Empty → the gate is a no-op (nothing to measure). */
|
|
85
|
+
targets: {
|
|
86
|
+
/** Budget key + display name, e.g. "web". */
|
|
87
|
+
name: string;
|
|
88
|
+
/** turbo `--filter` selector to build before measuring, e.g. "@x/web". */
|
|
89
|
+
filter: string;
|
|
90
|
+
/** Repo-relative dir of built assets to scan (hashed filenames ok). */
|
|
91
|
+
distDir: string;
|
|
92
|
+
/** Bucket name → file extensions, e.g. { "js": [".js"], "css": [".css"] }. */
|
|
93
|
+
buckets: Record<string, string[]>;
|
|
94
|
+
}[];
|
|
95
|
+
};
|
|
96
|
+
report: {
|
|
97
|
+
/** Globs matching per-package junit XML (report:test-timing). Empty ⇒ no-op. */
|
|
98
|
+
junitGlobs: string[];
|
|
99
|
+
/** How many slowest tests to list. */
|
|
100
|
+
topN: number;
|
|
101
|
+
};
|
|
102
|
+
agents: {
|
|
103
|
+
/** Repo-relative agent-doc files to validate (e.g. ["AGENTS.md"]). Empty ⇒ no-op. */
|
|
104
|
+
targets: string[];
|
|
105
|
+
/** Backticked paths in the docs that intentionally don't exist on disk
|
|
106
|
+
* (generated artifacts, runtime files, illustrative names). */
|
|
107
|
+
knownMissingPaths: string[];
|
|
108
|
+
/** Package-manager command whose script invocations are validated (e.g. "pnpm"). */
|
|
109
|
+
runnerCommand: string;
|
|
110
|
+
/** Subcommands of runnerCommand that are builtins, not scripts (ignored). */
|
|
111
|
+
ignoredSubcommands: string[];
|
|
112
|
+
};
|
|
113
|
+
docsCoverage: {
|
|
114
|
+
/** Globs (repo-relative) a PR must touch for a triggered surface to
|
|
115
|
+
* count as documented. Empty `surfaces` ⇒ the gate is a no-op. */
|
|
116
|
+
docsGlobs: string[];
|
|
117
|
+
/** User-facing surfaces that require docs when changed. */
|
|
118
|
+
surfaces: {
|
|
119
|
+
/** Human-readable label shown in the failure/notice output. */
|
|
120
|
+
label: string;
|
|
121
|
+
/** Glob matched against the PR's changed-file paths. */
|
|
122
|
+
glob: string;
|
|
123
|
+
/** "added" — only a brand-new file triggers; "changed" — added,
|
|
124
|
+
* modified, renamed, or copied all trigger. */
|
|
125
|
+
on: "added" | "changed";
|
|
126
|
+
}[];
|
|
127
|
+
/** Globs removed from BOTH surface and docs matching (tests, fixtures). */
|
|
128
|
+
exclude: string[];
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
declare const DEFAULT_CONFIG: RepoGatesConfig;
|
|
132
|
+
declare const CONFIG_FILENAMES: readonly ["repo-gates.config.json"];
|
|
133
|
+
type Ctx = {
|
|
134
|
+
repoRoot: string;
|
|
135
|
+
config: RepoGatesConfig;
|
|
136
|
+
};
|
|
137
|
+
type DeepPartial<T> = {
|
|
138
|
+
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
139
|
+
};
|
|
140
|
+
/** One-level-deep merge: overlay's top-level object sections are merged
|
|
141
|
+
* key-by-key over the base; scalars and arrays replace wholesale. */
|
|
142
|
+
declare function mergeConfig(base: RepoGatesConfig, overlay: DeepPartial<RepoGatesConfig>): RepoGatesConfig;
|
|
143
|
+
declare function findConfigFile(repoRoot: string): string | undefined;
|
|
144
|
+
declare function loadConfig(repoRoot?: string): RepoGatesConfig;
|
|
145
|
+
declare function loadContext(repoRoot?: string): Ctx;
|
|
146
|
+
|
|
147
|
+
export { CONFIG_FILENAMES, type Ctx, DEFAULT_CONFIG, type GateSpec, type RepoGatesConfig, findConfigFile, loadConfig, loadContext, mergeConfig };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
var DEFAULT_CONFIG = {
|
|
5
|
+
runner: "pnpm run",
|
|
6
|
+
gates: [
|
|
7
|
+
{ name: "lint", conditional: false },
|
|
8
|
+
{ name: "check:design-system", conditional: true },
|
|
9
|
+
{ name: "format:check", conditional: false },
|
|
10
|
+
{ name: "typecheck", conditional: false },
|
|
11
|
+
{ name: "check:scripts", conditional: true },
|
|
12
|
+
{ name: "check:deps", conditional: true },
|
|
13
|
+
{ name: "check:dups", conditional: true },
|
|
14
|
+
{ name: "check:size", conditional: false },
|
|
15
|
+
{ name: "check:debt", conditional: false },
|
|
16
|
+
{ name: "check:circular", conditional: true },
|
|
17
|
+
{ name: "check:secrets", conditional: true },
|
|
18
|
+
{ name: "check:agents", conditional: true },
|
|
19
|
+
{ name: "check:shadscan", conditional: true },
|
|
20
|
+
{ name: "check:docs-coverage", conditional: true },
|
|
21
|
+
{ name: "check:bundle-size", conditional: true },
|
|
22
|
+
{ name: "test", conditional: false },
|
|
23
|
+
{ name: "check:coverage", conditional: true },
|
|
24
|
+
{ name: "check:ci-parity", conditional: false }
|
|
25
|
+
],
|
|
26
|
+
scanRoots: ["apps", "packages", "scripts"],
|
|
27
|
+
excludeDirSegments: [
|
|
28
|
+
"node_modules",
|
|
29
|
+
"dist",
|
|
30
|
+
"dist-e2e",
|
|
31
|
+
"out",
|
|
32
|
+
".turbo",
|
|
33
|
+
".wrangler",
|
|
34
|
+
"__golden__",
|
|
35
|
+
"_generated",
|
|
36
|
+
"coverage"
|
|
37
|
+
],
|
|
38
|
+
excludePathPrefixes: [],
|
|
39
|
+
sourceExtensions: [".ts", ".tsx"],
|
|
40
|
+
fileSize: { threshold: 600, budgetsPath: "gates/file-size-budgets.json" },
|
|
41
|
+
debt: {
|
|
42
|
+
allowlistPath: "gates/debt-marker-allowlist.json",
|
|
43
|
+
markerTokens: ["TODO", "FIXME", "HACK", "XXX"],
|
|
44
|
+
trackerPatterns: ["\\b[A-Z]{2,}-\\d+\\b", "#\\d+\\b", "https?:\\/\\/\\S+"]
|
|
45
|
+
},
|
|
46
|
+
circular: { allowlistPath: "gates/circular-imports-allowlist.json" },
|
|
47
|
+
secrets: {
|
|
48
|
+
allowlistPath: "gates/secrets-allowlist.json",
|
|
49
|
+
patterns: [
|
|
50
|
+
"\\bAKIA[0-9A-Z]{16}\\b",
|
|
51
|
+
// AWS access key
|
|
52
|
+
"\\bASIA[0-9A-Z]{16}\\b",
|
|
53
|
+
// AWS temporary access key
|
|
54
|
+
"\\bgh[pousr]_[A-Za-z0-9]{36,}\\b",
|
|
55
|
+
// GitHub personal/OAuth/app/refresh token
|
|
56
|
+
"\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b",
|
|
57
|
+
// Slack token
|
|
58
|
+
"\\bsk_(?:live|test)_[A-Za-z0-9]{16,}\\b",
|
|
59
|
+
// Stripe secret key
|
|
60
|
+
"\\bnpm_[A-Za-z0-9]{36}\\b",
|
|
61
|
+
// npm access token
|
|
62
|
+
"\\bAIza[0-9A-Za-z_-]{35}\\b",
|
|
63
|
+
// Google API key
|
|
64
|
+
"-----BEGIN(?: RSA| EC| OPENSSH| DSA)? PRIVATE KEY-----",
|
|
65
|
+
// PEM private key
|
|
66
|
+
"://[^/\\s:@]+:[^/\\s:@]+@"
|
|
67
|
+
// credentials embedded in a URL
|
|
68
|
+
],
|
|
69
|
+
binaryExtensions: [
|
|
70
|
+
".png",
|
|
71
|
+
".jpg",
|
|
72
|
+
".jpeg",
|
|
73
|
+
".gif",
|
|
74
|
+
".ico",
|
|
75
|
+
".webp",
|
|
76
|
+
".pdf",
|
|
77
|
+
".zip",
|
|
78
|
+
".gz",
|
|
79
|
+
".woff",
|
|
80
|
+
".woff2",
|
|
81
|
+
".ttf",
|
|
82
|
+
".eot",
|
|
83
|
+
".mp4",
|
|
84
|
+
".mp3",
|
|
85
|
+
".wasm"
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
coverage: {
|
|
89
|
+
budgetsPath: "gates/coverage-budgets.json",
|
|
90
|
+
summaryGlobs: [
|
|
91
|
+
"apps/*/coverage/coverage-summary.json",
|
|
92
|
+
"packages/*/coverage/coverage-summary.json"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
ciParity: {
|
|
96
|
+
configPath: "gates/ci-parity-config.json",
|
|
97
|
+
rootGate: "check:all",
|
|
98
|
+
entryGates: ["check:all", "verify"],
|
|
99
|
+
workflowPrefix: "ci"
|
|
100
|
+
},
|
|
101
|
+
bundleSize: { budgetsPath: "gates/bundle-size-budgets.json", targets: [] },
|
|
102
|
+
report: { junitGlobs: [], topN: 20 },
|
|
103
|
+
agents: {
|
|
104
|
+
targets: [],
|
|
105
|
+
knownMissingPaths: [],
|
|
106
|
+
runnerCommand: "pnpm",
|
|
107
|
+
ignoredSubcommands: [
|
|
108
|
+
"run",
|
|
109
|
+
"install",
|
|
110
|
+
"i",
|
|
111
|
+
"add",
|
|
112
|
+
"remove",
|
|
113
|
+
"rm",
|
|
114
|
+
"up",
|
|
115
|
+
"update",
|
|
116
|
+
"exec",
|
|
117
|
+
"dlx",
|
|
118
|
+
"why",
|
|
119
|
+
"store",
|
|
120
|
+
"prune",
|
|
121
|
+
"audit",
|
|
122
|
+
"outdated",
|
|
123
|
+
"list",
|
|
124
|
+
"ls",
|
|
125
|
+
"link",
|
|
126
|
+
"unlink",
|
|
127
|
+
"publish",
|
|
128
|
+
"pack",
|
|
129
|
+
"rebuild",
|
|
130
|
+
"approve-builds",
|
|
131
|
+
"config",
|
|
132
|
+
"dedupe",
|
|
133
|
+
"fetch",
|
|
134
|
+
"import",
|
|
135
|
+
"patch",
|
|
136
|
+
"setup",
|
|
137
|
+
"create"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
140
|
+
docsCoverage: { docsGlobs: [], surfaces: [], exclude: [] }
|
|
141
|
+
};
|
|
142
|
+
var CONFIG_FILENAMES = ["repo-gates.config.json"];
|
|
143
|
+
function isPlainObject(value) {
|
|
144
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
145
|
+
}
|
|
146
|
+
function mergeConfig(base, overlay) {
|
|
147
|
+
const out = { ...base };
|
|
148
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
149
|
+
if (value === void 0) continue;
|
|
150
|
+
const baseValue = base[key];
|
|
151
|
+
out[key] = isPlainObject(baseValue) && isPlainObject(value) ? { ...baseValue, ...value } : value;
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function findConfigFile(repoRoot) {
|
|
156
|
+
for (const name of CONFIG_FILENAMES) {
|
|
157
|
+
const candidate = resolve(repoRoot, name);
|
|
158
|
+
if (existsSync(candidate)) return candidate;
|
|
159
|
+
}
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
function loadConfig(repoRoot = process.cwd()) {
|
|
163
|
+
const file = findConfigFile(repoRoot);
|
|
164
|
+
if (!file) return DEFAULT_CONFIG;
|
|
165
|
+
const overlay = JSON.parse(readFileSync(file, "utf8"));
|
|
166
|
+
return mergeConfig(DEFAULT_CONFIG, overlay);
|
|
167
|
+
}
|
|
168
|
+
function loadContext(repoRoot = process.cwd()) {
|
|
169
|
+
return { repoRoot, config: loadConfig(repoRoot) };
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
CONFIG_FILENAMES,
|
|
173
|
+
DEFAULT_CONFIG,
|
|
174
|
+
findConfigFile,
|
|
175
|
+
loadConfig,
|
|
176
|
+
loadContext,
|
|
177
|
+
mergeConfig
|
|
178
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Shared policy for consumer ESLint and Oxlint configs. No linter runtime dependency. */
|
|
2
|
+
declare const designSystemRules: {
|
|
3
|
+
readonly "shadcn/no-restyle": readonly ["error", {
|
|
4
|
+
readonly allow: readonly ["layout"];
|
|
5
|
+
}];
|
|
6
|
+
readonly "shadcn/no-raw-colors": "error";
|
|
7
|
+
readonly "shadcn/no-arbitrary-values": "error";
|
|
8
|
+
readonly "shadcn/no-inline-styles": "error";
|
|
9
|
+
readonly "shadcn/no-unknown-classes": "error";
|
|
10
|
+
readonly "shadcn/require-static-classes": "error";
|
|
11
|
+
};
|
|
12
|
+
/** Component implementations may define their own styling. Other checks still apply. */
|
|
13
|
+
declare const componentDefinitionRules: {
|
|
14
|
+
readonly "shadcn/no-restyle": "off";
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { componentDefinitionRules, designSystemRules };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/design-system.ts
|
|
2
|
+
var designSystemRules = {
|
|
3
|
+
"shadcn/no-restyle": ["error", { allow: ["layout"] }],
|
|
4
|
+
"shadcn/no-raw-colors": "error",
|
|
5
|
+
"shadcn/no-arbitrary-values": "error",
|
|
6
|
+
"shadcn/no-inline-styles": "error",
|
|
7
|
+
"shadcn/no-unknown-classes": "error",
|
|
8
|
+
"shadcn/require-static-classes": "error"
|
|
9
|
+
};
|
|
10
|
+
var componentDefinitionRules = { "shadcn/no-restyle": "off" };
|
|
11
|
+
export {
|
|
12
|
+
componentDefinitionRules,
|
|
13
|
+
designSystemRules
|
|
14
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import-boundary → ESLint flat-config generator (repo-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* The *rules* (which import specifiers are forbidden in which files) are
|
|
5
|
+
* DATA the consumer supplies from its own `repo-gates.config.json`; this
|
|
6
|
+
* module only shapes that data into `@typescript-eslint/no-restricted-imports`
|
|
7
|
+
* flat-config entries. repo-gates never learns any repo's specific packages.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT — ESLint flat config does NOT merge rule options across matching
|
|
10
|
+
* configs; the LAST matching config wins outright. So a file must get ALL its
|
|
11
|
+
* forbidden patterns from a SINGLE config. Hence each boundary is ONE file
|
|
12
|
+
* scope carrying a LIST of pattern groups (each with its own `allowTypeImports`
|
|
13
|
+
* / message). When scopes nest (e.g. `apps/x/renderer/**` ⊂ `apps/x/**`), the
|
|
14
|
+
* narrower boundary must carry the broader patterns too. Rather than copy-paste
|
|
15
|
+
* them, give it `extends: ["<broader-boundary>"]` — this module merges the named
|
|
16
|
+
* boundaries' patterns in ahead of its own, so each rule is authored ONCE.
|
|
17
|
+
*/
|
|
18
|
+
type BoundaryPattern = {
|
|
19
|
+
/** Forbidden import-specifier glob patterns (minimatch on the specifier). */
|
|
20
|
+
forbid: string[];
|
|
21
|
+
/** Allow `import type { … }` through (blocks only value imports). */
|
|
22
|
+
allowTypeImports?: boolean;
|
|
23
|
+
/** Custom violation message for this group. */
|
|
24
|
+
message?: string;
|
|
25
|
+
};
|
|
26
|
+
type Boundary = {
|
|
27
|
+
/** Stable id, surfaced in the ESLint config name. */
|
|
28
|
+
name: string;
|
|
29
|
+
/** File globs the rule applies to (ESLint `files`). */
|
|
30
|
+
files: string[];
|
|
31
|
+
/** File globs excluded from this boundary (ESLint `ignores`) — e.g. test
|
|
32
|
+
* files, which run in Node and may touch anything for setup. */
|
|
33
|
+
ignores?: string[];
|
|
34
|
+
/** One or more forbidden-pattern groups applied to those files. */
|
|
35
|
+
patterns: BoundaryPattern[];
|
|
36
|
+
/** Names of other boundaries whose `patterns` are merged in ahead of this
|
|
37
|
+
* boundary's own. Lets a nested scope inherit a broader scope's rules
|
|
38
|
+
* without copy-pasting them (only `patterns` are inherited, never `files`). */
|
|
39
|
+
extends?: string[];
|
|
40
|
+
};
|
|
41
|
+
type BoundaryEslintConfig = {
|
|
42
|
+
name: string;
|
|
43
|
+
files: string[];
|
|
44
|
+
ignores?: string[];
|
|
45
|
+
rules: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
declare function boundariesToEslintConfigs(boundaries: Boundary[]): BoundaryEslintConfig[];
|
|
48
|
+
|
|
49
|
+
export { type Boundary, type BoundaryEslintConfig, type BoundaryPattern, boundariesToEslintConfigs };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/eslint-boundaries.ts
|
|
2
|
+
function resolvePatterns(b, byName, seen) {
|
|
3
|
+
if (seen.has(b.name)) {
|
|
4
|
+
throw new Error(
|
|
5
|
+
`repo-gates boundaries: circular \`extends\` involving "${b.name}" (${[...seen, b.name].join(" \u2192 ")}).`
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
const inherited = [];
|
|
9
|
+
for (const parentName of b.extends ?? []) {
|
|
10
|
+
const parent = byName.get(parentName);
|
|
11
|
+
if (!parent) {
|
|
12
|
+
throw new Error(`repo-gates boundary "${b.name}" extends unknown boundary "${parentName}".`);
|
|
13
|
+
}
|
|
14
|
+
inherited.push(...resolvePatterns(parent, byName, /* @__PURE__ */ new Set([...seen, b.name])));
|
|
15
|
+
}
|
|
16
|
+
return [...inherited, ...b.patterns];
|
|
17
|
+
}
|
|
18
|
+
function boundariesToEslintConfigs(boundaries) {
|
|
19
|
+
const byName = new Map(boundaries.map((b) => [b.name, b]));
|
|
20
|
+
return boundaries.map((b) => ({
|
|
21
|
+
name: `repo-gates/boundary/${b.name}`,
|
|
22
|
+
files: b.files,
|
|
23
|
+
...b.ignores ? { ignores: b.ignores } : {},
|
|
24
|
+
rules: {
|
|
25
|
+
"@typescript-eslint/no-restricted-imports": [
|
|
26
|
+
"error",
|
|
27
|
+
{
|
|
28
|
+
patterns: resolvePatterns(b, byName, /* @__PURE__ */ new Set()).map((p) => ({
|
|
29
|
+
group: p.forbid,
|
|
30
|
+
allowTypeImports: p.allowTypeImports ?? false,
|
|
31
|
+
message: p.message ?? `Import boundary "${b.name}" violated.`
|
|
32
|
+
}))
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
boundariesToEslintConfigs
|
|
40
|
+
};
|