@geonosis/oxlint-plugin-biological-architecture 3.0.0 → 3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @geonosis/oxlint-plugin-biological-architecture
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 27d169d: `icon-control-has-name`, in the `frontend` preset: a `<button>` or `<a>` — or a control atom the repo names in `controls` — whose only content is an icon (a component whose name matches `icons`, `Icon$` by default, or an inline `<svg>`) and that carries no `aria-label`, `aria-labelledby` or `title` has no name a screen reader can read. A spread is left alone. It needs no option to run.
8
+
3
9
  ## 3.0.0
4
10
 
5
11
  ### Major Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @geonosis/oxlint-plugin-biological-architecture
2
2
 
3
- 80 architecture rules as an oxlint JS plugin: which tier a file belongs to, what it may import,
3
+ 81 architecture rules as an oxlint JS plugin: which tier a file belongs to, what it may import,
4
4
  where state is allowed to live, and the backend invariants that go with them. Nothing is hardcoded
5
5
  to a repo — every list of brands, features, tables or paths is an option. A rule enabled without the
6
6
  options it needs says so once a run and judges nothing, rather than passing silently.
@@ -57,7 +57,7 @@ everything below the organelle is inert. A cell never composes another cell.
57
57
 
58
58
  | preset | rules | what it is |
59
59
  | --- | --- | --- |
60
- | `frontend` (= `recommended`) | 37 | tier direction, state placement, element choice, file shape |
60
+ | `frontend` (= `recommended`) | 38 | tier direction, state placement, element choice, file shape |
61
61
  | `backend-workflows` | 15 | workflows, time, the data layer, single source of truth |
62
62
  | `backend-medusa` | 13 | Medusa steps, emits, subscribers, loaders and the store route surface |
63
63
  | `tenant-scoping` | 4 | every row placed, scoped and narrowed to one tenant |
package/RULES.md CHANGED
@@ -11,12 +11,12 @@ One plugin name, two rule sets, no shared code. **33 rules had the same filename
11
11
 
12
12
  The union here is **52 rules** — 54 when it was cut, less `document-sagas-are-generic` and
13
13
  `documents-share-one-table`, retired in 2.7.0 because both enforced one consumer's document kinds —
14
- plus twenty-eight the kit wrote for itself: `layer-walls`, `emit-declares-attempts`,
14
+ plus twenty-nine the kit wrote for itself: `layer-walls`, `emit-declares-attempts`,
15
15
  `cell-no-cross-feature-organelles`, the `backend-medusa` ten, `no-unregistered-plugin-dir` +
16
16
  `router-schema-parity`, the `themekit` two, `no-mock-db-in-integration`, `todo-names-its-plan`,
17
17
  `boundary-time-is-primitive`, `durable-body-reads-through-steps`, `subpath-over-barrel` and the six
18
- of the W67 batch — **80**. The two retired ids went in 3.0.0.
19
- Every one of the twenty-eight is marked **written here** in the inventory, and the table below is
18
+ of the W67 batch, and `icon-control-has-name` — **81**. The two retired ids went in 3.0.0.
19
+ Every one of the twenty-nine is marked **written here** in the inventory, and the table below is
20
20
  held against that column rather than kept by hand.
21
21
 
22
22
  | | count |
@@ -26,8 +26,8 @@ held against that column rather than kept by hand.
26
26
  | A Medusa storefront only | 3 |
27
27
  | A Workers + D1 app only | 16 |
28
28
  | **union** | **52** |
29
- | written here | 28 |
30
- | **total** | **80** |
29
+ | written here | 29 |
30
+ | **total** | **81** |
31
31
 
32
32
  ## How divergence was resolved
33
33
 
@@ -721,6 +721,7 @@ every repo's.
721
721
  | `dialect-through-the-seam` | backend-workflows | `seam` | `onImportSource` + `mergeVisitors` + `paths` + `options` | — | yes | a Workers + D1 app only, de-hardcoded |
722
722
  | `durable-body-reads-through-steps` | backend-workflows | `ports` | `nodes` + `objects` + `steps` + `options` | — | — | **written here** (#167, design-026 §8) |
723
723
  | `effect-hook-naming` | frontend | — | `paths` | yes | yes | diverged |
724
+ | `icon-control-has-name` | frontend | — | `nodes` + `options` | — | — | **written here** |
724
725
  | `emit-declares-attempts` | backend-medusa | — | `options` | — | — | **written here** |
725
726
  | `font-roles-only` | themekit | `roles` | `paths` + `options` + `styling` | — | — | **written here** (U2) |
726
727
  | `layer-walls` | tooling | `layers` | `onImportSource` + `imports` + `paths` + `options` | — | — | **written here** |
@@ -878,6 +879,7 @@ option not named here is one no rule reads.
878
879
  | `effect-hook-naming` | — |
879
880
  | `emit-declares-attempts` | `budgetHelpers`, `emitters` |
880
881
  | `font-roles-only` | `allow`, `exemptModules`, `prefix`, `roles` |
882
+ | `icon-control-has-name` | `controls`, `icons` |
881
883
  | `layer-walls` | `externals`, `layers`, `typeOnly` |
882
884
  | `list-take-without-order` | `callees`, `orderKeys` |
883
885
  | `max-comment-density` | — |
@@ -171,6 +171,7 @@
171
171
  ]
