@geonosis/oxlint-plugin-biological-architecture 0.2.1 → 0.4.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.
Files changed (29) hide show
  1. package/corpus/.oxlintrc.json +1 -0
  2. package/corpus/admin/order-widget.tsx +13 -0
  3. package/corpus/atoms-discovery/generic-only.tsx +9 -0
  4. package/corpus/atoms-discovery/html-attributes-select.tsx +15 -0
  5. package/corpus/atoms-discovery/icon-check.tsx +7 -0
  6. package/corpus/atoms-discovery/label.tsx +3 -0
  7. package/corpus/atoms-discovery/props-button.tsx +27 -0
  8. package/corpus/atoms-discovery/props-without-ref-textarea.tsx +11 -0
  9. package/corpus/atoms-discovery/react-namespaced-input.tsx +13 -0
  10. package/corpus/atoms-discovery/slot-anchor.stories.tsx +9 -0
  11. package/corpus/atoms-discovery/slot-anchor.tsx +15 -0
  12. package/corpus/emits/bad-no-options.ts +7 -0
  13. package/corpus/emits/bad-priority-only.ts +11 -0
  14. package/corpus/emits/bad-single-attempt.ts +8 -0
  15. package/corpus/emits/good-retry-budget.ts +9 -0
  16. package/corpus/emits/good-shared-constant.ts +10 -0
  17. package/corpus/emits/retry.ts +3 -0
  18. package/corpus/features/invoice/cells/bad-relative-cross-feature-store.tsx +9 -0
  19. package/corpus/features/invoice/cells/good-relative-own-store.tsx +8 -0
  20. package/corpus/features/invoice/organelles/bad-relative-cross-feature-organelle.tsx +11 -0
  21. package/corpus/features/invoice/organelles/good-relative-same-feature-organelle.tsx +11 -0
  22. package/corpus/manifest.json +1 -0
  23. package/corpus/molecules/raw-button.tsx +10 -0
  24. package/dist/{chunk-HXBZSOIU.js → chunk-55NK7NJN.js} +1 -1
  25. package/dist/discover-atoms.d.ts +12 -0
  26. package/dist/discover-atoms.js +197 -5
  27. package/dist/index.js +407 -528
  28. package/dist/presets.js +1 -1
  29. package/package.json +2 -2
@@ -35,6 +35,7 @@
35
35
  }
36
36
  ],
37
37
  "biological-architecture/effect-hook-naming": "error",
