@geonosis/oxlint-plugin-biological-architecture 2.7.0 → 2.8.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 +25 -0
- package/RULES.md +1 -1
- package/dist/index.js +95 -26
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @geonosis/oxlint-plugin-biological-architecture
|
|
2
2
|
|
|
3
|
+
## 2.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e880a6c: The tiers have no dead ends a consumer can reach (#1, #2). Three shapes from one migration had no
|
|
8
|
+
legal expression, because two rules together forbade every place the file could live:
|
|
9
|
+
|
|
10
|
+
- **`tissue-no-data-props`** accepts layout configuration: a `number` or `boolean` prop the tissue
|
|
11
|
+
gives a literal default (`initialCount = 10`, `wrap = false`). A stateless adapter between two
|
|
12
|
+
cells is a tissue, and now it can be one. A string, a default that is not a literal of the prop's
|
|
13
|
+
type, and a primitive with no default are still data.
|
|
14
|
+
- **`no-hook-in-component-disguise`** accepts the client-boundary beacon: a file whose directive
|
|
15
|
+
prologue says `'use client'`, whose component calls a hook and returns only `null`. Its parent — a
|
|
16
|
+
Server Component, or a tissue under `tissue-no-hooks` — cannot call the hook, so there was no
|
|
17
|
+
real component to move it into. A `'use client'` component that returns its `children`, or calls
|
|
18
|
+
no hook, is still reported. The no-hook message now tells a layout about metadata
|
|
19
|
+
(`passthroughLayout`) and tells any other component nothing of the kind.
|
|
20
|
+
- **`no-invalid-feature-folders`** takes `roots` (default: none). Directly under each declared
|
|
21
|
+
directory, a folder that is not a tier is a finding per file, so an unmigrated pile
|
|
22
|
+
(`src/components/organisms/`) is debt the ratchet counts and moving a file into it grows the
|
|
23
|
+
count, instead of silently clearing every tier rule.
|
|
24
|
+
|
|
25
|
+
`cell-must-not-compose-cell` keeps no exception; its fix now names the stateful host: two cells
|
|
26
|
+
side by side, coordinating through the store, arranged by a tissue.
|
|
27
|
+
|
|
3
28
|
## 2.7.0
|
|
4
29
|
|
|
5
30
|
### Minor Changes
|
package/RULES.md
CHANGED
|
@@ -867,7 +867,7 @@ option not named here is one no rule reads.
|
|
|
867
867
|
| `no-index-count-as-exact` | `estimateFields`, `labelKey` |
|
|
868
868
|
| `no-inert-hidden-jsx` | — |
|
|
869
869
|
| `no-inline-data-in-jsx` | — |
|
|
870
|
-
| `no-invalid-feature-folders` |
|
|
870
|
+
| `no-invalid-feature-folders` | `roots` |
|
|
871
871
|
| `no-loader-side-effect-in-index-file` | `directories`, `resourceFactories` |
|
|
872
872
|
| `no-logic-in-component-files` | — |
|
|
873
873
|
| `no-mock-db-in-integration` | `dbModules`, `integrationDirs`, `mockNames` |
|
package/dist/index.js
CHANGED
|
@@ -538,7 +538,12 @@ var cellMustNotComposeCell = directionRule({
|
|
|
538
538
|
description: "Cells must not import from any other cell anywhere in the repo. No same-feature exception, no shared/document/charts exception. Cells compose only atoms, molecules, compounds, and organelles. Type-only imports are allowed.",
|
|
539
539
|
fixShape: `A cell never composes another cell \u2014 not same-feature, not \`shared/\`, no exception. If you want to,
|
|
540
540
|
one of the two is misclassified: arrangement goes up to a \`tissues/\` file, a stateless display goes
|
|
541
|
-
down to \`compounds/\`, a store adapter goes sideways to \`organelles/\`.
|
|
541
|
+
down to \`compounds/\`, a store adapter goes sideways to \`organelles/\`. A parent that holds state AND
|
|
542
|
+
hosts a cell (a summary card that opens a modal-hosted form) is two cells side by side: the child
|
|
543
|
+
hosts its own modal and reads whether it is open from the store, the parent writes it, and a
|
|
544
|
+
\`tissues/\` file arranges both. Cells coordinate through a store, never through props from one to
|
|
545
|
+
the other. A stateless adapter between cells is a tissue \u2014 it may take number and boolean layout
|
|
546
|
+
configuration with a literal default (\`step = 10\`). Private siblings of the SAME
|
|
542
547
|
component (\`invoice-editor.parts.tsx\` from \`invoice-editor.tsx\`) are one cell and are fine. The
|
|
543
548
|
barrels \`cells/index.ts\`, \`cells/dynamic.ts\` and \`cells/dynamic.tsx\` are exempt \u2014 re-exporting
|
|
544
549
|
siblings is the shape \`cells-folder-index-is-barrel\` requires of them.`,
|
|
@@ -2621,6 +2626,7 @@ var EXPLICIT_SIDE_EFFECT_HOOKS = /* @__PURE__ */ new Set([
|
|
|
2621
2626
|
]);
|
|
2622
2627
|
var CUSTOM_EFFECT_HOOK_PATTERN = /^use[A-Z][A-Za-z0-9]*Effect$/;
|
|
2623
2628
|
var STORE_HOOK_PATTERN = /^use[A-Z][A-Za-z0-9]*Store$/;
|
|
2629
|
+
var ANY_HOOK_PATTERN = /^use[A-Z]/;
|
|
2624
2630
|
function isSideEffectHook(name) {
|
|
2625
2631
|
if (EXPLICIT_SIDE_EFFECT_HOOKS.has(name)) return true;
|
|
2626
2632
|
if (CUSTOM_EFFECT_HOOK_PATTERN.test(name)) return true;
|
|
@@ -2634,6 +2640,7 @@ var ROUTE_FALLBACK_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
2634
2640
|
"loading.tsx",
|
|
2635
2641
|
"not-found.tsx"
|
|
2636
2642
|
]);
|
|
2643
|
+
var LAYOUT_BASENAMES = /* @__PURE__ */ new Set(["layout.tsx", "template.tsx"]);
|
|
2637
2644
|
var R3F_PACKAGES = /* @__PURE__ */ new Set(["@react-three/drei", "@react-three/fiber"]);
|
|
2638
2645
|
function isPascalCase(name) {
|
|
2639
2646
|
return /^[A-Z]/.test(name);
|
|
@@ -2666,11 +2673,21 @@ function isIdentityFragment(n) {
|
|
|
2666
2673
|
return false;
|
|
2667
2674
|
}
|
|
2668
2675
|
function classifyReturnShape(arg) {
|
|
2669
|
-
if (isNullLiteral(arg)) return "
|
|
2670
|
-
if (isChildrenIdentifier(arg)) return "
|
|
2671
|
-
if (isIdentityFragment(arg)) return "
|
|
2676
|
+
if (isNullLiteral(arg)) return "null";
|
|
2677
|
+
if (isChildrenIdentifier(arg)) return "passthrough";
|
|
2678
|
+
if (isIdentityFragment(arg)) return "passthrough";
|
|
2672
2679
|
return "real";
|
|
2673
2680
|
}
|
|
2681
|
+
function recordReturn(scope, arg) {
|
|
2682
|
+
if (!scope) return;
|
|
2683
|
+
const shape = classifyReturnShape(arg);
|
|
2684
|
+
if (shape === "real") {
|
|
2685
|
+
scope.hasNonIdentityReturn = true;
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
scope.hasIdentityReturn = true;
|
|
2689
|
+
if (shape === "passthrough") scope.hasPassthroughReturn = true;
|
|
2690
|
+
}
|
|
2674
2691
|
function suggestedHookName(componentName) {
|
|
2675
2692
|
const trimmed = componentName.replace(/(Provider|Sync|Listener|Hydration)$/, "");
|
|
2676
2693
|
const kebab = trimmed.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
@@ -2684,6 +2701,7 @@ var noHookInComponentDisguise = {
|
|
|
2684
2701
|
const basename = normalized.split("/").pop() ?? "";
|
|
2685
2702
|
if (ROUTE_FALLBACK_BASENAMES.has(basename)) return {};
|
|
2686
2703
|
let fileUsesR3F = false;
|
|
2704
|
+
let fileIsClientBoundary = false;
|
|
2687
2705
|
const scopeStack = [];
|
|
2688
2706
|
let pendingName = null;
|
|
2689
2707
|
function currentScope() {
|
|
@@ -2691,9 +2709,11 @@ var noHookInComponentDisguise = {
|
|
|
2691
2709
|
}
|
|
2692
2710
|
function enterFunction(name, node) {
|
|
2693
2711
|
scopeStack.push({
|
|
2712
|
+
calledAnyHook: false,
|
|
2694
2713
|
calledHooks: /* @__PURE__ */ new Set(),
|
|
2695
2714
|
hasIdentityReturn: false,
|
|
2696
2715
|
hasNonIdentityReturn: false,
|
|
2716
|
+
hasPassthroughReturn: false,
|
|
2697
2717
|
name,
|
|
2698
2718
|
node
|
|
2699
2719
|
});
|
|
@@ -2706,14 +2726,16 @@ var noHookInComponentDisguise = {
|
|
|
2706
2726
|
if (scope.hasNonIdentityReturn) return;
|
|
2707
2727
|
if (!scope.hasIdentityReturn) return;
|
|
2708
2728
|
if (basename === "page.tsx" && scope.calledHooks.size === 0) return;
|
|
2729
|
+
if (fileIsClientBoundary && scope.calledAnyHook && !scope.hasPassthroughReturn) return;
|
|
2709
2730
|
const hasHooks = scope.calledHooks.size > 0;
|
|
2731
|
+
const passthrough = LAYOUT_BASENAMES.has(basename) ? "passthroughLayout" : "passthroughComponent";
|
|
2710
2732
|
context.report({
|
|
2711
2733
|
data: {
|
|
2712
2734
|
hooks: hasHooks ? [...scope.calledHooks].toSorted((a, b) => a.localeCompare(b)).join(", ") : "",
|
|
2713
2735
|
name: scope.name,
|
|
2714
2736
|
suggested: suggestedHookName(scope.name)
|
|
2715
2737
|
},
|
|
2716
|
-
messageId: hasHooks ? "hookInDisguise" :
|
|
2738
|
+
messageId: hasHooks ? "hookInDisguise" : passthrough,
|
|
2717
2739
|
node: scope.node
|
|
2718
2740
|
});
|
|
2719
2741
|
}
|
|
@@ -2723,12 +2745,7 @@ var noHookInComponentDisguise = {
|
|
|
2723
2745
|
pendingName = null;
|
|
2724
2746
|
enterFunction(name, node);
|
|
2725
2747
|
if (node.body && node.body.type !== "BlockStatement") {
|
|
2726
|
-
|
|
2727
|
-
const scope = currentScope();
|
|
2728
|
-
if (scope) {
|
|
2729
|
-
if (shape === "identity") scope.hasIdentityReturn = true;
|
|
2730
|
-
else scope.hasNonIdentityReturn = true;
|
|
2731
|
-
}
|
|
2748
|
+
recordReturn(currentScope(), node.body);
|
|
2732
2749
|
}
|
|
2733
2750
|
},
|
|
2734
2751
|
"ArrowFunctionExpression:exit"() {
|
|
@@ -2740,6 +2757,7 @@ var noHookInComponentDisguise = {
|
|
|
2740
2757
|
if (!scope) return;
|
|
2741
2758
|
const name = node.callee.name;
|
|
2742
2759
|
if (!name) return;
|
|
2760
|
+
if (ANY_HOOK_PATTERN.test(name)) scope.calledAnyHook = true;
|
|
2743
2761
|
if (isSideEffectHook(name)) {
|
|
2744
2762
|
scope.calledHooks.add(name);
|
|
2745
2763
|
}
|
|
@@ -2765,12 +2783,16 @@ var noHookInComponentDisguise = {
|
|
|
2765
2783
|
fileUsesR3F = true;
|
|
2766
2784
|
}
|
|
2767
2785
|
},
|
|
2786
|
+
// Only the prologue is a directive: the same string after an import is an expression
|
|
2787
|
+
// statement, and React reads it as nothing.
|
|
2788
|
+
Program(node) {
|
|
2789
|
+
for (const statement2 of node.body ?? []) {
|
|
2790
|
+
if (typeof statement2.directive !== "string") break;
|
|
2791
|
+
if (statement2.directive === "use client") fileIsClientBoundary = true;
|
|
2792
|
+
}
|
|
2793
|
+
},
|
|
2768
2794
|
ReturnStatement(node) {
|
|
2769
|
-
|
|
2770
|
-
if (!scope) return;
|
|
2771
|
-
const shape = classifyReturnShape(node.argument);
|
|
2772
|
-
if (shape === "identity") scope.hasIdentityReturn = true;
|
|
2773
|
-
else scope.hasNonIdentityReturn = true;
|
|
2795
|
+
recordReturn(currentScope(), node.argument);
|
|
2774
2796
|
},
|
|
2775
2797
|
VariableDeclarator(node) {
|
|
2776
2798
|
if (node.id?.type === "Identifier" && node.init && (node.init.type === "ArrowFunctionExpression" || node.init.type === "FunctionExpression")) {
|
|
@@ -2781,14 +2803,19 @@ var noHookInComponentDisguise = {
|
|
|
2781
2803
|
},
|
|
2782
2804
|
fixShape: `A component that calls hooks, renders nothing, and exists only to run an effect is a hook wearing
|
|
2783
2805
|
\`.tsx\`. Move it to \`organelles/use-<thing>.ts\`, export it as \`use<Thing>\`, and let the cell call it.
|
|
2784
|
-
A component returns UI; a hook returns state
|
|
2806
|
+
A component returns UI; a hook returns state. The one exception is the client-boundary beacon, for a
|
|
2807
|
+
parent that cannot call hooks \u2014 a Server Component, or a tissue: \`'use client'\` as the file's first
|
|
2808
|
+
statement, a hook call, and \`return null\`. Render it beside the children, never around them \u2014 a
|
|
2809
|
+
component that returns its \`children\` is still a disguise. A component that calls no hook and
|
|
2810
|
+
returns nothing is not a beacon either; delete it.`,
|
|
2785
2811
|
meta: {
|
|
2786
2812
|
docs: {
|
|
2787
|
-
description: "Forbid components whose body calls side-effect hooks (useEffect/useLayoutEffect/useInsertionEffect/useRef) but whose every return is identity (null, bare `children`, `<></>`, `<>{children}</>`). Such a component renders no real UI \u2014 it's a hook masquerading as a component. Move to organelles/use-*-effect.ts and call the hook from a real component. Exempt: route fallback files (default/not-found/loading/error/global-error.tsx)
|
|
2813
|
+
description: "Forbid components whose body calls side-effect hooks (useEffect/useLayoutEffect/useInsertionEffect/useRef) but whose every return is identity (null, bare `children`, `<></>`, `<>{children}</>`). Such a component renders no real UI \u2014 it's a hook masquerading as a component. Move to organelles/use-*-effect.ts and call the hook from a real component. Exempt: route fallback files (default/not-found/loading/error/global-error.tsx), files importing from @react-three/fiber or @react-three/drei, and the client-boundary beacon \u2014 a 'use client' file whose component calls a hook and returns only null. That is lint-theater."
|
|
2788
2814
|
},
|
|
2789
2815
|
messages: {
|
|
2790
|
-
hookInDisguise: "Component '{{name}}' has side-effect hooks ({{hooks}}) but renders only identity/null. It's a hook in disguise \u2014 move to organelles/use-{{suggested}}-effect.ts and call the hook from a real component.",
|
|
2791
|
-
passthroughComponent: "Component '{{name}}' renders only identity/null
|
|
2816
|
+
hookInDisguise: "Component '{{name}}' has side-effect hooks ({{hooks}}) but renders only identity/null. It's a hook in disguise \u2014 move to organelles/use-{{suggested}}-effect.ts and call the hook from a real component. If its parent cannot call hooks (a Server Component, a tissue), make it the client boundary instead: 'use client' first, and return null \u2014 rendered beside the children, not around them.",
|
|
2817
|
+
passthroughComponent: "Component '{{name}}' renders only identity/null and calls no hook. It serves no architectural purpose: delete it and render its children where it is used, or give it real content.",
|
|
2818
|
+
passthroughLayout: "Component '{{name}}' renders only identity/null with no effects. It serves no architectural purpose. If this is a metadata-only layout, move `metadata`/`generateMetadata` to the sibling page.tsx and delete this file. If it's structural, give it real content."
|
|
2792
2819
|
},
|
|
2793
2820
|
schema: [],
|
|
2794
2821
|
type: "problem"
|
|
@@ -3017,10 +3044,31 @@ var VALID_FOLDERS = /* @__PURE__ */ new Set([
|
|
|
3017
3044
|
"tissues"
|
|
3018
3045
|
]);
|
|
3019
3046
|
var FEATURE_PATH_PATTERN = /features\/([^/]+)\/([^/]+)\//;
|
|
3047
|
+
var folderUnderRoot = (normalized, roots) => {
|
|
3048
|
+
const path2 = `/${normalized}`;
|
|
3049
|
+
for (const declared of roots) {
|
|
3050
|
+
const root = declared.replaceAll(/^\/+|\/+$/g, "");
|
|
3051
|
+
const at = path2.indexOf(`/${root}/`);
|
|
3052
|
+
if (root === "" || at === -1) continue;
|
|
3053
|
+
const rest = path2.slice(at + root.length + 2);
|
|
3054
|
+
const slash = rest.indexOf("/");
|
|
3055
|
+
if (slash !== -1) return { folder: rest.slice(0, slash), root };
|
|
3056
|
+
}
|
|
3057
|
+
return void 0;
|
|
3058
|
+
};
|
|
3020
3059
|
var noInvalidFeatureFolders = {
|
|
3021
3060
|
create(context) {
|
|
3022
3061
|
const normalized = normalise(context.filename);
|
|
3023
3062
|
const match = normalized.match(FEATURE_PATH_PATTERN);
|
|
3063
|
+
const { roots = [] } = context.options?.[0] ?? {};
|
|
3064
|
+
const underRoot = match ? void 0 : folderUnderRoot(normalized, roots);
|
|
3065
|
+
if (underRoot && underRoot.folder !== "features" && !VALID_FOLDERS.has(underRoot.folder)) {
|
|
3066
|
+
return {
|
|
3067
|
+
Program(node) {
|
|
3068
|
+
context.report({ data: underRoot, messageId: "untiered", node });
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3024
3072
|
if (!match) return {};
|
|
3025
3073
|
const feature = match[1] ?? "";
|
|
3026
3074
|
const folder = match[2] ?? "";
|
|
@@ -3050,16 +3098,28 @@ var noInvalidFeatureFolders = {
|
|
|
3050
3098
|
fixShape: `A feature folder holds only the tiers and their two neighbours: \`atoms\`, \`molecules\`, \`compounds\`,
|
|
3051
3099
|
\`organelles\`, \`cells\`, \`tissues\`, \`stores\`, \`lib\`. Anything else (\`components\`, \`utils\`, \`helpers\`,
|
|
3052
3100
|
\`types\`) is an unclassified pile \u2014 put the file in the tier that describes what it does, and pure
|
|
3053
|
-
logic in \`lib
|
|
3101
|
+
logic in \`lib/\`. A repo whose components live outside \`features/\` names those directories in
|
|
3102
|
+
\`roots\` (default: none), and the same holds directly under each one. An unmigrated folder there
|
|
3103
|
+
(\`organisms/\`) is a finding per file \u2014 debt the ratchet counts, never a place to park a file whose
|
|
3104
|
+
tier would not accept it.`,
|
|
3054
3105
|
meta: {
|
|
3055
3106
|
docs: {
|
|
3056
|
-
description: 'Files inside features/*/ must be in a valid biological hierarchy folder. Hooks (use-*) must be in organelles/, not lib/. Folders like "dynamic", "utils", "helpers", "components", "hooks" are forbidden.'
|
|
3107
|
+
description: 'Files inside features/*/ \u2014 and directly under each directory the `roots` option declares \u2014 must be in a valid biological hierarchy folder. Hooks (use-*) must be in organelles/, not lib/. Folders like "dynamic", "utils", "helpers", "components", "hooks" are forbidden.'
|
|
3057
3108
|
},
|
|
3058
3109
|
messages: {
|
|
3059
3110
|
hookInLib: 'Hook file "{{file}}" is in features/{{feature}}/lib/ but hooks must be in features/{{feature}}/organelles/. lib/ is for pure logic only \u2014 no hooks, no state.',
|
|
3060
|
-
invalid: 'File is in "features/{{feature}}/{{folder}}/" which is not a valid biological hierarchy folder. Valid folders: compounds, organelles, cells, tissues, stores, lib.'
|
|
3111
|
+
invalid: 'File is in "features/{{feature}}/{{folder}}/" which is not a valid biological hierarchy folder. Valid folders: compounds, organelles, cells, tissues, stores, lib.',
|
|
3112
|
+
untiered: 'File is in "{{root}}/{{folder}}/", which is not a tier, so no tier rule inspects it. Move it to the tier that describes what it does: atoms, molecules, compounds, organelles, cells, tissues, stores or lib.'
|
|
3061
3113
|
},
|
|
3062
|
-
schema: [
|
|
3114
|
+
schema: [
|
|
3115
|
+
{
|
|
3116
|
+
additionalProperties: false,
|
|
3117
|
+
properties: {
|
|
3118
|
+
roots: { items: { type: "string" }, type: "array" }
|
|
3119
|
+
},
|
|
3120
|
+
type: "object"
|
|
3121
|
+
}
|
|
3122
|
+
],
|
|
3063
3123
|
type: "problem"
|
|
3064
3124
|
}
|
|
3065
3125
|
};
|
|
@@ -6297,6 +6357,12 @@ function isAllowedType(t) {
|
|
|
6297
6357
|
return true;
|
|
6298
6358
|
}
|
|
6299
6359
|
}
|
|
6360
|
+
var literalDefaultsOf = (param) => new Map(
|
|
6361
|
+
(param.properties ?? []).flatMap(
|
|
6362
|
+
(property) => property.type === "Property" && property.key?.type === "Identifier" && property.value?.type === "AssignmentPattern" && property.value.right?.type === "Literal" ? [[property.key.name ?? "", property.value.right.value]] : []
|
|
6363
|
+
)
|
|
6364
|
+
);
|
|
6365
|
+
var isLayoutConfiguration = (t, defaults, prop) => t.type === "TSNumberKeyword" && typeof defaults.get(prop) === "number" || t.type === "TSBooleanKeyword" && typeof defaults.get(prop) === "boolean";
|
|
6300
6366
|
var tissueNoDataProps = {
|
|
6301
6367
|
create(context) {
|
|
6302
6368
|
const normalized = normalise(context.filename);
|
|
@@ -6312,6 +6378,7 @@ var tissueNoDataProps = {
|
|
|
6312
6378
|
const typeLiteral = firstParam.typeAnnotation?.typeAnnotation;
|
|
6313
6379
|
if (!typeLiteral || typeLiteral.type !== "TSTypeLiteral") return;
|
|
6314
6380
|
const members = typeLiteral.members ?? [];
|
|
6381
|
+
const defaults = literalDefaultsOf(firstParam);
|
|
6315
6382
|
for (const member of members) {
|
|
6316
6383
|
if (member.key?.type !== "Identifier") continue;
|
|
6317
6384
|
const propName = member.key.name ?? "";
|
|
@@ -6320,6 +6387,7 @@ var tissueNoDataProps = {
|
|
|
6320
6387
|
const t = member.typeAnnotation?.typeAnnotation;
|
|
6321
6388
|
if (!t) continue;
|
|
6322
6389
|
if (isAllowedType(t)) continue;
|
|
6390
|
+
if (isLayoutConfiguration(t, defaults, propName)) continue;
|
|
6323
6391
|
context.report({
|
|
6324
6392
|
data: { prop: propName, type: describeTypeAnnotation(t) },
|
|
6325
6393
|
messageId: "dataProp",
|
|
@@ -6359,13 +6427,14 @@ var tissueNoDataProps = {
|
|
|
6359
6427
|
fixShape: `A tissue takes \`children\`, ReactNode slot props, layout variants, \`params\` and \`searchParams\` \u2014 and
|
|
6360
6428
|
nothing else. A prop typed \`string\`, \`number\`, an array or a domain type (\`id\`, \`initialData\`,
|
|
6361
6429
|
\`documents\`) means the file owns data, which makes it a CELL: move it to \`cells/\`. A tissue gets its
|
|
6362
|
-
content as slots, never as data
|
|
6430
|
+
content as slots, never as data. Layout configuration is not data: a \`number\` or \`boolean\` prop the
|
|
6431
|
+
tissue gives a literal default (\`{ step = 10, wrap = false }\`) is allowed.`,
|
|
6363
6432
|
meta: {
|
|
6364
6433
|
docs: {
|
|
6365
6434
|
description: "Tissues must not own data. Props on a tissue must be limited to `children`, ReactNode slot props, `params`, `searchParams`, icon types, or layout-variant literal unions. Any primitive (`string`/`number`/`boolean`), array type, or arbitrary domain type reference means the file is actually a cell \u2014 move it to cells/. That is lint-theater.2 (Tissue self-sufficiency rule)."
|
|
6366
6435
|
},
|
|
6367
6436
|
messages: {
|
|
6368
|
-
dataProp: "Tissue prop '{{prop}}' has type '{{type}}' which is not allowed on a tissue. Tissues arrange children without owning data. Move the file from features/*/tissues/ to features/*/cells/ \u2014 cells own data. Allowed tissue props: children, params, searchParams, ReactNode slots, literal-union variants, icon types."
|
|
6437
|
+
dataProp: "Tissue prop '{{prop}}' has type '{{type}}' which is not allowed on a tissue. Tissues arrange children without owning data. Move the file from features/*/tissues/ to features/*/cells/ \u2014 cells own data. Allowed tissue props: children, params, searchParams, ReactNode slots, literal-union variants, icon types, and number/boolean layout configuration with a literal default."
|
|
6369
6438
|
},
|
|
6370
6439
|
schema: [],
|
|
6371
6440
|
type: "problem"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geonosis/oxlint-plugin-biological-architecture",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"types": "./dist/index.d.ts",
|
|
5
5
|
"description": "Biological tier architecture as lint — the union of the dielime and during.day 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": "2.
|
|
48
|
+
"@geonosis/lint-parity": "2.8.0"
|
|
49
49
|
},
|
|
50
50
|
"engines": {
|
|
51
51
|
"node": ">=22"
|