@nadicodeai/ui 12.1.164 → 12.1.166
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/AGENTS.md +8 -9
- package/README.md +2 -17
- package/dist/components/accordion.d.ts.map +1 -1
- package/dist/components/accordion.js +2 -1
- package/dist/components/account-menu.d.ts.map +1 -1
- package/dist/components/account-menu.js +5 -3
- package/dist/components/activity-calendar.d.ts.map +1 -1
- package/dist/components/activity-calendar.js +3 -2
- package/dist/components/agent-card.js +1 -1
- package/dist/components/agent-orb.d.ts.map +1 -1
- package/dist/components/agent-orb.js +1 -0
- package/dist/components/agent-profile.d.ts.map +1 -1
- package/dist/components/agent-profile.js +7 -4
- package/dist/components/alert-dialog.d.ts +1 -1
- package/dist/components/alert-dialog.d.ts.map +1 -1
- package/dist/components/alert-dialog.js +3 -3
- package/dist/components/app-mark.js +1 -1
- package/dist/components/avatar.js +8 -8
- package/dist/components/badge.d.ts +11 -16
- package/dist/components/badge.d.ts.map +1 -1
- package/dist/components/badge.js +7 -43
- package/dist/components/breadcrumb.d.ts.map +1 -1
- package/dist/components/breadcrumb.js +3 -2
- package/dist/components/button.d.ts +5 -21
- package/dist/components/button.d.ts.map +1 -1
- package/dist/components/button.js +6 -76
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card-row.js +1 -1
- package/dist/components/card.d.ts +4 -1
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +3 -2
- package/dist/components/catalog-drawer.d.ts.map +1 -1
- package/dist/components/catalog-drawer.js +3 -2
- package/dist/components/chart-axis.d.ts.map +1 -1
- package/dist/components/chart-axis.js +0 -2
- package/dist/components/chart-bar-horizontal.d.ts.map +1 -1
- package/dist/components/chart-bar-horizontal.js +16 -14
- package/dist/components/chart-card.d.ts.map +1 -1
- package/dist/components/chart-card.js +0 -1
- package/dist/components/chart.d.ts.map +1 -1
- package/dist/components/chart.js +18 -32
- package/dist/components/checkbox.d.ts +3 -2
- package/dist/components/checkbox.d.ts.map +1 -1
- package/dist/components/checkbox.js +4 -3
- package/dist/components/choicebox.d.ts +5 -5
- package/dist/components/choicebox.d.ts.map +1 -1
- package/dist/components/choicebox.js +6 -5
- package/dist/components/clearable-input.d.ts.map +1 -1
- package/dist/components/clearable-input.js +2 -2
- package/dist/components/code-block.d.ts.map +1 -1
- package/dist/components/code-block.js +4 -2
- package/dist/components/collapsible.js +1 -1
- package/dist/components/collection-filters.d.ts.map +1 -1
- package/dist/components/collection-filters.js +1 -0
- package/dist/components/collection-page.js +1 -1
- package/dist/components/collection-sort.d.ts.map +1 -1
- package/dist/components/collection-sort.js +1 -4
- package/dist/components/collection-toolbar.js +1 -1
- package/dist/components/combobox.js +4 -4
- package/dist/components/command.d.ts.map +1 -1
- package/dist/components/command.js +1 -1
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/context-menu.d.ts +1 -1
- package/dist/components/context-menu.d.ts.map +1 -1
- package/dist/components/copy-button.js +1 -1
- package/dist/components/data-table.d.ts +11 -3
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +12 -4
- package/dist/components/dialog.d.ts +1 -1
- package/dist/components/dialog.d.ts.map +1 -1
- package/dist/components/dialog.js +2 -2
- package/dist/components/drawer.d.ts +4 -4
- package/dist/components/drawer.d.ts.map +1 -1
- package/dist/components/drawer.js +14 -15
- package/dist/components/dropdown-menu.d.ts +1 -1
- package/dist/components/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu.js +9 -5
- package/dist/components/empty-art.js +1 -1
- package/dist/components/empty-state.js +2 -2
- package/dist/components/entry-scene.d.ts.map +1 -1
- package/dist/components/entry-scene.js +2 -3
- package/dist/components/field.js +3 -3
- package/dist/components/file-diff.d.ts.map +1 -1
- package/dist/components/file-diff.js +5 -2
- package/dist/components/footer.d.ts +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +3 -3
- package/dist/components/form-dialog.d.ts.map +1 -1
- package/dist/components/form-dialog.js +2 -1
- package/dist/components/form-drawer.d.ts +7 -9
- package/dist/components/form-drawer.d.ts.map +1 -1
- package/dist/components/form-drawer.js +5 -5
- package/dist/components/form.d.ts +26 -0
- package/dist/components/form.d.ts.map +1 -0
- package/dist/components/form.js +29 -0
- package/dist/components/gauge.js +2 -2
- package/dist/components/heatmap/calendar-heatmap.d.ts.map +1 -1
- package/dist/components/heatmap/calendar-heatmap.js +15 -13
- package/dist/components/hive-scene.d.ts +3 -3
- package/dist/components/hive-scene.d.ts.map +1 -1
- package/dist/components/hive-scene.js +4 -2
- package/dist/components/hover-card.d.ts +1 -1
- package/dist/components/hover-card.d.ts.map +1 -1
- package/dist/components/hover-card.js +1 -1
- package/dist/components/input-group.d.ts +5 -6
- package/dist/components/input-group.d.ts.map +1 -1
- package/dist/components/input-group.js +8 -11
- package/dist/components/input-otp.d.ts.map +1 -1
- package/dist/components/input-otp.js +2 -2
- package/dist/components/item.d.ts +1 -1
- package/dist/components/item.js +2 -2
- package/dist/components/kbd.d.ts +6 -9
- package/dist/components/kbd.d.ts.map +1 -1
- package/dist/components/kbd.js +7 -3
- package/dist/components/message-notice.js +1 -1
- package/dist/components/middle-truncate.d.ts.map +1 -1
- package/dist/components/middle-truncate.js +1 -0
- package/dist/components/model-identity.d.ts +4 -4
- package/dist/components/model-identity.d.ts.map +1 -1
- package/dist/components/model-identity.js +2 -2
- package/dist/components/model-mark.d.ts.map +1 -1
- package/dist/components/model-mark.js +4 -2
- package/dist/components/multi-select.js +2 -2
- package/dist/components/native-select.d.ts.map +1 -1
- package/dist/components/native-select.js +2 -6
- package/dist/components/notice-banner.js +1 -1
- package/dist/components/object-page.js +2 -2
- package/dist/components/object-workspace.d.ts.map +1 -1
- package/dist/components/object-workspace.js +1 -3
- package/dist/components/overview-page.d.ts +1 -1
- package/dist/components/overview-page.d.ts.map +1 -1
- package/dist/components/overview-page.js +2 -1
- package/dist/components/page-actions.d.ts.map +1 -1
- package/dist/components/page-actions.js +9 -4
- package/dist/components/pairing-code.js +1 -1
- package/dist/components/pending-label.d.ts.map +1 -1
- package/dist/components/pending-label.js +0 -1
- package/dist/components/popover.d.ts +1 -1
- package/dist/components/popover.d.ts.map +1 -1
- package/dist/components/portal-sidebar-frame.d.ts.map +1 -1
- package/dist/components/portal-sidebar-frame.js +1 -1
- package/dist/components/product-message.d.ts.map +1 -1
- package/dist/components/product-message.js +1 -0
- package/dist/components/progress-checklist.d.ts.map +1 -1
- package/dist/components/progress-checklist.js +2 -2
- package/dist/components/progress.d.ts.map +1 -1
- package/dist/components/progress.js +5 -4
- package/dist/components/radio-group.js +2 -2
- package/dist/components/reading-column.js +1 -1
- package/dist/components/record-identity.d.ts.map +1 -1
- package/dist/components/record-identity.js +2 -5
- package/dist/components/record-list.d.ts.map +1 -1
- package/dist/components/record-list.js +4 -3
- package/dist/components/record-sheet.js +1 -1
- package/dist/components/record-time.d.ts.map +1 -1
- package/dist/components/record-time.js +2 -1
- package/dist/components/resizable.d.ts.map +1 -1
- package/dist/components/resizable.js +1 -1
- package/dist/components/row-edit-form.d.ts +2 -1
- package/dist/components/row-edit-form.d.ts.map +1 -1
- package/dist/components/row-edit-form.js +2 -1
- package/dist/components/run-timeline.js +1 -1
- package/dist/components/scope-switcher.js +1 -1
- package/dist/components/segmented-control.js +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/settings-facts.js +1 -1
- package/dist/components/settings-section.js +1 -1
- package/dist/components/sheet.d.ts +1 -1
- package/dist/components/sheet.d.ts.map +1 -1
- package/dist/components/sheet.js +1 -1
- package/dist/components/sidebar.d.ts +2 -2
- package/dist/components/sidebar.d.ts.map +1 -1
- package/dist/components/sidebar.js +16 -18
- package/dist/components/site-menu.d.ts +28 -0
- package/dist/components/site-menu.d.ts.map +1 -0
- package/dist/components/site-menu.js +23 -0
- package/dist/components/slider.d.ts.map +1 -1
- package/dist/components/slider.js +2 -1
- package/dist/components/snippet.d.ts +2 -0
- package/dist/components/snippet.d.ts.map +1 -1
- package/dist/components/snippet.js +13 -6
- package/dist/components/sonner.d.ts.map +1 -1
- package/dist/components/sonner.js +3 -4
- package/dist/components/status-badge.d.ts +1 -1
- package/dist/components/status-badge.d.ts.map +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table-card.js +1 -1
- package/dist/components/table.js +4 -4
- package/dist/components/tabs-nav.d.ts +52 -0
- package/dist/components/tabs-nav.d.ts.map +1 -0
- package/dist/components/tabs-nav.js +95 -0
- package/dist/components/tabs.d.ts +2 -56
- package/dist/components/tabs.d.ts.map +1 -1
- package/dist/components/tabs.js +2 -91
- package/dist/components/text-link.d.ts +1 -2
- package/dist/components/text-link.d.ts.map +1 -1
- package/dist/components/text-link.js +1 -2
- package/dist/components/textarea.d.ts +5 -2
- package/dist/components/textarea.d.ts.map +1 -1
- package/dist/components/textarea.js +12 -4
- package/dist/components/toast.d.ts +2 -0
- package/dist/components/toast.d.ts.map +1 -1
- package/dist/components/toast.js +4 -0
- package/dist/components/toggle-group.d.ts +3 -1
- package/dist/components/toggle-group.d.ts.map +1 -1
- package/dist/components/toggle-group.js +3 -6
- package/dist/components/toggle.js +1 -1
- package/dist/components/tooltip.d.ts +12 -12
- package/dist/components/tooltip.d.ts.map +1 -1
- package/dist/components/tooltip.js +9 -9
- package/dist/components/trend-badge.js +2 -2
- package/dist/components/update-card.js +1 -1
- package/dist/components/usage-meter.d.ts +1 -1
- package/dist/components/usage-meter.d.ts.map +1 -1
- package/dist/components/usage-meter.js +9 -7
- package/dist/components/waiting-line.js +1 -1
- package/dist/index.d.ts +3 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/internal/agent-orb-motion.d.ts +4 -1
- package/dist/internal/agent-orb-motion.d.ts.map +1 -1
- package/dist/internal/agent-orb-motion.js +9 -7
- package/dist/internal/agent-orb.d.ts +4 -1
- package/dist/internal/agent-orb.d.ts.map +1 -1
- package/dist/internal/agent-orb.js +7 -1
- package/dist/internal/assistant-ui/attachment.d.ts.map +1 -1
- package/dist/internal/assistant-ui/attachment.js +15 -15
- package/dist/internal/assistant-ui/follow-up-suggestions.js +1 -1
- package/dist/internal/assistant-ui/markdown-text.js +26 -25
- package/dist/internal/assistant-ui/reasoning.d.ts.map +1 -1
- package/dist/internal/assistant-ui/reasoning.js +13 -16
- package/dist/internal/assistant-ui/thread.d.ts.map +1 -1
- package/dist/internal/assistant-ui/thread.js +22 -16
- package/dist/internal/assistant-ui/tool-fallback.d.ts.map +1 -1
- package/dist/internal/assistant-ui/tool-fallback.js +29 -21
- package/dist/internal/assistant-ui/tool-group.d.ts.map +1 -1
- package/dist/internal/assistant-ui/tool-group.js +6 -7
- package/dist/internal/assistant-ui/tooltip-icon-button.js +1 -1
- package/dist/internal/badge-frame.d.ts +15 -0
- package/dist/internal/badge-frame.d.ts.map +1 -0
- package/dist/internal/badge-frame.js +44 -0
- package/dist/internal/button-classes.d.ts +20 -0
- package/dist/internal/button-classes.d.ts.map +1 -0
- package/dist/internal/button-classes.js +74 -0
- package/dist/internal/catalog-list.d.ts.map +1 -1
- package/dist/internal/catalog-list.js +8 -4
- package/dist/internal/collection-rail.js +3 -3
- package/dist/internal/form-drawer-frame.d.ts +10 -0
- package/dist/internal/form-drawer-frame.d.ts.map +1 -0
- package/dist/internal/form-drawer-frame.js +9 -0
- package/dist/internal/form-submit.d.ts +2 -1
- package/dist/internal/form-submit.d.ts.map +1 -1
- package/dist/internal/settings-save.d.ts.map +1 -1
- package/dist/internal/settings-save.js +4 -2
- package/dist/internal/status-mark.d.ts.map +1 -1
- package/dist/internal/status-mark.js +4 -1
- package/dist/internal/tab-row.d.ts +10 -0
- package/dist/internal/tab-row.d.ts.map +1 -0
- package/dist/internal/tab-row.js +2 -0
- package/dist/internal/table-compact-line.d.ts +1 -1
- package/dist/internal/table-compact-line.d.ts.map +1 -1
- package/dist/internal/table-compact-line.js +1 -1
- package/dist/internal/tooltip-content-frame.d.ts +14 -0
- package/dist/internal/tooltip-content-frame.d.ts.map +1 -0
- package/dist/internal/tooltip-content-frame.js +11 -0
- package/dist/lib/css-variables.d.ts +8 -0
- package/dist/lib/css-variables.d.ts.map +1 -0
- package/dist/lib/css-variables.js +4 -0
- package/dist/lib/merge-tokens.generated.js +1 -1
- package/dist/lib/use-copy-to-clipboard.d.ts.map +1 -1
- package/dist/lib/use-copy-to-clipboard.js +1 -2
- package/dist/showcase/samples.js +1 -1
- package/dist/showcase/specimens/choices.d.ts.map +1 -1
- package/dist/showcase/specimens/choices.js +3 -2
- package/dist/showcase/specimens/compositions.d.ts.map +1 -1
- package/dist/showcase/specimens/compositions.js +7 -5
- package/dist/showcase/specimens/data.d.ts.map +1 -1
- package/dist/showcase/specimens/data.js +1 -1
- package/dist/showcase/specimens/feedback.d.ts.map +1 -1
- package/dist/showcase/specimens/feedback.js +1 -2
- package/dist/showcase/specimens/index.d.ts +2 -0
- package/dist/showcase/specimens/index.d.ts.map +1 -1
- package/dist/showcase/specimens/inputs.d.ts +1 -0
- package/dist/showcase/specimens/inputs.d.ts.map +1 -1
- package/dist/showcase/specimens/inputs.js +5 -2
- package/dist/showcase/specimens/menus.d.ts +1 -0
- package/dist/showcase/specimens/menus.d.ts.map +1 -1
- package/dist/showcase/specimens/menus.js +7 -2
- package/dist/showcase/specimens/navigation.d.ts.map +1 -1
- package/dist/showcase/specimens/navigation.js +6 -5
- package/dist/showcase/specimens/status.d.ts.map +1 -1
- package/dist/showcase/specimens/status.js +4 -4
- package/dist/showcase/usage.generated.d.ts +1 -1
- package/dist/showcase/usage.generated.d.ts.map +1 -1
- package/dist/showcase/usage.generated.js +39 -17
- package/dist/standalone/index.js +1 -1
- package/dist/standalone/orb-page.js +10 -8
- package/dist/styles/components.css +14 -19
- package/package.json +32 -33
- package/dist/eslint/index.js +0 -224
- package/dist/eslint/rules/badge-is-static-label.js +0 -93
- package/dist/eslint/rules/button-icon-slot.js +0 -102
- package/dist/eslint/rules/button-link-for-navigation.js +0 -146
- package/dist/eslint/rules/button-no-aria-label-with-text.js +0 -75
- package/dist/eslint/rules/button-type-roles-only-in-buttons.js +0 -83
- package/dist/eslint/rules/choice-control-has-a-name.js +0 -98
- package/dist/eslint/rules/choice-group-in-fieldset.js +0 -86
- package/dist/eslint/rules/command-shortcut-single-owner.js +0 -118
- package/dist/eslint/rules/confirm-dialog-words.js +0 -63
- package/dist/eslint/rules/data-table-one-unsized-column.js +0 -96
- package/dist/eslint/rules/destructive-action-placement.js +0 -69
- package/dist/eslint/rules/disabled-choice-says-why.js +0 -71
- package/dist/eslint/rules/disabled-control-appearance.js +0 -75
- package/dist/eslint/rules/ellipsis-opens-a-dialog.js +0 -110
- package/dist/eslint/rules/empty-state-owner.js +0 -145
- package/dist/eslint/rules/input-group-owners.js +0 -155
- package/dist/eslint/rules/jsx-class-utility.js +0 -200
- package/dist/eslint/rules/kbd-children-no-modifier-text.js +0 -128
- package/dist/eslint/rules/literal-text.js +0 -41
- package/dist/eslint/rules/menu-owner.js +0 -75
- package/dist/eslint/rules/middle-truncate-placement.js +0 -69
- package/dist/eslint/rules/no-app-feature-component.js +0 -100
- package/dist/eslint/rules/no-bare-button-label.js +0 -55
- package/dist/eslint/rules/no-choice-control-wrapper.js +0 -97
- package/dist/eslint/rules/no-comment.js +0 -77
- package/dist/eslint/rules/no-control-in-read-only-surface.js +0 -106
- package/dist/eslint/rules/no-decorative-icon.js +0 -164
- package/dist/eslint/rules/no-deprecated-zod-api.js +0 -215
- package/dist/eslint/rules/no-global-patch.js +0 -115
- package/dist/eslint/rules/no-hand-built-busy.js +0 -104
- package/dist/eslint/rules/no-hand-built-theme-picker.js +0 -144
- package/dist/eslint/rules/no-hand-built-tile.js +0 -41
- package/dist/eslint/rules/no-hand-formatted-date.js +0 -87
- package/dist/eslint/rules/no-hand-stacked-avatars.js +0 -67
- package/dist/eslint/rules/no-nested-collapse.js +0 -60
- package/dist/eslint/rules/no-package-component-redefinition.js +0 -175
- package/dist/eslint/rules/no-search-in-form.js +0 -47
- package/dist/eslint/rules/no-shadcn-appearance-override.js +0 -305
- package/dist/eslint/rules/no-shell-prompt-in-snippet.js +0 -74
- package/dist/eslint/rules/no-stock-error-words.js +0 -56
- package/dist/eslint/rules/no-subject-derived-expectation.js +0 -183
- package/dist/eslint/rules/no-tooltip-around-labelled-field.js +0 -72
- package/dist/eslint/rules/no-weight-class-on-strong-in-type-role.js +0 -57
- package/dist/eslint/rules/no-write-in-subscription.js +0 -140
- package/dist/eslint/rules/one-submenu-level.js +0 -43
- package/dist/eslint/rules/one-toast-per-flow.js +0 -91
- package/dist/eslint/rules/option-count.js +0 -167
- package/dist/eslint/rules/page-command-link-not-render.js +0 -43
- package/dist/eslint/rules/page-composition.js +0 -232
- package/dist/eslint/rules/progress-needs-name.js +0 -57
- package/dist/eslint/rules/rendered-import.js +0 -121
- package/dist/eslint/rules/server-action-discipline.js +0 -233
- package/dist/eslint/rules/settings-limit-in-footer.js +0 -94
- package/dist/eslint/rules/skeleton-in-region.js +0 -80
- package/dist/eslint/rules/sortable-heading-uses-sort.js +0 -79
- package/dist/eslint/rules/table-cell-inset-owner.js +0 -107
- package/dist/eslint/rules/tabs-selection-is-controlled.js +0 -47
- package/dist/eslint/rules/test-files-from-one-definition.js +0 -67
- package/dist/eslint/rules/tooltip-only-arrow.js +0 -38
- package/dist/eslint/rules/tooltip-trigger-accessible-name.js +0 -114
- package/dist/eslint/rules/workspace-declares-imports.js +0 -163
- package/dist/eslint/strict-design-system.js +0 -250
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `no-restricted-imports` bans a module or a
|
|
3
|
-
* name, but a part may be imported for a type or by its owner, and what is
|
|
4
|
-
* wrong is rendering it; `no-restricted-syntax` matches an element by the name
|
|
5
|
-
* the file wrote, which an import alias (`PageHeader as Header`) slips past.
|
|
6
|
-
* This rule joins the import binding to the element that renders it.
|
|
7
|
-
*/
|
|
8
|
-
import {
|
|
9
|
-
findBelow,
|
|
10
|
-
isRenderedChild,
|
|
11
|
-
packageModules,
|
|
12
|
-
partOf,
|
|
13
|
-
renderedImports,
|
|
14
|
-
staticAttribute,
|
|
15
|
-
walk,
|
|
16
|
-
} from "./rendered-import.js";
|
|
17
|
-
|
|
18
|
-
const PART_FIX = {
|
|
19
|
-
PageHeader: "PageHeader belongs to the product's chrome or a composition's `title`.",
|
|
20
|
-
ReadingColumn: "A page's column is its composition's: SettingsPage, or the product's page frame.",
|
|
21
|
-
TableCard:
|
|
22
|
-
"A list of records is CollectionPage or RecordList; a comparison is a DataTable, which brings its card.",
|
|
23
|
-
CollectionToolbar: "The toolbar is CollectionPage's: `search`, `filters`, `sort`.",
|
|
24
|
-
CollectionSort: "The toolbar is CollectionPage's: `search`, `filters`, `sort`.",
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const PACKAGE_MODULE = {
|
|
28
|
-
PageHeader: "page-header",
|
|
29
|
-
ReadingColumn: "reading-column",
|
|
30
|
-
TableCard: "table-card",
|
|
31
|
-
CollectionToolbar: "collection-toolbar",
|
|
32
|
-
CollectionSort: "collection-sort",
|
|
33
|
-
DataTable: "data-table",
|
|
34
|
-
FormDrawer: "form-drawer",
|
|
35
|
-
SettingsSection: "settings-section",
|
|
36
|
-
TabsTrigger: "tabs",
|
|
37
|
-
TabsLink: "tabs",
|
|
38
|
-
Badge: "badge",
|
|
39
|
-
StatusBadge: "status-badge",
|
|
40
|
-
Tag: "tag",
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const TABS = new Set(["TabsTrigger", "TabsLink"]);
|
|
44
|
-
const COUNT_MARKS = new Set(["Badge", "StatusBadge", "Tag"]);
|
|
45
|
-
const countName = (name) =>
|
|
46
|
-
/(?:^|_)(?:count|total|length|size|unread|number)$/i.test(name) ||
|
|
47
|
-
/[a-z0-9](?:Count|Total|Length|Size|Unread)$/.test(name);
|
|
48
|
-
|
|
49
|
-
function exportersOf(modules) {
|
|
50
|
-
const parts = new Map();
|
|
51
|
-
const add = (name, from, part) => parts.set(name, [...(parts.get(name) ?? []), { from, part }]);
|
|
52
|
-
for (const [part, module] of Object.entries(PACKAGE_MODULE)) {
|
|
53
|
-
for (const from of packageModules(module)) add(part, from, part);
|
|
54
|
-
for (const adapter of modules[part] ?? []) {
|
|
55
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- a composition's adapter list holds a module path or a { name, from } pair; only typeof tells them apart.
|
|
56
|
-
if (typeof adapter === "string") add(part, adapter, part);
|
|
57
|
-
else add(adapter.name, adapter.from, part);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return parts;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const isObjectKey = (node) =>
|
|
64
|
-
node.parent.type === "Property" && node.parent.key === node && !node.parent.computed;
|
|
65
|
-
|
|
66
|
-
function readsACount(expression, visitorKeys) {
|
|
67
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- ESTree types Literal.value as string | number | boolean | RegExp | bigint | null; only typeof picks the number.
|
|
68
|
-
if (expression.type === "Literal" && typeof expression.value === "number") return true;
|
|
69
|
-
if (expression.type === "Identifier" && countName(expression.name)) return true;
|
|
70
|
-
let found = false;
|
|
71
|
-
walk(expression, visitorKeys, (node) => {
|
|
72
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment") return false;
|
|
73
|
-
if (node.type === "Identifier" && countName(node.name) && !isObjectKey(node)) {
|
|
74
|
-
found = true;
|
|
75
|
-
return "stop";
|
|
76
|
-
}
|
|
77
|
-
return true;
|
|
78
|
-
});
|
|
79
|
-
return found;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const pageComposition = {
|
|
83
|
-
meta: {
|
|
84
|
-
type: "problem",
|
|
85
|
-
docs: {
|
|
86
|
-
description: "A product page renders a composition, never the parts a composition owns.",
|
|
87
|
-
},
|
|
88
|
-
schema: [
|
|
89
|
-
{
|
|
90
|
-
type: "object",
|
|
91
|
-
properties: {
|
|
92
|
-
allow: {
|
|
93
|
-
type: "array",
|
|
94
|
-
items: { enum: Object.keys(PART_FIX) },
|
|
95
|
-
uniqueItems: true,
|
|
96
|
-
},
|
|
97
|
-
modules: {
|
|
98
|
-
type: "object",
|
|
99
|
-
propertyNames: { enum: Object.keys(PACKAGE_MODULE) },
|
|
100
|
-
additionalProperties: {
|
|
101
|
-
type: "array",
|
|
102
|
-
items: {
|
|
103
|
-
oneOf: [
|
|
104
|
-
{ type: "string" },
|
|
105
|
-
{
|
|
106
|
-
type: "object",
|
|
107
|
-
properties: { from: { type: "string" }, name: { type: "string" } },
|
|
108
|
-
required: ["from", "name"],
|
|
109
|
-
additionalProperties: false,
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
additionalProperties: false,
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
defaultOptions: [{ allow: [], modules: {} }],
|
|
120
|
-
messages: {
|
|
121
|
-
part: "{{fix}}",
|
|
122
|
-
"entity-table": "Rows that name themselves are CollectionPage or RecordList.",
|
|
123
|
-
"catalog-drawer": "A catalogue is CatalogDrawer.",
|
|
124
|
-
"settings-form": "A settings section saves through its `save`; no form around or inside it.",
|
|
125
|
-
"step-marker":
|
|
126
|
-
"A sequence of steps is NumberedSteps, which owns the marker, the current step and the step's action.",
|
|
127
|
-
"tab-count": "A tab names its view; the count belongs to the list's footer.",
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
create(context) {
|
|
131
|
-
const [{ allow, modules }] = context.options;
|
|
132
|
-
const allowed = new Set(allow);
|
|
133
|
-
const parts = exportersOf(modules);
|
|
134
|
-
const importOf = renderedImports(context);
|
|
135
|
-
const { visitorKeys } = context.sourceCode;
|
|
136
|
-
const partRendered = (openingElement) =>
|
|
137
|
-
partOf(importOf(openingElement.name), parts, context.filename);
|
|
138
|
-
const isForm = (node) =>
|
|
139
|
-
node.type === "JSXOpeningElement" &&
|
|
140
|
-
node.name.type === "JSXIdentifier" &&
|
|
141
|
-
node.name.name === "form";
|
|
142
|
-
|
|
143
|
-
function checkSettingsSection(element) {
|
|
144
|
-
const opening = element.openingElement;
|
|
145
|
-
if (findBelow(element, visitorKeys, isForm)) {
|
|
146
|
-
context.report({ node: opening, messageId: "settings-form" });
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
const footer = opening.attributes.find(
|
|
150
|
-
(attribute) => attribute.type === "JSXAttribute" && attribute.name.name === "footer",
|
|
151
|
-
);
|
|
152
|
-
const submits =
|
|
153
|
-
footer?.value &&
|
|
154
|
-
findBelow(
|
|
155
|
-
footer.value,
|
|
156
|
-
visitorKeys,
|
|
157
|
-
(node) => node.type === "JSXOpeningElement" && staticAttribute(node, "type") === "submit",
|
|
158
|
-
);
|
|
159
|
-
if (submits) context.report({ node: opening, messageId: "settings-form" });
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const showsACount = (node) =>
|
|
163
|
-
(node.type === "JSXOpeningElement" && COUNT_MARKS.has(partRendered(node))) ||
|
|
164
|
-
(node.type === "JSXExpressionContainer" &&
|
|
165
|
-
isRenderedChild(node) &&
|
|
166
|
-
readsACount(node.expression, visitorKeys));
|
|
167
|
-
|
|
168
|
-
function checkTab(element) {
|
|
169
|
-
for (const child of element.children) {
|
|
170
|
-
const offence = showsACount(child) ? child : findBelow(child, visitorKeys, showsACount);
|
|
171
|
-
if (offence) {
|
|
172
|
-
context.report({ node: offence, messageId: "tab-count" });
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
JSXElement(element) {
|
|
180
|
-
const opening = element.openingElement;
|
|
181
|
-
if (isForm(opening)) {
|
|
182
|
-
const section = findBelow(
|
|
183
|
-
element,
|
|
184
|
-
visitorKeys,
|
|
185
|
-
(node) => node.type === "JSXOpeningElement" && partRendered(node) === "SettingsSection",
|
|
186
|
-
);
|
|
187
|
-
if (section) context.report({ node: opening, messageId: "settings-form" });
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const part = partRendered(opening);
|
|
191
|
-
if (!part) return;
|
|
192
|
-
if (Object.hasOwn(PART_FIX, part)) {
|
|
193
|
-
if (!allowed.has(part)) {
|
|
194
|
-
context.report({ node: opening, messageId: "part", data: { fix: PART_FIX[part] } });
|
|
195
|
-
}
|
|
196
|
-
} else if (part === "DataTable") {
|
|
197
|
-
if (staticAttribute(opening, "showColumnHeaders") === false) {
|
|
198
|
-
context.report({ node: opening, messageId: "entity-table" });
|
|
199
|
-
}
|
|
200
|
-
} else if (part === "FormDrawer") {
|
|
201
|
-
if (staticAttribute(opening, "job") === "catalog") {
|
|
202
|
-
context.report({ node: opening, messageId: "catalog-drawer" });
|
|
203
|
-
}
|
|
204
|
-
} else if (part === "SettingsSection") {
|
|
205
|
-
checkSettingsSection(element);
|
|
206
|
-
} else if (TABS.has(part)) {
|
|
207
|
-
checkTab(element);
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
JSXExpressionContainer(container) {
|
|
211
|
-
if (!isRenderedChild(container)) return;
|
|
212
|
-
const { expression } = container;
|
|
213
|
-
const addsOne =
|
|
214
|
-
expression.type === "BinaryExpression" &&
|
|
215
|
-
expression.operator === "+" &&
|
|
216
|
-
expression.right.type === "Literal" &&
|
|
217
|
-
expression.right.value === 1;
|
|
218
|
-
if (!addsOne) return;
|
|
219
|
-
for (let current = container.parent; current; current = current.parent) {
|
|
220
|
-
if (
|
|
221
|
-
current.type === "JSXElement" &&
|
|
222
|
-
current.openingElement.name.type === "JSXIdentifier" &&
|
|
223
|
-
current.openingElement.name.name === "li"
|
|
224
|
-
) {
|
|
225
|
-
context.report({ node: container, messageId: "step-marker" });
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
},
|
|
232
|
-
};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `jsx-a11y` has no rule for a progressbar's name,
|
|
3
|
-
* and a `no-restricted-syntax` selector cannot tell that the package's Progress
|
|
4
|
-
* is meant rather than a local one, nor look for a ProgressLabel anywhere in
|
|
5
|
-
* its children. Those need the import bindings and the element's subtree read
|
|
6
|
-
* together.
|
|
7
|
-
*/
|
|
8
|
-
import { findBelow, packageModules, partOf, renderedImports } from "./rendered-import.js";
|
|
9
|
-
|
|
10
|
-
const partsIn = (module, names) =>
|
|
11
|
-
names.map((name) => [
|
|
12
|
-
name,
|
|
13
|
-
[...packageModules(module), `@/components/${module}`].map((from) => ({ from, part: name })),
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
const PROGRESS = new Map(partsIn("progress", ["Progress"]));
|
|
17
|
-
const PROGRESS_LABEL = new Map(partsIn("progress", ["ProgressLabel"]));
|
|
18
|
-
|
|
19
|
-
const NAMING_ATTRIBUTES = new Set(["aria-label", "aria-labelledby"]);
|
|
20
|
-
|
|
21
|
-
export const progressNeedsName = {
|
|
22
|
-
meta: {
|
|
23
|
-
type: "problem",
|
|
24
|
-
docs: {
|
|
25
|
-
description:
|
|
26
|
-
"A Progress is named by an aria-label, an aria-labelledby or a ProgressLabel among its children.",
|
|
27
|
-
},
|
|
28
|
-
schema: [],
|
|
29
|
-
messages: {
|
|
30
|
-
unnamed:
|
|
31
|
-
"Name the work: add <ProgressLabel> (Uploading files) or aria-label, and print the count in ProgressValue.",
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
create(context) {
|
|
35
|
-
const importOf = renderedImports(context);
|
|
36
|
-
const isPart = (parts, openingElement) =>
|
|
37
|
-
Boolean(partOf(importOf(openingElement.name), parts, context.filename));
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
JSXElement(node) {
|
|
41
|
-
const { openingElement } = node;
|
|
42
|
-
if (!isPart(PROGRESS, openingElement)) return;
|
|
43
|
-
const namedByAttribute = openingElement.attributes.some(
|
|
44
|
-
(attribute) =>
|
|
45
|
-
attribute.type === "JSXSpreadAttribute" || NAMING_ATTRIBUTES.has(attribute.name.name),
|
|
46
|
-
);
|
|
47
|
-
if (namedByAttribute) return;
|
|
48
|
-
const label = findBelow(
|
|
49
|
-
node,
|
|
50
|
-
context.sourceCode.visitorKeys,
|
|
51
|
-
(child) => child.type === "JSXElement" && isPart(PROGRESS_LABEL, child.openingElement),
|
|
52
|
-
);
|
|
53
|
-
if (!label) context.report({ node: openingElement, messageId: "unnamed" });
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
|
-
};
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { dirname, resolve, sep } from "node:path";
|
|
2
|
-
|
|
3
|
-
export const PACKAGE_BARREL = "@nadicodeai/ui";
|
|
4
|
-
|
|
5
|
-
export const packageModules = (module) => [`@nadicodeai/ui/components/${module}`, PACKAGE_BARREL];
|
|
6
|
-
|
|
7
|
-
const withoutExtension = (path) => path.replace(/\.[cm]?[jt]sx?$/, "").replace(/\/index$/, "");
|
|
8
|
-
|
|
9
|
-
export function sourceMatches(source, specifier, filename) {
|
|
10
|
-
if (source === specifier) return true;
|
|
11
|
-
if (!specifier.startsWith("@/") || !source.startsWith(".")) return false;
|
|
12
|
-
const target = withoutExtension(resolve(dirname(filename), source).split(sep).join("/"));
|
|
13
|
-
return target.endsWith(`/src/${withoutExtension(specifier.slice(2))}`);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Reads the file's value imports once and returns, for a JSX element's name,
|
|
18
|
-
* the `{ source, name }` of the export it renders, or null. A namespace import
|
|
19
|
-
* renders `<ns.Part>` as the export `Part`.
|
|
20
|
-
*/
|
|
21
|
-
export function renderedImports(context) {
|
|
22
|
-
const named = new Map();
|
|
23
|
-
const namespaces = new Map();
|
|
24
|
-
for (const statement of context.sourceCode.ast.body) {
|
|
25
|
-
if (statement.type !== "ImportDeclaration" || statement.importKind === "type") continue;
|
|
26
|
-
const source = statement.source.value;
|
|
27
|
-
for (const specifier of statement.specifiers) {
|
|
28
|
-
if (specifier.importKind === "type") continue;
|
|
29
|
-
if (specifier.type === "ImportSpecifier") {
|
|
30
|
-
const name = specifier.imported.name ?? specifier.imported.value;
|
|
31
|
-
named.set(specifier.local.name, { source, name });
|
|
32
|
-
} else if (specifier.type === "ImportNamespaceSpecifier") {
|
|
33
|
-
namespaces.set(specifier.local.name, source);
|
|
34
|
-
} else {
|
|
35
|
-
named.set(specifier.local.name, { source, name: "default" });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return (elementName) => {
|
|
40
|
-
if (elementName.type === "JSXIdentifier") return named.get(elementName.name) ?? null;
|
|
41
|
-
if (
|
|
42
|
-
elementName.type === "JSXMemberExpression" &&
|
|
43
|
-
elementName.object.type === "JSXIdentifier" &&
|
|
44
|
-
namespaces.has(elementName.object.name)
|
|
45
|
-
) {
|
|
46
|
-
return { source: namespaces.get(elementName.object.name), name: elementName.property.name };
|
|
47
|
-
}
|
|
48
|
-
return null;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* The part an import renders, when `parts` (a Map from an export's name to the
|
|
54
|
-
* `{ from, part }` modules that export it, a product's adapter under its own
|
|
55
|
-
* name included) lists that export under a matching specifier.
|
|
56
|
-
*/
|
|
57
|
-
export function partOf(imported, parts, filename) {
|
|
58
|
-
if (!imported) return null;
|
|
59
|
-
const exporter = parts
|
|
60
|
-
.get(imported.name)
|
|
61
|
-
?.find(({ from }) => sourceMatches(imported.source, from, filename));
|
|
62
|
-
return exporter?.part ?? null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function writtenName(elementName) {
|
|
66
|
-
if (elementName.type === "JSXIdentifier") return elementName.name;
|
|
67
|
-
if (elementName.type === "JSXMemberExpression") return elementName.property.name;
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** An attribute's value when the source states it: `true` for a bare attribute, else a literal string, number or boolean. */
|
|
72
|
-
export function staticAttribute(openingElement, attributeName) {
|
|
73
|
-
const attribute = openingElement.attributes.find(
|
|
74
|
-
(candidate) => candidate.type === "JSXAttribute" && candidate.name.name === attributeName,
|
|
75
|
-
);
|
|
76
|
-
if (!attribute) return undefined;
|
|
77
|
-
const { value } = attribute;
|
|
78
|
-
if (value === null) return true;
|
|
79
|
-
if (value.type === "Literal") return value.value;
|
|
80
|
-
if (value.type === "JSXExpressionContainer") {
|
|
81
|
-
const { expression } = value;
|
|
82
|
-
if (expression.type === "Literal") return expression.value;
|
|
83
|
-
if (expression.type === "TemplateLiteral" && expression.expressions.length === 0) {
|
|
84
|
-
return expression.quasis[0].value.cooked;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const isRenderedChild = (node) =>
|
|
91
|
-
node.parent?.type === "JSXElement" || node.parent?.type === "JSXFragment";
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Visits every node under `node`, depth first, `node` excluded. `visit`
|
|
95
|
-
* returning `false` skips that node's subtree; returning `"stop"` ends the walk.
|
|
96
|
-
*/
|
|
97
|
-
export function walk(node, visitorKeys, visit) {
|
|
98
|
-
const keys = visitorKeys[node.type] ?? [];
|
|
99
|
-
for (const key of keys) {
|
|
100
|
-
const value = node[key];
|
|
101
|
-
for (const child of Array.isArray(value) ? value : [value]) {
|
|
102
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- walks the generic AST, where only an object whose type is a string is a node.
|
|
103
|
-
if (!child || typeof child.type !== "string") continue;
|
|
104
|
-
const verdict = visit(child);
|
|
105
|
-
if (verdict === "stop") return "stop";
|
|
106
|
-
if (verdict === false) continue;
|
|
107
|
-
if (walk(child, visitorKeys, visit) === "stop") return "stop";
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function findBelow(node, visitorKeys, matches) {
|
|
114
|
-
let found = null;
|
|
115
|
-
walk(node, visitorKeys, (child) => {
|
|
116
|
-
if (!matches(child)) return true;
|
|
117
|
-
found = child;
|
|
118
|
-
return "stop";
|
|
119
|
-
});
|
|
120
|
-
return found;
|
|
121
|
-
}
|
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A Next.js server action is a POST endpoint with a function's syntax: anything
|
|
3
|
-
* on the internet can call it with any arguments. So an exported server action
|
|
4
|
-
* must resolve its viewer before it acts, and must parse whatever the client
|
|
5
|
-
* sent before it trusts it.
|
|
6
|
-
*
|
|
7
|
-
* Standard tools checked first. `@next/eslint-plugin-next` is already on
|
|
8
|
-
* through `eslint-config-next`, and none of its rules read a server action's
|
|
9
|
-
* body — the set is script, head, font and image hygiene. `typescript-eslint`
|
|
10
|
-
* can type the argument but cannot require that a call happened. The tier-1
|
|
11
|
-
* option — a `defineAction` wrapper that makes an unvalidated or unauthorized
|
|
12
|
-
* action untypeable — was tried and rejected for this change: Next binds the
|
|
13
|
-
* exported function itself, so adopting it rewrites every exported action in
|
|
14
|
-
* the same diff that retires the guard.
|
|
15
|
-
*
|
|
16
|
-
* The rule reads one function at a time — no whole-tree scan, no cross-file
|
|
17
|
-
* dataflow, no exception ledger. A deliberately public action carries an inline
|
|
18
|
-
* `eslint-disable-next-line` with its reason, which is reviewed in the diff that
|
|
19
|
-
* adds it rather than in a list somewhere else.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/** Every helper that resolves the viewer and refuses an unauthorized one. */
|
|
23
|
-
const AUTH_CALLS = new Set([
|
|
24
|
-
"actionActor",
|
|
25
|
-
"accountActionContext",
|
|
26
|
-
"companyActionContext",
|
|
27
|
-
"getPortalSession",
|
|
28
|
-
"installationActionContext",
|
|
29
|
-
"pageActor",
|
|
30
|
-
"requirePortalAdmin",
|
|
31
|
-
"requirePortalViewer",
|
|
32
|
-
"requireViewerForPage",
|
|
33
|
-
"resolveDeviceApprover",
|
|
34
|
-
"resolvePortalAccessState",
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A schema's `parse`/`safeParse`. `JSON.parse` turns a string into an object
|
|
39
|
-
* and asserts nothing about its shape, so it is not validation and is excluded
|
|
40
|
-
* by name.
|
|
41
|
-
*/
|
|
42
|
-
const VALIDATION_CALLS = new Set(["parse", "safeParse"]);
|
|
43
|
-
|
|
44
|
-
const FUNCTION_TYPES = new Set([
|
|
45
|
-
"FunctionDeclaration",
|
|
46
|
-
"FunctionExpression",
|
|
47
|
-
"ArrowFunctionExpression",
|
|
48
|
-
]);
|
|
49
|
-
|
|
50
|
-
function hasUseServer(body) {
|
|
51
|
-
return (body ?? []).some(
|
|
52
|
-
(statement) =>
|
|
53
|
-
statement.type === "ExpressionStatement" &&
|
|
54
|
-
statement.expression.type === "Literal" &&
|
|
55
|
-
statement.expression.value === "use server",
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function calleeName(node) {
|
|
60
|
-
if (node.callee.type === "Identifier") return node.callee.name;
|
|
61
|
-
if (node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier") {
|
|
62
|
-
if (node.callee.object.type === "Identifier" && node.callee.object.name === "JSON") {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
return node.callee.property.name;
|
|
66
|
-
}
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* How a module-level function reaches the outside, which is what decides
|
|
72
|
-
* whether Next binds it as a POST endpoint. All five spellings bind the same
|
|
73
|
-
* way, so the rule reads all five: `export async function a`, `export default
|
|
74
|
-
* async function a`, `export const a = async () => {}`, `export const a =
|
|
75
|
-
* async function () {}`, and a bare declaration later named in `export { a }`.
|
|
76
|
-
* Returns the identifier to report on, or undefined when the function is not a
|
|
77
|
-
* module-level export.
|
|
78
|
-
*/
|
|
79
|
-
function exportedIdentifier(node) {
|
|
80
|
-
const parent = node.parent;
|
|
81
|
-
if (!parent) return undefined;
|
|
82
|
-
// export [default] async function a() {}
|
|
83
|
-
if (parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration") {
|
|
84
|
-
return node.id ?? node;
|
|
85
|
-
}
|
|
86
|
-
// export const a = async () => {} / async function () {}
|
|
87
|
-
if (parent.type === "VariableDeclarator" && parent.id.type === "Identifier") {
|
|
88
|
-
const declaration = parent.parent;
|
|
89
|
-
if (declaration?.parent?.type === "ExportNamedDeclaration") return parent.id;
|
|
90
|
-
// A bare `const a = async () => {}` that a later `export { a }` names.
|
|
91
|
-
if (declaration?.parent?.type === "Program") return parent.id;
|
|
92
|
-
return undefined;
|
|
93
|
-
}
|
|
94
|
-
// A bare `async function a() {}` that a later `export { a }` names.
|
|
95
|
-
if (parent.type === "Program") return node.id ?? undefined;
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function inServerActionCode(node, moduleIsServer) {
|
|
100
|
-
if (moduleIsServer) return true;
|
|
101
|
-
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
102
|
-
if (FUNCTION_TYPES.has(ancestor.type) && hasUseServer(ancestor.body?.body)) return true;
|
|
103
|
-
}
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function inlineRejectionHandler(node) {
|
|
108
|
-
const handler = node.arguments[0];
|
|
109
|
-
if (node.callee.type !== "MemberExpression" || node.callee.property.type !== "Identifier") {
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
if (node.callee.property.name !== "catch" || !FUNCTION_TYPES.has(handler?.type)) return undefined;
|
|
113
|
-
return handler;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function dropsTheError(sourceCode, node, bindings) {
|
|
117
|
-
if (bindings.length === 0) return true;
|
|
118
|
-
return sourceCode
|
|
119
|
-
.getDeclaredVariables(node)
|
|
120
|
-
.filter((variable) =>
|
|
121
|
-
variable.defs.some((def) => def.type === "CatchClause" || def.type === "Parameter"),
|
|
122
|
-
)
|
|
123
|
-
.every((variable) => variable.references.length === 0);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** @type {import("eslint").Rule.RuleModule} */
|
|
127
|
-
export const serverActionDiscipline = {
|
|
128
|
-
meta: {
|
|
129
|
-
type: "problem",
|
|
130
|
-
docs: {
|
|
131
|
-
description:
|
|
132
|
-
"An exported server action resolves its viewer and parses its client input, and server-action code never drops an error it catches.",
|
|
133
|
-
},
|
|
134
|
-
schema: [],
|
|
135
|
-
messages: {
|
|
136
|
-
missingAuth:
|
|
137
|
-
"Server action '{{name}}' calls no viewer check. Resolve the viewer first (actionActor, requirePortalViewer, companyActionContext, …), or disable this rule inline with the reason it is public.",
|
|
138
|
-
missingValidation:
|
|
139
|
-
"Server action '{{name}}' takes client input and never parses it. Run the Zod schema's parse/safeParse before trusting the argument.",
|
|
140
|
-
droppedError:
|
|
141
|
-
"This catch drops the error it handles, so an outage reaches the form as an ordinary answer and leaves no log line. Name the error and translate it, answer the form's unavailable state with actionUnavailable, which logs it once, or rethrow it.",
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
create(context) {
|
|
145
|
-
const { sourceCode } = context;
|
|
146
|
-
const moduleIsServer = hasUseServer(sourceCode.ast.body);
|
|
147
|
-
/**
|
|
148
|
-
* One frame per function, holding only the calls written in that function's
|
|
149
|
-
* own body. A call parked inside a nested callback is not a call the action
|
|
150
|
-
* itself makes, so it does not count for the action.
|
|
151
|
-
* @type {Array<Set<string>>}
|
|
152
|
-
*/
|
|
153
|
-
const callStack = [];
|
|
154
|
-
/**
|
|
155
|
-
* Candidates are judged at `Program:exit` rather than when each function
|
|
156
|
-
* closes, because `export { a }` can name a declaration written above it.
|
|
157
|
-
* @type {Array<{ identifier: object, name: string, calls: Set<string>, hasParams: boolean, inlineExported: boolean }>}
|
|
158
|
-
*/
|
|
159
|
-
const candidates = [];
|
|
160
|
-
/** Names an `export { … }` specifier list makes public. @type {Set<string>} */
|
|
161
|
-
const specifierExports = new Set();
|
|
162
|
-
|
|
163
|
-
function enter() {
|
|
164
|
-
callStack.push(new Set());
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function exit(node) {
|
|
168
|
-
const calls = callStack.pop() ?? new Set();
|
|
169
|
-
if (!node.async) return;
|
|
170
|
-
if (!(moduleIsServer || hasUseServer(node.body?.body))) return;
|
|
171
|
-
const identifier = exportedIdentifier(node);
|
|
172
|
-
if (!identifier) return;
|
|
173
|
-
const parent = node.parent;
|
|
174
|
-
const inlineExported =
|
|
175
|
-
parent?.type === "ExportNamedDeclaration" ||
|
|
176
|
-
parent?.type === "ExportDefaultDeclaration" ||
|
|
177
|
-
parent?.parent?.parent?.type === "ExportNamedDeclaration";
|
|
178
|
-
candidates.push({
|
|
179
|
-
identifier,
|
|
180
|
-
name: identifier.name ?? "default",
|
|
181
|
-
calls,
|
|
182
|
-
hasParams: node.params.length > 0,
|
|
183
|
-
inlineExported,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
FunctionDeclaration: enter,
|
|
189
|
-
"FunctionDeclaration:exit": exit,
|
|
190
|
-
FunctionExpression: enter,
|
|
191
|
-
"FunctionExpression:exit": exit,
|
|
192
|
-
ArrowFunctionExpression: enter,
|
|
193
|
-
"ArrowFunctionExpression:exit": exit,
|
|
194
|
-
ExportNamedDeclaration(node) {
|
|
195
|
-
for (const specifier of node.specifiers ?? []) {
|
|
196
|
-
const local = specifier.local;
|
|
197
|
-
if (local?.type === "Identifier") specifierExports.add(local.name);
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
CatchClause(node) {
|
|
201
|
-
const bindings = node.param ? [node.param] : [];
|
|
202
|
-
if (inServerActionCode(node, moduleIsServer) && dropsTheError(sourceCode, node, bindings)) {
|
|
203
|
-
context.report({ node, messageId: "droppedError" });
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
CallExpression(node) {
|
|
207
|
-
const handler = inlineRejectionHandler(node);
|
|
208
|
-
if (
|
|
209
|
-
handler &&
|
|
210
|
-
inServerActionCode(node, moduleIsServer) &&
|
|
211
|
-
dropsTheError(sourceCode, handler, handler.params)
|
|
212
|
-
) {
|
|
213
|
-
context.report({ node: handler, messageId: "droppedError" });
|
|
214
|
-
}
|
|
215
|
-
const name = calleeName(node);
|
|
216
|
-
if (!name) return;
|
|
217
|
-
callStack.at(-1)?.add(name);
|
|
218
|
-
},
|
|
219
|
-
"Program:exit"() {
|
|
220
|
-
for (const candidate of candidates) {
|
|
221
|
-
if (!candidate.inlineExported && !specifierExports.has(candidate.name)) continue;
|
|
222
|
-
const { identifier, name, calls, hasParams } = candidate;
|
|
223
|
-
if (![...AUTH_CALLS].some((call) => calls.has(call))) {
|
|
224
|
-
context.report({ node: identifier, messageId: "missingAuth", data: { name } });
|
|
225
|
-
}
|
|
226
|
-
if (hasParams && ![...VALIDATION_CALLS].some((call) => calls.has(call))) {
|
|
227
|
-
context.report({ node: identifier, messageId: "missingValidation", data: { name } });
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
},
|
|
233
|
-
};
|