38
+ "biological-architecture/emit-declares-attempts": "error",
38
39
  "biological-architecture/layer-walls": [
39
40
  "error",
40
41
  {
@@ -0,0 +1,13 @@
1
+ // A file in no tier folder at all — a Medusa admin widget, a Next route, a one-off surface. The
2
+ // atom exists there too, so no-raw-html-atoms still has something to say.
3
+ import { Card } from '@kit/ui/atoms/card'
4
+
5
+ export function OrderWidget({ onRefresh }: { onRefresh: () => void }) {
6
+ return (
7
+ <Card>
8
+ <button onClick={onRefresh} type="button">
9
+ Refresh
10
+ </button>
11
+ </Card>
12
+ )
13
+ }
@@ -0,0 +1,9 @@
1
+ import { useMemo } from 'react'
2
+
3
+ const weights: Record<string, number> = { bold: 700, regular: 400 }
4
+
5
+ export function Spacer({ weight = 'regular' }: { weight?: string }) {
6
+ const value = useMemo<number>(() => weights[weight] ?? 400, [weight])
7
+
8
+ return <div style={{ fontWeight: value }} />
9
+ }
@@ -0,0 +1,15 @@
1
+ import type { HTMLAttributes } from 'react'
2
+
3
+ interface NativeSelectProps extends Omit<HTMLAttributes<HTMLSelectElement>, 'onChange'> {
4
+ onPick?: (value: string) => void
5
+ }
6
+
7
+ export function NativeSelect({ onPick, children, ...props }: NativeSelectProps) {
8
+ return (
9
+ <div className="relative">
10
+ <select onChange={(event) => onPick?.(event.target.value)} {...props}>
11
+ {children}
12
+ </select>
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,7 @@
1
+ export function CheckIcon({ size = 16 }: { size?: number }) {
2
+ return (
3
+ <svg fill="none" height={size} viewBox="0 0 24 24" width={size}>
4
+ <path d="M20 6 9 17l-5-5" stroke="currentColor" strokeWidth="2" />
5
+ </svg>
6
+ )
7
+ }
@@ -0,0 +1,3 @@
1
+ export function Label({ children }: { children?: unknown }) {
2
+ return <label className="text-sm">{children}</label>
3
+ }
@@ -0,0 +1,27 @@
1
+ import { cva, type VariantProps } from 'class-variance-authority'
2
+ import { Slot } from 'radix-ui'
3
+ import type { ComponentProps, Ref } from 'react'
4
+
5
+ const buttonVariants = cva('inline-flex', {
6
+ variants: { size: { medium: 'px-7', small: 'px-5' } },
7
+ })
8
+
9
+ type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>['size']>
10
+
11
+ const labels: Record<string, string> = { medium: 'Medium', small: 'Small' }
12
+
13
+ interface ButtonProps extends ComponentProps<'button'>, VariantProps<typeof buttonVariants> {
14
+ asChild?: boolean
15
+ ref?: Ref<HTMLButtonElement>
16
+ }
17
+
18
+ export function Button({ asChild = false, size, children, ...props }: ButtonProps) {
19
+ const Comp = asChild ? Slot.Root : 'button'
20
+ const resolved: ButtonSize = size ?? 'medium'
21
+
22
+ return (
23
+ <Comp aria-label={labels[resolved]} {...props}>
24
+ {children}
25
+ </Comp>
26
+ )
27
+ }
@@ -0,0 +1,11 @@
1
+ import type { ComponentPropsWithoutRef } from 'react'
2
+
3
+ type TextAreaProps = ComponentPropsWithoutRef<'textarea'> & { invalid?: boolean }
4
+
5
+ export function TextArea({ invalid = false, ...props }: TextAreaProps) {
6
+ return (
7
+ <div className="relative">
8
+ <textarea data-invalid={invalid} {...props} />
9
+ </div>
10
+ )
11
+ }
@@ -0,0 +1,13 @@
1
+ import * as React from 'react'
2
+
3
+ interface TextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
4
+ invalid?: boolean
5
+ }
6
+
7
+ export function TextInput({ invalid = false, ...props }: TextInputProps) {
8
+ return (
9
+ <div className="relative">
10
+ <input data-invalid={invalid} {...props} />
11
+ </div>
12
+ )
13
+ }
@@ -0,0 +1,9 @@
1
+ import { LinkAtom } from './slot-anchor'
2
+
3
+ export default { component: LinkAtom, title: 'Atoms/LinkAtom' }
4
+
5
+ export const Loading = () => (
6
+ <figure>
7
+ <progress max={100} value={40} />
8
+ </figure>
9
+ )
@@ -0,0 +1,15 @@
1
+ import { Slot } from 'radix-ui'
2
+
3
+ import { cn } from '#lib/utils'
4
+
5
+ interface LinkAtomProps {
6
+ asChild?: boolean
7
+ children?: unknown
8
+ className?: string
9
+ }
10
+
11
+ export function LinkAtom({ asChild = false, children, className }: LinkAtomProps) {
12
+ const Comp = asChild ? Slot.Root : 'a'
13
+
14
+ return <Comp className={cn('underline', className)}>{children}</Comp>
15
+ }
@@ -0,0 +1,7 @@
1
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
2
+
3
+ export const emitMilestone = (data: { tracking_number: string }) =>
4
+ emitEventStep({
5
+ data,
6
+ eventName: 'shipment.milestone',
7
+ })
@@ -0,0 +1,11 @@
1
+ import { EventPriority } from '@medusajs/framework/utils'
2
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
3
+
4
+ // An options bag that names a priority and no attempts: configured to the eye, droppable to the
5
+ // worker, which reads only `attempts > 1` as "a retry was asked for".
6
+ export const emitSampleRequested = (data: { order_id: string }) =>
7
+ emitEventStep({
8
+ data,
9
+ eventName: 'sample.requested',
10
+ options: { priority: EventPriority.CRITICAL },
11
+ }).config({ name: 'emit-sample-requested' })
@@ -0,0 +1,8 @@
1
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
2
+
3
+ export const emitConverted = (data: { order_id: string }) =>
4
+ emitEventStep({
5
+ data,
6
+ eventName: 'sample.converted',
7
+ options: { attempts: 1 },
8
+ })
@@ -0,0 +1,9 @@
1
+ import { EventPriority } from '@medusajs/framework/utils'
2
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
3
+
4
+ export const emitOrderPlaced = (data: { id: string }) =>
5
+ emitEventStep({
6
+ data,
7
+ eventName: 'order.placed',
8
+ options: { attempts: 3, priority: EventPriority.CRITICAL },
9
+ }).config({ name: 'emit-order-placed' })
@@ -0,0 +1,10 @@
1
+ import { emitEventStep } from '@medusajs/medusa/core-flows'
2
+
3
+ import { RETRY_BACKGROUND } from './retry'
4
+
5
+ export const emitProofReady = (data: { proof_id: string }) =>
6
+ emitEventStep({
7
+ data,
8
+ eventName: 'proof.ready',
9
+ options: { attempts: RETRY_BACKGROUND.attempts },
10
+ })
@@ -0,0 +1,3 @@
1
+ // The shared retry budgets. One place, so a change to what "background" means reaches every emit.
2
+ export const RETRY_BACKGROUND = { attempts: 5, backoff: 2000 }
3
+ export const RETRY_CRITICAL = { attempts: 9, backoff: 500 }
@@ -0,0 +1,9 @@
1
+ // The same breach as bad-cross-feature-store.tsx, written the way a file two directories away
2
+ // writes it. The source names no feature until it is resolved against THIS file — which is exactly
3
+ // what no-cross-feature-stores used to be blind to.
4
+ import { useBillingStore } from '../../billing/stores/billing-store'
5
+
6
+ export function BadRelativeCrossFeatureStore() {
7
+ const total = useBillingStore((state) => state.total)
8
+ return <span>{total}</span>
9
+ }
@@ -0,0 +1,8 @@
1
+ // A relative import of this feature's OWN store — allowed, and it must stay allowed once relative
2
+ // sources are resolved.
3
+ import { useInvoiceStore } from '../stores/invoice-store'
4
+
5
+ export function GoodRelativeOwnStore() {
6
+ const total = useInvoiceStore((state) => state.total)
7
+ return <span>{total}</span>
8
+ }
@@ -0,0 +1,11 @@
1
+ // A relative climb out of this feature into ANOTHER feature's organelle. organelle-dependency must
2
+ // fire: where the import LANDS is the fact, not how it was spelled.
3
+ import { BillingTotals } from '../../billing/organelles/billing-totals'
4
+
5
+ export function BadRelativeCrossFeatureOrganelle() {
6
+ return (
7
+ <section>
8
+ <BillingTotals />
9
+ </section>
10
+ )
11
+ }
@@ -0,0 +1,11 @@
1
+ // A sub-organelle in the SAME feature — the nucleolus in the nucleus. Allowed, and it must stay
2
+ // allowed once relative sources are resolved.
3
+ import { InvoiceLines } from './invoice-lines'
4
+
5
+ export function GoodRelativeSameFeatureOrganelle() {
6
+ return (
7
+ <section>
8
+ <InvoiceLines />
9
+ </section>
10
+ )
11
+ }
@@ -13,6 +13,7 @@
13
13
  "biological-architecture/document-sagas-are-generic",
14
14
  "biological-architecture/documents-share-one-table",
15
15
  "biological-architecture/effect-hook-naming",
16
+ "biological-architecture/emit-declares-attempts",
16
17
  "biological-architecture/layer-walls",
17
18
  "biological-architecture/max-comment-density",
18
19
  "biological-architecture/molecule-atoms-only",
@@ -0,0 +1,10 @@
1
+ // A molecule hand-rolling the element an atom already wraps: level 2, where the rule bites.
2
+ import { Icon } from '../atoms/icon'
3
+
4
+ export function IconAction({ onPress }: { onPress: () => void }) {
5
+ return (
6
+ <button onClick={onPress} type="button">
7
+ <Icon name="plus" />
8
+ </button>
9
+ )
10
+ }
@@ -52,7 +52,7 @@ var BACKEND_WORKFLOWS = [
52
52
  ];
53
53
  var BACKEND_D1 = ["no-d1-transaction", "worker-handles-are-scoped"];
54
54
  var BACKEND_SAGAFLOW_CF = ["step-opens-its-own-cell"];
55
- var BACKEND_MEDUSA = ["store-route-scopes-tenant-data"];
55
+ var BACKEND_MEDUSA = ["emit-declares-attempts", "store-route-scopes-tenant-data"];
56
56
  var TENANT_SCOPING = [
57
57
  "queries-require-org-scope",
58
58
  "store-route-scopes-tenant-data",
@@ -34,6 +34,18 @@ declare function buildAtomRuleRestrictions(atomsDir: string, importPathPrefix?:
34
34
  * Variants are kept as alternatives in the error message.
35
35
  */
36
36
  declare function scanAtomMappings(atomsDir: string): AtomMapping[];
37
+ /**
38
+ * The native element an atom wraps, read from the one place every atom says so: its props type.
39
+ *
40
+ * `ComponentProps<'button'>`, `ComponentPropsWithoutRef<'textarea'>`,
41
+ * `HTMLAttributes<HTMLSelectElement>` and `React.ButtonHTMLAttributes<HTMLButtonElement>` all name
42
+ * the element outright, and an atom that types its props any other way is an atom nobody can
43
+ * compose. The Radix `asChild ? Slot.Root : 'button'` form comes next, and the JSX-tag scan is
44
+ * only the fallback — it was the whole detector once, and on a real tree it returned `string`,
45
+ * `typeof` and `path` while missing every shadcn button in the repo.
46
+ *
47
+ * The name is kept because consumers import it.
48
+ */
37
49
  declare function detectFirstNativeJsxElement(source: string): null | string;
38
50
 
39
51
  export { buildAtomRestrictions, buildAtomRuleRestrictions, detectFirstNativeJsxElement, scanAtomMappings };
@@ -2,6 +2,164 @@
2
2
  import { readdirSync, readFileSync } from "fs";
3
3
  import { basename, join } from "path";
4
4
  var GENERIC_ELEMENTS = /* @__PURE__ */ new Set(["div", "span"]);
5
+ var HTML_ELEMENTS = /* @__PURE__ */ new Set([
6
+ "a",
7
+ "abbr",
8
+ "address",
9
+ "area",
10
+ "article",
11
+ "aside",
12
+ "audio",
13
+ "b",
14
+ "bdi",
15
+ "bdo",
16
+ "blockquote",
17
+ "br",
18
+ "button",
19
+ "canvas",
20
+ "caption",
21
+ "cite",
22
+ "code",
23
+ "col",
24
+ "colgroup",
25
+ "data",
26
+ "datalist",
27
+ "dd",
28
+ "del",
29
+ "details",
30
+ "dfn",
31
+ "dialog",
32
+ "div",
33
+ "dl",
34
+ "dt",
35
+ "em",
36
+ "embed",
37
+ "fieldset",
38
+ "figcaption",
39
+ "figure",
40
+ "footer",
41
+ "form",
42
+ "h1",
43
+ "h2",
44
+ "h3",
45
+ "h4",
46
+ "h5",
47
+ "h6",
48
+ "header",
49
+ "hgroup",
50
+ "hr",
51
+ "i",
52
+ "iframe",
53
+ "img",
54
+ "input",
55
+ "ins",
56
+ "kbd",
57
+ "label",
58
+ "legend",
59
+ "li",
60
+ "main",
61
+ "map",
62
+ "mark",
63
+ "menu",
64
+ "meter",
65
+ "nav",
66
+ "noscript",
67
+ "object",
68
+ "ol",
69
+ "optgroup",
70
+ "option",
71
+ "output",
72
+ "p",
73
+ "picture",
74
+ "pre",
75
+ "progress",
76
+ "q",
77
+ "rp",
78
+ "rt",
79
+ "ruby",
80
+ "s",
81
+ "samp",
82
+ "search",
83
+ "section",
84
+ "select",
85
+ "small",
86
+ "source",
87
+ "span",
88
+ "strong",
89
+ "sub",
90
+ "summary",
91
+ "sup",
92
+ "table",
93
+ "tbody",
94
+ "td",
95
+ "template",
96
+ "textarea",
97
+ "tfoot",
98
+ "th",
99
+ "thead",
100
+ "time",
101
+ "tr",
102
+ "track",
103
+ "u",
104
+ "ul",
105
+ "var",
106
+ "video",
107
+ "wbr"
108
+ ]);
109
+ var TAG_OF_DOM_INTERFACE = {
110
+ HTMLAnchorElement: "a",
111
+ HTMLAreaElement: "area",
112
+ HTMLAudioElement: "audio",
113
+ HTMLBRElement: "br",
114
+ HTMLButtonElement: "button",
115
+ HTMLCanvasElement: "canvas",
116
+ HTMLDListElement: "dl",
117
+ HTMLDataElement: "data",
118
+ HTMLDataListElement: "datalist",
119
+ HTMLDetailsElement: "details",
120
+ HTMLDialogElement: "dialog",
121
+ HTMLDivElement: "div",
122
+ HTMLEmbedElement: "embed",
123
+ HTMLFieldSetElement: "fieldset",
124
+ HTMLFormElement: "form",
125
+ HTMLHRElement: "hr",
126
+ HTMLIFrameElement: "iframe",
127
+ HTMLImageElement: "img",
128
+ HTMLInputElement: "input",
129
+ HTMLLIElement: "li",
130
+ HTMLLabelElement: "label",
131
+ HTMLLegendElement: "legend",
132
+ HTMLMapElement: "map",
133
+ HTMLMenuElement: "menu",
134
+ HTMLMeterElement: "meter",
135
+ HTMLOListElement: "ol",
136
+ HTMLObjectElement: "object",
137
+ HTMLOptGroupElement: "optgroup",
138
+ HTMLOptionElement: "option",
139
+ HTMLOutputElement: "output",
140
+ HTMLParagraphElement: "p",
141
+ HTMLPictureElement: "picture",
142
+ HTMLPreElement: "pre",
143
+ HTMLProgressElement: "progress",
144
+ HTMLSelectElement: "select",
145
+ HTMLSourceElement: "source",
146
+ HTMLSpanElement: "span",
147
+ HTMLTableCaptionElement: "caption",
148
+ HTMLTableColElement: "col",
149
+ HTMLTableElement: "table",
150
+ HTMLTableRowElement: "tr",
151
+ HTMLTextAreaElement: "textarea",
152
+ HTMLTimeElement: "time",
153
+ HTMLTrackElement: "track",
154
+ HTMLUListElement: "ul",
155
+ HTMLVideoElement: "video"
156
+ };
157
+ var TAG_OF_ATTRIBUTES_PREFIX = { anchor: "a" };
158
+ var NOT_AN_ATOM = /\.(?:stories|test|spec)\.tsx$/;
159
+ var COMPONENT_PROPS = /(?:React\.)?ComponentProps(?:WithoutRef|WithRef)?\s*<\s*['"]([a-z][a-z0-9]*)['"]\s*>/g;
160
+ var HTML_ATTRIBUTES = /(?:React\.)?([A-Z][A-Za-z]*?)?HTMLAttributes\s*<\s*(HTML[A-Za-z]*Element)\s*>/g;
161
+ var SLOT_FALLBACK = /Slot(?:\.Root)?\s*:\s*['"]([a-z][a-z0-9]*)['"]/;
162
+ var JSX_TAG = /(?<![A-Za-z0-9_$])<([a-z][a-z0-9]*)(?=[\s/>])/g;
5
163
  function buildAtomRestrictions(atomsDir, importPathPrefix = "@kit/ui/atoms") {
6
164
  const mappings = scanAtomMappings(atomsDir);
7
165
  return mappings.map(({ atomName, nativeElement }) => ({
@@ -23,6 +181,7 @@ function scanAtomMappings(atomsDir) {
23
181
  for (const entry of entries) {
24
182
  if (!entry.isFile()) continue;
25
183
  if (!entry.name.endsWith(".tsx")) continue;
184
+ if (NOT_AN_ATOM.test(entry.name)) continue;
26
185
  const atomFile = entry.name;
27
186
  const atomName = basename(atomFile, ".tsx");
28
187
  const content = readFileSync(join(atomsDir, atomFile), "utf8");
@@ -42,11 +201,44 @@ function scanAtomMappings(atomsDir) {
42
201
  return mappings;
43
202
  }
44
203
  function detectFirstNativeJsxElement(source) {
45
- const slotMatch = source.match(/Slot\s*:\s*['"]([a-z][a-z0-9]*)['"]/);
46
- if (slotMatch?.[1]) return slotMatch[1];
47
- const cleaned = source.replaceAll(/\/\/.*$/gm, "").replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""');
48
- const match = cleaned.match(/<([a-z][a-z0-9]*)\b/);
49
- return match?.[1] ?? null;
204
+ const withoutComments = stripComments(source);
205
+ const declared = elementFromPropsType(withoutComments);
206
+ if (declared) return declared;
207
+ const slot = SLOT_FALLBACK.exec(withoutComments)?.[1];
208
+ if (slot && HTML_ELEMENTS.has(slot)) return slot;
209
+ return elementFromJsx(withoutComments);
210
+ }
211
+ function elementFromPropsType(source) {
212
+ const found = [];
213
+ for (const match of source.matchAll(COMPONENT_PROPS)) {
214
+ const element = match[1];
215
+ if (element && HTML_ELEMENTS.has(element)) found.push({ at: match.index, element });
216
+ }
217
+ for (const match of source.matchAll(HTML_ATTRIBUTES)) {
218
+ const element = elementOfAttributes(match[1], match[2]);
219
+ if (element) found.push({ at: match.index, element });
220
+ }
221
+ return found.toSorted((a, b) => a.at - b.at)[0]?.element ?? null;
222
+ }
223
+ function elementOfAttributes(prefix, domInterface) {
224
+ if (prefix !== void 0) {
225
+ const lower = prefix.toLowerCase();
226
+ const named = TAG_OF_ATTRIBUTES_PREFIX[lower] ?? lower;
227
+ if (HTML_ELEMENTS.has(named)) return named;
228
+ }
229
+ const mapped = domInterface === void 0 ? void 0 : TAG_OF_DOM_INTERFACE[domInterface];
230
+ return mapped !== void 0 && HTML_ELEMENTS.has(mapped) ? mapped : null;
231
+ }
232
+ function elementFromJsx(withoutComments) {
233
+ const cleaned = withoutComments.replaceAll(/(["'`])(?:\\.|(?!\1)[^\\])*\1/g, '""');
234
+ for (const match of cleaned.matchAll(JSX_TAG)) {
235
+ const tag = match[1];
236
+ if (tag && HTML_ELEMENTS.has(tag)) return tag;
237
+ }
238
+ return null;
239
+ }
240
+ function stripComments(source) {
241
+ return source.replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/(?<!:)\/\/.*$/gm, "");
50
242
  }
51
243
  function toPascalCase(kebab) {
52
244
  return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");