@slip-stream-kit/eslint-plugin 0.10.0 → 0.11.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +338 -10
- package/dist/index.js.map +4 -4
- package/dist/rules/package-structure/index.d.ts +1 -0
- package/dist/rules/package-structure/package-structure.d.ts +27 -0
- package/dist/utils/package-root.d.ts +16 -0
- package/dist/utils/package-type-reader.d.ts +24 -0
- package/package.json +2 -1
- package/readme.md +141 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { packageStructure } from './package-structure';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
export declare const PACKAGE_TYPE_VALUES: readonly ["frontend", "backend", "lib", "e2e", "mobile"];
|
|
3
|
+
export type PackageType = (typeof PACKAGE_TYPE_VALUES)[number];
|
|
4
|
+
export interface LayerEntry {
|
|
5
|
+
/** First-level directories allowed under `src/`. */
|
|
6
|
+
layers: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Allowed directories one level further down, keyed by where they sit: `features/*` means "inside
|
|
9
|
+
* every folder of `features/`" (each folder is a unit of any name), `services` means "directly
|
|
10
|
+
* inside `services/`". A layer without a key is not inspected below its first level.
|
|
11
|
+
*/
|
|
12
|
+
segments?: Record<string, string[]>;
|
|
13
|
+
/** Skill or doc that owns this layout, appended to the message. `''` or absent → no "See …" sentence. */
|
|
14
|
+
skill?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Options extends Partial<Record<PackageType, LayerEntry>> {
|
|
17
|
+
/** Globs; the rule is skipped for matching files. */
|
|
18
|
+
ignore?: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Built-in entries per package type. A present option key replaces the whole entry (layers,
|
|
22
|
+
* segments AND skill) — the same absent→default / set→replace semantics as every other option in
|
|
23
|
+
* the plugin. `mobile` and `lib` have no entry until their layouts settle, so they are silent.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DEFAULT_ENTRIES: Partial<Record<PackageType, LayerEntry>>;
|
|
26
|
+
export declare const resolveEntry: (type: PackageType, options: Options) => LayerEntry | undefined;
|
|
27
|
+
export declare const packageStructure: Rule.RuleModule;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const CONFIG_FILENAME = "infra-kit.config.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The package directory a linted file belongs to: the nearest ancestor holding `infra-kit.config.ts`
|
|
4
|
+
* or `package.json`. Null for virtual / relative filenames, which have no place on disk to walk from.
|
|
5
|
+
*/
|
|
6
|
+
export declare const findPackageRoot: (filename: string) => string | null;
|
|
7
|
+
/**
|
|
8
|
+
* The directory names between `<packageRoot>/src/` and `filename`, outermost first — `[]` for a
|
|
9
|
+
* file directly in `src/`, null when the file is not under `src/` at all (so `dist/src/x/...`,
|
|
10
|
+
* built output a consumer ships, is null too).
|
|
11
|
+
*/
|
|
12
|
+
export declare const dirsUnderSrc: (packageRoot: string, filename: string) => string[] | null;
|
|
13
|
+
/** Nearest ancestor of `packageRoot` (inclusive) that is a pnpm workspace root, or null outside one. */
|
|
14
|
+
export declare const findRepoRoot: (packageRoot: string) => string | null;
|
|
15
|
+
/** Drops the dirname → root memo; only tests that build and tear down fixture trees need this. */
|
|
16
|
+
export declare const resetPackageRootCache: () => void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PackageType } from '@slip-stream-kit/config/package-type';
|
|
2
|
+
/**
|
|
3
|
+
* The literal `type` declared in an `infra-kit.config.ts` source text, or undefined when it is
|
|
4
|
+
* absent, computed, or not one of the known types.
|
|
5
|
+
*
|
|
6
|
+
* The config is read as text rather than imported: `require(esm)` is cached by the ESM loader, so a
|
|
7
|
+
* rewritten config would keep returning its old value, and importing it would execute consumer code
|
|
8
|
+
* (and its zod import) inside the ESLint process. Comments are stripped first so a commented-out
|
|
9
|
+
* `// type: 'backend'` above the live literal cannot win; the first live match does.
|
|
10
|
+
*/
|
|
11
|
+
export declare const readDeclaredType: (text: string) => PackageType | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The package's type: the literal declared in its `infra-kit.config.ts` when there is one, else the
|
|
14
|
+
* CLI's inference from directory and dependency signals.
|
|
15
|
+
*
|
|
16
|
+
* One `statSync` on the config per call is the steady-state cost; the text is re-read only when the
|
|
17
|
+
* mtime moved, so a config edit is seen by the next lint of any file in the package — in a CLI run
|
|
18
|
+
* or a long-lived IDE server alike. `package.json` is read once per root and never revalidated: it
|
|
19
|
+
* only feeds the dependency fallback, and a dependency change is a `pnpm install` event, not an
|
|
20
|
+
* edit-lint loop.
|
|
21
|
+
*/
|
|
22
|
+
export declare const resolvePackageType: (packageRoot: string) => PackageType;
|
|
23
|
+
/** Drops the root → type memo; only tests that build and tear down fixture trees need this. */
|
|
24
|
+
export declare const resetPackageTypeCache: () => void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slip-stream-kit/eslint-plugin",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.10
|
|
4
|
+
"version": "0.11.10",
|
|
5
5
|
"description": "Custom ESLint rules enforcing the white-label frontend architecture conventions",
|
|
6
6
|
"author": "Arthur Saenko <arthur.saenz7@gmail.com> (https://github.com/ArthurSaenz)",
|
|
7
7
|
"license": "MIT",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"eslint": ">=9"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
+
"@slip-stream-kit/config": "0.11.10",
|
|
39
40
|
"@types/node": "^26.5.1",
|
|
40
41
|
"@typescript-eslint/parser": "^8.70.0",
|
|
41
42
|
"@wl/eslint-config": "0.1.0",
|
package/readme.md
CHANGED
|
@@ -526,6 +526,108 @@ story is satisfied when any candidate (`.tsx`, `.jsx`, `.ts`, `.js`) exists on d
|
|
|
526
526
|
- **Unanchored globs.** `paths`/`ignore` patterns match anywhere in the path, so anchor them
|
|
527
527
|
(e.g. start with `**/`) when you need precision.
|
|
528
528
|
|
|
529
|
+
### `package-structure`
|
|
530
|
+
|
|
531
|
+
Allowlist the **layers under a package's `src/`**, and the **segments inside them**, per package
|
|
532
|
+
type. A file that sits in a directory the lists do not name is reported once, at the top of the
|
|
533
|
+
file — every file under the offending directory carries the same message, which is how a "red
|
|
534
|
+
folder" surfaces in the IDE explorer. The message names the skill that owns that layout, so the fix
|
|
535
|
+
has an owner:
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
`core` is not an allowed `src/` layer for package type `frontend` (allowed: app, features, lib, components, pages, routes). See /infra-kit:fe-architect for the frontend layout.
|
|
539
|
+
`hooks` is not an allowed segment of `features/user` for package type `frontend` (allowed: containers, components, services, __stories__, __tests__). See /infra-kit:fe-architect for the frontend layout.
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Two levels are checked:
|
|
543
|
+
|
|
544
|
+
- **Layers** — the first directory under `src/` must be in `layers`.
|
|
545
|
+
- **Segments** — inside a layer that has a `segments` key, the next directory must be in that list.
|
|
546
|
+
`features/*` means "inside every folder of `features/`" (each folder is a unit of any name, e.g. a
|
|
547
|
+
feature or a service); `services` (no `/*`) would mean "directly inside `services/`". A layer
|
|
548
|
+
without a key is not inspected below its first level.
|
|
549
|
+
|
|
550
|
+
Never reported: files — in `src/` itself, in a layer, in a unit (`features/user/index.ts`) — and
|
|
551
|
+
anything deeper than a segment (`features/user/components/default/…`). A file not under
|
|
552
|
+
`<package>/src/` at all is silent, so a shipped `dist/src/**` is too. A `__tests__` or
|
|
553
|
+
`__stories__` folder is an ordinary name: allowed exactly where a list says so.
|
|
554
|
+
|
|
555
|
+
```js
|
|
556
|
+
{
|
|
557
|
+
rules: {
|
|
558
|
+
'@wl/package-structure': 'error',
|
|
559
|
+
},
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
#### How the package type is resolved
|
|
564
|
+
|
|
565
|
+
The package root is the nearest ancestor of the linted file holding an `infra-kit.config.ts` or a
|
|
566
|
+
`package.json`. Its type is the `type` **literal** declared in `infra-kit.config.ts` when there is one
|
|
567
|
+
(`type: 'backend'`); otherwise the same inference the `infra-kit` CLI uses — `apps/<app>/ui` →
|
|
568
|
+
`frontend`, `apps/<app>/api` → `backend`, `apps/<app>/tests` → `e2e`, a `mobile-app` directory →
|
|
569
|
+
`mobile`, then dependency signals (`@playwright/test` → `e2e`, `@capacitor/*` → `mobile`,
|
|
570
|
+
`serverless` → `backend`), falling back to `lib`.
|
|
571
|
+
|
|
572
|
+
The config file is read as **text** — it is never imported or executed — and re-read when its mtime
|
|
573
|
+
changes, so an edit is picked up by the next lint of any file in that package, in CI and in the IDE
|
|
574
|
+
language server alike. A computed `type` (`type: pickType()`) is not a literal and falls back to
|
|
575
|
+
inference.
|
|
576
|
+
|
|
577
|
+
#### Options
|
|
578
|
+
|
|
579
|
+
One entry per package type. An **absent** key uses the built-in default below; a **present** key
|
|
580
|
+
replaces the whole entry (`layers`, `segments` and `skill`) — there is no merging. A type with no
|
|
581
|
+
entry is silent, which is why `mobile` and `lib` report nothing by default: their layouts are not
|
|
582
|
+
settled.
|
|
583
|
+
|
|
584
|
+
| Type | Default `layers` | Default `segments` | Default `skill` |
|
|
585
|
+
| ---------- | -------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------- |
|
|
586
|
+
| `frontend` | `app`, `features`, `lib`, `components`, `pages`, `routes` | `features/*` → `containers`, `components`, `services`, `__stories__`, `__tests__` | `/infra-kit:fe-architect` |
|
|
587
|
+
| `backend` | `controllers`, `services`, `lib`, `config` | `services/*` → `__tests__` | `/infra-kit:be-architect` |
|
|
588
|
+
| `e2e` | `tests`, `pages`, `config`, `lib`, `components`, `mocks`, `fixtures` | `tests/*` → `fixtures`, `data`, `mocks` | `/infra-kit:e2e-architect` |
|
|
589
|
+
| `mobile` | — (silent) | — | — |
|
|
590
|
+
| `lib` | — (silent) | — | — |
|
|
591
|
+
|
|
592
|
+
| Option | Type | Description |
|
|
593
|
+
| ----------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
594
|
+
| `<type>.layers` | `string[]` | Allowed first-level `src/` directories for that type. |
|
|
595
|
+
| `<type>.segments` | `Record<string, string[]>` | Allowed directories one level down, keyed by `<layer>/*` (inside each folder of the layer) or `<layer>`. A key must name a layer from `layers`. |
|
|
596
|
+
| `<type>.skill` | `string` | Appended as "See `<skill>` for the `<type>` layout."; `''` or absent drops the sentence. |
|
|
597
|
+
| `ignore` | `string[]` | Skip files matching these globs. |
|
|
598
|
+
|
|
599
|
+
```js
|
|
600
|
+
// Override one type wholesale; the other types keep their defaults.
|
|
601
|
+
export default config({
|
|
602
|
+
rules: {
|
|
603
|
+
'@wl/package-structure': [
|
|
604
|
+
'error',
|
|
605
|
+
{
|
|
606
|
+
backend: {
|
|
607
|
+
layers: ['lambda_controllers', 'services', 'libs', 'local'],
|
|
608
|
+
segments: { 'services/*': ['__tests__', 'fixtures'] },
|
|
609
|
+
skill: '/infra-kit:be-architect',
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
})
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Flat config **replaces** rule options across matching blocks (it does not merge), so a consumer
|
|
618
|
+
override restates every type it wants to change — and only those.
|
|
619
|
+
|
|
620
|
+
#### Caveats
|
|
621
|
+
|
|
622
|
+
- **Filesystem-coupled.** The rule stats the package's `infra-kit.config.ts` once per linted file
|
|
623
|
+
(microseconds) and reads it only when the mtime changed. `package.json` dependency signals are
|
|
624
|
+
read once per lint process; adding `@playwright/test` mid-session is a `pnpm install` event, not an
|
|
625
|
+
edit-lint loop, and is seen by the next process.
|
|
626
|
+
- **`--cache`.** Renaming a directory changes every file path under it, so ESLint's cache already
|
|
627
|
+
misses; editing `infra-kit.config.ts` does not touch the linted files, so a cached result can go
|
|
628
|
+
stale until those files change. Run without `--cache` in CI if you rely on this rule as a gate.
|
|
629
|
+
- **The backend skill is a forward reference.** `/infra-kit:be-architect` is named in the default
|
|
630
|
+
message ahead of the skill shipping; override `backend.skill` if the pointer should go elsewhere.
|
|
529
631
|
### `max-jsdoc-lines`
|
|
530
632
|
|
|
531
633
|
Cap the height of a JSDoc block. The block's **prose** and its **`@example` bodies**
|
|
@@ -731,3 +833,42 @@ Module-level rationale is exempted the same way: tag the block `@fileoverview`
|
|
|
731
833
|
(or `@module` / `@packageDocumentation`). There is no positional exemption, and
|
|
732
834
|
the message names the tag inline so the escape hatch is visible at the point of
|
|
733
835
|
failure.
|
|
836
|
+
|
|
837
|
+
### `require-jsdoc-example`
|
|
838
|
+
|
|
839
|
+
Graduated JSDoc requirement driven by **cognitive complexity**: at or above `minComplexity` a named
|
|
840
|
+
top-level function must carry a leading JSDoc block, and at or above `exampleComplexity` that block
|
|
841
|
+
must also include an `@example` tag. Functions below the first threshold are left alone — the rule
|
|
842
|
+
asks for documentation where a reader needs it, not everywhere.
|
|
843
|
+
|
|
844
|
+
```ts
|
|
845
|
+
// ❌ Incorrect — complexity 13, no JSDoc at all
|
|
846
|
+
export const resolveRoute = (ctx: Ctx) => { /* nested branches, loops, early returns … */ }
|
|
847
|
+
|
|
848
|
+
// ✅ Correct — a block, and because 13 ≥ 12, a worked `@example`
|
|
849
|
+
/**
|
|
850
|
+
* Pick the route for a request, honouring locale, redirects and the fallback page.
|
|
851
|
+
*
|
|
852
|
+
* @example
|
|
853
|
+
* resolveRoute({ path: '/he/deals', locale: 'he' }) // → { page: 'deals', locale: 'he' }
|
|
854
|
+
*/
|
|
855
|
+
export const resolveRoute = (ctx: Ctx) => { … }
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
Targets are named `function` declarations and `const name = () => …` / `function` expressions at the
|
|
859
|
+
top level, with or without an `export` wrapper; the JSDoc may sit on the declaration or on the
|
|
860
|
+
`export` statement. Complexity is the same cognitive-complexity metric `sonarjs` uses. The
|
|
861
|
+
recommended preset ships this rule at `warn` in its own `**/*.{ts,tsx}` block, because the functions
|
|
862
|
+
it targets overwhelmingly live in plain `.ts` modules.
|
|
863
|
+
|
|
864
|
+
#### Options (all optional)
|
|
865
|
+
|
|
866
|
+
| Option | Default | Description |
|
|
867
|
+
| ------------------- | ------- | ----------------------------------------------------------------------------------- |
|
|
868
|
+
| `minComplexity` | `8` | Cognitive complexity at or above which a function must carry a leading JSDoc block. |
|
|
869
|
+
| `exampleComplexity` | `12` | Cognitive complexity at or above which that block must also include an `@example`. |
|
|
870
|
+
| `paths` | `[]` | Restrict the rule to files matching these globs. |
|
|
871
|
+
| `ignore` | `[]` | Skip files matching these globs (takes precedence over `paths`). |
|
|
872
|
+
|
|
873
|
+
The `@example` bodies this rule mandates are charged to their own budget by
|
|
874
|
+
[`max-jsdoc-lines`](#max-jsdoc-lines), so satisfying one rule can never violate the other.
|