@nadicodeai/ui 12.1.165 → 12.1.167
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 +14 -7
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +24 -15
- 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.map +1 -1
- package/dist/components/hive-scene.js +3 -1
- 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 +5 -4
- package/dist/components/record-list.d.ts.map +1 -1
- package/dist/components/record-list.js +31 -17
- 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-layouts.d.ts +24 -9
- package/dist/components/table-layouts.d.ts.map +1 -1
- package/dist/components/table-layouts.js +27 -10
- 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 +40 -18
- package/dist/standalone/index.js +2 -2
- package/dist/standalone/orb-page.js +10 -8
- package/dist/styles/components.css +14 -1
- 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,47 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` descendant selector
|
|
3
|
-
* (`JSXElement[openingElement.name.name=/Form$/] JSXOpeningElement`) matches by
|
|
4
|
-
* element name, so it cannot tell the package's CollectionSearch from a local
|
|
5
|
-
* component of the same name, nor a renamed import.
|
|
6
|
-
*/
|
|
7
|
-
import { packageModules, partOf, renderedImports, writtenName } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const COLLECTION_SEARCH = new Map([
|
|
10
|
-
[
|
|
11
|
-
"CollectionSearch",
|
|
12
|
-
packageModules("collection-search").map((from) => ({ from, part: "CollectionSearch" })),
|
|
13
|
-
],
|
|
14
|
-
]);
|
|
15
|
-
|
|
16
|
-
const isForm = (element) => {
|
|
17
|
-
const name = writtenName(element.openingElement.name);
|
|
18
|
-
return name === "form" || name?.endsWith("Form") === true;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const noSearchInForm = {
|
|
22
|
-
meta: {
|
|
23
|
-
type: "problem",
|
|
24
|
-
docs: {
|
|
25
|
-
description: "A collection's search sits in its toolbar, never inside a form that saves.",
|
|
26
|
-
},
|
|
27
|
-
schema: [],
|
|
28
|
-
messages: {
|
|
29
|
-
"search-in-form":
|
|
30
|
-
"A search box does not belong in a form that saves something; a collection's search sits in its toolbar.",
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
create(context) {
|
|
34
|
-
const importOf = renderedImports(context);
|
|
35
|
-
return {
|
|
36
|
-
JSXOpeningElement(node) {
|
|
37
|
-
if (partOf(importOf(node.name), COLLECTION_SEARCH, context.filename) === null) return;
|
|
38
|
-
for (let ancestor = node.parent.parent; ancestor; ancestor = ancestor.parent) {
|
|
39
|
-
if (ancestor.type === "JSXElement" && isForm(ancestor)) {
|
|
40
|
-
context.report({ node, messageId: "search-in-form" });
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
},
|
|
47
|
-
};
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The design system (`packages/design-system/DESIGN.md`, "Components"): "A page
|
|
3
|
-
* arranges and sizes a component and never restyles it: a new appearance is a
|
|
4
|
-
* typed variant or slot added to the component." A caller that
|
|
5
|
-
* repaints an imported primitive forks its appearance into the call site, where
|
|
6
|
-
* the next token change does not reach it.
|
|
7
|
-
*
|
|
8
|
-
* The theme's palette restriction (design-system, #821) removed the raw
|
|
9
|
-
* Tailwind palette, so `bg-red-500` is no longer a class anywhere. What it did
|
|
10
|
-
* NOT remove is repainting with the SEMANTIC roles — `bg-background`,
|
|
11
|
-
* `text-foreground`, `rounded-xl`, `shadow-lg`, `opacity-50`, `border-0` — all
|
|
12
|
-
* of which still compile and still fork the primitive. Those are what survives
|
|
13
|
-
* here; the raw-palette names are gone from the sets below with the palette.
|
|
14
|
-
*/
|
|
15
|
-
import { posix } from "node:path";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
classUtilities,
|
|
19
|
-
openingElementClassUtilities,
|
|
20
|
-
packageOwnedPrimitivesSchema,
|
|
21
|
-
skipsPackageOwnedPrimitive,
|
|
22
|
-
} from "./jsx-class-utility.js";
|
|
23
|
-
|
|
24
|
-
// This exact Portal adapter forwards props to package primitives. Card
|
|
25
|
-
// chooses its typed surface default. Its other exports are direct reexports.
|
|
26
|
-
const portalCardExports = new Set([
|
|
27
|
-
"Card",
|
|
28
|
-
"CardHeader",
|
|
29
|
-
"CardTitle",
|
|
30
|
-
"CardDescription",
|
|
31
|
-
"CardAction",
|
|
32
|
-
"CardContent",
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
function primitiveModule(source, filename) {
|
|
36
|
-
if (source.startsWith("@nadicodeai/ui/components/")) {
|
|
37
|
-
return { card: source === "@nadicodeai/ui/components/card", exports: undefined };
|
|
38
|
-
}
|
|
39
|
-
const normalized = filename.replaceAll("\\", "/");
|
|
40
|
-
const portalStart = normalized.lastIndexOf("/apps/portal/src/");
|
|
41
|
-
if (portalStart === -1) return undefined;
|
|
42
|
-
const portalSource = `${normalized.slice(0, portalStart)}/apps/portal/src`;
|
|
43
|
-
const resolved = source.startsWith(".")
|
|
44
|
-
? posix.normalize(posix.join(posix.dirname(normalized), source)).replace(/\.[jt]sx?$/, "")
|
|
45
|
-
: undefined;
|
|
46
|
-
if (source === "@/components/kit/card" || resolved === `${portalSource}/components/kit/card`) {
|
|
47
|
-
return { card: true, exports: portalCardExports };
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function primitiveExport(module, name) {
|
|
53
|
-
if (module.exports && !module.exports.has(name)) return undefined;
|
|
54
|
-
return { cardRoot: module.card && name === "Card" };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const appearanceStyleProperties = new Set([
|
|
58
|
-
"appearance",
|
|
59
|
-
"WebkitAppearance",
|
|
60
|
-
"MozAppearance",
|
|
61
|
-
"color",
|
|
62
|
-
"background",
|
|
63
|
-
"backgroundColor",
|
|
64
|
-
"backgroundImage",
|
|
65
|
-
"boxShadow",
|
|
66
|
-
"textShadow",
|
|
67
|
-
"opacity",
|
|
68
|
-
"outlineColor",
|
|
69
|
-
"filter",
|
|
70
|
-
"backdropFilter",
|
|
71
|
-
"WebkitBackdropFilter",
|
|
72
|
-
"fill",
|
|
73
|
-
"fillOpacity",
|
|
74
|
-
"stroke",
|
|
75
|
-
"strokeOpacity",
|
|
76
|
-
...[
|
|
77
|
-
"",
|
|
78
|
-
"Top",
|
|
79
|
-
"Right",
|
|
80
|
-
"Bottom",
|
|
81
|
-
"Left",
|
|
82
|
-
"Block",
|
|
83
|
-
"Inline",
|
|
84
|
-
"BlockStart",
|
|
85
|
-
"BlockEnd",
|
|
86
|
-
"InlineStart",
|
|
87
|
-
"InlineEnd",
|
|
88
|
-
].map((side) => `border${side}Color`),
|
|
89
|
-
...[
|
|
90
|
-
"",
|
|
91
|
-
"TopLeft",
|
|
92
|
-
"TopRight",
|
|
93
|
-
"BottomLeft",
|
|
94
|
-
"BottomRight",
|
|
95
|
-
"StartStart",
|
|
96
|
-
"StartEnd",
|
|
97
|
-
"EndStart",
|
|
98
|
-
"EndEnd",
|
|
99
|
-
].map((corner) => `border${corner}Radius`),
|
|
100
|
-
]);
|
|
101
|
-
|
|
102
|
-
function staticStyleValue(node) {
|
|
103
|
-
if (node.type === "Literal") return node.value;
|
|
104
|
-
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
105
|
-
return node.quasis[0]?.value.cooked;
|
|
106
|
-
}
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Inline object literals only. Unknown values, computed keys, imported style
|
|
111
|
-
// objects and spreads remain review work; layout properties are not paint.
|
|
112
|
-
function inlineAppearanceProperties(attribute) {
|
|
113
|
-
if (attribute.type !== "JSXAttribute" || attribute.name.name !== "style") return [];
|
|
114
|
-
const expression =
|
|
115
|
-
attribute.value?.type === "JSXExpressionContainer" ? attribute.value.expression : undefined;
|
|
116
|
-
if (expression?.type !== "ObjectExpression") return [];
|
|
117
|
-
return expression.properties.filter((property) => {
|
|
118
|
-
if (property.type !== "Property") return false;
|
|
119
|
-
const name =
|
|
120
|
-
property.key.type === "Literal"
|
|
121
|
-
? property.key.value
|
|
122
|
-
: !property.computed && property.key.type === "Identifier"
|
|
123
|
-
? property.key.name
|
|
124
|
-
: undefined;
|
|
125
|
-
if (!appearanceStyleProperties.has(name)) return false;
|
|
126
|
-
const value = staticStyleValue(property.value);
|
|
127
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- staticStyleValue returns the literal's string, number or undefined; only typeof tells them apart.
|
|
128
|
-
return typeof value === "number" || (typeof value === "string" && value.trim() !== "");
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function withoutVariants(utility) {
|
|
133
|
-
return utility.slice(utility.lastIndexOf(":") + 1);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function isTypographySize(utility) {
|
|
137
|
-
return /^text-(?:xs|sm|base|lg|xl|[2-9]xl|\[[\d.]+(?:px|rem|em|%|lh)\])$/.test(utility);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function isTextLayoutUtility(utility) {
|
|
141
|
-
return /^text-(?:left|center|right|justify|start|end|balance|pretty|clip|ellipsis)$/.test(
|
|
142
|
-
utility,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function isBorderMechanic(utility) {
|
|
147
|
-
return /^(?:border(?:-(?:[xy]|[trblse]{1,2}|[0-9]+|px|dashed|dotted|double|hidden|none))?)$/.test(
|
|
148
|
-
utility,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function isBorderRemoval(utility) {
|
|
153
|
-
return /^border(?:-[xytrblse]{1,2})?-(?:0|none|\[(?:length:)?0(?:px|rem|em|%)?\])$/.test(utility);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function isRingOrOutlineMechanic(utility) {
|
|
157
|
-
return /^(?:ring(?:-(?:[0-9]+|inset|offset(?:-[0-9]+)?))?|outline(?:-(?:none|[0-9]+|offset-[0-9]+))?)$/.test(
|
|
158
|
-
utility,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/*
|
|
163
|
-
* A border/ring/outline utility is either one of the enumerated MECHANICS
|
|
164
|
-
* (width, side, style, inset/offset — structure the caller owns) or it paints.
|
|
165
|
-
* There is no third case left to test for: the palette restriction means an
|
|
166
|
-
* unrecognised `border-…` value is not a class, and
|
|
167
|
-
* `better-tailwindcss/no-unknown-classes` reports it as one. That is what
|
|
168
|
-
* retired the hand-written colour-name list this rule used to carry.
|
|
169
|
-
*/
|
|
170
|
-
function isAppearanceUtility(utility) {
|
|
171
|
-
const base = withoutVariants(utility);
|
|
172
|
-
return (
|
|
173
|
-
/^appearance-/.test(base) ||
|
|
174
|
-
/^bg-/.test(base) ||
|
|
175
|
-
isBorderRemoval(base) ||
|
|
176
|
-
(base.startsWith("border") && !isBorderMechanic(base)) ||
|
|
177
|
-
/^shadow(?:-|$)/.test(base) ||
|
|
178
|
-
/^rounded(?:-|$)/.test(base) ||
|
|
179
|
-
((base.startsWith("ring") || base.startsWith("outline")) && !isRingOrOutlineMechanic(base)) ||
|
|
180
|
-
/^opacity-/.test(base) ||
|
|
181
|
-
(/^text-/.test(base) && !isTypographySize(base) && !isTextLayoutUtility(base))
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function isProhibitedAppearanceUtility(utility, { cardRoot = false } = {}) {
|
|
186
|
-
const base = withoutVariants(utility);
|
|
187
|
-
return (
|
|
188
|
-
targetsSharedSlot(utility) ||
|
|
189
|
-
(isAppearanceUtility(utility) && (!isBorderRemoval(base) || cardRoot))
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function targetsSharedSlot(utility) {
|
|
194
|
-
return utility.includes("data-slot") || utility.includes("data-[slot");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function targetsCardSlot(utility) {
|
|
198
|
-
return (
|
|
199
|
-
/data-slot=(?:card|["']card["'])(?=\])/.test(utility) ||
|
|
200
|
-
/data-\[slot=(?:card|["']card["'])\]/.test(utility)
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export const noShadcnAppearanceOverride = {
|
|
205
|
-
meta: {
|
|
206
|
-
type: "problem",
|
|
207
|
-
docs: {
|
|
208
|
-
description: "Keep shared component appearance and internal slot layout at their owner",
|
|
209
|
-
},
|
|
210
|
-
// Default "exempt": inside @nadicodeai/ui the primitives ARE the appearance,
|
|
211
|
-
// so every utility here would be a false positive.
|
|
212
|
-
schema: [packageOwnedPrimitivesSchema],
|
|
213
|
-
messages: {
|
|
214
|
-
appearanceOverride:
|
|
215
|
-
"Do not override shared component appearance or internal slot layout with {{utility}}; use a component prop or fix its shared owner.",
|
|
216
|
-
},
|
|
217
|
-
},
|
|
218
|
-
create(context) {
|
|
219
|
-
if (skipsPackageOwnedPrimitive(context)) {
|
|
220
|
-
return {};
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const sourceCode = context.sourceCode;
|
|
224
|
-
const sharedBindings = new Map();
|
|
225
|
-
|
|
226
|
-
function importedBinding(identifier) {
|
|
227
|
-
for (let scope = sourceCode.getScope(identifier); scope; scope = scope.upper) {
|
|
228
|
-
const variable = scope.set.get(identifier.name);
|
|
229
|
-
if (variable) return sharedBindings.get(variable);
|
|
230
|
-
}
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function sharedPrimitive(node) {
|
|
235
|
-
if (node?.type !== "JSXOpeningElement") return undefined;
|
|
236
|
-
if (node.name.type === "JSXIdentifier") {
|
|
237
|
-
const binding = importedBinding(node.name);
|
|
238
|
-
return binding && binding.name !== "*"
|
|
239
|
-
? primitiveExport(binding.module, binding.name)
|
|
240
|
-
: undefined;
|
|
241
|
-
}
|
|
242
|
-
if (node.name.type === "JSXMemberExpression" && node.name.object.type === "JSXIdentifier") {
|
|
243
|
-
const binding = importedBinding(node.name.object);
|
|
244
|
-
return binding?.name === "*"
|
|
245
|
-
? primitiveExport(binding.module, node.name.property.name)
|
|
246
|
-
: undefined;
|
|
247
|
-
}
|
|
248
|
-
return undefined;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
Program(node) {
|
|
253
|
-
// Imports are hoisted; collect their declared variables before visiting
|
|
254
|
-
// JSX. Binding identity also distinguishes shadowing parameters/locals.
|
|
255
|
-
for (const declaration of node.body) {
|
|
256
|
-
if (declaration.type !== "ImportDeclaration" || declaration.importKind === "type")
|
|
257
|
-
continue;
|
|
258
|
-
const module = primitiveModule(declaration.source.value, context.filename);
|
|
259
|
-
if (!module) continue;
|
|
260
|
-
for (const specifier of declaration.specifiers) {
|
|
261
|
-
if (specifier.importKind === "type") continue;
|
|
262
|
-
const name =
|
|
263
|
-
specifier.type === "ImportNamespaceSpecifier"
|
|
264
|
-
? "*"
|
|
265
|
-
: specifier.type === "ImportSpecifier"
|
|
266
|
-
? (specifier.imported.name ?? specifier.imported.value)
|
|
267
|
-
: "default";
|
|
268
|
-
for (const variable of sourceCode.getDeclaredVariables(specifier)) {
|
|
269
|
-
sharedBindings.set(variable, { module, name });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
JSXOpeningElement(node) {
|
|
275
|
-
const primitive = sharedPrimitive(node);
|
|
276
|
-
if (!primitive) return;
|
|
277
|
-
for (const utility of openingElementClassUtilities(node, context, true)) {
|
|
278
|
-
if (utility && isProhibitedAppearanceUtility(utility, primitive)) {
|
|
279
|
-
context.report({ node, messageId: "appearanceOverride", data: { utility } });
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
for (const property of node.attributes.flatMap(inlineAppearanceProperties)) {
|
|
283
|
-
const name = property.key.name ?? property.key.value;
|
|
284
|
-
context.report({
|
|
285
|
-
node: property,
|
|
286
|
-
messageId: "appearanceOverride",
|
|
287
|
-
data: { utility: `style.${name}` },
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
},
|
|
291
|
-
JSXAttribute(node) {
|
|
292
|
-
if (sharedPrimitive(node.parent)) return;
|
|
293
|
-
for (const utility of classUtilities(node, context)) {
|
|
294
|
-
if (
|
|
295
|
-
!targetsSharedSlot(utility) ||
|
|
296
|
-
!isProhibitedAppearanceUtility(utility, { cardRoot: targetsCardSlot(utility) })
|
|
297
|
-
) {
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
context.report({ node, messageId: "appearanceOverride", data: { utility } });
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
};
|
|
304
|
-
},
|
|
305
|
-
};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* A Snippet draws its own prompt and copies only the command, so a `$ ` or `> `
|
|
3
|
-
* typed into `text` shows twice and is on the clipboard.
|
|
4
|
-
*
|
|
5
|
-
* Standard tool checked first: `no-restricted-syntax` can match a string
|
|
6
|
-
* starting with `$ `, but not whether it is the `text` of the package's Snippet
|
|
7
|
-
* (an import binding, so a renamed or local `Snippet` is judged correctly),
|
|
8
|
-
* and it cannot read the items of an array or exempt `prompt={false}`.
|
|
9
|
-
*/
|
|
10
|
-
import { partOf, renderedImports, staticAttribute } from "./rendered-import.js";
|
|
11
|
-
|
|
12
|
-
const SNIPPET = new Map([
|
|
13
|
-
[
|
|
14
|
-
"Snippet",
|
|
15
|
-
[
|
|
16
|
-
{ from: "@nadicodeai/ui/components/snippet", part: "Snippet" },
|
|
17
|
-
{ from: "@nadicodeai/ui", part: "Snippet" },
|
|
18
|
-
],
|
|
19
|
-
],
|
|
20
|
-
]);
|
|
21
|
-
const OWNER_FILE = "packages/ui/src/components/snippet.tsx";
|
|
22
|
-
|
|
23
|
-
const startsWithPrompt = (text) => text === "$" || text.startsWith("$ ") || text.startsWith("> ");
|
|
24
|
-
|
|
25
|
-
function writtenText(node) {
|
|
26
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- ESTree types Literal.value as string | number | boolean | RegExp | bigint | null; only typeof picks the string.
|
|
27
|
-
if (node.type === "Literal") return typeof node.value === "string" ? node.value : null;
|
|
28
|
-
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
29
|
-
return node.quasis[0].value.cooked;
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function promptedItems(value) {
|
|
35
|
-
if (!value) return [];
|
|
36
|
-
const written = value.type === "JSXExpressionContainer" ? value.expression : value;
|
|
37
|
-
const items = written.type === "ArrayExpression" ? written.elements : [written];
|
|
38
|
-
return items.filter((item) => {
|
|
39
|
-
if (!item || item.type === "SpreadElement") return false;
|
|
40
|
-
const text = writtenText(item);
|
|
41
|
-
return text !== null && startsWithPrompt(text);
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const noShellPromptInSnippet = {
|
|
46
|
-
meta: {
|
|
47
|
-
type: "problem",
|
|
48
|
-
docs: {
|
|
49
|
-
description: "A Snippet's text never starts with the prompt the Snippet draws.",
|
|
50
|
-
},
|
|
51
|
-
schema: [],
|
|
52
|
-
messages: {
|
|
53
|
-
"typed-prompt":
|
|
54
|
-
'The Snippet draws the prompt. Remove the leading "$ " from text, or pass prompt={false} when the value is not a shell command.',
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
create(context) {
|
|
58
|
-
if (context.filename.replaceAll("\\", "/").endsWith(`/${OWNER_FILE}`)) return {};
|
|
59
|
-
const importOf = renderedImports(context);
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
JSXOpeningElement(node) {
|
|
63
|
-
if (partOf(importOf(node.name), SNIPPET, context.filename) !== "Snippet") return;
|
|
64
|
-
if (staticAttribute(node, "prompt") === false) return;
|
|
65
|
-
const text = node.attributes.find(
|
|
66
|
-
(attribute) => attribute.type === "JSXAttribute" && attribute.name.name === "text",
|
|
67
|
-
);
|
|
68
|
-
for (const item of promptedItems(text?.value)) {
|
|
69
|
-
context.report({ node: item, messageId: "typed-prompt" });
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
},
|
|
74
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `no-restricted-syntax` matches a string by one
|
|
3
|
-
* regular expression per selector and would need one entry per phrase and per
|
|
4
|
-
* node kind (a literal, a template's text, JSX text); one rule lists the
|
|
5
|
-
* phrases once and reads them in the catalogue files only, by path.
|
|
6
|
-
*/
|
|
7
|
-
import { literalText } from "./literal-text.js";
|
|
8
|
-
|
|
9
|
-
const PHRASES = [
|
|
10
|
-
["Something went wrong", /\bsomething went wrong\b/i],
|
|
11
|
-
["Unable to", /\bunable to\b/i],
|
|
12
|
-
["Heads up", /\bheads[ -]up\b/i],
|
|
13
|
-
["please", /\bplease\b/i],
|
|
14
|
-
["successfully", /\bsuccessfully\b/i],
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const CATALOGUE_FILE = /(?:^|\/)(?:copy|i18n)\/|(?:^|\/)(?:words|messages)\.[jt]s$/;
|
|
18
|
-
const TEST_FILE = /\.test(?:-[^./]+)?\.[jt]sx?$/;
|
|
19
|
-
|
|
20
|
-
export const noStockErrorWords = {
|
|
21
|
-
meta: {
|
|
22
|
-
type: "problem",
|
|
23
|
-
docs: {
|
|
24
|
-
description:
|
|
25
|
-
'A catalogue message never says "Something went wrong", "Unable to", "Heads up", "please" or "successfully".',
|
|
26
|
-
},
|
|
27
|
-
schema: [],
|
|
28
|
-
messages: {
|
|
29
|
-
"stock-phrase":
|
|
30
|
-
'A catalogue message does not say "{{phrase}}": name what happened to which record and what the person can do, in the active voice ("Couldn\'t pause Discord. The Gateway didn\'t answer."), with no please and no "successfully".',
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
create(context) {
|
|
34
|
-
const filename = context.filename.replaceAll("\\", "/");
|
|
35
|
-
if (!CATALOGUE_FILE.test(filename) || TEST_FILE.test(filename)) return {};
|
|
36
|
-
|
|
37
|
-
function check(node, text) {
|
|
38
|
-
if (text === null) return;
|
|
39
|
-
for (const [phrase, pattern] of PHRASES) {
|
|
40
|
-
if (pattern.test(text))
|
|
41
|
-
context.report({ node, messageId: "stock-phrase", data: { phrase } });
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return {
|
|
46
|
-
Literal(node) {
|
|
47
|
-
if (node.parent.type === "ImportDeclaration" || node.parent.type === "ExportAllDeclaration")
|
|
48
|
-
return;
|
|
49
|
-
check(node, literalText(node));
|
|
50
|
-
},
|
|
51
|
-
TemplateElement(node) {
|
|
52
|
-
check(node, node.value.cooked);
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
},
|
|
56
|
-
};
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The expected value cannot come out of the module under test.
|
|
3
|
-
*
|
|
4
|
-
* When a test compares a result with a constant it imported from the sibling
|
|
5
|
-
* module it is testing, both sides move together: editing the implementation
|
|
6
|
-
* edits the expectation, and no change to the value can ever turn the test
|
|
7
|
-
* red. What looks like a pinned contract is a restatement of the code.
|
|
8
|
-
*
|
|
9
|
-
* Three of these were live here. `viewer-time-zone.test.ts` asserted
|
|
10
|
-
* `expect(await viewerTimeZone()).toBe(DEFAULT_VIEWER_TIME_ZONE)` with
|
|
11
|
-
* `DEFAULT_VIEWER_TIME_ZONE` imported from `./viewer-time-zone`, so the
|
|
12
|
-
* default the Portal falls back to was never pinned to a time zone at all.
|
|
13
|
-
* `contract-v1.test.ts` asserted the served ETag header against
|
|
14
|
-
* `openApiV1Etag` from `./contract-v1`, so the published API contract's
|
|
15
|
-
* validator was compared with itself, and a token TTL went the same way.
|
|
16
|
-
*
|
|
17
|
-
* The fix is to write the literal (`"Europe/Rome"`), or to compute the
|
|
18
|
-
* expectation from something the subject does not own — a checked-in fixture,
|
|
19
|
-
* a hash taken in the test, the wire format itself.
|
|
20
|
-
*
|
|
21
|
-
* Scope: only a relative import (`./`, `../`) is the subject. A constant from
|
|
22
|
-
* a package or an alias (`@/…`, `vitest`) is a third party's contract, and a
|
|
23
|
-
* value the test file declares itself is a value the test chose. Only a bare
|
|
24
|
-
* identifier or a member read off one counts: a call, a template literal or a
|
|
25
|
-
* literal built around the identifier is a transformation the test performs,
|
|
26
|
-
* and the transformation is the thing being asserted.
|
|
27
|
-
*
|
|
28
|
-
* A relative import that resolves into a `tests/` or `fixtures/` directory is
|
|
29
|
-
* the harness, not the subject: `PORTAL_TEST_ADMIN.userId` from
|
|
30
|
-
* `apps/portal/tests/support/postgres-testdb` is the row the test seeded, so
|
|
31
|
-
* comparing a query result with it asserts that the code returned what was put
|
|
32
|
-
* in. Those are excluded by the resolved path, not by the specifier, because a
|
|
33
|
-
* test under `tests/` reaches its own support modules as `../support/…`.
|
|
34
|
-
*/
|
|
35
|
-
import { dirname, resolve, sep } from "node:path";
|
|
36
|
-
|
|
37
|
-
/*
|
|
38
|
-
* The matcher of `expect(subject).<modifiers>.<matcher>(args)` is not the
|
|
39
|
-
* callee's property in the general case: `not`, `resolves` and `rejects` sit
|
|
40
|
-
* between `expect(...)` and the matcher, so it is read by walking the member
|
|
41
|
-
* chain back to the `expect` call.
|
|
42
|
-
*/
|
|
43
|
-
function propertyName(member) {
|
|
44
|
-
if (!member.computed && member.property.type === "Identifier") return member.property.name;
|
|
45
|
-
if (member.computed && member.property.type === "Literal") {
|
|
46
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- ESTree types Literal.value as string | number | boolean | RegExp | bigint | null; only typeof picks the string.
|
|
47
|
-
return typeof member.property.value === "string" ? member.property.value : undefined;
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** `a?.b` and `a?.()` arrive wrapped; the rules want the expression itself. */
|
|
53
|
-
function unwrapChain(node) {
|
|
54
|
-
return node?.type === "ChainExpression" ? node.expression : node;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isExpectCall(node) {
|
|
58
|
-
if (node?.type !== "CallExpression") return false;
|
|
59
|
-
if (node.callee.type === "Identifier") return node.callee.name === "expect";
|
|
60
|
-
// `expect.soft(value)` and `expect.poll(fn)` open the same chain.
|
|
61
|
-
return node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier"
|
|
62
|
-
? node.callee.object.name === "expect"
|
|
63
|
-
: false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Reads an assertion call. Returns `{ matcher, modifiers, subject, args }` for
|
|
68
|
-
* `expect(subject).<modifiers>.<matcher>(args)`, or `undefined` when the call
|
|
69
|
-
* is not an `expect` chain. `subject` is unwrapped of its `ChainExpression`.
|
|
70
|
-
*/
|
|
71
|
-
function expectAssertion(node) {
|
|
72
|
-
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return undefined;
|
|
73
|
-
const matcher = propertyName(node.callee);
|
|
74
|
-
if (!matcher) return undefined;
|
|
75
|
-
|
|
76
|
-
const modifiers = [];
|
|
77
|
-
let current = node.callee.object;
|
|
78
|
-
while (current.type === "MemberExpression") {
|
|
79
|
-
const name = propertyName(current);
|
|
80
|
-
if (!name) return undefined;
|
|
81
|
-
modifiers.unshift(name);
|
|
82
|
-
current = current.object;
|
|
83
|
-
}
|
|
84
|
-
if (!isExpectCall(current)) return undefined;
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
matcher,
|
|
88
|
-
modifiers,
|
|
89
|
-
subject: unwrapChain(current.arguments[0]),
|
|
90
|
-
args: node.arguments,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const EQUALITY_MATCHERS = new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
95
|
-
const TEST_FILE = /\.test\.tsx?$/;
|
|
96
|
-
const HARNESS_DIRECTORIES = new Set(["tests", "test", "__tests__", "fixtures", "__fixtures__"]);
|
|
97
|
-
|
|
98
|
-
/** True when the import lands in a directory the test side owns. */
|
|
99
|
-
function isHarnessModule(filename, source) {
|
|
100
|
-
const resolved = resolve(dirname(filename), source);
|
|
101
|
-
const segments = resolved.split(sep);
|
|
102
|
-
if (segments.some((segment) => HARNESS_DIRECTORIES.has(segment))) return true;
|
|
103
|
-
// `openrouter-wire.fixture.ts` beside its subject: a fixture by name, not by directory.
|
|
104
|
-
return /\.fixture(\.[cm]?[jt]sx?)?$/.test(segments[segments.length - 1]);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The identifier a bare read or a member read (`contract.etag`) starts from. */
|
|
108
|
-
function rootIdentifier(node) {
|
|
109
|
-
let current = node;
|
|
110
|
-
while (current?.type === "MemberExpression" || current?.type === "ChainExpression") {
|
|
111
|
-
current = current.type === "ChainExpression" ? current.expression : current.object;
|
|
112
|
-
}
|
|
113
|
-
return current?.type === "Identifier" ? current : undefined;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export const noSubjectDerivedExpectation = {
|
|
117
|
-
meta: {
|
|
118
|
-
type: "problem",
|
|
119
|
-
docs: {
|
|
120
|
-
description: "Disallow an expected value imported from the sibling module the test covers",
|
|
121
|
-
},
|
|
122
|
-
schema: [],
|
|
123
|
-
messages: {
|
|
124
|
-
subjectDerived:
|
|
125
|
-
"`{{name}}` comes from `{{source}}`, the module this test covers, so the expectation moves with the implementation and no change to that value can fail this test. Write the expected value as a literal here, or compute it from something the subject does not own.",
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
create(context) {
|
|
130
|
-
if (!TEST_FILE.test(context.filename)) return {};
|
|
131
|
-
|
|
132
|
-
const sourceCode = context.sourceCode;
|
|
133
|
-
/** @type {Map<import('eslint').Scope.Variable, string>} */
|
|
134
|
-
const subjectBindings = new Map();
|
|
135
|
-
|
|
136
|
-
function bindingSource(identifier) {
|
|
137
|
-
for (let scope = sourceCode.getScope(identifier); scope; scope = scope.upper) {
|
|
138
|
-
const variable = scope.set.get(identifier.name);
|
|
139
|
-
// The nearest binding wins: a local declaration shadowing the import
|
|
140
|
-
// is a value this test chose, so it drops out here.
|
|
141
|
-
if (variable) return subjectBindings.get(variable);
|
|
142
|
-
}
|
|
143
|
-
return undefined;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
Program(node) {
|
|
148
|
-
for (const statement of node.body) {
|
|
149
|
-
if (statement.type !== "ImportDeclaration") continue;
|
|
150
|
-
if (statement.importKind === "type") continue;
|
|
151
|
-
const source = statement.source.value;
|
|
152
|
-
if (!source.startsWith("./") && !source.startsWith("../")) continue;
|
|
153
|
-
if (isHarnessModule(context.filename, source)) continue;
|
|
154
|
-
for (const specifier of statement.specifiers) {
|
|
155
|
-
if (specifier.importKind === "type") continue;
|
|
156
|
-
for (const variable of sourceCode.getDeclaredVariables(specifier)) {
|
|
157
|
-
subjectBindings.set(variable, source);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
|
|
163
|
-
CallExpression(node) {
|
|
164
|
-
const assertion = expectAssertion(node);
|
|
165
|
-
if (!assertion || !EQUALITY_MATCHERS.has(assertion.matcher)) return;
|
|
166
|
-
const [expected] = assertion.args;
|
|
167
|
-
if (!expected) return;
|
|
168
|
-
if (expected.type !== "Identifier" && expected.type !== "MemberExpression") return;
|
|
169
|
-
|
|
170
|
-
const root = rootIdentifier(expected);
|
|
171
|
-
if (!root) return;
|
|
172
|
-
const source = bindingSource(root);
|
|
173
|
-
if (!source) return;
|
|
174
|
-
|
|
175
|
-
context.report({
|
|
176
|
-
node: expected,
|
|
177
|
-
messageId: "subjectDerived",
|
|
178
|
-
data: { name: sourceCode.getText(expected), source },
|
|
179
|
-
});
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
},
|
|
183
|
-
};
|