@redocly/config-rules-builder 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ Copyright 2022 (C) Redocly Inc, all rights reserved.
2
+
3
+ See license agreement here: https://redocly.com/subscription-agreement/
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ # @redocly/config-rules-builder
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Visual, two-panel builder for [Redocly configurable rules](https://redocly.com/docs/cli/rules/configurable-rules)
4
+ (`rule/*` entries in `redocly.yaml`). Build a rule with sentence-style form controls on the left
5
+ and instantly see which nodes of a real OpenAPI document it matches, and which of them fail the
6
+ assertions, on the right.
7
+
8
+ ```
9
+ ┌──────────────────────────────┬───────────────────────────────┐
10
+ │ Rule cards (collapsible) │ OpenAPI document editor │
11
+ │ subject / where / assertions │ matched nodes highlighted, │
12
+ │ message / suggest / severity │ failures underlined + tooltip │
13
+ │ import rules · redocly.yaml ├───────────────────────────────┤
14
+ │ export · add rule │ Problems (click to jump) │
15
+ └──────────────────────────────┴───────────────────────────────┘
16
+ ```
17
+
18
+ ## Features
19
+
20
+ - **Real lint engine.** Rules are evaluated by `@redocly/openapi-core` in the browser, so results
21
+ match `redocly lint` exactly.
22
+ - **Live matching.** Nodes matched by the selected rule are highlighted in the document. When the
23
+ rule targets a property (`subject.property`), only that property is highlighted. The
24
+ `N nodes matched` badge is a button: each click scrolls to the next match.
25
+ - **Inline problems.** Failing assertions get a squiggly underline with a hover tooltip, a gutter
26
+ marker, and an entry in the Problems list that jumps to the location.
27
+ - **Import and export.** Paste an existing `redocly.yaml`, or copy and download the generated one.
28
+ - **Bring your own document.** Upload any OpenAPI file (YAML or JSON, up to 2 MB) or edit the
29
+ sample in place.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ npm install @redocly/config-rules-builder
35
+ ```
36
+
37
+ Peer dependencies: `react` ≥18, `react-dom` ≥18, `styled-components` ≥6.
38
+
39
+ The package ships **ESM only**. `styled-components` v6 has no `exports` map, so a CJS build would
40
+ resolve its broken CJS entry in Node. Any modern bundler (Vite, webpack 5, esbuild) and Node ESM
41
+ consumers work; `require()` is not supported.
42
+
43
+ ## Usage
44
+
45
+ ```tsx
46
+ import { RulesBuilder } from '@redocly/config-rules-builder';
47
+
48
+ export function ConfigRulesBuilderPage() {
49
+ return <RulesBuilder />;
50
+ }
51
+ ```
52
+
53
+ The component is self-contained: it ships with a sample OpenAPI document, a demo rule, and
54
+ copy/download buttons for the generated `redocly.yaml` snippet. Pass `onRuleChange` to receive the
55
+ snippet in the host app; see [Props](#props).
56
+
57
+ ### Server-side rendering
58
+
59
+ CodeMirror and the lint engine are browser-only. When the host framework renders on the server
60
+ (for example a Realm or Next.js page), mount the component after hydration:
61
+
62
+ ```tsx
63
+ import { useEffect, useState } from 'react';
64
+ import { RulesBuilder } from '@redocly/config-rules-builder';
65
+
66
+ export default function RulesBuilderPage() {
67
+ const [isClientSide, setIsClientSide] = useState(false);
68
+ useEffect(() => setIsClientSide(true), []);
69
+
70
+ return isClientSide ? <RulesBuilder /> : <div style={{ minHeight: 640 }} />;
71
+ }
72
+ ```
73
+
74
+ The lint engine imports Node's `path` module. Bundlers that target the browser need it aliased to
75
+ `path-browserify` (Realm's client build already does this).
76
+
77
+ ### Props
78
+
79
+ All props are optional.
80
+
81
+ | Prop | Type | Default | Description |
82
+ | --- | --- | --- | --- |
83
+ | `defaultSpec` | `string` | Redocly Museum API | OpenAPI document shown initially |
84
+ | `defaultSpecFileName` | `string` | `museum-api.yaml` | File name shown above the editor |
85
+ | `initialRule` | `ConfigurableRule` | `Operation.summary` defined + `maxLength: 20` | Rule loaded into the first card |
86
+ | `initialRuleName` | `string` | `operation-summary` | Name of the first rule (`rule/<name>`) |
87
+ | `onRuleChange` | `(yaml: string, rules: NamedRule[]) => void` | | Called with the generated `redocly.yaml` snippet and the normalized rules on every change |
88
+ | `host` | `HostAdapters` | browser APIs | Environment integrations, see [Host adapters](#host-adapters) |
89
+ | `height` | `number` | `640` | Height of the two-panel area in px. Below 1024px the panels stack and auto-size |
90
+
91
+ ### Theming
92
+
93
+ Styling is fully scoped via `styled-components`: no global styles and no CSS framework, so the
94
+ component can be dropped into pages with their own global CSS. Colors and fonts default to
95
+ `@redocly/theme` CSS variables (`--color-primary-base`, `--font-family-base`, and so on) with
96
+ hard-coded fallbacks, so the component looks right inside Redocly products and standalone alike.
97
+ To restyle it, define those CSS variables on an ancestor element.
98
+
99
+ ### Host adapters
100
+
101
+ `host` swaps browser defaults for environment-specific integrations, for example inside a VS Code
102
+ webview:
103
+
104
+ | Adapter | Default | Used by |
105
+ | --- | --- | --- |
106
+ | `copyToClipboard(text)` | `navigator.clipboard.writeText` | Copy button in the `redocly.yaml` preview |
107
+ | `downloadFile(fileName, content)` | Blob download | Download button in the `redocly.yaml` preview |
108
+ | `pickSpecFile()` | `<input type="file">` | Upload button above the OpenAPI editor |
109
+
110
+ ### Other exports
111
+
112
+ Besides the component, the package exports its building blocks:
113
+
114
+ - `evaluateRules(specText, rules, fileName?)` runs the lint engine and returns matched nodes and
115
+ problems.
116
+ - `serializeRules(rules)` and `parseImportedRules(yaml)` convert between `NamedRule[]` and a
117
+ `redocly.yaml` snippet.
118
+ - `DEFAULT_SPEC` is the bundled Museum API sample.
119
+ - All public types: `RulesBuilderProps`, `ConfigurableRule`, `NamedRule`, `HostAdapters`,
120
+ `EvaluationResult`, `MatchedNode`, `ProblemInfo`, and the rule-shape types.
121
+
122
+ ## Development
123
+
124
+ This package lives in the Redocly monorepo. From `packages/config-rules-builder`:
125
+
126
+ ```bash
127
+ pnpm run dev # demo app at http://localhost:5199
128
+ pnpm run test # unit tests (Vitest)
129
+ pnpm run build # library build to dist/ (ESM bundle + .d.ts)
130
+ pnpm run ts:check # type-check
131
+ ```
132
+
133
+ CI runs `lint`, `ts:check`, `build`, and `vitest:test` through Nx, so
134
+ `pnpm nx run @redocly/config-rules-builder:lint` mirrors what the pipeline does.
135
+
136
+ ## Release
137
+
138
+ Versioning and npm releases use [Changesets](https://github.com/changesets/changesets). Run
139
+ everything from the monorepo root.
140
+
141
+ 1. Add a changeset with `pnpm run changeset:common` and include `@redocly/config-rules-builder`.
142
+ 2. Merge the PR with the changeset into `main`.
143
+ 3. Release automation opens a version PR; merging it publishes the package to npm.
@@ -0,0 +1,12 @@
1
+ import { SectionTone } from '../styles';
2
+ import { AssertionObject } from '../types';
3
+ import { RuleErrors } from '../validation';
4
+ export interface AssertionsEditorProps {
5
+ assertions: AssertionObject;
6
+ onChange: (assertions: AssertionObject) => void;
7
+ keyword: string;
8
+ tone: SectionTone;
9
+ errors: RuleErrors;
10
+ errorPrefix: string;
11
+ }
12
+ export declare function AssertionsEditor({ assertions, onChange, keyword, tone, errors, errorPrefix, }: AssertionsEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { HostAdapters } from '../types';
2
+ export interface ConfigPreviewProps {
3
+ yaml: string;
4
+ host?: HostAdapters;
5
+ fileName?: string;
6
+ }
7
+ export declare function ConfigPreview({ yaml, host, fileName }: ConfigPreviewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { NamedRule } from '../types';
2
+ export interface ImportPanelProps {
3
+ onImport: (rules: NamedRule[]) => void;
4
+ }
5
+ export declare function ImportPanel({ onImport }: ImportPanelProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { ReactNode } from 'react';
2
+ export interface ModalProps {
3
+ title: string;
4
+ subtitle?: string;
5
+ onClose: () => void;
6
+ children: ReactNode;
7
+ footer?: ReactNode;
8
+ width?: number;
9
+ }
10
+ export declare function Modal({ title, subtitle, onClose, children, footer, width }: ModalProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,13 @@
1
+ export interface RulerMarker {
2
+ line: number;
3
+ from: number;
4
+ to: number;
5
+ kind: 'match' | 'error' | 'warn';
6
+ title?: string;
7
+ }
8
+ export interface OverviewRulerProps {
9
+ markers: RulerMarker[];
10
+ totalLines: number;
11
+ onMarkerClick: (marker: RulerMarker) => void;
12
+ }
13
+ export declare function OverviewRuler({ markers, totalLines, onMarkerClick }: OverviewRulerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { ProblemInfo } from '../types';
2
+ export interface ProblemsPanelProps {
3
+ problems: ProblemInfo[];
4
+ ruleErrors: string[];
5
+ specError?: string;
6
+ isRunning: boolean;
7
+ onSelect: (problem: ProblemInfo) => void;
8
+ }
9
+ export declare function ProblemsPanel({ problems, ruleErrors, specError, isRunning, onSelect, }: ProblemsPanelProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { RuleEntry } from '../state';
2
+ export interface RuleCardProps {
3
+ entry: RuleEntry;
4
+ selected: boolean;
5
+ matchCount: number;
6
+ isDuplicateName: boolean;
7
+ isEvaluating: boolean;
8
+ onToggle: () => void;
9
+ onChange: (entry: RuleEntry) => void;
10
+ onRemove: () => void;
11
+ }
12
+ export declare function RuleCard({ entry, selected, matchCount, isDuplicateName, isEvaluating, onToggle, onChange, onRemove, }: RuleCardProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare const Badge: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "$dim"> & {
14
+ $dim?: boolean;
15
+ }, never> & Partial<Pick<import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "$dim"> & {
16
+ $dim?: boolean;
17
+ }, never>>> & string;
@@ -0,0 +1,16 @@
1
+ import { RuleEntry } from '../state';
2
+ import { ConfigPreviewProps } from './ConfigPreview';
3
+ import { ImportPanelProps } from './ImportPanel';
4
+ export interface RuleListProps {
5
+ entries: RuleEntry[];
6
+ selectedId: string | null;
7
+ matchCountById: Record<string, number>;
8
+ onToggle: (id: string) => void;
9
+ onChangeEntry: (entry: RuleEntry) => void;
10
+ onRemove: (id: string) => void;
11
+ onAdd: () => void;
12
+ isEvaluating: boolean;
13
+ importProps: ImportPanelProps;
14
+ configPreviewProps: ConfigPreviewProps;
15
+ }
16
+ export declare function RuleList({ entries, selectedId, matchCountById, onToggle, onChangeEntry, onRemove, onAdd, isEvaluating, importProps, configPreviewProps, }: RuleListProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { ConfigurableRule, HostAdapters, NamedRule } from '../types';
2
+ export interface RulesBuilderProps {
3
+ defaultSpec?: string;
4
+ defaultSpecFileName?: string;
5
+ initialRule?: ConfigurableRule;
6
+ initialRuleName?: string;
7
+ onRuleChange?: (yaml: string, rules: NamedRule[]) => void;
8
+ host?: HostAdapters;
9
+ height?: number;
10
+ }
11
+ export declare function RulesBuilder({ defaultSpec, defaultSpecFileName, initialRule, initialRuleName, onRuleChange, host, height, }: RulesBuilderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,48 @@
1
+ import { HostAdapters } from '../types';
2
+ import { RulerMarker } from './OverviewRuler';
3
+ export interface FailRange {
4
+ from: number;
5
+ to: number;
6
+ messages: string[];
7
+ severity: 'error' | 'warn';
8
+ }
9
+ export interface SpecHighlights {
10
+ matchKeys: {
11
+ from: number;
12
+ to: number;
13
+ }[];
14
+ matchBlocks: {
15
+ from: number;
16
+ to: number;
17
+ }[];
18
+ matchProps: {
19
+ from: number;
20
+ to: number;
21
+ }[];
22
+ fails: FailRange[];
23
+ }
24
+ export declare const EMPTY_HIGHLIGHTS: SpecHighlights;
25
+ export interface SpecPanelProps {
26
+ spec: string;
27
+ fileName: string;
28
+ highlights: SpecHighlights;
29
+ rulerMarkers: RulerMarker[];
30
+ matchTargets: {
31
+ from: number;
32
+ to: number;
33
+ }[];
34
+ totalLines: number;
35
+ jump?: {
36
+ from: number;
37
+ to: number;
38
+ };
39
+ matchCount: number;
40
+ showMatchStatus: boolean;
41
+ isEvaluating: boolean;
42
+ onSpecChange: (content: string, fileName?: string) => void;
43
+ onResetSpec: () => void;
44
+ uploadError?: string;
45
+ onUploadError: (message?: string) => void;
46
+ host?: HostAdapters;
47
+ }
48
+ export declare function SpecPanel({ spec, fileName, highlights, rulerMarkers, matchTargets, totalLines, jump, matchCount, showMatchStatus, isEvaluating, onSpecChange, onResetSpec, uploadError, onUploadError, host, }: SpecPanelProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,11 @@
1
+ import { SubjectObject } from '../types';
2
+ import { RuleErrors } from '../validation';
3
+ export interface SubjectEditorProps {
4
+ subject: SubjectObject;
5
+ onChange: (subject: SubjectObject) => void;
6
+ lead: string;
7
+ ariaPrefix: string;
8
+ errors: RuleErrors;
9
+ errorPrefix: string;
10
+ }
11
+ export declare function SubjectEditor({ subject, onChange, lead, ariaPrefix, errors, errorPrefix, }: SubjectEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ export interface ChipListInputProps {
2
+ values: string[];
3
+ onChange: (values: string[]) => void;
4
+ ariaLabel: string;
5
+ placeholder?: string;
6
+ }
7
+ export declare function ChipListInput({ values, onChange, ariaLabel, placeholder }: ChipListInputProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,9 @@
1
+ export interface ComboboxProps {
2
+ value: string;
3
+ options: string[];
4
+ onChange: (value: string) => void;
5
+ ariaLabel: string;
6
+ placeholder?: string;
7
+ width?: number;
8
+ }
9
+ export declare function Combobox({ value, options, onChange, ariaLabel, placeholder, width, }: ComboboxProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,12 @@
1
+ export interface SegmentedOption<T extends string> {
2
+ value: T;
3
+ label: string;
4
+ activeColor?: string;
5
+ }
6
+ export interface SegmentedProps<T extends string> {
7
+ value: T;
8
+ options: SegmentedOption<T>[];
9
+ onChange: (value: T) => void;
10
+ ariaLabel: string;
11
+ }
12
+ export declare function Segmented<T extends string>({ value, options, onChange, ariaLabel, }: SegmentedProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import { Severity } from '../../types';
2
+ export declare function SeverityPill({ value, onChange, }: {
3
+ value: Severity;
4
+ onChange: (value: Severity) => void;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { SectionTone } from '../../styles';
3
+ export declare const InlineSelect: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>, never>>> & string;
4
+ export declare const InlineInput: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, never>>> & string;
5
+ export declare const MonoInput: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('styled-components').FastOmit<import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, never>>, "as" | "forwardedAs"> & {
6
+ as?: import('styled-components').WebTarget | undefined;
7
+ forwardedAs?: import('styled-components').WebTarget | undefined;
8
+ }, never> & Partial<Pick<import('styled-components').FastOmit<import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, never>>, "as" | "forwardedAs"> & {
9
+ as?: import('styled-components').WebTarget | undefined;
10
+ forwardedAs?: import('styled-components').WebTarget | undefined;
11
+ }, never>>> & string;
12
+ export declare const ToolbarButton: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>>> & string;
13
+ export declare const TriggerButton: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>>> & string;
14
+ export declare const SecondaryButton: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>>> & string;
15
+ export declare const PrimaryButton: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, never>>> & string;
16
+ export declare const Keyword: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, never>>> & string;
17
+ export declare function Section({ tone, label, hint, children, }: {
18
+ tone: SectionTone;
19
+ label: string;
20
+ hint: string;
21
+ children: ReactNode;
22
+ }): import("react/jsx-runtime").JSX.Element;
23
+ export declare const SentenceRow: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never> & Partial<Pick<import('react').DetailedHTMLProps<import('react').HTMLAttributes<HTMLDivElement>, HTMLDivElement>, never>>> & string;
24
+ export declare function Chevron({ open }: {
25
+ open: boolean;
26
+ }): import("react/jsx-runtime").JSX.Element;
27
+ export declare function FieldSquiggle({ error, children, style, }: {
28
+ error?: string;
29
+ children: ReactNode;
30
+ style?: CSSProperties;
31
+ }): import("react/jsx-runtime").JSX.Element;