172
172
  ],
173
173
  "biological-architecture/no-raw-sql-outside-allowed": "error",
174
+ "biological-architecture/icon-control-has-name": "error",
174
175
  "biological-architecture/no-react-namespace": "error",
175
176
  "biological-architecture/no-runtime-dead-affordance": [
176
177
  "error",
@@ -0,0 +1,8 @@
1
+ // BREACH: a screen reader announces "button" and nothing else.
2
+ import { BinIcon } from '@kit/ui/atoms/icons/bin-icon'
3
+
4
+ export const RemoveLine = ({ onRemove }: { onRemove: () => void }): JSX.Element => (
5
+ <button type="button" onClick={onRemove}>
6
+ <BinIcon />
7
+ </button>
8
+ )
@@ -0,0 +1,19 @@
1
+ import { BinIcon } from '@kit/ui/atoms/icons/bin-icon'
2
+
3
+ export const RemoveLine = ({
4
+ label,
5
+ onRemove
6
+ }: {
7
+ label: string
8
+ onRemove: () => void
9
+ }): JSX.Element => (
10
+ <div>
11
+ <button type="button" onClick={onRemove} aria-label={label}>
12
+ <BinIcon />
13
+ </button>
14
+ <button type="button" onClick={onRemove}>
15
+ <BinIcon />
16
+ <span className="sr-only">{label}</span>
17
+ </button>
18
+ </div>
19
+ )
@@ -16,6 +16,7 @@
16
16
  "biological-architecture/effect-hook-naming",
17
17
  "biological-architecture/emit-declares-attempts",
18
18
  "biological-architecture/font-roles-only",
19
+ "biological-architecture/icon-control-has-name",
19
20
  "biological-architecture/layer-walls",
20
21
  "biological-architecture/list-take-without-order",
21
22
  "biological-architecture/max-comment-density",
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import durableBodyReadsThroughSteps from "./rules/durable-body-reads-through-ste
16
16
  import effectHookNaming from "./rules/effect-hook-naming.js";
17
17
  import emitDeclaresAttempts from "./rules/emit-declares-attempts.js";
18
18
  import fontRolesOnly from "./rules/font-roles-only.js";
19
+ import iconControlHasName from "./rules/icon-control-has-name.js";
19
20
  import layerWalls from "./rules/layer-walls.js";
20
21
  import { reportingOncePerRun } from "./rules/lib/options.js";
21
22
  import listTakeWithoutOrder from "./rules/list-take-without-order.js";
@@ -110,6 +111,7 @@ var rules = reportingOnce(withEngines({
110
111
  "effect-hook-naming": effectHookNaming,
111
112
  "emit-declares-attempts": emitDeclaresAttempts,
112
113
  "font-roles-only": fontRolesOnly,
114
+ "icon-control-has-name": iconControlHasName,
113
115
  "layer-walls": layerWalls,
114
116
  "list-take-without-order": listTakeWithoutOrder,
115
117
  "max-comment-density": maxCommentDensity,
package/dist/presets.js CHANGED
@@ -9,6 +9,7 @@ var FRONTEND = [
9
9
  "compound-must-be-stateless",
10
10
  "compound-no-stores",
11
11
  "effect-hook-naming",
12
+ "icon-control-has-name",
12
13
  "molecule-atoms-only",
13
14
  "molecule-must-compose",
14
15
  "next-route-segment-is-thin-delegate",
@@ -0,0 +1,53 @@
1
+ import type { RuleContextWithRawOptions } from './lib/context.js';
2
+ import { type Probe } from './lib/probe.js';
3
+ type Name = {
4
+ name?: string;
5
+ object?: Name;
6
+ property?: Name;
7
+ type: string;
8
+ };
9
+ type Attribute = {
10
+ name?: Name;
11
+ type: string;
12
+ };
13
+ type Child = {
14
+ children?: Child[];
15
+ openingElement?: {
16
+ attributes?: Attribute[];
17
+ name: Name;
18
+ };
19
+ type: string;
20
+ value?: string;
21
+ };
22
+ declare const iconControlHasName: {
23
+ create(context: RuleContextWithRawOptions): {
24
+ JSXElement(node: Child): void;
25
+ };
26
+ fixShape: string;
27
+ meta: {
28
+ docs: {
29
+ description: string;
30
+ };
31
+ messages: {
32
+ unnamed: string;
33
+ };
34
+ schema: {
35
+ additionalProperties: boolean;
36
+ properties: {
37
+ controls: {
38
+ items: {
39
+ type: 'string';
40
+ };
41
+ type: 'array';
42
+ };
43
+ icons: {
44
+ type: 'string';
45
+ };
46
+ };
47
+ type: 'object';
48
+ }[];
49
+ type: 'problem';
50
+ };
51
+ probe(options: readonly unknown[]): Probe;
52
+ };
53
+ export default iconControlHasName;
@@ -0,0 +1,65 @@
1
+ // src/rules/icon-control-has-name.ts
2
+ import { bagOf, firstOf } from "./lib/probe.js";
3
+ import { optionsBag, STRING, STRINGS } from "./lib/schema.js";
4
+ var NATIVE_CONTROLS = ["button", "a"];
5
+ var ICONS = "Icon$";
6
+ var NAMED_BY = new Set(["aria-label", "aria-labelledby", "title"]);
7
+ var nameOf = (node) => node === undefined ? "" : node.type === "JSXMemberExpression" ? `${nameOf(node.object)}.${nameOf(node.property)}` : node.name ?? "";
8
+ var said = (child) => child.type !== "JSXText" || (child.value ?? "").trim() !== "";
9
+ var iconControlHasName = {
10
+ create(context) {
11
+ const options = bagOf(context.options);
12
+ const controls = new Set([...NATIVE_CONTROLS, ...options.controls ?? []]);
13
+ const icon = new RegExp(options.icons ?? ICONS);
14
+ return {
15
+ JSXElement(node) {
16
+ const control = nameOf(node.openingElement?.name);
17
+ if (!controls.has(control))
18
+ return;
19
+ const attributes = node.openingElement?.attributes ?? [];
20
+ if (attributes.some((one) => one.type !== "JSXAttribute" || NAMED_BY.has(nameOf(one.name)))) {
21
+ return;
22
+ }
23
+ const shown = (node.children ?? []).filter(said);
24
+ const icons = shown.map((one) => one.type === "JSXElement" ? nameOf(one.openingElement?.name) : "");
25
+ if (icons.length === 0 || !icons.every((name) => name === "svg" || icon.test(name)))
26
+ return;
27
+ context.report({
28
+ data: { control, icons: icons.join(", ") },
29
+ messageId: "unnamed",
30
+ node
31
+ });
32
+ }
33
+ };
34
+ },
35
+ fixShape: `A control whose only content is an icon has no name: the icon is decorative, so a screen reader
36
+ announces "button" and nothing else. Give it \`aria-label\` (or \`aria-labelledby\`, or \`title\`) in the
37
+ app's own words — a label prop the caller passes, never a string the component invents — or put text
38
+ beside the icon, visible or \`sr-only\`. \`<button>\` and \`<a>\` are read by default; \`controls\` names the
39
+ repo's control atoms, and \`icons\` is the pattern an icon component's name matches (\`Icon$\` by default);
40
+ an inline \`<svg>\` is always one.
41
+ A spread may carry the name and is left alone.`,
42
+ meta: {
43
+ docs: {
44
+ description: "A button or link whose only content is an icon component, with no `aria-label`, `aria-labelledby` or `title`, has no accessible name — a screen reader announces a control that says nothing."
45
+ },
46
+ messages: {
47
+ unnamed: "<{{control}}> shows only {{icons}} and has no name a screen reader can read. Give it aria-label in the words the app passes in, or text beside the icon."
48
+ },
49
+ schema: optionsBag({ controls: STRINGS, icons: STRING }),
50
+ type: "problem"
51
+ },
52
+ probe(options) {
53
+ const bag = bagOf(options);
54
+ const control = firstOf(bag.controls, NATIVE_CONTROLS);
55
+ return {
56
+ path: "molecules/geonosis-probe.tsx",
57
+ source: `export const Probe = () => <${control}><svg /></${control}>
58
+ `
59
+ };
60
+ }
61
+ };
62
+ var icon_control_has_name_default = iconControlHasName;
63
+ export {
64
+ icon_control_has_name_default as default
65
+ };
@@ -2,9 +2,9 @@ import { type RuleContext } from './lib/context.js';
2
2
  import { type CompositionImportNode, type JsxOpeningElementNode } from './lib/render-tracking.js';
3
3
  declare const moleculeMustCompose: {
4
4
  create(context: RuleContext): {
5
- 'Program:exit'?: undefined;
6
5
  ImportDeclaration?: undefined;
7
6
  JSXOpeningElement?: undefined;
7
+ 'Program:exit'?: undefined;
8
8
  } | {
9
9
  ImportDeclaration(node: CompositionImportNode): void;
10
10
  JSXOpeningElement(node: JsxOpeningElementNode): void;
@@ -33,8 +33,8 @@ interface AnyNode {
33
33
  }
34
34
  declare const noDuplicateJsxPatterns: {
35
35
  create(context: RuleContext): {
36
- JSXOpeningElement?: undefined;
37
36
  'Program:exit'?: undefined;
37
+ JSXOpeningElement?: undefined;
38
38
  } | {
39
39
  JSXOpeningElement(node: JSXNode): void;
40
40
  'Program:exit'(): void;
@@ -55,15 +55,15 @@ interface ProgramNode {
55
55
  }
56
56
  declare const noHookInComponentDisguise: {
57
57
  create(context: RuleContext): {
58
- Program?: undefined;
59
- CallExpression?: undefined;
60
58
  ImportDeclaration?: undefined;
59
+ CallExpression?: undefined;
61
60
  ArrowFunctionExpression?: undefined;
62
61
  'ArrowFunctionExpression:exit'?: undefined;
63
62
  FunctionDeclaration?: undefined;
64
63
  'FunctionDeclaration:exit'?: undefined;
65
64
  FunctionExpression?: undefined;
66
65
  'FunctionExpression:exit'?: undefined;
66
+ Program?: undefined;
67
67
  ReturnStatement?: undefined;
68
68
  VariableDeclarator?: undefined;
69
69
  } | {
@@ -9,9 +9,9 @@ interface Options {
9
9
  }
10
10
  declare const noLoaderSideEffectInIndexFile: {
11
11
  create(context: RuleContextWithOptions<Options>): {
12
- CallExpression?: undefined;
13
- ExportDefaultDeclaration?: undefined;
14
12
  ExportNamedDeclaration?: undefined;
13
+ ExportDefaultDeclaration?: undefined;
14
+ CallExpression?: undefined;
15
15
  } | {
16
16
  CallExpression(node: AnyNode): void;
17
17
  ExportDefaultDeclaration(node: AnyNode): void;
@@ -28,8 +28,8 @@ interface ImportNode {
28
28
  }
29
29
  declare const stepOpensItsOwnCell: {
30
30
  create(context: RuleContextWithOptions<Options>): {
31
- Program?: undefined;
32
31
  ImportDeclaration?: undefined;
32
+ Program?: undefined;
33
33
  } | {
34
34
  ImportDeclaration(node: ImportNode): void;
35
35
  Program(node: unknown): void;
@@ -3,8 +3,8 @@ import { type CompositionImportNode, type JsxOpeningElementNode } from './lib/re
3
3
  declare const tissueMustCompose: {
4
4
  create(context: RuleContext): {
5
5
  ExportNamedDeclaration?: undefined;
6
- JSXOpeningElement?: undefined;
7
6
  'Program:exit'?: undefined;
7
+ JSXOpeningElement?: undefined;
8
8
  ImportDeclaration?: undefined;
9
9
  } | {
10
10
  ExportNamedDeclaration(node: {
@@ -1,8 +1,8 @@
1
1
  import { type RuleContext } from './lib/context.js';
2
2
  declare const tissueNoDataProps: {
3
3
  create(context: RuleContext): {
4
- ExportNamedDeclaration?: undefined;
5
4
  ExportDefaultDeclaration?: undefined;
5
+ ExportNamedDeclaration?: undefined;
6
6
  } | {
7
7
  ExportDefaultDeclaration(node: {
8
8
  declaration?: unknown;
@@ -21,8 +21,8 @@ interface ImportSpecifier {
21
21
  }
22
22
  declare const tissueNoHooks: {
23
23
  create(context: RuleContext): {
24
- CallExpression?: undefined;
25
24
  ImportDeclaration?: undefined;
25
+ CallExpression?: undefined;
26
26
  } | {
27
27
  CallExpression(node: AnyNode): void;
28
28
  ImportDeclaration(node: ImportNode): void;
@@ -8,8 +8,8 @@ interface Options {
8
8
  }
9
9
  declare const cellHandlesAreScoped: {
10
10
  create(context: RuleContextWithOptions<Options>): {
11
- CallExpression?: undefined;
12
11
  'Program:exit'?: undefined;
12
+ CallExpression?: undefined;
13
13
  ExportDefaultDeclaration?: undefined;
14
14
  ClassDeclaration?: undefined;
15
15
  } | {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geonosis/oxlint-plugin-biological-architecture",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "description": "Biological tier architecture as lint — the union of a Medusa storefront's and a Workers + D1 app's rule sets, shipped as presets.",
6
6
  "keywords": [
@@ -45,7 +45,7 @@
45
45
  "oxlint": ">=1.77"
46
46
  },
47
47
  "devDependencies": {
48
- "@geonosis/lint-parity": "3.0.0"
48
+ "@geonosis/lint-parity": "3.1.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=22"