diff --git a/.cursor/rules/components-rules.mdc b/.cursor/rules/components-rules.mdc index 217e231c..6225ed80 100644 --- a/.cursor/rules/components-rules.mdc +++ b/.cursor/rules/components-rules.mdc @@ -32,7 +32,7 @@ Canvas components are located in packages/graph/src/components/canvas/ and divid - **Note:** These Canvas components manage rendering directly onto a `` element. They are distinct from React components (even those used *within* the HTML layer at high zoom levels, like `GraphBlock`), although they share some lifecycle concepts managed by the core library. Rules specific to Canvas component rendering might not apply directly to React components used for the HTML layer. # React Integration -For integration of Canvas components with React, wrappers are used in the packages/graph/src/react-components/ directory. These components allow the use of Canvas in React applications. +For integration of Canvas components with React, wrappers are used in the packages/graph-react/src/ directory. These components allow the use of Canvas in React applications. # Rendering Layers Canvas rendering is organized in layers with different priorities: diff --git a/.cursor/rules/event-model-rules.mdc b/.cursor/rules/event-model-rules.mdc index 2e7f7961..09fbc08a 100644 --- a/.cursor/rules/event-model-rules.mdc +++ b/.cursor/rules/event-model-rules.mdc @@ -42,7 +42,7 @@ The system uses **`CustomEvent`**, and interaction follows a `.on`/`.off`/`.emit - **Pros:** Automatically handles subscription/unsubscription; provides `detail` and `event` objects. - **Cons:** Only usable within React function components. ```jsx - import { useGraph, useGraphEvent, GraphCanvas } from '@gravity-ui/graph'; + import { useGraph, useGraphEvent, GraphCanvas } from '@gravity-ui/graph-react'; function MyReactComponent() { const { graph } = useGraph(/* config */); @@ -61,7 +61,7 @@ The system uses **`CustomEvent`**, and interaction follows a `.on`/`.off`/`.emit - **Pros:** Declarative, simple for common cases. - **Cons:** Limited to the specific events exposed as props. ```jsx - import { GraphCanvas } from '@gravity-ui/graph'; + import { GraphCanvas } from '@gravity-ui/graph-react'; // ... assume graph instance is available diff --git a/.cursor/rules/layer-rules.mdc b/.cursor/rules/layer-rules.mdc index 15345ec2..84d27004 100644 --- a/.cursor/rules/layer-rules.mdc +++ b/.cursor/rules/layer-rules.mdc @@ -158,7 +158,7 @@ this.html.style.transform = `matrix(${camera.scale}, 0, 0, ${camera.scale}, ${ca ## React Integration with useLayer - **Using `useLayer` Hook:** The recommended way to add and manage layers in React components: ```typescript - import { useLayer } from "@gravity-ui/graph"; + import { useLayer } from "@gravity-ui/graph-react"; function MyComponent() { const { graph } = useGraph(/* config */); @@ -219,4 +219,4 @@ protected afterInit() { } ``` -When the layer is unmounted, all handlers are automatically removed. \ No newline at end of file +When the layer is unmounted, all handlers are automatically removed. diff --git a/.cursor/rules/project-rules.mdc b/.cursor/rules/project-rules.mdc index e820a89b..6e3d8633 100644 --- a/.cursor/rules/project-rules.mdc +++ b/.cursor/rules/project-rules.mdc @@ -36,9 +36,9 @@ Project structure: - Connection Store State: `packages/graph/src/store/connection/ConnectionState.ts` (Exports `ConnectionState`, `TConnection`) - Base connection component: `packages/graph/src/components/canvas/connections/BaseConnection.ts` (Exports `BaseConnection`) - **React Integration:** - - `GraphCanvas` component: `packages/graph/src/react-components/GraphCanvas.tsx` - - `useGraph` hook: `packages/graph/src/react-components/hooks/useGraph.ts` - - `GraphBlock` component: `packages/graph/src/react-components/Block.tsx` + - `GraphCanvas` component: `packages/graph-react/src/GraphCanvas.tsx` + - `useGraph` hook: `packages/graph-react/src/hooks/useGraph.ts` + - `GraphBlock` component: `packages/graph-react/src/Block.tsx` ## Technologies Technology stack: @@ -52,7 +52,7 @@ Technology stack: ## Key Components Key components: - Graph - main class for graph management (packages/graph/src/graph.ts) -- GraphCanvas - React component for displaying the graph (packages/graph/src/react-components/GraphCanvas.tsx) +- GraphCanvas - React component for displaying the graph (packages/graph-react/src/GraphCanvas.tsx) - Block - component for representing a graph block - Connection - component for connections between blocks - Anchor - components for connection attachment points diff --git a/.cursor/rules/story-rules.mdc b/.cursor/rules/story-rules.mdc index d887df32..4f294bca 100644 --- a/.cursor/rules/story-rules.mdc +++ b/.cursor/rules/story-rules.mdc @@ -11,10 +11,10 @@ alwaysApply: false - Always use `GraphBlock` for rendering graph blocks in the HTML layer unless you have a specific custom renderer. ## Graph Initialization (Recommended Pattern) -- **Use `useGraph` hook:** Initialize the graph instance within your story component using the public `@gravity-ui/graph/react` entrypoint. +- **Use `useGraph` hook:** Initialize the graph instance within your story component using the public `@gravity-ui/graph-react` entrypoint. ```typescript import { Graph, GraphState } from "@gravity-ui/graph"; - import { GraphCanvas, useGraph } from "@gravity-ui/graph/react"; + import { GraphCanvas, useGraph } from "@gravity-ui/graph-react"; const MyStoryComponent = (props) => { const { graph } = useGraph({ diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 5ff0c73c..a83ac27d 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -49,7 +49,7 @@ jobs: name: playwright-report path: | apps/e2e/playwright-report/ - packages/graph/playwright-report/ + playwright-report/ retention-days: 7 - name: Upload test screenshots @@ -59,5 +59,5 @@ jobs: name: test-results path: | apps/e2e/test-results/ - packages/graph/test-results/ + test-results/ retention-days: 7 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 212b6e7b..9bf331bf 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,4 +1,5 @@ { "packages/graph": "1.11.3", - "packages/scheduler": "0.0.0" + "packages/scheduler": "0.0.0", + "packages/graph-react": "0.0.0" } diff --git a/CLAUDE.md b/CLAUDE.md index f5e53a1d..4aa43ce6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,8 +49,8 @@ The library uses a **hybrid Canvas + React architecture**: **Key Files**: - `packages/graph/src/components/canvas/layers/graphLayer/GraphLayer.ts` - Main Canvas rendering -- `packages/graph/src/react-components/layer/ReactLayer.tsx` - React Portal integration -- `packages/graph/src/react-components/GraphCanvas.tsx` - React wrapper component +- `packages/graph-react/src/layer/ReactLayer.tsx` - React Portal integration +- `packages/graph-react/src/GraphCanvas.tsx` - React wrapper component ### Custom Component Framework @@ -213,7 +213,7 @@ protected afterInit() { **In React**: ```typescript -import { useGraphEvent } from '@gravity-ui/graph'; +import { useGraphEvent } from '@gravity-ui/graph-react'; function MyComponent() { const { graph } = useGraph(config); @@ -251,7 +251,7 @@ graph.rootStore.settings.setBlockComponents(customBlocks); ### Adding Layers in React ```typescript -import { useLayer } from '@gravity-ui/graph'; +import { useLayer } from '@gravity-ui/graph-react'; function MyComponent() { const { graph } = useGraph(config); @@ -952,7 +952,7 @@ private handleBlockSelect = (event: CustomEvent): void => { Use `useGraphEvent` hook - automatic cleanup. ```typescript -import { useGraphEvent } from "@gravity-ui/graph"; +import { useGraphEvent } from "@gravity-ui/graph-react"; function MyComponent() { const { graph } = useGraph(config); @@ -1008,7 +1008,7 @@ useGraphEvent(graph, "block:click", (detail, event) => { **GraphCanvas - Main Container:** ```tsx -import { GraphCanvas, useGraph } from "@gravity-ui/graph"; +import { GraphCanvas, useGraph } from "@gravity-ui/graph-react"; function App() { const { graph } = useGraph({ @@ -1075,7 +1075,7 @@ const layer = useLayer(graph, LayerConstructor, props); **Example:** ```typescript -import { useLayer } from "@gravity-ui/graph"; +import { useLayer } from "@gravity-ui/graph-react"; function MyComponent() { const { graph } = useGraph(config); @@ -1177,7 +1177,7 @@ const config: TGraphConfig = { **Use `useGraph` hook (Recommended):** ```typescript -import { useGraph, GraphCanvas, GraphBlock } from "@gravity-ui/graph"; +import { useGraph, GraphCanvas, GraphBlock } from "@gravity-ui/graph-react"; import { useEffect } from "react"; export const MyStory = () => { diff --git a/README.md b/README.md index 71d65ceb..66d01a5f 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,11 @@ This repository contains the Gravity UI Graph packages and their development too ## Packages -- [`@gravity-ui/graph`](packages/graph) — the graph editor library, React integration, public Playwright page objects, - documentation, and unit and package-contract tests. +- [`@gravity-ui/graph`](packages/graph) — the framework-independent graph editor, layouts, plugins, and public Playwright page objects. +- [`@gravity-ui/graph-react`](packages/graph-react) — React components, hooks, and their styles, depending on the core public API. +- [`@gravity-ui/graph-scheduler`](packages/scheduler) — private scheduling implementation inlined into the core build. + +Shared build tooling lives in `scripts/`; `tests/package-contract` installs the two public tarballs into isolated vanilla and React projects. ## Applications diff --git a/apps/e2e/global.d.ts b/apps/e2e/global.d.ts index f8b271b9..174726c2 100644 --- a/apps/e2e/global.d.ts +++ b/apps/e2e/global.d.ts @@ -1,7 +1,7 @@ import type { Graph } from "@gravity-ui/graph"; type GraphModule = typeof import("@gravity-ui/graph") & - Pick & { + Pick & { React: typeof import("react"); ReactDOM: typeof import("react-dom/client"); }; diff --git a/apps/e2e/package.json b/apps/e2e/package.json index 629c1053..9efef72d 100644 --- a/apps/e2e/package.json +++ b/apps/e2e/package.json @@ -6,15 +6,16 @@ "typecheck": "tsc -p tsconfig.json", "e2e:bundle": "node build-bundle.js", "e2e:serve": "node server.js", - "e2e:build": "pnpm --filter @gravity-ui/graph run build && pnpm run e2e:bundle", + "e2e:build": "pnpm --filter @gravity-ui/graph --filter @gravity-ui/graph-react run build && pnpm run e2e:bundle", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:e2e:debug": "playwright test --debug", "test:e2e:ci": "playwright test", - "e2e:dev": "pnpm run e2e:build && concurrently \"pnpm --filter @gravity-ui/graph run dev\" \"pnpm run e2e:serve\"" + "e2e:dev": "pnpm run e2e:build && concurrently \"pnpm -w run dev\" \"pnpm run e2e:serve\"" }, "dependencies": { "@gravity-ui/graph": "workspace:*", + "@gravity-ui/graph-react": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/apps/e2e/react-entry.ts b/apps/e2e/react-entry.ts index b035d9d4..08e8fafa 100644 --- a/apps/e2e/react-entry.ts +++ b/apps/e2e/react-entry.ts @@ -3,7 +3,8 @@ import React from "react"; import ReactDOM from "react-dom/client"; import "@gravity-ui/graph/styles.css"; +import "@gravity-ui/graph-react/styles.css"; export * from "@gravity-ui/graph"; -export { GraphBlock, GraphBlockAnchor, GraphCanvas } from "@gravity-ui/graph/react"; +export { GraphBlock, GraphBlockAnchor, GraphCanvas } from "@gravity-ui/graph-react"; export { React, ReactDOM }; diff --git a/apps/e2e/tests/anchor/anchor-css-variables.spec.ts b/apps/e2e/tests/anchor/anchor-css-variables.spec.ts index bd7c9c7d..98a67b2e 100644 --- a/apps/e2e/tests/anchor/anchor-css-variables.spec.ts +++ b/apps/e2e/tests/anchor/anchor-css-variables.spec.ts @@ -2,7 +2,8 @@ import { expect, test } from "@playwright/test"; test.describe("Anchor CSS variables", () => { test.beforeEach(async ({ page }) => { - await page.goto("/base.html"); + // Anchor DOM styles belong to the React package and are loaded by the React fixture. + await page.goto("/react.html"); await page.waitForFunction(() => (window as Window & { graphLibraryLoaded?: boolean }).graphLibraryLoaded === true); }); diff --git a/apps/storybook/.storybook/preview.tsx b/apps/storybook/.storybook/preview.tsx index e66169fe..12b5c423 100644 --- a/apps/storybook/.storybook/preview.tsx +++ b/apps/storybook/.storybook/preview.tsx @@ -5,6 +5,7 @@ import type { Preview } from "@storybook/react-webpack5"; import "./styles/global.css"; import "@gravity-ui/graph/styles.css"; +import "@gravity-ui/graph-react/styles.css"; import "@gravity-ui/uikit/styles/styles.css"; const preview: Preview = { diff --git a/apps/storybook/package.json b/apps/storybook/package.json index e55b8d2d..68a08825 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "storybook": "concurrently \"pnpm --filter @gravity-ui/graph run dev\" \"storybook dev -p 6006\"", + "storybook": "concurrently \"pnpm -w run dev\" \"storybook dev -p 6006\"", "build-storybook": "storybook build", "build": "pnpm run build-storybook", "typecheck": "tsc -p tsconfig.json --noEmit", @@ -11,6 +11,7 @@ }, "dependencies": { "@gravity-ui/graph": "workspace:*", + "@gravity-ui/graph-react": "workspace:*", "@gravity-ui/icons": "^2.15.0", "@gravity-ui/uikit": "^7.19.0", "@monaco-editor/react": "^4.6.0", @@ -29,8 +30,8 @@ "@storybook/react": "^9.1.2", "@storybook/react-webpack5": "^9.1.2", "@swc/core": "^1.13.3", - "@types/node": "^20.17.0", "@types/lodash": "^4.17.13", + "@types/node": "^20.17.0", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/eslint-plugin": "5.39.0", diff --git a/apps/storybook/src/stories/Playground/ActionBlock/ActionBlockHtml.tsx b/apps/storybook/src/stories/Playground/ActionBlock/ActionBlockHtml.tsx index 72aa060e..4a21477e 100644 --- a/apps/storybook/src/stories/Playground/ActionBlock/ActionBlockHtml.tsx +++ b/apps/storybook/src/stories/Playground/ActionBlock/ActionBlockHtml.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Graph } from "@gravity-ui/graph"; -import { GraphBlock, GraphBlockAnchor } from "@gravity-ui/graph/react"; +import { GraphBlock, GraphBlockAnchor } from "@gravity-ui/graph-react"; import { Database } from "@gravity-ui/icons"; import { Button, Flex, Icon, Text } from "@gravity-ui/uikit"; diff --git a/apps/storybook/src/stories/Playground/GraphPlayground.tsx b/apps/storybook/src/stories/Playground/GraphPlayground.tsx index 46a71d12..b34fed02 100644 --- a/apps/storybook/src/stories/Playground/GraphPlayground.tsx +++ b/apps/storybook/src/stories/Playground/GraphPlayground.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { ConnectionLayer, ECanDrag, Graph, GraphState, TBlock, TGraphConfig } from "@gravity-ui/graph"; -import { GraphBlock, GraphCanvas, HookGraphParams, useGraph, useGraphEvent, useLayer } from "@gravity-ui/graph/react"; +import { GraphBlock, GraphCanvas, HookGraphParams, useGraph, useGraphEvent, useLayer } from "@gravity-ui/graph-react"; import { Flex, SegmentedRadioGroup, diff --git a/apps/storybook/src/stories/Playground/TextBlock/TextBlockHtml.tsx b/apps/storybook/src/stories/Playground/TextBlock/TextBlockHtml.tsx index db108f67..f9597ccf 100644 --- a/apps/storybook/src/stories/Playground/TextBlock/TextBlockHtml.tsx +++ b/apps/storybook/src/stories/Playground/TextBlock/TextBlockHtml.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Graph } from "@gravity-ui/graph"; -import { GraphBlock } from "@gravity-ui/graph/react"; +import { GraphBlock } from "@gravity-ui/graph-react"; import { CircleInfo } from "@gravity-ui/icons"; import { Flex, Icon, Text } from "@gravity-ui/uikit"; diff --git a/apps/storybook/src/stories/Playground/Toolbox.tsx b/apps/storybook/src/stories/Playground/Toolbox.tsx index c1a1a4b8..84ebe48a 100644 --- a/apps/storybook/src/stories/Playground/Toolbox.tsx +++ b/apps/storybook/src/stories/Playground/Toolbox.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Graph } from "@gravity-ui/graph"; -import { useSignal } from "@gravity-ui/graph/react"; +import { useSignal } from "@gravity-ui/graph-react"; import { MagnifierMinus, MagnifierPlus, SquareDashed } from "@gravity-ui/icons"; import { Button, Flex, Icon, Tooltip } from "@gravity-ui/uikit"; diff --git a/apps/storybook/src/stories/api/connectionSelection/connectionSelection.stories.tsx b/apps/storybook/src/stories/api/connectionSelection/connectionSelection.stories.tsx index 51121004..1f2c4e57 100644 --- a/apps/storybook/src/stories/api/connectionSelection/connectionSelection.stories.tsx +++ b/apps/storybook/src/stories/api/connectionSelection/connectionSelection.stories.tsx @@ -1,7 +1,7 @@ import React, { useLayoutEffect, useState } from "react"; import { ESelectionStrategy, Graph, GraphState, TBlock, TConnection, TGraphConfig } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import { ThemeProvider } from "@gravity-ui/uikit"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; diff --git a/apps/storybook/src/stories/api/startStopGraph/startStop.stories.tsx b/apps/storybook/src/stories/api/startStopGraph/startStop.stories.tsx index 78f710b0..8c7aaf56 100644 --- a/apps/storybook/src/stories/api/startStopGraph/startStop.stories.tsx +++ b/apps/storybook/src/stories/api/startStopGraph/startStop.stories.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from "react"; import { Graph, GraphState, TBlock } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph } from "@gravity-ui/graph-react"; import { Button, Flex, ThemeProvider } from "@gravity-ui/uikit"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; diff --git a/apps/storybook/src/stories/canvas/groups/collapsible.stories.tsx b/apps/storybook/src/stories/canvas/groups/collapsible.stories.tsx index 227dcb33..2042e57c 100644 --- a/apps/storybook/src/stories/canvas/groups/collapsible.stories.tsx +++ b/apps/storybook/src/stories/canvas/groups/collapsible.stories.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { BlockGroups, CollapsibleGroup, ECanDrag, Graph, GraphState, TBlock, TConnection } from "@gravity-ui/graph"; import type { BlockGroupsProps, BlockState, TCollapsibleGroup } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; import { useFn } from "../../../useFn"; diff --git a/apps/storybook/src/stories/canvas/groups/default.stories.tsx b/apps/storybook/src/stories/canvas/groups/default.stories.tsx index 3735d2a2..e2b81a97 100644 --- a/apps/storybook/src/stories/canvas/groups/default.stories.tsx +++ b/apps/storybook/src/stories/canvas/groups/default.stories.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { BlockGroups, BlockState, ECanDrag, Graph, GraphState, Group, TBlock } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; import groupBy from "lodash/groupBy"; diff --git a/apps/storybook/src/stories/canvas/groups/extended.stories.tsx b/apps/storybook/src/stories/canvas/groups/extended.stories.tsx index e2f2f2a6..d587ba32 100644 --- a/apps/storybook/src/stories/canvas/groups/extended.stories.tsx +++ b/apps/storybook/src/stories/canvas/groups/extended.stories.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { BlockGroups, Graph, GraphState, Group, TBlock, TGroup } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; import { useFn } from "../../../useFn"; diff --git a/apps/storybook/src/stories/canvas/groups/large.stories.tsx b/apps/storybook/src/stories/canvas/groups/large.stories.tsx index 7a393a36..bbccdf4b 100644 --- a/apps/storybook/src/stories/canvas/groups/large.stories.tsx +++ b/apps/storybook/src/stories/canvas/groups/large.stories.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { BlockGroups, BlockState, Graph, GraphState, Group, TBlock } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; import groupBy from "lodash/groupBy"; diff --git a/apps/storybook/src/stories/canvas/groups/manual.stories.tsx b/apps/storybook/src/stories/canvas/groups/manual.stories.tsx index db48fa4a..9a4436e1 100644 --- a/apps/storybook/src/stories/canvas/groups/manual.stories.tsx +++ b/apps/storybook/src/stories/canvas/groups/manual.stories.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { BlockGroups, Graph, GraphState, Group, TBlock } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; import { useFn } from "../../../useFn"; diff --git a/apps/storybook/src/stories/canvas/groups/transfer.stories.tsx b/apps/storybook/src/stories/canvas/groups/transfer.stories.tsx index 60863364..a2f3224d 100644 --- a/apps/storybook/src/stories/canvas/groups/transfer.stories.tsx +++ b/apps/storybook/src/stories/canvas/groups/transfer.stories.tsx @@ -9,7 +9,7 @@ import { TBlock, TDefinitionGroup, } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent, useLayer } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent, useLayer } from "@gravity-ui/graph-react"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; import { useFn } from "../../../useFn"; diff --git a/apps/storybook/src/stories/examples/connectionLayer/connectionLayer.stories.tsx b/apps/storybook/src/stories/examples/connectionLayer/connectionLayer.stories.tsx index e886ce30..f62a8304 100644 --- a/apps/storybook/src/stories/examples/connectionLayer/connectionLayer.stories.tsx +++ b/apps/storybook/src/stories/examples/connectionLayer/connectionLayer.stories.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { ConnectionLayer, Graph, TBlock } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph } from "@gravity-ui/graph-react"; import { Flex, Hotkey, Switch, Text, ThemeProvider } from "@gravity-ui/uikit"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; diff --git a/apps/storybook/src/stories/examples/customConnectionLabel/customConnectionLabel.stories.tsx b/apps/storybook/src/stories/examples/customConnectionLabel/customConnectionLabel.stories.tsx index fac96f82..18ecb627 100644 --- a/apps/storybook/src/stories/examples/customConnectionLabel/customConnectionLabel.stories.tsx +++ b/apps/storybook/src/stories/examples/customConnectionLabel/customConnectionLabel.stories.tsx @@ -9,7 +9,7 @@ import { getFontSize, getLabelCoords, } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph } from "@gravity-ui/graph-react"; import { Flex, Switch, ThemeProvider } from "@gravity-ui/uikit"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; diff --git a/apps/storybook/src/stories/examples/mouseWheelBehaviorScroll/mouseWheelBehaviorScroll.stories.tsx b/apps/storybook/src/stories/examples/mouseWheelBehaviorScroll/mouseWheelBehaviorScroll.stories.tsx index 361866e3..76609b0c 100644 --- a/apps/storybook/src/stories/examples/mouseWheelBehaviorScroll/mouseWheelBehaviorScroll.stories.tsx +++ b/apps/storybook/src/stories/examples/mouseWheelBehaviorScroll/mouseWheelBehaviorScroll.stories.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useMemo, useState } from "react"; import type { TMouseWheelBehavior, TWheelInputDevice } from "@gravity-ui/graph"; import { ECanDrag, EWheelIntent, Graph, GraphState, TBlock, enableWheelIntentDebug } from "@gravity-ui/graph"; -import { GraphBlock, GraphCanvas, HookGraphParams, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphBlock, GraphCanvas, HookGraphParams, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import { Flex, Text, ThemeProvider } from "@gravity-ui/uikit"; import type { Meta, StoryObj } from "@storybook/react-webpack5"; diff --git a/apps/storybook/src/stories/examples/newBlockLayer/newBlockLayer.stories.tsx b/apps/storybook/src/stories/examples/newBlockLayer/newBlockLayer.stories.tsx index 60ebdce9..772bb7f9 100644 --- a/apps/storybook/src/stories/examples/newBlockLayer/newBlockLayer.stories.tsx +++ b/apps/storybook/src/stories/examples/newBlockLayer/newBlockLayer.stories.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import { Graph, NewBlockLayer, TBlock } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph } from "@gravity-ui/graph-react"; import { Flex, Hotkey, Switch, Text, ThemeProvider } from "@gravity-ui/uikit"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; diff --git a/apps/storybook/src/stories/examples/portConnectionLayer/portConnectionLayer.stories.tsx b/apps/storybook/src/stories/examples/portConnectionLayer/portConnectionLayer.stories.tsx index b02f6a9c..a96ba26e 100644 --- a/apps/storybook/src/stories/examples/portConnectionLayer/portConnectionLayer.stories.tsx +++ b/apps/storybook/src/stories/examples/portConnectionLayer/portConnectionLayer.stories.tsx @@ -11,7 +11,7 @@ import { TBlock, createAnchorPortId, } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph } from "@gravity-ui/graph-react"; import { ThemeProvider } from "@gravity-ui/uikit"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; diff --git a/apps/storybook/src/stories/examples/viewportInsets/viewportInsets.stories.tsx b/apps/storybook/src/stories/examples/viewportInsets/viewportInsets.stories.tsx index 333e6ac5..1f3a66d8 100644 --- a/apps/storybook/src/stories/examples/viewportInsets/viewportInsets.stories.tsx +++ b/apps/storybook/src/stories/examples/viewportInsets/viewportInsets.stories.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import { Graph, GraphState, TBlock } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent, useSceneChange } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent, useSceneChange } from "@gravity-ui/graph-react"; import { Button, Flex, Switch } from "@gravity-ui/uikit"; import type { Meta, StoryObj } from "@storybook/react-webpack5"; diff --git a/apps/storybook/src/stories/examples/wheelIntentProbe/wheelIntentProbe.stories.tsx b/apps/storybook/src/stories/examples/wheelIntentProbe/wheelIntentProbe.stories.tsx index 20aaadc8..ba11974f 100644 --- a/apps/storybook/src/stories/examples/wheelIntentProbe/wheelIntentProbe.stories.tsx +++ b/apps/storybook/src/stories/examples/wheelIntentProbe/wheelIntentProbe.stories.tsx @@ -9,7 +9,7 @@ import { createWheelIntentResolver, enableWheelIntentDebug, } from "@gravity-ui/graph"; -import { GraphBlock, GraphCanvas, HookGraphParams, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphBlock, GraphCanvas, HookGraphParams, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import { Flex, ThemeProvider } from "@gravity-ui/uikit"; import type { Meta, StoryObj } from "@storybook/react-webpack5"; diff --git a/apps/storybook/src/stories/main/Block.tsx b/apps/storybook/src/stories/main/Block.tsx index 2ea9bc68..c3b2f972 100644 --- a/apps/storybook/src/stories/main/Block.tsx +++ b/apps/storybook/src/stories/main/Block.tsx @@ -1,7 +1,7 @@ import React, { MouseEvent } from "react"; import { Graph, TBlock } from "@gravity-ui/graph"; -import { GraphBlock, GraphBlockAnchor } from "@gravity-ui/graph/react"; +import { GraphBlock, GraphBlockAnchor } from "@gravity-ui/graph-react"; import { AbbrApi, Bug, Database } from "@gravity-ui/icons"; import { Button, Icon, Text } from "@gravity-ui/uikit"; diff --git a/apps/storybook/src/stories/main/GraphEditor.tsx b/apps/storybook/src/stories/main/GraphEditor.tsx index d37e2427..59d53c6f 100644 --- a/apps/storybook/src/stories/main/GraphEditor.tsx +++ b/apps/storybook/src/stories/main/GraphEditor.tsx @@ -1,7 +1,7 @@ import React, { useLayoutEffect } from "react"; import { Graph, GraphState, TBlock, TGraphColors, TGraphConfig } from "@gravity-ui/graph"; -import { GraphCanvas, HookGraphParams, TGraphEventCallbacks, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphCanvas, HookGraphParams, TGraphEventCallbacks, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import { useFn } from "../../useFn"; diff --git a/apps/storybook/src/stories/plugins/cssVariables/cssVariables.stories.tsx b/apps/storybook/src/stories/plugins/cssVariables/cssVariables.stories.tsx index c503f38a..e917ae0b 100644 --- a/apps/storybook/src/stories/plugins/cssVariables/cssVariables.stories.tsx +++ b/apps/storybook/src/stories/plugins/cssVariables/cssVariables.stories.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { CSSVariablesLayer, Graph, GraphState } from "@gravity-ui/graph"; import type { TBlock } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent, useLayer } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent, useLayer } from "@gravity-ui/graph-react"; import type { Meta, StoryObj } from "@storybook/react-webpack5"; import { generatePrettyBlocks } from "../../../stories/configurations/generatePretty"; diff --git a/apps/storybook/src/stories/plugins/devtools/DevTools.stories.tsx b/apps/storybook/src/stories/plugins/devtools/DevTools.stories.tsx index cf1a656d..20064b78 100644 --- a/apps/storybook/src/stories/plugins/devtools/DevTools.stories.tsx +++ b/apps/storybook/src/stories/plugins/devtools/DevTools.stories.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect } from "react"; import { DEFAULT_DEVTOOLS_LAYER_PROPS, DevToolsLayer, Graph, TBlock, TDevToolsLayerProps } from "@gravity-ui/graph"; -import { GraphBlock, GraphCanvas, useGraph, useLayer } from "@gravity-ui/graph/react"; +import { GraphBlock, GraphCanvas, useGraph, useLayer } from "@gravity-ui/graph-react"; import type { Meta, StoryObj } from "@storybook/react-webpack5"; import { generatePrettyBlocks } from "../../configurations/generatePretty"; @@ -95,7 +95,7 @@ const meta: Meta = { code: ` import React from 'react'; import { DevToolsLayer } from '@gravity-ui/graph'; -import { GraphCanvas, useGraph, useLayer } from '@gravity-ui/graph/react'; +import { GraphCanvas, useGraph, useLayer } from '@gravity-ui/graph-react'; function MyGraphWithDevTools() { const { graph } = useGraph({ diff --git a/apps/storybook/src/stories/plugins/elk/elk.stories.tsx b/apps/storybook/src/stories/plugins/elk/elk.stories.tsx index 54514eb4..880ac53f 100644 --- a/apps/storybook/src/stories/plugins/elk/elk.stories.tsx +++ b/apps/storybook/src/stories/plugins/elk/elk.stories.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from "react"; import type { TMultipointConnection } from "@gravity-ui/graph"; import { Graph, GraphState, MultipointConnection, TBlock, TConnection, TGraphConfig } from "@gravity-ui/graph"; -import { GraphCanvas, useElk, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphCanvas, useElk, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import { ThemeProvider } from "@gravity-ui/uikit"; import { Description, Meta as StorybookMeta, Title } from "@storybook/addon-docs/blocks"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; @@ -130,7 +130,7 @@ const meta: Meta = { "## Example\n\n" + "```tsx\n" + "import React from 'react';\n" + - "import { GraphCanvas, useGraph, useElk } from '@gravity-ui/graph/react';\n" + + "import { GraphCanvas, useGraph, useElk } from '@gravity-ui/graph-react';\n" + "import ELK from 'elkjs';\n\n" + "const elkConfig = {\n" + ' id: "root",\n' + diff --git a/apps/storybook/src/stories/plugins/layered/layered.stories.tsx b/apps/storybook/src/stories/plugins/layered/layered.stories.tsx index 34bf020c..d7c2bfd6 100644 --- a/apps/storybook/src/stories/plugins/layered/layered.stories.tsx +++ b/apps/storybook/src/stories/plugins/layered/layered.stories.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo } from "react"; import type { LayeredLayoutOptions, TMultipointConnection } from "@gravity-ui/graph"; import { BezierMultipointConnection, Graph, GraphState, TBlock, TConnection } from "@gravity-ui/graph"; -import { GraphCanvas, useGraph, useGraphEvent, useLayeredLayout } from "@gravity-ui/graph/react"; +import { GraphCanvas, useGraph, useGraphEvent, useLayeredLayout } from "@gravity-ui/graph-react"; import { ThemeProvider } from "@gravity-ui/uikit"; import { Description, Meta as StorybookMeta, Title } from "@storybook/addon-docs/blocks"; import type { Meta, StoryFn } from "@storybook/react-webpack5"; @@ -137,7 +137,7 @@ const meta: Meta = { "- `layerSpacingFactor`: Multiplier for spacing between layers (default: 1.7)\n\n" + "## Example\n\n" + "```tsx\n" + - "import { useGraph, useLayeredLayout } from '@gravity-ui/graph/react';\n\n" + + "import { useGraph, useLayeredLayout } from '@gravity-ui/graph-react';\n\n" + "const { graph, setEntities, start } = useGraph();\n" + "const { isLoading, result } = useLayeredLayout({\n" + " blocks: [{ id: '1', width: 100, height: 50 }, { id: '2', width: 100, height: 50 }],\n" + diff --git a/package.json b/package.json index 6cad04f9..a0e308fa 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,16 @@ "url": "https://github.com/gravity-ui/graph" }, "scripts": { - "typecheck": "pnpm --filter @gravity-ui/graph-scheduler run typecheck && pnpm --filter @gravity-ui/graph run typecheck && pnpm --filter @gravity-ui/graph-storybook run typecheck && pnpm --filter @gravity-ui/graph-e2e run typecheck", + "typecheck": "pnpm --filter @gravity-ui/graph-scheduler run typecheck && pnpm --filter @gravity-ui/graph run typecheck && pnpm --filter @gravity-ui/graph-react run typecheck && pnpm --filter @gravity-ui/graph-storybook run typecheck && pnpm --filter @gravity-ui/graph-e2e run typecheck", "typecheck:published-playwright-types": "pnpm --filter @gravity-ui/graph run typecheck:published-playwright-types", "lint": "pnpm --filter \"./packages/*\" run lint && pnpm --filter @gravity-ui/graph-storybook run lint", "test:unit": "pnpm --filter \"./packages/*\" run test:unit", "test": "pnpm --filter \"./packages/*\" run test", - "storybook": "pnpm --filter @gravity-ui/graph run build && pnpm --filter @gravity-ui/graph-storybook run storybook", - "build-storybook": "pnpm --filter @gravity-ui/graph run build && pnpm --filter @gravity-ui/graph-storybook run build-storybook", + "storybook": "pnpm run build && pnpm --filter @gravity-ui/graph-storybook run storybook", + "build-storybook": "pnpm run build && pnpm --filter @gravity-ui/graph-storybook run build-storybook", "build:docs": "pnpm --filter @gravity-ui/graph run build:docs", - "build": "pnpm --filter @gravity-ui/graph run build", - "dev": "pnpm --filter @gravity-ui/graph run dev", + "build": "pnpm --filter @gravity-ui/graph --filter @gravity-ui/graph-react run build", + "dev": "chokidar \"packages/*/src/**/*\" \"scripts/build-package.mjs\" -c \"pnpm run build\" --initial", "e2e:bundle": "pnpm --filter @gravity-ui/graph-e2e run e2e:bundle", "e2e:serve": "pnpm --filter @gravity-ui/graph-e2e run e2e:serve", "e2e:build": "pnpm --filter @gravity-ui/graph-e2e run e2e:build", @@ -26,15 +26,23 @@ "test:e2e:ui": "pnpm --filter @gravity-ui/graph-e2e run test:e2e:ui", "test:e2e:debug": "pnpm --filter @gravity-ui/graph-e2e run test:e2e:debug", "test:e2e:ci": "pnpm --filter @gravity-ui/graph-e2e run test:e2e:ci", - "test:package-contract": "pnpm --filter @gravity-ui/graph run test:package-contract", + "test:package-contract": "node tests/package-contract/run.mjs", "e2e:dev": "pnpm --filter @gravity-ui/graph-e2e run e2e:dev" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.5", "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", + "@gravity-ui/graph": "workspace:*", "@gravity-ui/prettier-config": "^1.1.0", + "@playwright/test": "^1.58.0", + "@types/node": "^20.17.0", + "chokidar-cli": "^3.0.0", + "esbuild": "^0.27.2", "prettier": "^3.0.0", + "publint": "^0.3.23", "release-please": "17.6.0", + "typescript": "^5.9.2", "yaml": "2.8.1" } } diff --git a/packages/graph-react/.eslintrc b/packages/graph-react/.eslintrc new file mode 100644 index 00000000..896ec10e --- /dev/null +++ b/packages/graph-react/.eslintrc @@ -0,0 +1,84 @@ +{ + "extends": [ + "@gravity-ui/eslint-config", + "@gravity-ui/eslint-config/import-order", + "@gravity-ui/eslint-config/prettier", + "prettier" + ], + "parserOptions": { + "project": [ + "./tsconfig.json" + ] + }, + "root": true, + "env": { + "node": true, + "jest": true + }, + "overrides": [ + { + // enable the rule specifically for TypeScript files + "files": [ + "*.ts", + "*.mts", + "*.cts", + "*.tsx" + ], + "rules": { + "@typescript-eslint/explicit-member-accessibility": [ + "error", + { + "accessibility": "explicit", + "overrides": { + "accessors": "explicit", + "constructors": "no-public", + "methods": "explicit", + "properties": "off", + "parameterProperties": "explicit" + } + } + ], + "no-bitwise": [ + "error", + { + "int32Hint": true + } + ] + } + }, + { + "files": [ + "**/*.test.ts", + "**/*.test.tsx" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } + } + ], + "rules": { + "no-negated-condition": "off", + "@typescript-eslint/parameter-properties": "off", + "no-param-reassign": "off", + "guard-for-in": "off", + "no-return-assign": "off", + "@typescript-eslint/member-ordering": "off", + "@typescript-eslint/no-shadow": "off", + "valid-jsdoc": "off", + "import/consistent-type-specifier-style": [ + "error", + "prefer-top-level" + ], + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "lodash", + "message": "Import from lodash leads to bundle pollute. Use import from submodules, for example 'lodash/isEqual'." + } + ] + } + ] + } +} diff --git a/packages/graph-react/CHANGELOG.md b/packages/graph-react/CHANGELOG.md new file mode 100644 index 00000000..825c32f0 --- /dev/null +++ b/packages/graph-react/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/packages/graph-react/LICENSE b/packages/graph-react/LICENSE new file mode 100644 index 00000000..c86090b9 --- /dev/null +++ b/packages/graph-react/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 YANDEX LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/graph-react/README.md b/packages/graph-react/README.md new file mode 100644 index 00000000..71133b92 --- /dev/null +++ b/packages/graph-react/README.md @@ -0,0 +1,53 @@ +# @gravity-ui/graph-react + +React components and hooks for [Gravity UI Graph](https://github.com/gravity-ui/graph). +This package is part of the v2 monorepo and has not been published yet. + +## Installation + +Use matching builds of `@gravity-ui/graph` and `@gravity-ui/graph-react`, with React 18 and React DOM 18. +The React package declares the core library and React as peer dependencies so the application owns their instances. + +## Usage + +Import graph data types, canvas layers and plugins from `@gravity-ui/graph`; import React components and hooks from +`@gravity-ui/graph-react`. Load both stylesheets once in the application entrypoint. + +```tsx +import React, { useLayoutEffect } from "react"; +import type { TBlock } from "@gravity-ui/graph"; +import { GraphBlock, GraphCanvas, useGraph } from "@gravity-ui/graph-react"; +import "@gravity-ui/graph/styles.css"; +import "@gravity-ui/graph-react/styles.css"; + +const blocks: TBlock[] = [ + { id: "example", is: "Block", name: "Example", x: 0, y: 0, width: 200, height: 100, anchors: [] }, +]; + +export function Editor() { + const { graph, setEntities, start } = useGraph({ settings: {} }); + useLayoutEffect(() => { + setEntities({ blocks, connections: [] }); + start(); + }, [setEntities, start]); + + return ( + ( + {block.name} + )} + /> + ); +} +``` + +Give the editor container a nonzero width and height. + +The public entrypoint includes `GraphCanvas`, `GraphBlock`, `GraphBlockAnchor`, `GraphLayer`, `GraphPortal`, context +helpers, graph/signal/scheduler hooks, `useElk`, and `useLayeredLayout`. The old `@gravity-ui/graph/react` subpath is removed +in v2; replace its imports with `@gravity-ui/graph-react` and add the React stylesheet. Core and Playwright consumers +continue to use `@gravity-ui/graph` and `@gravity-ui/graph/playwright` without installing React. + +See the [React guide](https://github.com/gravity-ui/graph/blob/v2/packages/graph/docs/react/usage.md) and +[hooks reference](https://github.com/gravity-ui/graph/blob/v2/packages/graph/docs/react/hooks.md). diff --git a/packages/graph-react/__mocks__/styleMock.cjs b/packages/graph-react/__mocks__/styleMock.cjs new file mode 100644 index 00000000..f053ebf7 --- /dev/null +++ b/packages/graph-react/__mocks__/styleMock.cjs @@ -0,0 +1 @@ +module.exports = {}; diff --git a/packages/graph-react/jest.config.cjs b/packages/graph-react/jest.config.cjs new file mode 100644 index 00000000..4f682ea0 --- /dev/null +++ b/packages/graph-react/jest.config.cjs @@ -0,0 +1,12 @@ +module.exports = { + testPathIgnorePatterns: ["/node_modules/", "/build/"], + testEnvironment: "jsdom", + setupFiles: ["/setupJest.cjs", "jest-canvas-mock"], + transformIgnorePatterns: [], + moduleNameMapper: { + // Unit tests consume only the core public entrypoint; installed artifacts are tested separately. + "^@gravity-ui/graph$": "/../graph/src/index.ts", + "\\.(css|less)$": "/__mocks__/styleMock.cjs", + }, + transform: { "^.+\\.(t|j)sx?$": "@swc/jest" }, +}; diff --git a/packages/graph-react/package.json b/packages/graph-react/package.json new file mode 100644 index 00000000..0d85134c --- /dev/null +++ b/packages/graph-react/package.json @@ -0,0 +1,71 @@ +{ + "name": "@gravity-ui/graph-react", + "version": "0.0.0", + "type": "module", + "packageManager": "pnpm@10.34.5", + "description": "React components and hooks for Gravity UI Graph", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/gravity-ui/graph", + "directory": "packages/graph-react" + }, + "main": "build/index.js", + "module": "build/index.js", + "types": "build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "import": "./build/index.js", + "default": "./build/index.js" + }, + "./styles.css": "./build/styles.css" + }, + "files": [ + "build" + ], + "scripts": { + "build": "node scripts/build.mjs", + "typecheck": "tsc -p tsconfig.json --noEmit && pnpm run build", + "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"", + "test:unit": "jest --passWithNoTests", + "test": "pnpm run test:unit", + "test:package-contract": "node ../../tests/package-contract/run.mjs graph-react", + "prepublishOnly": "pnpm run typecheck && pnpm run test", + "dev": "chokidar \"src/**/*\" \"../graph/build/**/*\" -c \"pnpm run build\" --initial" + }, + "peerDependencies": { + "@gravity-ui/graph": "workspace:^", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "dependencies": { + "@preact/signals-core": "^1.12.2", + "elkjs": "^0.9.3", + "lodash": "^4.17.21" + }, + "devDependencies": { + "@gravity-ui/eslint-config": "^3.2.0", + "@swc/core": "^1.13.3", + "@swc/jest": "^0.2.39", + "@testing-library/react": "^16.3.0", + "@types/jest": "^30.0.0", + "@types/lodash": "^4.17.13", + "@types/react": "^18.3.31", + "@types/react-dom": "^18.3.7", + "@typescript-eslint/eslint-plugin": "5.39.0", + "@typescript-eslint/parser": "5.39.0", + "chokidar-cli": "^3.0.0", + "eslint": "^8.0.0", + "eslint-config-prettier": "^8.10.0", + "eslint-import-resolver-typescript": "2.5.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^30.0.5", + "jest-canvas-mock": "^2.5.2", + "jest-environment-jsdom": "^30.0.5", + "prettier": "^3.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.9.2" + } +} diff --git a/packages/graph-react/scripts/build.mjs b/packages/graph-react/scripts/build.mjs new file mode 100644 index 00000000..d5cb8d9c --- /dev/null +++ b/packages/graph-react/scripts/build.mjs @@ -0,0 +1,4 @@ +import { fileURLToPath } from "node:url"; +import { buildPackage } from "../../../scripts/build-package.mjs"; + +await buildPackage({ packageRoot: fileURLToPath(new URL("../", import.meta.url)) }); diff --git a/packages/graph-react/setupJest.cjs b/packages/graph-react/setupJest.cjs new file mode 100644 index 00000000..da66fa4f --- /dev/null +++ b/packages/graph-react/setupJest.cjs @@ -0,0 +1,5 @@ +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); diff --git a/packages/graph/src/react-components/Anchor.css b/packages/graph-react/src/Anchor.css similarity index 100% rename from packages/graph/src/react-components/Anchor.css rename to packages/graph-react/src/Anchor.css diff --git a/packages/graph/src/react-components/Anchor.tsx b/packages/graph-react/src/Anchor.tsx similarity index 93% rename from packages/graph/src/react-components/Anchor.tsx rename to packages/graph-react/src/Anchor.tsx index dcd4f336..40786e69 100644 --- a/packages/graph/src/react-components/Anchor.tsx +++ b/packages/graph-react/src/Anchor.tsx @@ -1,8 +1,6 @@ import React, { useEffect, useMemo } from "react"; -import { TAnchor } from "../components/canvas/anchors"; -import { Graph } from "../graph"; -import { AnchorState } from "../store/anchor/Anchor"; +import { AnchorState, Graph, TAnchor } from "@gravity-ui/graph"; import { useSignal } from "./hooks"; import { useBlockAnchorPosition, useBlockAnchorState } from "./hooks/useBlockAnchorState"; diff --git a/packages/graph/src/react-components/Block.css b/packages/graph-react/src/Block.css similarity index 100% rename from packages/graph/src/react-components/Block.css rename to packages/graph-react/src/Block.css diff --git a/packages/graph/src/react-components/Block.tsx b/packages/graph-react/src/Block.tsx similarity index 97% rename from packages/graph/src/react-components/Block.tsx rename to packages/graph-react/src/Block.tsx index 64e6673b..487e6f55 100644 --- a/packages/graph/src/react-components/Block.tsx +++ b/packages/graph-react/src/Block.tsx @@ -9,9 +9,7 @@ import React, { useState, } from "react"; -import { TBlock } from "../components/canvas/blocks/Block"; -import { Graph } from "../graph"; -import { ESchedulerPriority } from "../lib/Scheduler"; +import { ESchedulerPriority, Graph, TBlock } from "@gravity-ui/graph"; import { useComputedSignal, useSchedulerDebounce, useSignalEffect } from "./hooks"; import { useBlockState } from "./hooks/useBlockState"; diff --git a/packages/graph/src/react-components/BlocksList.tsx b/packages/graph-react/src/BlocksList.tsx similarity index 91% rename from packages/graph/src/react-components/BlocksList.tsx rename to packages/graph-react/src/BlocksList.tsx index abb41ff0..1e3ad8ca 100644 --- a/packages/graph/src/react-components/BlocksList.tsx +++ b/packages/graph-react/src/BlocksList.tsx @@ -1,9 +1,6 @@ import React, { memo, useEffect, useState } from "react"; -import { Block as CanvasBlock, TBlock } from "../components/canvas/blocks/Block"; -import { Graph, GraphState } from "../graph"; -import { ECameraScaleLevel } from "../services/camera/CameraService"; -import { BlockState } from "../store/block/Block"; +import { BlockState, CanvasBlock, ECameraScaleLevel, Graph, GraphState, TBlock } from "@gravity-ui/graph"; import { useSignal } from "./hooks"; import { useSceneChange } from "./hooks/useSceneChange"; diff --git a/packages/graph/src/react-components/GraphCanvas.tsx b/packages/graph-react/src/GraphCanvas.tsx similarity index 94% rename from packages/graph/src/react-components/GraphCanvas.tsx rename to packages/graph-react/src/GraphCanvas.tsx index 62ebe7a6..87dd5771 100644 --- a/packages/graph/src/react-components/GraphCanvas.tsx +++ b/packages/graph-react/src/GraphCanvas.tsx @@ -1,8 +1,6 @@ import React, { useEffect, useLayoutEffect, useRef } from "react"; -import { TGraphColors } from ".."; -import { Graph } from "../graph"; -import { setCssProps } from "../utils/functions/cssProp"; +import { Graph, TGraphColors } from "@gravity-ui/graph"; import { TBlockListProps } from "./BlocksList"; import { GraphContextProvider } from "./GraphContext"; @@ -11,6 +9,7 @@ import { useLayer } from "./hooks"; import { useGraphEvent, useGraphEvents } from "./hooks/useGraphEvents"; import { ReactLayer } from "./layer"; import { cn } from "./utils/cn"; +import { setCssProps } from "./utils/cssProps"; import { useFn } from "./utils/hooks/useFn"; import "./graph-canvas.css"; diff --git a/packages/graph/src/react-components/GraphContext.tsx b/packages/graph-react/src/GraphContext.tsx similarity index 95% rename from packages/graph/src/react-components/GraphContext.tsx rename to packages/graph-react/src/GraphContext.tsx index f0f9079d..f046e35e 100644 --- a/packages/graph/src/react-components/GraphContext.tsx +++ b/packages/graph-react/src/GraphContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext } from "react"; -import type { Graph } from "../graph"; +import type { Graph } from "@gravity-ui/graph"; export interface GraphContextType { graph: Graph; diff --git a/packages/graph/src/react-components/GraphLayer.test.tsx b/packages/graph-react/src/GraphLayer.test.tsx similarity index 96% rename from packages/graph/src/react-components/GraphLayer.test.tsx rename to packages/graph-react/src/GraphLayer.test.tsx index a111b761..408428ac 100644 --- a/packages/graph/src/react-components/GraphLayer.test.tsx +++ b/packages/graph-react/src/GraphLayer.test.tsx @@ -1,10 +1,8 @@ import React, { createRef } from "react"; +import { Graph, Layer } from "@gravity-ui/graph"; import { act, render, waitFor } from "@testing-library/react"; -import { Graph } from "../graph"; -import { Layer } from "../services/Layer"; - import { GraphCanvas } from "./GraphCanvas"; import { GraphLayer } from "./GraphLayer"; diff --git a/packages/graph/src/react-components/GraphLayer.tsx b/packages/graph-react/src/GraphLayer.tsx similarity index 94% rename from packages/graph/src/react-components/GraphLayer.tsx rename to packages/graph-react/src/GraphLayer.tsx index 0eff3798..2c657bb7 100644 --- a/packages/graph/src/react-components/GraphLayer.tsx +++ b/packages/graph-react/src/GraphLayer.tsx @@ -1,7 +1,7 @@ import React, { forwardRef, useImperativeHandle, useState } from "react"; -import { GraphState } from "../graph"; -import type { Layer, LayerPublicProps } from "../services/Layer"; +import { GraphState } from "@gravity-ui/graph"; +import type { Layer, LayerPublicProps } from "@gravity-ui/graph"; import { useGraphContext } from "./GraphContext"; import { useGraphEvent } from "./hooks/useGraphEvents"; diff --git a/packages/graph/src/react-components/GraphPortal.test.tsx b/packages/graph-react/src/GraphPortal.test.tsx similarity index 98% rename from packages/graph/src/react-components/GraphPortal.test.tsx rename to packages/graph-react/src/GraphPortal.test.tsx index 17cf49c8..0c88d712 100644 --- a/packages/graph/src/react-components/GraphPortal.test.tsx +++ b/packages/graph-react/src/GraphPortal.test.tsx @@ -1,9 +1,8 @@ import React, { createRef } from "react"; +import { Graph } from "@gravity-ui/graph"; import { act, render, waitFor } from "@testing-library/react"; -import { Graph } from "../graph"; - import { GraphCanvas } from "./GraphCanvas"; import { GraphPortal } from "./GraphPortal"; diff --git a/packages/graph/src/react-components/GraphPortal.tsx b/packages/graph-react/src/GraphPortal.tsx similarity index 96% rename from packages/graph/src/react-components/GraphPortal.tsx rename to packages/graph-react/src/GraphPortal.tsx index 2ccf5c18..37b8bef2 100644 --- a/packages/graph/src/react-components/GraphPortal.tsx +++ b/packages/graph-react/src/GraphPortal.tsx @@ -1,11 +1,8 @@ import React, { forwardRef, useImperativeHandle, useState } from "react"; +import { Graph, GraphState, Layer, LayerContext, LayerProps, TComponentState } from "@gravity-ui/graph"; import { createPortal } from "react-dom"; -import { Graph, GraphState } from "../graph"; -import { TComponentState } from "../lib/Component"; -import { Layer, LayerContext, LayerProps } from "../services/Layer"; - import { useGraphContext } from "./GraphContext"; import { useGraphEvent } from "./hooks/useGraphEvents"; import { useLayer } from "./hooks/useLayer"; diff --git a/packages/graph/src/react-components/elk/converters/eklConverter.ts b/packages/graph-react/src/elk/converters/eklConverter.ts similarity index 100% rename from packages/graph/src/react-components/elk/converters/eklConverter.ts rename to packages/graph-react/src/elk/converters/eklConverter.ts diff --git a/packages/graph/src/react-components/elk/hooks/useElk.ts b/packages/graph-react/src/elk/hooks/useElk.ts similarity index 100% rename from packages/graph/src/react-components/elk/hooks/useElk.ts rename to packages/graph-react/src/elk/hooks/useElk.ts diff --git a/packages/graph/src/react-components/elk/index.ts b/packages/graph-react/src/elk/index.ts similarity index 80% rename from packages/graph/src/react-components/elk/index.ts rename to packages/graph-react/src/elk/index.ts index 86ae692b..6ae8ea1f 100644 --- a/packages/graph/src/react-components/elk/index.ts +++ b/packages/graph-react/src/elk/index.ts @@ -1,4 +1,4 @@ -import { MultipointConnection as CanvasMultipointConnection } from "../../components/canvas/connections/MultipointConnection"; +import { MultipointConnection as CanvasMultipointConnection } from "@gravity-ui/graph"; export { useElk } from "./hooks/useElk"; diff --git a/packages/graph-react/src/elk/types/index.ts b/packages/graph-react/src/elk/types/index.ts new file mode 100644 index 00000000..cbf16314 --- /dev/null +++ b/packages/graph-react/src/elk/types/index.ts @@ -0,0 +1,6 @@ +import type { TConnectionId, TMultipointConnection, TPoint } from "@gravity-ui/graph"; + +export type ConverterResult = { + edges: Record>; + blocks: Record; +}; diff --git a/packages/graph/src/react-components/events.ts b/packages/graph-react/src/events.ts similarity index 98% rename from packages/graph/src/react-components/events.ts rename to packages/graph-react/src/events.ts index d105a782..ffd43b59 100644 --- a/packages/graph/src/react-components/events.ts +++ b/packages/graph-react/src/events.ts @@ -1,4 +1,4 @@ -import { GraphEventsDefinitions, UnwrapGraphEvents, UnwrapGraphEventsDetail } from "../graphEvents"; +import { GraphEventsDefinitions, UnwrapGraphEvents, UnwrapGraphEventsDetail } from "@gravity-ui/graph"; export type TGraphEventCallbacks = { click: (data: UnwrapGraphEventsDetail<"click">, event: UnwrapGraphEvents<"click">) => void; diff --git a/packages/graph-react/src/graph-canvas.css b/packages/graph-react/src/graph-canvas.css new file mode 100644 index 00000000..0db712f5 --- /dev/null +++ b/packages/graph-react/src/graph-canvas.css @@ -0,0 +1,15 @@ +.graph-wrapper { + --graph-block-anchor-width: 16px; + --graph-block-anchor-height: 16px; + position: relative; + width: 100%; + height: 100%; + flex: 1; +} + +.graph-canvas { + position: absolute; + overflow: hidden; + width: 100%; + height: 100%; +} diff --git a/packages/graph/src/react-components/hooks/index.ts b/packages/graph-react/src/hooks/index.ts similarity index 100% rename from packages/graph/src/react-components/hooks/index.ts rename to packages/graph-react/src/hooks/index.ts diff --git a/packages/graph/src/react-components/hooks/schedulerHooks.test.ts b/packages/graph-react/src/hooks/schedulerHooks.test.ts similarity index 99% rename from packages/graph/src/react-components/hooks/schedulerHooks.test.ts rename to packages/graph-react/src/hooks/schedulerHooks.test.ts index 73acc1f4..5b86bae2 100644 --- a/packages/graph/src/react-components/hooks/schedulerHooks.test.ts +++ b/packages/graph-react/src/hooks/schedulerHooks.test.ts @@ -1,9 +1,10 @@ +import { ESchedulerPriority, Graph } from "@gravity-ui/graph"; import { act, renderHook } from "@testing-library/react"; -import { ESchedulerPriority, scheduler } from "../../lib"; - import { useSchedulerDebounce, useSchedulerThrottle } from "./schedulerHooks"; +const scheduler = new Graph({}).scheduler; + describe("useSchedulerDebounce hook", () => { beforeEach(() => { // Use modern fake timers - automatically mocks performance.now() and synchronizes it diff --git a/packages/graph/src/react-components/hooks/schedulerHooks.ts b/packages/graph-react/src/hooks/schedulerHooks.ts similarity index 97% rename from packages/graph/src/react-components/hooks/schedulerHooks.ts rename to packages/graph-react/src/hooks/schedulerHooks.ts index 6fba177d..56dac045 100644 --- a/packages/graph/src/react-components/hooks/schedulerHooks.ts +++ b/packages/graph-react/src/hooks/schedulerHooks.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo } from "react"; -import { debounce } from "../../utils/functions"; -import { TDebounceOptions, TScheduleOptions, schedule, throttle } from "../../utils/utils/schedule"; +import { TDebounceOptions, TScheduleOptions, debounce, schedule, throttle } from "@gravity-ui/graph"; + import { useFn } from "../utils/hooks/useFn"; /** diff --git a/packages/graph/src/react-components/hooks/useBlockAnchorState.ts b/packages/graph-react/src/hooks/useBlockAnchorState.ts similarity index 91% rename from packages/graph/src/react-components/hooks/useBlockAnchorState.ts rename to packages/graph-react/src/hooks/useBlockAnchorState.ts index 37f87db5..23c8b43b 100644 --- a/packages/graph/src/react-components/hooks/useBlockAnchorState.ts +++ b/packages/graph-react/src/hooks/useBlockAnchorState.ts @@ -1,6 +1,4 @@ -import { TAnchor } from "../../components/canvas/anchors"; -import { Graph } from "../../graph"; -import { AnchorState } from "../../store/anchor/Anchor"; +import { AnchorState, Graph, TAnchor } from "@gravity-ui/graph"; import { useBlockState } from "./useBlockState"; import { useComputedSignal, useSignalEffect } from "./useSignal"; diff --git a/packages/graph/src/react-components/hooks/useBlockState.ts b/packages/graph-react/src/hooks/useBlockState.ts similarity index 85% rename from packages/graph/src/react-components/hooks/useBlockState.ts rename to packages/graph-react/src/hooks/useBlockState.ts index 5661ea1b..9bdc8dd9 100644 --- a/packages/graph/src/react-components/hooks/useBlockState.ts +++ b/packages/graph-react/src/hooks/useBlockState.ts @@ -1,5 +1,4 @@ -import { TBlock, isTBlock } from "../../components/canvas/blocks/Block"; -import { Graph } from "../../graph"; +import { Graph, TBlock, isTBlock } from "@gravity-ui/graph"; import { useComputedSignal } from "./useSignal"; diff --git a/packages/graph/src/react-components/hooks/useGraph.ts b/packages/graph-react/src/hooks/useGraph.ts similarity index 82% rename from packages/graph/src/react-components/hooks/useGraph.ts rename to packages/graph-react/src/hooks/useGraph.ts index b914dbad..e70f0c26 100644 --- a/packages/graph/src/react-components/hooks/useGraph.ts +++ b/packages/graph-react/src/hooks/useGraph.ts @@ -1,13 +1,16 @@ import { useLayoutEffect, useMemo } from "react"; -import { ZoomConfig } from "../../api/PublicGraphApi"; -import type { TBlock } from "../../components/canvas/blocks/Block"; -import { Graph, GraphState, TGraphConfig } from "../../graph"; -import type { TGraphZoomTarget } from "../../graph"; -import type { TGraphColors, TGraphConstants } from "../../graphConfig"; -import type { Layer, LayerPublicProps } from "../../services/Layer"; -import type { TConnection } from "../../store/connection/ConnectionState"; -import { RecursivePartial } from "../../utils/types/helpers"; +import type { + Layer, + LayerPublicProps, + TBlock, + TConnection, + TGraphColors, + TGraphConstants, + TGraphZoomTarget, +} from "@gravity-ui/graph"; +import { Graph, GraphState, RecursivePartial, TGraphConfig, ZoomConfig } from "@gravity-ui/graph"; + import { useFn } from "../utils/hooks/useFn"; export type HookGraphParams = Pick & { diff --git a/packages/graph/src/react-components/hooks/useGraphEvents.ts b/packages/graph-react/src/hooks/useGraphEvents.ts similarity index 91% rename from packages/graph/src/react-components/hooks/useGraphEvents.ts rename to packages/graph-react/src/hooks/useGraphEvents.ts index 6151e27f..ab0cb447 100644 --- a/packages/graph/src/react-components/hooks/useGraphEvents.ts +++ b/packages/graph-react/src/hooks/useGraphEvents.ts @@ -1,9 +1,14 @@ import { useLayoutEffect, useMemo, useRef } from "react"; -import { Graph } from "../../graph"; -import { GraphEventsDefinitions, UnwrapGraphEvents, UnwrapGraphEventsDetail } from "../../graphEvents"; -import { ESchedulerPriority } from "../../lib"; -import { debounce } from "../../utils/utils/schedule"; +import { + ESchedulerPriority, + Graph, + GraphEventsDefinitions, + UnwrapGraphEvents, + UnwrapGraphEventsDetail, + debounce, +} from "@gravity-ui/graph"; + import { GraphCallbacksMap, TGraphEventCallbacks } from "../events"; import { useFn } from "../utils/hooks/useFn"; diff --git a/packages/graph/src/react-components/hooks/useLayer.test.ts b/packages/graph-react/src/hooks/useLayer.test.ts similarity index 98% rename from packages/graph/src/react-components/hooks/useLayer.test.ts rename to packages/graph-react/src/hooks/useLayer.test.ts index 6df8fa0f..24627a04 100644 --- a/packages/graph/src/react-components/hooks/useLayer.test.ts +++ b/packages/graph-react/src/hooks/useLayer.test.ts @@ -1,9 +1,7 @@ +import { Graph, Layer, LayerProps } from "@gravity-ui/graph"; import { act, renderHook } from "@testing-library/react"; import isEqual from "lodash/isEqual"; -import { Graph } from "../../graph"; -import { Layer, LayerProps } from "../../services/Layer"; - import { useLayer } from "./useLayer"; // Mock dependencies diff --git a/packages/graph/src/react-components/hooks/useLayer.ts b/packages/graph-react/src/hooks/useLayer.ts similarity index 94% rename from packages/graph/src/react-components/hooks/useLayer.ts rename to packages/graph-react/src/hooks/useLayer.ts index 66bf9bd3..b5671195 100644 --- a/packages/graph/src/react-components/hooks/useLayer.ts +++ b/packages/graph-react/src/hooks/useLayer.ts @@ -1,10 +1,8 @@ import { useDeferredValue, useLayoutEffect, useState } from "react"; +import type { Graph, Layer, LayerPublicProps } from "@gravity-ui/graph"; import isEqual from "lodash/isEqual"; -import type { Graph } from "../../graph"; -import type { Layer, LayerPublicProps } from "../../services/Layer"; - import { usePrevious } from "./usePrevious"; /** diff --git a/packages/graph/src/react-components/hooks/usePrevious.test.ts b/packages/graph-react/src/hooks/usePrevious.test.ts similarity index 100% rename from packages/graph/src/react-components/hooks/usePrevious.test.ts rename to packages/graph-react/src/hooks/usePrevious.test.ts diff --git a/packages/graph/src/react-components/hooks/usePrevious.ts b/packages/graph-react/src/hooks/usePrevious.ts similarity index 100% rename from packages/graph/src/react-components/hooks/usePrevious.ts rename to packages/graph-react/src/hooks/usePrevious.ts diff --git a/packages/graph/src/react-components/hooks/useSceneChange.ts b/packages/graph-react/src/hooks/useSceneChange.ts similarity index 92% rename from packages/graph/src/react-components/hooks/useSceneChange.ts rename to packages/graph-react/src/hooks/useSceneChange.ts index 2c3f92ae..9f805da4 100644 --- a/packages/graph/src/react-components/hooks/useSceneChange.ts +++ b/packages/graph-react/src/hooks/useSceneChange.ts @@ -1,7 +1,6 @@ import { useEffect, useLayoutEffect } from "react"; -import { Graph } from "../../graph"; -import { ESchedulerPriority } from "../../lib"; +import { ESchedulerPriority, Graph } from "@gravity-ui/graph"; import { useSchedulerDebounce } from "./schedulerHooks"; diff --git a/packages/graph/src/react-components/hooks/useSignal.test.ts b/packages/graph-react/src/hooks/useSignal.test.ts similarity index 100% rename from packages/graph/src/react-components/hooks/useSignal.test.ts rename to packages/graph-react/src/hooks/useSignal.test.ts diff --git a/packages/graph/src/react-components/hooks/useSignal.ts b/packages/graph-react/src/hooks/useSignal.ts similarity index 100% rename from packages/graph/src/react-components/hooks/useSignal.ts rename to packages/graph-react/src/hooks/useSignal.ts diff --git a/packages/graph/src/react-components/index.ts b/packages/graph-react/src/index.ts similarity index 60% rename from packages/graph/src/react-components/index.ts rename to packages/graph-react/src/index.ts index f487e981..8fc45959 100644 --- a/packages/graph/src/react-components/index.ts +++ b/packages/graph-react/src/index.ts @@ -7,7 +7,6 @@ export * from "./GraphContext"; export * from "./hooks"; export * from "./events"; export * from "./elk"; -export { useLayeredLayout } from "../plugins/layered/hooks/useLayeredLayout"; -export type { UseLayeredLayoutParams } from "../plugins/layered/hooks/useLayeredLayout"; +export { useLayeredLayout } from "./layered/hooks/useLayeredLayout"; +export type { UseLayeredLayoutParams } from "./layered/hooks/useLayeredLayout"; export { TRenderBlockFn } from "./BlocksList"; -export * from "./events"; diff --git a/packages/graph/src/react-components/layer/ReactLayer.test.ts b/packages/graph-react/src/layer/ReactLayer.test.ts similarity index 99% rename from packages/graph/src/react-components/layer/ReactLayer.test.ts rename to packages/graph-react/src/layer/ReactLayer.test.ts index 7ee66a12..72e57f50 100644 --- a/packages/graph/src/react-components/layer/ReactLayer.test.ts +++ b/packages/graph-react/src/layer/ReactLayer.test.ts @@ -1,11 +1,10 @@ -import { Graph } from "../../graph"; -import { CameraService } from "../../services/camera/CameraService"; +import { Graph } from "@gravity-ui/graph"; import { ReactLayer } from "./ReactLayer"; describe("ReactLayer", () => { let graph: Graph; - let camera: CameraService; + let camera: Graph["cameraService"]; let rootElement: HTMLDivElement; // Constants for default classes diff --git a/packages/graph/src/react-components/layer/ReactLayer.tsx b/packages/graph-react/src/layer/ReactLayer.tsx similarity index 90% rename from packages/graph/src/react-components/layer/ReactLayer.tsx rename to packages/graph-react/src/layer/ReactLayer.tsx index 797bb49c..c6edcb61 100644 --- a/packages/graph/src/react-components/layer/ReactLayer.tsx +++ b/packages/graph-react/src/layer/ReactLayer.tsx @@ -1,13 +1,10 @@ import React from "react"; +import { Graph, ICamera, Layer, LayerContext, LayerProps, TBlock } from "@gravity-ui/graph"; import { createPortal } from "react-dom"; -import { TBlock } from "../../components/canvas/blocks/Block"; -import { Graph } from "../../graph"; -import { Layer, LayerContext, LayerProps } from "../../services/Layer"; -import { ICamera } from "../../services/camera/CameraService"; -import { parseClassNames } from "../../utils/functions"; import { BlocksList } from "../BlocksList"; +import { parseClassNames } from "../utils/classNames"; export type TReactLayerProps = LayerProps & { camera: ICamera; diff --git a/packages/graph/src/react-components/layer/index.ts b/packages/graph-react/src/layer/index.ts similarity index 100% rename from packages/graph/src/react-components/layer/index.ts rename to packages/graph-react/src/layer/index.ts diff --git a/packages/graph/src/plugins/layered/hooks/useLayeredLayout.test.ts b/packages/graph-react/src/layered/hooks/useLayeredLayout.test.ts similarity index 95% rename from packages/graph/src/plugins/layered/hooks/useLayeredLayout.test.ts rename to packages/graph-react/src/layered/hooks/useLayeredLayout.test.ts index 4f835da9..02975388 100644 --- a/packages/graph/src/plugins/layered/hooks/useLayeredLayout.test.ts +++ b/packages/graph-react/src/layered/hooks/useLayeredLayout.test.ts @@ -1,17 +1,13 @@ +import { layeredConverter, layoutGraph } from "@gravity-ui/graph"; +import type { ConverterResult } from "@gravity-ui/graph"; import { act, renderHook, waitFor } from "@testing-library/react"; -import { layeredConverter } from "../converters/layeredConverter"; -import { layoutGraph } from "../layout"; -import type { ConverterResult } from "../types"; - import { useLayeredLayout } from "./useLayeredLayout"; import type { UseLayeredLayoutParams } from "./useLayeredLayout"; -jest.mock("../layout", () => ({ +jest.mock("@gravity-ui/graph", () => ({ + ...jest.requireActual("@gravity-ui/graph"), layoutGraph: jest.fn(), -})); - -jest.mock("../converters/layeredConverter", () => ({ layeredConverter: jest.fn(), })); diff --git a/packages/graph/src/plugins/layered/hooks/useLayeredLayout.ts b/packages/graph-react/src/layered/hooks/useLayeredLayout.ts similarity index 94% rename from packages/graph/src/plugins/layered/hooks/useLayeredLayout.ts rename to packages/graph-react/src/layered/hooks/useLayeredLayout.ts index 22d15849..f224ef0f 100644 --- a/packages/graph/src/plugins/layered/hooks/useLayeredLayout.ts +++ b/packages/graph-react/src/layered/hooks/useLayeredLayout.ts @@ -1,9 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { layeredConverter } from "../converters/layeredConverter"; -import { Node, layoutGraph } from "../layout"; -import type { ConverterResult, LayeredLayoutInput, LayeredLayoutOptions } from "../types"; -import { computeLevels } from "../utils/computeLevels"; +import type { ConverterResult, LayeredLayoutInput, LayeredLayoutOptions } from "@gravity-ui/graph"; +import { Node, computeLevels, layeredConverter, layoutGraph } from "@gravity-ui/graph"; export type UseLayeredLayoutParams = LayeredLayoutInput & { /** Layout algorithm options (gaps, default sizes, spacing factor, etc.) */ diff --git a/packages/graph-react/src/styles.css b/packages/graph-react/src/styles.css new file mode 100644 index 00000000..c5f05d83 --- /dev/null +++ b/packages/graph-react/src/styles.css @@ -0,0 +1,3 @@ +@import "./graph-canvas.css"; +@import "./Block.css"; +@import "./Anchor.css"; diff --git a/packages/graph/src/react-components/utils/applyBlockContainerLayout.ts b/packages/graph-react/src/utils/applyBlockContainerLayout.ts similarity index 100% rename from packages/graph/src/react-components/utils/applyBlockContainerLayout.ts rename to packages/graph-react/src/utils/applyBlockContainerLayout.ts diff --git a/packages/graph/src/utils/functions/classNames.test.ts b/packages/graph-react/src/utils/classNames.test.ts similarity index 100% rename from packages/graph/src/utils/functions/classNames.test.ts rename to packages/graph-react/src/utils/classNames.test.ts diff --git a/packages/graph/src/utils/functions/classNames.ts b/packages/graph-react/src/utils/classNames.ts similarity index 100% rename from packages/graph/src/utils/functions/classNames.ts rename to packages/graph-react/src/utils/classNames.ts diff --git a/packages/graph/src/react-components/utils/cn.test.ts b/packages/graph-react/src/utils/cn.test.ts similarity index 100% rename from packages/graph/src/react-components/utils/cn.test.ts rename to packages/graph-react/src/utils/cn.test.ts diff --git a/packages/graph/src/react-components/utils/cn.ts b/packages/graph-react/src/utils/cn.ts similarity index 100% rename from packages/graph/src/react-components/utils/cn.ts rename to packages/graph-react/src/utils/cn.ts diff --git a/packages/graph-react/src/utils/cssProps.ts b/packages/graph-react/src/utils/cssProps.ts new file mode 100644 index 00000000..f0d273c8 --- /dev/null +++ b/packages/graph-react/src/utils/cssProps.ts @@ -0,0 +1,6 @@ +export function setCssProps(target: HTMLElement | null | undefined, vars: Record<`--${string}`, string>) { + if (!target) return; + for (const [name, value] of Object.entries(vars)) { + target.style.setProperty(name, value); + } +} diff --git a/packages/graph/src/react-components/utils/hooks/useCompareState.ts b/packages/graph-react/src/utils/hooks/useCompareState.ts similarity index 100% rename from packages/graph/src/react-components/utils/hooks/useCompareState.ts rename to packages/graph-react/src/utils/hooks/useCompareState.ts diff --git a/packages/graph/src/react-components/utils/hooks/useFn.ts b/packages/graph-react/src/utils/hooks/useFn.ts similarity index 100% rename from packages/graph/src/react-components/utils/hooks/useFn.ts rename to packages/graph-react/src/utils/hooks/useFn.ts diff --git a/packages/graph-react/tsconfig.json b/packages/graph-react/tsconfig.json new file mode 100644 index 00000000..7214fc7a --- /dev/null +++ b/packages/graph-react/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "./build/", + "moduleResolution": "node", + "module": "esnext", + "target": "es2020", + "noEmitOnError": true, + "allowJs": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "typeRoots": ["./node_modules/@types"], + "skipLibCheck": true, + "jsx": "react" + }, + "include": ["./src/**/*.ts", "src/**/*.tsx"] +} diff --git a/packages/graph-react/tsconfig.publish.json b/packages/graph-react/tsconfig.publish.json new file mode 100644 index 00000000..5412aa9a --- /dev/null +++ b/packages/graph-react/tsconfig.publish.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "allowJs": true, + "rootDir": "./src", + "skipLibCheck": false + }, + "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.spec.ts", "src/**/*.spec.tsx"] +} diff --git a/packages/graph/README-ru.md b/packages/graph/README-ru.md index 3e3056e5..098023f0 100644 --- a/packages/graph/README-ru.md +++ b/packages/graph/README-ru.md @@ -59,6 +59,9 @@ const MyGraph = () => { npm install @gravity-ui/graph ``` +Для React-интеграции в v2 также нужен соответствующий пакет `@gravity-ui/graph-react`, React 18 и React DOM 18. + + ## Примеры ### Пример на React @@ -69,8 +72,9 @@ npm install @gravity-ui/graph import React, { useEffect } from "react"; import type { Graph, TBlock } from "@gravity-ui/graph"; import { EAnchorType, GraphState } from "@gravity-ui/graph"; -import { GraphCanvas, GraphBlock, useGraph } from "@gravity-ui/graph/react"; +import { GraphCanvas, GraphBlock, useGraph } from "@gravity-ui/graph-react"; import "@gravity-ui/graph/styles.css"; +import "@gravity-ui/graph-react/styles.css"; const config = {}; diff --git a/packages/graph/README.md b/packages/graph/README.md index eb3ef35d..8f78d980 100644 --- a/packages/graph/README.md +++ b/packages/graph/README.md @@ -59,6 +59,9 @@ const MyGraph = () => { npm install @gravity-ui/graph ``` +For React integration in v2, also install the matching `@gravity-ui/graph-react` package, React 18, and React DOM 18. + + ## Usage ### React Example @@ -69,8 +72,9 @@ npm install @gravity-ui/graph import React, { useEffect } from "react"; import type { Graph, TBlock } from "@gravity-ui/graph"; import { EAnchorType, GraphState } from "@gravity-ui/graph"; -import { GraphCanvas, GraphBlock, useGraph } from "@gravity-ui/graph/react"; +import { GraphCanvas, GraphBlock, useGraph } from "@gravity-ui/graph-react"; import "@gravity-ui/graph/styles.css"; +import "@gravity-ui/graph-react/styles.css"; const config = {}; @@ -280,7 +284,7 @@ A hybrid Canvas/React graph editor for node-based diagrams — reach for it to b - Node-based editors (flowcharts, pipelines, visual builders) with hundreds/thousands of nodes and connections. - Mixed rendering: Canvas for the full-graph overview, React components for the blocks visible in the viewport at high zoom. -- Vanilla JS or React consumers — the core `Graph` class is framework-agnostic; `@gravity-ui/graph/react` provides the React bindings. +- Vanilla JS or React consumers — the core `Graph` class is framework-agnostic; `@gravity-ui/graph-react` provides the React bindings. ### When not to use @@ -289,7 +293,7 @@ A hybrid Canvas/React graph editor for node-based diagrams — reach for it to b ### Common pitfalls -- **Hallucinated import `GraphEditor`** — the React components are `GraphCanvas`, `GraphBlock`, and the `useGraph` hook, imported from `@gravity-ui/graph/react`; the core class is `Graph` from `@gravity-ui/graph`. +- **Hallucinated import `GraphEditor`** — the React components are `GraphCanvas`, `GraphBlock`, and the `useGraph` hook, imported from `@gravity-ui/graph-react`; the core class is `Graph` from `@gravity-ui/graph`. - **Calling graph methods before `ATTACHED` state** — call `start()`/`zoomTo(...)` inside the `onStateChanged` callback when `state === GraphState.ATTACHED`, not on mount. - **Forgetting `setEntities`** — `useGraph` returns `graph`, `setEntities`, `start`; data only appears after `setEntities({blocks, connections})`. - **Mixing anchor types** — connections must reference existing anchor ids with matching `EAnchorType` (`IN`/`OUT`) on the source and target blocks. diff --git a/packages/graph/docs/react/declarative-components.md b/packages/graph/docs/react/declarative-components.md index c515db78..f57be014 100644 --- a/packages/graph/docs/react/declarative-components.md +++ b/packages/graph/docs/react/declarative-components.md @@ -13,8 +13,8 @@ The `GraphLayer` component provides a declarative way to add existing Layer clas ### Basic Usage ```tsx -import { GraphLayer, GraphCanvas, useGraph } from '@gravity-ui/graph/react'; -import { DevToolsLayer } from '@gravity-ui/graph/plugins'; +import { GraphLayer, GraphCanvas, useGraph } from '@gravity-ui/graph-react'; +import { DevToolsLayer } from '@gravity-ui/graph'; function MyGraph() { const { graph, setEntities, start } = useGraph({}); @@ -84,7 +84,7 @@ The `GraphPortal` component allows creating HTML layers without writing separate ### Basic Usage ```tsx -import { GraphPortal, GraphCanvas, useGraph } from '@gravity-ui/graph/react'; +import { GraphPortal, GraphCanvas, useGraph } from '@gravity-ui/graph-react'; function MyGraph() { const { graph, setEntities, start } = useGraph({}); @@ -281,4 +281,3 @@ const devTools = useLayer(graph, DevToolsLayer, { ``` The declarative approach provides better integration with React's component lifecycle and makes your code more readable and maintainable. - diff --git a/packages/graph/docs/react/hooks.md b/packages/graph/docs/react/hooks.md index 9476ae2f..9ea09c32 100644 --- a/packages/graph/docs/react/hooks.md +++ b/packages/graph/docs/react/hooks.md @@ -53,7 +53,7 @@ import { useSchedulerThrottle, useScheduledTask, useSceneChange, -} from "@gravity-ui/graph/react"; +} from "@gravity-ui/graph-react"; ``` ## Core Hooks @@ -63,7 +63,7 @@ import { The main hook for creating and managing a Graph instance. ```typescript -import { useGraph, type HookGraphParams } from "@gravity-ui/graph/react"; +import { useGraph, type HookGraphParams } from "@gravity-ui/graph-react"; import type { Graph, TBlock, TConnection } from "@gravity-ui/graph"; const config: HookGraphParams = { @@ -137,8 +137,8 @@ function MyGraph(): JSX.Element { Hook for managing graph layers. Automatically handles layer initialization, props updates, and cleanup. ```typescript -import { useLayer } from "@gravity-ui/graph/react"; -import { DevToolsLayer, type DevToolsLayerProps } from "@gravity-ui/graph/plugins"; +import { useLayer } from "@gravity-ui/graph-react"; +import { DevToolsLayer, type DevToolsLayerProps } from "@gravity-ui/graph"; import type { Graph } from "@gravity-ui/graph"; function MyGraph(): JSX.Element { @@ -182,7 +182,7 @@ Returns `InstanceType | null` - the layer instance or `null` if graph is not Hook for subscribing to a single graph event with optional debouncing. ```typescript -import { useGraphEvent } from "@gravity-ui/graph/react"; +import { useGraphEvent } from "@gravity-ui/graph-react"; import type { Graph, ESchedulerPriority } from "@gravity-ui/graph"; import type { UnwrapGraphEventsDetail, UnwrapGraphEvents } from "@gravity-ui/graph"; @@ -247,9 +247,9 @@ function MyComponent({ graph }: Props): JSX.Element | null { Hook for subscribing to multiple graph events at once using callback props style. ```typescript -import { useGraphEvents } from "@gravity-ui/graph/react"; +import { useGraphEvents } from "@gravity-ui/graph-react"; import type { Graph } from "@gravity-ui/graph"; -import type { TGraphEventCallbacks } from "@gravity-ui/graph/react"; +import type { TGraphEventCallbacks } from "@gravity-ui/graph-react"; interface Props { graph: Graph; @@ -289,7 +289,7 @@ function MyComponent({ graph }: Props): JSX.Element | null { Hook to get and subscribe to block state changes. ```typescript -import { useBlockState } from "@gravity-ui/graph/react"; +import { useBlockState } from "@gravity-ui/graph-react"; import type { Graph, TBlock, TBlockId } from "@gravity-ui/graph"; import type { BlockState } from "@gravity-ui/graph"; @@ -333,7 +333,7 @@ Returns `BlockState | undefined` - the block state object that updates reactivel Hook to get the view component of a block. Useful for accessing rendering-specific state. ```typescript -import { useBlockViewState } from "@gravity-ui/graph/react"; +import { useBlockViewState } from "@gravity-ui/graph-react"; import type { Graph, TBlockId } from "@gravity-ui/graph"; import type { Block } from "@gravity-ui/graph"; @@ -371,7 +371,7 @@ Returns the block's view component (`Block`) or `undefined`. Hook to get and subscribe to anchor state changes. ```typescript -import { useBlockAnchorState } from "@gravity-ui/graph/react"; +import { useBlockAnchorState } from "@gravity-ui/graph-react"; import type { Graph } from "@gravity-ui/graph"; import type { TAnchor, AnchorState } from "@gravity-ui/graph"; @@ -414,7 +414,7 @@ These hooks provide integration with @preact/signals-core for reactive state man Hook to subscribe to a signal and get the current value. Re-renders component when signal value changes. ```typescript -import { useSignal } from "@gravity-ui/graph/react"; +import { useSignal } from "@gravity-ui/graph-react"; import type { Signal } from "@preact/signals-core"; import type { BlockState, TBlockGeometry } from "@gravity-ui/graph"; @@ -451,7 +451,7 @@ Returns `T` - the current value of the signal. Hook to create and subscribe to a computed signal. Useful for derived state. ```typescript -import { useComputedSignal } from "@gravity-ui/graph/react"; +import { useComputedSignal } from "@gravity-ui/graph-react"; import type { DependencyList } from "react"; import type { BlockState } from "@gravity-ui/graph"; @@ -498,7 +498,7 @@ Returns `T` - the computed value, updated when dependent signals change. Hook to run side effects when signal values change. Similar to useEffect but for signals. ```typescript -import { useSignalEffect } from "@gravity-ui/graph/react"; +import { useSignalEffect } from "@gravity-ui/graph-react"; import type { DependencyList } from "react"; import type { BlockState } from "@gravity-ui/graph"; @@ -531,7 +531,7 @@ function BlockLogger({ blockState }: Props): null { Like `useSignalEffect`, but uses `useLayoutEffect` internally. Use when the side effect must run synchronously after DOM updates and before the browser paints — for example, layout-dependent overlays tied to the camera. ```typescript -import { useSignalLayoutEffect } from "@gravity-ui/graph/react"; +import { useSignalLayoutEffect } from "@gravity-ui/graph-react"; useSignalLayoutEffect(() => { const camera = graph.$camera.value; @@ -614,7 +614,7 @@ The function will only execute when BOTH conditions are satisfied: - At least `frameTimeout` milliseconds have passed since the last invocation ```typescript -import { useSchedulerDebounce, useGraphEvent } from "@gravity-ui/graph/react"; +import { useSchedulerDebounce, useGraphEvent } from "@gravity-ui/graph-react"; import type { ESchedulerPriority, Graph, TBlock } from "@gravity-ui/graph"; interface DebouncedFn void> { @@ -683,7 +683,7 @@ Hook to create a throttled function that limits execution frequency. Unlike debounce, throttle executes immediately on the first call and then enforces the delay for subsequent calls. ```typescript -import { useSchedulerThrottle, useSignalLayoutEffect } from "@gravity-ui/graph/react"; +import { useSchedulerThrottle, useSignalLayoutEffect } from "@gravity-ui/graph-react"; import type { ESchedulerPriority, Graph, TCameraState } from "@gravity-ui/graph"; interface ThrottledFn void> { @@ -751,7 +751,7 @@ Hook to schedule a task for execution after a certain number of frames have pass The scheduled task will execute once the specified frame interval has elapsed. The task is automatically cancelled when the component unmounts. ```typescript -import { useScheduledTask, useBlockState, useSignal } from "@gravity-ui/graph/react"; +import { useScheduledTask, useBlockState, useSignal } from "@gravity-ui/graph-react"; import type { ESchedulerPriority, Graph, TBlockId } from "@gravity-ui/graph"; // Example: Prepare derived state for the next render frame @@ -826,7 +826,7 @@ The hook automatically: - Cleans up subscriptions on unmount ```typescript -import { useSceneChange } from "@gravity-ui/graph/react"; +import { useSceneChange } from "@gravity-ui/graph-react"; import type { Graph, TRect } from "@gravity-ui/graph"; // Example: Update usable rect indicator when scene changes @@ -951,7 +951,7 @@ import { useBlockState, useSignal, GraphCanvas, -} from "@gravity-ui/graph/react"; +} from "@gravity-ui/graph-react"; import type { TBlockId, TBlockGeometry, BlockState } from "@gravity-ui/graph"; function MyGraph(): JSX.Element { @@ -991,7 +991,7 @@ function MyGraph(): JSX.Element { ### Performance Optimization with Debounced Events ```typescript -import { useSignal } from "@gravity-ui/graph/react"; +import { useSignal } from "@gravity-ui/graph-react"; import type { Graph } from "@gravity-ui/graph"; interface Props { diff --git a/packages/graph/docs/react/usage.md b/packages/graph/docs/react/usage.md index 6634c207..e7e6e528 100644 --- a/packages/graph/docs/react/usage.md +++ b/packages/graph/docs/react/usage.md @@ -1,5 +1,13 @@ # React Components API +In v2, React integration is provided by `@gravity-ui/graph-react`. Use the matching core package and load both stylesheets +in the application entrypoint: + +```ts +import "@gravity-ui/graph/styles.css"; +import "@gravity-ui/graph-react/styles.css"; +``` + ## Import Structure The library separates core functionality from React components: @@ -9,10 +17,11 @@ The library separates core functionality from React components: import { Graph } from "@gravity-ui/graph"; // React components (requires React) -import { GraphCanvas, GraphBlock, GraphBlockAnchor, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { GraphCanvas, GraphBlock, GraphBlockAnchor, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; // Public component styles import "@gravity-ui/graph/styles.css"; +import "@gravity-ui/graph-react/styles.css"; ``` ## Architecture @@ -31,7 +40,7 @@ This separation allows the core library to be framework-agnostic while providing The main container component that renders your graph: ```tsx -import { GraphCanvas } from "@gravity-ui/graph/react"; +import { GraphCanvas } from "@gravity-ui/graph-react"; { const camera = graph.$camera.value; diff --git a/packages/graph/docs/v1-v2-transition.md b/packages/graph/docs/v1-v2-transition.md index ee10dc53..2664eb77 100644 --- a/packages/graph/docs/v1-v2-transition.md +++ b/packages/graph/docs/v1-v2-transition.md @@ -188,3 +188,22 @@ mechanism. Do not run recovery with a different SHA, move `next` manually, publish another locally packed artifact, or leave a bootstrap `release-as` option in place after its release PR merges. No additional role system, SHA ledger, ruleset framework, or scheduled synchronization process is required. +## React package boundary + +React components and hooks now live in `packages/graph-react` and are exported by `@gravity-ui/graph-react`. +The old `@gravity-ui/graph/react` subpath is removed. Core does not depend on React, React DOM, their types, or ELK. +The React package has a workspace peer dependency on core, plus required React 18 and React DOM 18 peers. + +Replace React imports and load the two independently owned stylesheets: + +```ts +import { Graph } from "@gravity-ui/graph"; +import { GraphCanvas, useGraph, useLayeredLayout } from "@gravity-ui/graph-react"; +import "@gravity-ui/graph/styles.css"; +import "@gravity-ui/graph-react/styles.css"; +``` + +The layered layout algorithm and converters remain framework-independent core APIs; the React package owns the hook. +Storybook and E2E consume the same public package entrypoints. `pnpm run build` builds core before React, and the shared +`tests/package-contract` suite builds and installs one tarball per public package, verifies native imports and strict +declarations, and checks that the React adapter uses the application's core classes. diff --git a/packages/graph/package.json b/packages/graph/package.json index 6aaee45c..3989b46f 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -9,9 +9,6 @@ "types": "build/index.d.ts", "typesVersions": { "*": { - "react": [ - "build/react-components/index.d.ts" - ], "playwright": [ "build/playwright/index.d.ts" ] @@ -36,11 +33,6 @@ "import": "./build/index.js", "default": "./build/index.js" }, - "./react": { - "types": "./build/react-components/index.d.ts", - "import": "./build/react-components/index.js", - "default": "./build/react-components/index.js" - }, "./playwright": { "import": { "types": "./build/playwright/index.d.ts", @@ -74,34 +66,24 @@ "build": "node scripts/build.mjs", "prepublishOnly": "pnpm run typecheck && pnpm run test", "dev": "chokidar \"src/**/*\" \"../scheduler/src/**/*\" -c \"pnpm run build\" --initial", - "test:package-contract": "node tests/package-contract/run.mjs" + "test:package-contract": "node ../../tests/package-contract/run.mjs" }, "peerDependencies": { - "@playwright/test": ">=1.58.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" + "@playwright/test": ">=1.58.0" }, "peerDependenciesMeta": { "@playwright/test": { "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true } }, "dependencies": { "@preact/signals-core": "^1.12.2", - "elkjs": "^0.9.3", "intersects": "^2.7.2", "lodash": "^4.17.21", "rbush": "^3.0.1", "style-observer": "^0.1.1" }, "devDependencies": { - "@arethetypeswrong/cli": "^0.18.5", "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", "@gravity-ui/eslint-config": "^3.2.0", @@ -112,19 +94,15 @@ "@playwright/test": "^1.58.0", "@swc/core": "^1.13.3", "@swc/jest": "^0.2.39", - "@testing-library/react": "^16.3.0", "@types/intersects": "^2.5.0", "@types/jest": "^30.0.0", "@types/lodash": "^4.17.13", "@types/node": "^20.17.0", "@types/rbush": "^3.0.0", - "@types/react": "^18.3.31", - "@types/react-dom": "^18.3.7", "@typescript-eslint/eslint-plugin": "5.39.0", "@typescript-eslint/parser": "5.39.0", "chokidar-cli": "^3.0.0", "cross-env": "^7.0.3", - "esbuild": "^0.27.2", "eslint": "^8.0.0", "eslint-config-prettier": "^8.10.0", "eslint-import-resolver-typescript": "2.5.0", @@ -136,9 +114,6 @@ "jest-environment-jsdom": "^30.0.5", "mitata": "^1.0.34", "prettier": "^3.0.0", - "publint": "^0.3.23", - "react": "^18.2.0", - "react-dom": "^18.2.0", "ts-node": "^10.9.2", "typescript": "^5.9.2" } diff --git a/packages/graph/scripts/build.mjs b/packages/graph/scripts/build.mjs index 4f3c95b2..822bef94 100644 --- a/packages/graph/scripts/build.mjs +++ b/packages/graph/scripts/build.mjs @@ -1,297 +1,11 @@ -import { spawn } from "node:child_process"; -import { access, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; -import path from "node:path"; import { fileURLToPath } from "node:url"; - -import { build } from "esbuild"; - -const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); -const buildDirectory = path.join(packageRoot, "build"); -const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8")); -const inlinedWorkspacePackages = new Map([ - ["@gravity-ui/graph-scheduler", fileURLToPath(import.meta.resolve("@gravity-ui/graph-scheduler"))], -]); -const externalPackages = [ - ...new Set([...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.peerDependencies ?? {})]), -]; - -// lodash does not expose extensionless subpaths through an exports map, so -// native Node ESM needs the real .js filename. Keep source imports idiomatic -// and normalize only the external specifiers emitted by the production build. -const resolveLodashSubpathsForNodeEsm = { - name: "resolve-lodash-subpaths-for-node-esm", - setup(buildContext) { - buildContext.onResolve({ filter: /^lodash\/[^.]+$/ }, ({ path: importPath }) => ({ - path: `${importPath}.js`, - external: true, - })); - }, -}; - -function run(command, args) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - cwd: packageRoot, - stdio: "inherit", - }); - - child.on("error", reject); - child.on("close", (code, signal) => { - if (code === 0) { - resolve(); - return; - } - - reject( - new Error( - signal - ? `${command} ${args.join(" ")} was terminated by ${signal}` - : `${command} ${args.join(" ")} exited with code ${code}` - ) - ); - }); - }); -} - -function assertNoBundledPackages(results) { - const bundledPackages = new Set(); - - for (const result of results) { - for (const input of Object.keys(result.metafile.inputs)) { - const normalizedInput = input.split(path.sep).join("/"); - const nodeModulesMarker = "node_modules/"; - const markerIndex = normalizedInput.lastIndexOf(nodeModulesMarker); - - if (markerIndex !== -1) { - const packagePath = normalizedInput.slice(markerIndex + nodeModulesMarker.length); - const [firstSegment, secondSegment] = packagePath.split("/"); - const packageName = firstSegment.startsWith("@") ? `${firstSegment}/${secondSegment}` : firstSegment; - - if (!inlinedWorkspacePackages.has(packageName)) { - bundledPackages.add(packageName); - } - } - } - } - - if (bundledPackages.size > 0) { - throw new Error( - `Production bundles unexpectedly contain npm packages:\n${[...bundledPackages] - .sort() - .map((dependency) => `- ${dependency}`) - .join("\n")}` - ); - } -} - -async function assertInlinedWorkspacePackages(results) { - const bundledInputs = new Set( - results.flatMap((result) => Object.keys(result.metafile.inputs).map((input) => path.resolve(packageRoot, input))) - ); - - for (const [packageName, entryPath] of inlinedWorkspacePackages) { - const workspaceSpecifier = manifest.devDependencies?.[packageName]; - const workspacePackageRoot = path.resolve(path.dirname(entryPath), ".."); - const workspaceManifest = JSON.parse(await readFile(path.join(workspacePackageRoot, "package.json"), "utf8")); - - if (typeof workspaceSpecifier !== "string" || !workspaceSpecifier.startsWith("workspace:")) { - throw new Error(`${packageName} must be an explicit workspace devDependency of ${manifest.name}.`); - } - - if (workspaceManifest.name !== packageName || workspaceManifest.private !== true) { - throw new Error(`${packageName} must resolve to a private workspace package.`); - } - - if (!bundledInputs.has(entryPath)) { - throw new Error(`Production bundles do not inline the private workspace package ${packageName}.`); - } - - for (const result of results) { - for (const output of Object.values(result.metafile.outputs)) { - const unresolvedImport = output.imports.find( - ({ path: importPath }) => importPath === packageName || importPath.startsWith(`${packageName}/`) - ); - - if (unresolvedImport) { - throw new Error(`Production bundles contain an unresolved private import of ${packageName}.`); - } - } - } - } -} - -async function pathExists(filePath) { - try { - await access(filePath); - return true; - } catch (error) { - if (error?.code === "ENOENT") { - return false; - } - - throw error; - } -} - -async function collectDeclarationFiles(directory) { - const declarationFiles = []; - - for (const entry of await readdir(directory, { withFileTypes: true })) { - const entryPath = path.join(directory, entry.name); - - if (entry.isDirectory()) { - declarationFiles.push(...(await collectDeclarationFiles(entryPath))); - } else if (entry.name.endsWith(".d.ts")) { - declarationFiles.push(entryPath); - } - } - - return declarationFiles; -} - -async function rewriteDeclarationSpecifiersForNodeEsm() { - const relativeSpecifierPattern = /(["'])(\.{1,2}(?:\/[^"'?#]+)?)\1/g; - - for (const declarationFile of await collectDeclarationFiles(buildDirectory)) { - let contents = await readFile(declarationFile, "utf8"); - - // CSS is published through the explicit styles.css entrypoint. Keeping source-side - // CSS imports in declarations would make type-only Node resolution look for files - // that are intentionally not part of the declaration graph. - contents = contents.replace(/^\s*import\s+["']\.{1,2}\/[^"']+\.css["'];\s*\n?/gm, ""); - - const replacements = new Map(); - - for (const [, , specifier] of contents.matchAll(relativeSpecifierPattern)) { - if (replacements.has(specifier)) { - continue; - } - - const declarationDirectory = path.dirname(declarationFile); - const directDeclaration = path.resolve(declarationDirectory, `${specifier}.d.ts`); - const indexDeclaration = path.resolve(declarationDirectory, specifier, "index.d.ts"); - - if (await pathExists(directDeclaration)) { - replacements.set(specifier, `${specifier}.js`); - } else if (await pathExists(indexDeclaration)) { - replacements.set(specifier, `${specifier.replace(/\/$/, "")}/index.js`); - } - } - - contents = contents.replace(relativeSpecifierPattern, (match, quote, specifier) => { - const replacement = replacements.get(specifier); - - return replacement ? `${quote}${replacement}${quote}` : match; - }); - - await writeFile(declarationFile, contents); - } -} - -async function createPlaywrightCjsDeclarations() { - const entryDeclaration = path.join(buildDirectory, "playwright/index.d.ts"); - const pendingDeclarations = [entryDeclaration]; - const processedDeclarations = new Set(); - const relativeSpecifierPattern = /(["'])(\.{1,2}(?:\/[^"'?#]+)?)\1/g; - - while (pendingDeclarations.length > 0) { - const declarationFile = pendingDeclarations.pop(); - - if (!declarationFile || processedDeclarations.has(declarationFile)) { - continue; - } - - processedDeclarations.add(declarationFile); - let contents = await readFile(declarationFile, "utf8"); - const replacements = new Map(); - - for (const [, , specifier] of contents.matchAll(relativeSpecifierPattern)) { - if (!specifier.endsWith(".js") || replacements.has(specifier)) { - continue; - } - - const referencedDeclaration = path.resolve( - path.dirname(declarationFile), - `${specifier.slice(0, -".js".length)}.d.ts` - ); - - if (await pathExists(referencedDeclaration)) { - replacements.set(specifier, `${specifier.slice(0, -".js".length)}.cjs`); - pendingDeclarations.push(referencedDeclaration); - } - } - - contents = contents.replace(relativeSpecifierPattern, (match, quote, specifier) => { - const replacement = replacements.get(specifier); - - return replacement ? `${quote}${replacement}${quote}` : match; - }); - - await writeFile(declarationFile.replace(/\.d\.ts$/, ".d.cts"), contents); - } -} - -await rm(buildDirectory, { recursive: true, force: true }); -await mkdir(buildDirectory, { recursive: true }); - -const sharedOptions = { - absWorkingDir: packageRoot, - bundle: true, - charset: "utf8", - external: externalPackages, - legalComments: "none", - logLevel: "info", - metafile: true, - plugins: [resolveLodashSubpathsForNodeEsm], - sourcemap: false, - target: "es2020", -}; - -const [browserResult, playwrightEsmResult, playwrightCjsResult, stylesResult] = await Promise.all([ - build({ - ...sharedOptions, - chunkNames: "chunks/[name]-[hash]", - entryNames: "[dir]/[name]", - entryPoints: { - index: "src/index.ts", - "react-components/index": "src/react-components/index.ts", - }, - format: "esm", - loader: { ".css": "empty" }, - outdir: buildDirectory, - platform: "neutral", - splitting: true, - }), - build({ - ...sharedOptions, - entryPoints: ["src/playwright/index.ts"], - format: "esm", - outfile: path.join(buildDirectory, "playwright/index.js"), - platform: "node", - }), - build({ - ...sharedOptions, - entryPoints: ["src/playwright/index.ts"], - format: "cjs", - outfile: path.join(buildDirectory, "playwright/index.cjs"), - platform: "node", - }), - build({ - ...sharedOptions, - entryPoints: ["src/styles.css"], - logLevel: "info", - outfile: path.join(buildDirectory, "styles.css"), - platform: "browser", - }), -]); - -const buildResults = [browserResult, playwrightEsmResult, playwrightCjsResult, stylesResult]; - -await assertInlinedWorkspacePackages(buildResults); -assertNoBundledPackages(buildResults); - -const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; -await run(pnpmCommand, ["exec", "tsc", "-p", "tsconfig.publish.json"]); -await rewriteDeclarationSpecifiersForNodeEsm(); -await createPlaywrightCjsDeclarations(); -await import(`./build-docs.mjs?build=${Date.now()}`); +import { buildPackage } from "../../../scripts/build-package.mjs"; + +await buildPackage({ + packageRoot: fileURLToPath(new URL("../", import.meta.url)), + inlinedWorkspacePackages: new Map([ + ["@gravity-ui/graph-scheduler", fileURLToPath(import.meta.resolve("@gravity-ui/graph-scheduler"))], + ]), + playwright: true, + docs: true, +}); diff --git a/packages/graph/scripts/check-playwright-consumer-types.mjs b/packages/graph/scripts/check-playwright-consumer-types.mjs index 34f06d19..b8922b8e 100644 --- a/packages/graph/scripts/check-playwright-consumer-types.mjs +++ b/packages/graph/scripts/check-playwright-consumer-types.mjs @@ -6,6 +6,7 @@ import ts from "typescript"; const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const fixturePath = path.join( rootDir, + "../..", "tests", "package-contract", "fixtures", diff --git a/packages/graph/src/index.ts b/packages/graph/src/index.ts index 13d124da..0d30b4dc 100644 --- a/packages/graph/src/index.ts +++ b/packages/graph/src/index.ts @@ -1,5 +1,5 @@ export { Anchor, type TAnchor, type TAnchorProps } from "./components/canvas/anchors"; -export { Block as CanvasBlock, type TBlock } from "./components/canvas/blocks/Block"; +export { Block as CanvasBlock, isTBlock, type TBlock, type TBlockProps } from "./components/canvas/blocks/Block"; export { GraphComponent } from "./components/canvas/GraphComponent"; export * from "./components/canvas/connections"; export * from "./graph"; @@ -20,12 +20,18 @@ export { EWheelIntent, WHEEL_INTENT_RULE, } from "./utils/functions/wheelIntent"; -export { type UnwrapGraphEventsDetail, type SelectionEvent } from "./graphEvents"; +export { + type UnwrapGraphEventsDetail, + type SelectionEvent, + type GraphEventsDefinitions, + type UnwrapGraphEvents, +} from "./graphEvents"; export * from "./plugins"; export { defaultGetCameraBlockScaleLevel, ECameraScaleLevel, type TGetCameraBlockScaleLevel, + type ICamera, } from "./services/camera/CameraService"; export * from "./services/Layer"; export * from "./store"; @@ -55,3 +61,7 @@ export * from "./components/canvas/layers/connectionLayer/ConnectionLayer"; export * from "./lib/Component"; export * from "./services/selection/index.public"; + +export type { PublicGraphApi, ZoomConfig } from "./api/PublicGraphApi"; +export type { RecursivePartial } from "./utils/types/helpers"; +export type { TDebounceOptions, TScheduleOptions } from "./utils/utils/schedule"; diff --git a/packages/graph/src/plugins/layered/index.ts b/packages/graph/src/plugins/layered/index.ts index 58cd6f48..79485f89 100644 --- a/packages/graph/src/plugins/layered/index.ts +++ b/packages/graph/src/plugins/layered/index.ts @@ -3,3 +3,7 @@ export type { LayoutGraphParams } from "./layout"; export type { LayeredLayoutOptions } from "./types"; export type { Node, Edge } from "./layout"; export type { ConverterResult, LayeredLayoutInput } from "./types"; + +export { layeredConverter } from "./converters/layeredConverter"; +export type { LayeredConverterParams, LayeredLayoutResult } from "./converters/layeredConverter"; +export { computeLevels } from "./utils/computeLevels"; diff --git a/packages/graph/src/react-components/elk/types/index.ts b/packages/graph/src/react-components/elk/types/index.ts deleted file mode 100644 index b000c555..00000000 --- a/packages/graph/src/react-components/elk/types/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { TMultipointConnection } from "../../../components/canvas/connections/types"; -import type { TConnectionId } from "../../../store/connection/ConnectionState"; -import type { TPoint } from "../../../utils/types/shapes"; - -export type ConverterResult = { - edges: Record>; - blocks: Record; -}; diff --git a/packages/graph/src/react-components/graph-canvas.css b/packages/graph/src/react-components/graph-canvas.css deleted file mode 100644 index cdb7593c..00000000 --- a/packages/graph/src/react-components/graph-canvas.css +++ /dev/null @@ -1,15 +0,0 @@ -.graph-wrapper { - --graph-block-anchor-width: 16px; - --graph-block-anchor-height: 16px; - position: relative; - width: 100%; - height: 100%; - flex: 1; -} - -.graph-canvas { - position: absolute; - overflow: hidden; - width: 100%; - height: 100%; -} diff --git a/packages/graph/src/styles.css b/packages/graph/src/styles.css index ce6b75a8..dc340339 100644 --- a/packages/graph/src/styles.css +++ b/packages/graph/src/styles.css @@ -1,5 +1,2 @@ @import "./services/Layer.css"; -@import "./react-components/graph-canvas.css"; -@import "./react-components/Block.css"; -@import "./react-components/Anchor.css"; @import "./plugins/devtools/devtools-layer.css"; diff --git a/packages/graph/src/utils/functions/index.ts b/packages/graph/src/utils/functions/index.ts index c925a1e2..feb1a05a 100644 --- a/packages/graph/src/utils/functions/index.ts +++ b/packages/graph/src/utils/functions/index.ts @@ -4,7 +4,6 @@ import { ECanDrag } from "../../store/settings"; import { SELECTION_EVENT_TYPES } from "../types/events"; import { Rect, TRect } from "../types/shapes"; -export { parseClassNames } from "./classNames"; export { applyAlpha, clearColorCache } from "./color"; // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/graph/tests/package-contract/README.md b/packages/graph/tests/package-contract/README.md deleted file mode 100644 index 275f5970..00000000 --- a/packages/graph/tests/package-contract/README.md +++ /dev/null @@ -1,206 +0,0 @@ -# Published package contract - -This suite validates the exact `@gravity-ui/graph` tarball prepared for npm. Repository source tests cannot detect -missing packed files, incorrect `exports`, broken generated declarations, ESM/CommonJS resolution differences, -incorrect optional-peer metadata, eager React coupling, or runtime duplication of shared dependencies. - -> When a public entrypoint, output file, declaration, peer dependency, stylesheet contract, or consumer scenario -> changes, update the corresponding check or fixture and this README in the same change. - -## Run - -From the repository root: - -```sh -pnpm run typecheck -pnpm run test:package-contract -``` - -Install Chromium once if it is not already available: - -```sh -pnpm --filter @gravity-ui/graph exec playwright install chromium -``` - -Do not run the package-contract suite concurrently with another Graph build or typecheck: the suite deliberately -rebuilds `packages/graph/build`. - -To preserve the temporary consumers for inspection, whether the run passes or fails: - -```sh -KEEP_PACKAGE_CONTRACT_TMP=1 pnpm run test:package-contract -``` - -The runner prints the preserved directory. On a CI failure, Playwright reports and test results are copied to -`packages/graph/playwright-report/package-contract/` and `packages/graph/test-results/package-contract/`. - -### Release output - -The default command builds one production tarball in a temporary directory and installs that exact file in both -consumers. A release workflow can preserve the already validated tarball outside that directory: - -```sh -PACKAGE_CONTRACT_TARBALL_PATH=/absolute/path/gravity-ui-graph.tgz pnpm run test:package-contract -``` - -`PACKAGE_CONTRACT_TARBALL_PATH` is an output path: the runner still builds, packs, lints, type-checks, and installs the -artifact. The release workflow must publish that same file rather than packing again. - -## Structure - -```text -tests/package-contract/ -├── README.md -├── run.mjs -├── utils.mjs -├── checks/ -│ ├── artifact.mjs -│ ├── types.mjs -│ ├── runtime.mjs -│ └── browser.mjs -└── fixtures/ - ├── shared/ - ├── apps/ - │ ├── vanilla/ - │ └── react/ - └── types/ - ├── playwright-bundler/ - ├── node-esm/ - └── node-cjs-playwright/ -``` - -`run.mjs` owns orchestration. It must: - -1. build the package exactly once; -2. create exactly one tarball, using the same `pnpm pack --out ... --json` call for both its file list and archive; -3. create isolated vanilla and React consumers from that tarball; -4. install each consumer exactly once; -5. pass the same tarball and consumer directories to every check; -6. derive generated consumers' `packageManager` from the workspace manifest; -7. clean temporary files, or preserve them when requested. - -Checks must not build, pack, or install independently. Splitting checks by goal must not turn the suite into several -package pipelines. - -`checks/` contains package policy and executable assertions. `fixtures/` contains only source files that represent -external consumers: TypeScript inputs, tsconfigs, application code, HTML, CSS, Playwright scenarios, and their server -configuration. Fixtures must import only documented package specifiers such as `@gravity-ui/graph`, -`@gravity-ui/graph/react`, `@gravity-ui/graph/playwright`, and `@gravity-ui/graph/styles.css`; they must never reach into -repository source or `build/` directly. - -The entire fixture tree is copied into each temporary consumer to keep shared relative paths stable. The runner executes -these purpose-specific fixtures: - -- vanilla: `apps/vanilla`, `types/playwright-bundler`, and `types/node-cjs-playwright`; -- React: `apps/react` and `types/node-esm`; -- both applications reuse `shared` browser and TypeScript configuration. - -The vanilla consumer intentionally contains neither React nor React types. - -## Responsibility boundary - -This suite answers one question: **can an external project install the packed package and use every documented public -entrypoint in its supported module and browser environments?** - -It is not a second functional E2E suite: - -- package contract owns tarball contents, package metadata, dependency isolation, declaration resolution, native module - loading, browser bundling, and one installed-package smoke scenario per browser-facing integration; -- `apps/e2e` owns graph and Playwright page-object behavior such as selection, clicks, drag and drop, connections, camera - controls, error states, and interaction edge cases; -- Storybook owns representative component examples and compatibility with the repository's React/Webpack development - environment. - -Add a browser assertion here only when the failure could be caused specifically by packing, installing, resolving, or -bundling the public package. If the same assertion would be equally useful against repository code, it normally belongs -in `apps/e2e` instead. - -## What is checked - -### Artifact - -`checks/artifact.mjs` protects the published archive: - -- a stale sentinel placed in `build/` is removed by the production build; -- the production build rejects accidentally bundled npm dependencies; -- the packed file list stays within a bounded public allowlist and excludes sources, tests, stories, configs, and - lockfiles; -- required root, ESM, CommonJS, declaration, stylesheet, and documentation files are present; -- the installed manifest has the expected package entrypoints, `files`, `exports`, `typesVersions`, and optional peer - metadata; -- the private scheduler remains a build-only workspace dependency, its implementation is inlined into generated - JavaScript, its package specifier is absent from JavaScript and declarations, and it is neither exposed as a runtime - dependency nor installed in isolated consumers; -- published CSS contains the vanilla canvas, React canvas, block, anchor, and devtools selectors; -- `publint --strict` accepts the exact tarball installed by the consumers. - -This catches stale output, missing release artifacts, repository-only files leaking into npm, broken export targets, and -unreviewed package metadata changes. - -### Types - -`checks/types.mjs` checks declarations from the packed or installed package with `strict: true`, `skipLibCheck: false`, -and `noEmit: true`: - -- ATTW checks `.` and `./react` with the `esm-only` profile; -- ATTW checks `./playwright` with the `node16` profile for both ESM and CommonJS; -- the vanilla and React applications type-check with Bundler resolution; -- `types/playwright-bundler` verifies that `GraphPO.evaluate` receives the public `Graph`, block and connection state - methods return public types rather than `any`, and `GraphPoint` and `clickAt` remain usable; -- `types/node-esm` imports `Graph`, the public scheduler contracts, `GraphCanvas`, and `useElk` through Node16 ESM - resolution; -- `types/node-cjs-playwright` imports `GraphPO` and `GraphCameraState` through Node16 CommonJS resolution. - -“Node16” names TypeScript's module-resolution semantics here; it does not mean the suite executes on Node.js 16. - -The Playwright Bundler fixture is deliberately reused by `scripts/check-playwright-consumer-types.mjs` during -`pnpm run typecheck`, where it checks local generated declarations before the slower installed-consumer suite. Keep both -callers pointed at the same fixture. - -### Runtime - -`checks/runtime.mjs` runs Node against the installed package: - -- ESM imports expose `Graph`, `ESchedulerPriority`, `schedule`, `debounce`, `throttle`, `GraphCanvas`, and `GraphPO` from - their documented entrypoints; -- CommonJS `require("@gravity-ui/graph/playwright")` exposes `GraphPO`; -- the vanilla consumer cannot resolve React, proving the core and Playwright entrypoints do not require it eagerly; -- a consumer-side `@preact/signals-core` effect observes a Graph signal update exactly once after its initial run. - -These probes catch invalid conditional exports, ESM/CJS loader failures, accidental React coupling, and a bundled or -otherwise disconnected signals runtime. - -### Browser - -`checks/browser.mjs` bundles the installed vanilla and React applications with esbuild, including the public stylesheet, -then runs a deliberately small Chromium smoke test against each one. Detailed interaction behavior remains in -`apps/e2e`. - -The vanilla scenario covers: - -- bundling and loading the installed `.` and `./playwright` entrypoints; -- `GraphPO.waitForReady` against an application-owned wrapper; -- rendering the canvas with the public stylesheet applied. - -The React scenario covers: - -- bundling and loading the installed `.` and `./react` entrypoints; -- ready-state propagation through `GraphCanvas`, `useGraph`, and `useGraphEvent`; -- rendered `GraphBlock` content and the React canvas stylesheet; -- use of the installed `./playwright` entrypoint with the React application. - -## Maintenance map - -| Change | Required updates | -| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Add, remove, or rename a public subpath | Update the build, manifest, artifact export/file contract, ATTW entrypoints, native import probe, and at least one relevant type fixture. Add a browser fixture when the entrypoint is browser-facing. | -| Change emitted files, chunks, declarations, docs, or CSS | Update the artifact allowlist and required-file set deliberately. Do not broaden patterns merely to make an unexpected file pass. | -| Change public TypeScript APIs | Update the narrow fixture that represents that consumer. Preserve `skipLibCheck: false` and exact anti-`any` assertions. | -| Change ESM/CJS conditions or module support | Update ATTW profiles, the `.mts` or `.cts` fixture, and the matching native `import` or `require` probe. | -| Change dependencies or optional peers | Update manifest assertions and generated consumer manifests. Add or update an absence/interoperability probe when a dependency must remain optional or singleton-like. | -| Change public styles or required selectors | Update the CSS artifact assertions, the consuming application, and its browser assertion together. | -| Change Playwright page-object types or behavior | Update `types/playwright-bundler` for the public type contract and `apps/e2e` for behavior. Change the package browser smoke only when the installed-package integration itself changes. | -| Move or rename fixtures | Update their check registration, consumer paths, server/config paths, and `scripts/check-playwright-consumer-types.mjs` when the shared Playwright type fixture moves. | -| Add a new goal | Add a focused check and the smallest representative fixture, but continue using the existing build, tarball, and installed consumers whenever possible. | - -Do not commit generated consumer manifests, lockfiles, `node_modules`, bundles, reports, or temporary projects. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd05e5df..354000d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,21 +8,45 @@ importers: .: devDependencies: + '@arethetypeswrong/cli': + specifier: ^0.18.5 + version: 0.18.5 '@commitlint/cli': specifier: ^19.5.0 version: 19.8.1(@types/node@20.19.43)(typescript@5.9.2) '@commitlint/config-conventional': specifier: ^19.5.0 version: 19.5.0 + '@gravity-ui/graph': + specifier: workspace:* + version: link:packages/graph '@gravity-ui/prettier-config': specifier: ^1.1.0 version: 1.1.0(prettier@3.3.2) + '@playwright/test': + specifier: ^1.58.0 + version: 1.58.0 + '@types/node': + specifier: ^20.17.0 + version: 20.19.43 + chokidar-cli: + specifier: ^3.0.0 + version: 3.0.0 + esbuild: + specifier: ^0.27.2 + version: 0.27.2 prettier: specifier: ^3.0.0 version: 3.3.2 + publint: + specifier: ^0.3.23 + version: 0.3.24 release-please: specifier: 17.6.0 version: 17.6.0 + typescript: + specifier: ^5.9.2 + version: 5.9.2 yaml: specifier: 2.8.1 version: 2.8.1 @@ -32,6 +56,9 @@ importers: '@gravity-ui/graph': specifier: workspace:* version: link:../../packages/graph + '@gravity-ui/graph-react': + specifier: workspace:* + version: link:../../packages/graph-react react: specifier: ^18.2.0 version: 18.3.1 @@ -66,6 +93,9 @@ importers: '@gravity-ui/graph': specifier: workspace:* version: link:../../packages/graph + '@gravity-ui/graph-react': + specifier: workspace:* + version: link:../../packages/graph-react '@gravity-ui/icons': specifier: ^2.15.0 version: 2.15.0(react@18.3.1) @@ -166,9 +196,6 @@ importers: '@preact/signals-core': specifier: ^1.12.2 version: 1.12.2 - elkjs: - specifier: ^0.9.3 - version: 0.9.3 intersects: specifier: ^2.7.2 version: 2.7.2 @@ -182,9 +209,6 @@ importers: specifier: ^0.1.1 version: 0.1.1 devDependencies: - '@arethetypeswrong/cli': - specifier: ^0.18.5 - version: 0.18.5 '@commitlint/cli': specifier: ^19.5.0 version: 19.8.1(@types/node@20.19.43)(typescript@5.9.2) @@ -215,9 +239,6 @@ importers: '@swc/jest': specifier: ^0.2.39 version: 0.2.39(@swc/core@1.13.3) - '@testing-library/react': - specifier: ^16.3.0 - version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/intersects': specifier: ^2.5.0 version: 2.5.0 @@ -233,12 +254,6 @@ importers: '@types/rbush': specifier: ^3.0.0 version: 3.0.0 - '@types/react': - specifier: ^18.3.31 - version: 18.3.31 - '@types/react-dom': - specifier: ^18.3.7 - version: 18.3.7(@types/react@18.3.31) '@typescript-eslint/eslint-plugin': specifier: 5.39.0 version: 5.39.0(@typescript-eslint/parser@5.39.0(eslint@8.57.0)(typescript@5.9.2))(eslint@8.57.0)(typescript@5.9.2) @@ -251,9 +266,6 @@ importers: cross-env: specifier: ^7.0.3 version: 7.0.3 - esbuild: - specifier: ^0.27.2 - version: 0.27.2 eslint: specifier: ^8.0.0 version: 8.57.0 @@ -287,18 +299,91 @@ importers: prettier: specifier: ^3.0.0 version: 3.3.2 - publint: - specifier: ^0.3.23 - version: 0.3.24 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@swc/core@1.13.3)(@types/node@20.19.43)(typescript@5.9.2) + typescript: + specifier: ^5.9.2 + version: 5.9.2 + + packages/graph-react: + dependencies: + '@gravity-ui/graph': + specifier: workspace:^ + version: link:../graph + '@preact/signals-core': + specifier: ^1.12.2 + version: 1.12.2 + elkjs: + specifier: ^0.9.3 + version: 0.9.3 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + devDependencies: + '@gravity-ui/eslint-config': + specifier: ^3.2.0 + version: 3.2.0(@types/eslint@9.6.1)(eslint@8.57.0)(prettier@3.3.2)(typescript@5.9.2) + '@swc/core': + specifier: ^1.13.3 + version: 1.13.3 + '@swc/jest': + specifier: ^0.2.39 + version: 0.2.39(@swc/core@1.13.3) + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/lodash': + specifier: ^4.17.13 + version: 4.17.13 + '@types/react': + specifier: ^18.3.31 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.31) + '@typescript-eslint/eslint-plugin': + specifier: 5.39.0 + version: 5.39.0(@typescript-eslint/parser@5.39.0(eslint@8.57.0)(typescript@5.9.2))(eslint@8.57.0)(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: 5.39.0 + version: 5.39.0(eslint@8.57.0)(typescript@5.9.2) + chokidar-cli: + specifier: ^3.0.0 + version: 3.0.0 + eslint: + specifier: ^8.0.0 + version: 8.57.0 + eslint-config-prettier: + specifier: ^8.10.0 + version: 8.10.0(eslint@8.57.0) + eslint-import-resolver-typescript: + specifier: 2.5.0 + version: 2.5.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-plugin-prettier: + specifier: ^5.0.0 + version: 5.1.3(@types/eslint@9.6.1)(eslint-config-prettier@8.10.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.2) + jest: + specifier: ^30.0.5 + version: 30.0.5(@types/node@20.19.43)(esbuild-register@3.6.0(esbuild@0.27.2))(ts-node@10.9.2(@swc/core@1.13.3)(@types/node@20.19.43)(typescript@5.9.2)) + jest-canvas-mock: + specifier: ^2.5.2 + version: 2.5.2 + jest-environment-jsdom: + specifier: ^30.0.5 + version: 30.0.5 + prettier: + specifier: ^3.0.0 + version: 3.3.2 react: specifier: ^18.2.0 version: 18.3.1 react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.13.3)(@types/node@20.19.43)(typescript@5.9.2) typescript: specifier: ^5.9.2 version: 5.9.2 @@ -2118,10 +2203,6 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - ansi-regex@6.3.0: resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} engines: {node: '>=12'} @@ -6232,7 +6313,7 @@ snapshots: eslint: 8.57.0 eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-prettier: 5.1.3(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.2) eslint-plugin-react: 7.37.2(eslint@8.57.0) @@ -7493,8 +7574,6 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.0.1: {} - ansi-regex@6.3.0: {} ansi-styles@3.2.1: @@ -8491,13 +8570,13 @@ snapshots: debug: 4.4.1 enhanced-resolve: 5.18.3 eslint: 8.57.0 - eslint-module-utils: 2.8.2(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) + eslint-module-utils: 2.8.2(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.2.1 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.39.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@2.5.0)(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -8526,16 +8605,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.2(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.9.2) - eslint: 8.57.0 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0) - transitivePeerDependencies: - - supports-color - eslint-plugin-file-progress@1.4.0(eslint@8.57.0): dependencies: chalk: 4.1.2 @@ -8569,7 +8638,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.9.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -11128,7 +11197,7 @@ snapshots: strip-ansi@7.1.0: dependencies: - ansi-regex: 6.0.1 + ansi-regex: 6.3.0 strip-bom@3.0.0: {} diff --git a/release-please-config.json b/release-please-config.json index 373c02b6..4f2ed54e 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -25,6 +25,14 @@ "package-name": "@gravity-ui/graph-scheduler", "component": "scheduler", "skip-github-release": true + }, + "packages/graph-react": { + "package-name": "@gravity-ui/graph-react", + "component": "graph-react", + "release-as": "1.0.0-next.0", + "versioning": "prerelease", + "prerelease": true, + "prerelease-type": "next" } } } diff --git a/scripts/build-package.mjs b/scripts/build-package.mjs new file mode 100644 index 00000000..86071b0b --- /dev/null +++ b/scripts/build-package.mjs @@ -0,0 +1,307 @@ +import { spawn } from "node:child_process"; +import { access, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { build } from "esbuild"; + +export async function buildPackage({ + packageRoot, + inlinedWorkspacePackages = new Map(), + playwright = false, + docs = false, +}) { + const buildDirectory = path.join(packageRoot, "build"); + const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8")); + const externalPackages = [ + ...new Set([...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.peerDependencies ?? {})]), + ]; + + // lodash does not expose extensionless subpaths through an exports map, so + // native Node ESM needs the real .js filename. Keep source imports idiomatic + // and normalize only the external specifiers emitted by the production build. + const resolveLodashSubpathsForNodeEsm = { + name: "resolve-lodash-subpaths-for-node-esm", + setup(buildContext) { + buildContext.onResolve({ filter: /^lodash\/[^.]+$/ }, ({ path: importPath }) => ({ + path: `${importPath}.js`, + external: true, + })); + }, + }; + + function run(command, args) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { + cwd: packageRoot, + stdio: "inherit", + }); + + child.on("error", reject); + child.on("close", (code, signal) => { + if (code === 0) { + resolve(); + return; + } + + reject( + new Error( + signal + ? `${command} ${args.join(" ")} was terminated by ${signal}` + : `${command} ${args.join(" ")} exited with code ${code}` + ) + ); + }); + }); + } + + function assertNoBundledPackages(results) { + const bundledPackages = new Set(); + + for (const result of results) { + for (const input of Object.keys(result.metafile.inputs)) { + const inputPath = path.resolve(packageRoot, input); + const allowedRoots = [ + path.join(packageRoot, "src"), + ...[...inlinedWorkspacePackages.values()].map((entry) => path.dirname(entry)), + ]; + if (!allowedRoots.some((allowedRoot) => inputPath.startsWith(`${allowedRoot}${path.sep}`))) { + throw new Error(`Production bundle includes a source outside ${manifest.name}: ${input}`); + } + const normalizedInput = input.split(path.sep).join("/"); + const nodeModulesMarker = "node_modules/"; + const markerIndex = normalizedInput.lastIndexOf(nodeModulesMarker); + + if (markerIndex !== -1) { + const packagePath = normalizedInput.slice(markerIndex + nodeModulesMarker.length); + const [firstSegment, secondSegment] = packagePath.split("/"); + const packageName = firstSegment.startsWith("@") ? `${firstSegment}/${secondSegment}` : firstSegment; + + if (!inlinedWorkspacePackages.has(packageName)) { + bundledPackages.add(packageName); + } + } + } + } + + if (bundledPackages.size > 0) { + throw new Error( + `Production bundles unexpectedly contain npm packages:\n${[...bundledPackages] + .sort() + .map((dependency) => `- ${dependency}`) + .join("\n")}` + ); + } + } + + async function assertInlinedWorkspacePackages(results) { + const bundledInputs = new Set( + results.flatMap((result) => Object.keys(result.metafile.inputs).map((input) => path.resolve(packageRoot, input))) + ); + + for (const [packageName, entryPath] of inlinedWorkspacePackages) { + const workspaceSpecifier = manifest.devDependencies?.[packageName]; + const workspacePackageRoot = path.resolve(path.dirname(entryPath), ".."); + const workspaceManifest = JSON.parse(await readFile(path.join(workspacePackageRoot, "package.json"), "utf8")); + + if (typeof workspaceSpecifier !== "string" || !workspaceSpecifier.startsWith("workspace:")) { + throw new Error(`${packageName} must be an explicit workspace devDependency of ${manifest.name}.`); + } + + if (workspaceManifest.name !== packageName || workspaceManifest.private !== true) { + throw new Error(`${packageName} must resolve to a private workspace package.`); + } + + if (!bundledInputs.has(entryPath)) { + throw new Error(`Production bundles do not inline the private workspace package ${packageName}.`); + } + + for (const result of results) { + for (const output of Object.values(result.metafile.outputs)) { + const unresolvedImport = output.imports.find( + ({ path: importPath }) => importPath === packageName || importPath.startsWith(`${packageName}/`) + ); + + if (unresolvedImport) { + throw new Error(`Production bundles contain an unresolved private import of ${packageName}.`); + } + } + } + } + } + + async function pathExists(filePath) { + try { + await access(filePath); + return true; + } catch (error) { + if (error?.code === "ENOENT") { + return false; + } + + throw error; + } + } + + async function collectDeclarationFiles(directory) { + const declarationFiles = []; + + for (const entry of await readdir(directory, { withFileTypes: true })) { + const entryPath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + declarationFiles.push(...(await collectDeclarationFiles(entryPath))); + } else if (entry.name.endsWith(".d.ts")) { + declarationFiles.push(entryPath); + } + } + + return declarationFiles; + } + + async function rewriteDeclarationSpecifiersForNodeEsm() { + const relativeSpecifierPattern = /(["'])(\.{1,2}(?:\/[^"'?#]+)?)\1/g; + + for (const declarationFile of await collectDeclarationFiles(buildDirectory)) { + let contents = await readFile(declarationFile, "utf8"); + + // CSS is published through the explicit styles.css entrypoint. Keeping source-side + // CSS imports in declarations would make type-only Node resolution look for files + // that are intentionally not part of the declaration graph. + contents = contents.replace(/^\s*import\s+["']\.{1,2}\/[^"']+\.css["'];\s*\n?/gm, ""); + + const replacements = new Map(); + + for (const [, , specifier] of contents.matchAll(relativeSpecifierPattern)) { + if (replacements.has(specifier)) { + continue; + } + + const declarationDirectory = path.dirname(declarationFile); + const directDeclaration = path.resolve(declarationDirectory, `${specifier}.d.ts`); + const indexDeclaration = path.resolve(declarationDirectory, specifier, "index.d.ts"); + + if (await pathExists(directDeclaration)) { + replacements.set(specifier, `${specifier}.js`); + } else if (await pathExists(indexDeclaration)) { + replacements.set(specifier, `${specifier.replace(/\/$/, "")}/index.js`); + } + } + + contents = contents.replace(relativeSpecifierPattern, (match, quote, specifier) => { + const replacement = replacements.get(specifier); + + return replacement ? `${quote}${replacement}${quote}` : match; + }); + + await writeFile(declarationFile, contents); + } + } + + async function createPlaywrightCjsDeclarations() { + const entryDeclaration = path.join(buildDirectory, "playwright/index.d.ts"); + const pendingDeclarations = [entryDeclaration]; + const processedDeclarations = new Set(); + const relativeSpecifierPattern = /(["'])(\.{1,2}(?:\/[^"'?#]+)?)\1/g; + + while (pendingDeclarations.length > 0) { + const declarationFile = pendingDeclarations.pop(); + + if (!declarationFile || processedDeclarations.has(declarationFile)) { + continue; + } + + processedDeclarations.add(declarationFile); + let contents = await readFile(declarationFile, "utf8"); + const replacements = new Map(); + + for (const [, , specifier] of contents.matchAll(relativeSpecifierPattern)) { + if (!specifier.endsWith(".js") || replacements.has(specifier)) { + continue; + } + + const referencedDeclaration = path.resolve( + path.dirname(declarationFile), + `${specifier.slice(0, -".js".length)}.d.ts` + ); + + if (await pathExists(referencedDeclaration)) { + replacements.set(specifier, `${specifier.slice(0, -".js".length)}.cjs`); + pendingDeclarations.push(referencedDeclaration); + } + } + + contents = contents.replace(relativeSpecifierPattern, (match, quote, specifier) => { + const replacement = replacements.get(specifier); + + return replacement ? `${quote}${replacement}${quote}` : match; + }); + + await writeFile(declarationFile.replace(/\.d\.ts$/, ".d.cts"), contents); + } + } + + await rm(buildDirectory, { recursive: true, force: true }); + await mkdir(buildDirectory, { recursive: true }); + + const sharedOptions = { + absWorkingDir: packageRoot, + bundle: true, + charset: "utf8", + external: externalPackages, + legalComments: "none", + logLevel: "info", + metafile: true, + plugins: [resolveLodashSubpathsForNodeEsm], + sourcemap: false, + target: "es2020", + }; + + const buildResults = await Promise.all([ + build({ + ...sharedOptions, + chunkNames: "chunks/[name]-[hash]", + entryNames: "[dir]/[name]", + entryPoints: { + index: "src/index.ts", + }, + format: "esm", + loader: { ".css": "empty" }, + outdir: buildDirectory, + platform: "neutral", + splitting: true, + }), + playwright && + build({ + ...sharedOptions, + entryPoints: ["src/playwright/index.ts"], + format: "esm", + outfile: path.join(buildDirectory, "playwright/index.js"), + platform: "node", + }), + playwright && + build({ + ...sharedOptions, + entryPoints: ["src/playwright/index.ts"], + format: "cjs", + outfile: path.join(buildDirectory, "playwright/index.cjs"), + platform: "node", + }), + build({ + ...sharedOptions, + entryPoints: ["src/styles.css"], + logLevel: "info", + outfile: path.join(buildDirectory, "styles.css"), + platform: "browser", + }), + ]); + + await assertInlinedWorkspacePackages(buildResults.filter(Boolean)); + assertNoBundledPackages(buildResults.filter(Boolean)); + + const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; + await run(pnpmCommand, ["exec", "tsc", "-p", "tsconfig.publish.json"]); + await rewriteDeclarationSpecifiersForNodeEsm(); + if (playwright) await createPlaywrightCjsDeclarations(); + if (docs) await import(pathToFileURL(path.join(packageRoot, "scripts/build-docs.mjs")).href); +} diff --git a/tests/package-contract/README.md b/tests/package-contract/README.md new file mode 100644 index 00000000..56b3b6cd --- /dev/null +++ b/tests/package-contract/README.md @@ -0,0 +1,68 @@ +# Public package contract + +This suite validates the exact `@gravity-ui/graph` and `@gravity-ui/graph-react` tarballs that an external application +installs. It owns package metadata, generated declarations, dependency isolation, native imports, and browser bundling. +Detailed graph behavior remains in `apps/e2e`. + +## Run + +From the repository root: + +```sh +pnpm run test:package-contract +``` + +Install Chromium once with `pnpm exec playwright install chromium`. Do not run the suite concurrently with another +package build or typecheck: it deliberately cleans and rebuilds both packages. Set `KEEP_PACKAGE_CONTRACT_TMP=1` to retain +the isolated consumers. With `CI=1`, failed browser reports are copied to the root `playwright-report/package-contract` +and `test-results/package-contract` directories. + +## Structure and ownership + +`run.mjs` builds and packs each public package once, then installs one vanilla consumer and one React consumer. Every +check uses those same artifacts. It resolves workspace tool versions for the generated manifests so the test does not +silently select a newer React, TypeScript, or Playwright release. + +- `checks/artifact.mjs` verifies clean output, exact package names and versions, metadata, bounded file lists, styles, and + private scheduler isolation. `publint --strict` checks both tarballs. +- `checks/types.mjs` checks both ESM roots with ATTW and the core Playwright entrypoint with the Node16 profile. External + TypeScript fixtures use Bundler, Node16 ESM, and Node16 CommonJS resolution with `strict: true` and `skipLibCheck: false`. +- `checks/runtime.mjs` imports each package with native Node ESM and requires the Playwright subpath with CommonJS. It + confirms that React's connection class shares the application's core runtime and that core signals + interoperate with the consumer's signals runtime. +- `checks/browser.mjs` bundles the installed artifacts and runs one vanilla and one React smoke scenario. The React + fixture uses the public components and both stylesheets; its `useGraph` result must be an instance of the consumer's + `Graph` class. The portal's render callback verifies that its layer inherits the consumer's `Layer` and receives the + same graph through context. + +The vanilla consumer contains neither React, React DOM, ELK, nor `@gravity-ui/graph-react`. The core manifest and emitted +JavaScript/declarations must not depend on React. The removed `@gravity-ui/graph/react` subpath must fail resolution. +The React package declares core and React as required peers; core has no dependency on the React package. The shared +production builder rejects bundled external dependencies and source imports outside the owning package, except for the +explicitly inlined private scheduler in core. + +Core styles own canvas layers and devtools; React styles own `.graph-wrapper`, `.graph-block-container`, and +`.graph-block-anchor`. Each package's tarball must contain its own stylesheet and exclude the other's selectors. + +## Fixtures + +`fixtures/apps/vanilla` and `fixtures/apps/react` represent external applications and import only documented package +entrypoints. `fixtures/types/playwright-bundler` is also reused by core's fast declaration check during `pnpm run typecheck`. +`fixtures/types/node-esm` checks core and React together; `fixtures/types/node-cjs-playwright` checks the CommonJS contract. +The shared fixture tree is copied into each consumer so relative paths remain stable. + +## Preserving a validated artifact + +`PACKAGE_CONTRACT_TARBALL_PATH` is an output path for the package whose command is invoked: + +```sh +PACKAGE_CONTRACT_TARBALL_PATH=/absolute/path/graph.tgz pnpm --filter @gravity-ui/graph run test:package-contract +PACKAGE_CONTRACT_TARBALL_PATH=/absolute/path/graph-react.tgz pnpm --filter @gravity-ui/graph-react run test:package-contract +``` + +Both commands validate the package pair. The requested tarball remains after temporary consumers are removed. The +release process must publish that validated file. When a dependency tarball is supplied through +`PACKAGE_CONTRACT_WORKSPACE_TARBALLS`, the suite installs that same artifact instead of repacking the dependency. + +When changing an entrypoint, dependency, declaration, stylesheet, or packed file, update its assertions and the relevant +consumer fixture together. Do not broaden an allowlist merely to make an unexpected artifact pass. diff --git a/packages/graph/tests/package-contract/checks/artifact.mjs b/tests/package-contract/checks/artifact.mjs similarity index 63% rename from packages/graph/tests/package-contract/checks/artifact.mjs rename to tests/package-contract/checks/artifact.mjs index e4f1aa18..128ffc2e 100644 --- a/packages/graph/tests/package-contract/checks/artifact.mjs +++ b/tests/package-contract/checks/artifact.mjs @@ -12,11 +12,6 @@ const expectedExports = { import: "./build/index.js", default: "./build/index.js", }, - "./react": { - types: "./build/react-components/index.d.ts", - import: "./build/react-components/index.js", - default: "./build/react-components/index.js", - }, "./playwright": { import: { types: "./build/playwright/index.d.ts", @@ -33,7 +28,6 @@ const expectedExports = { const expectedTypesVersions = { "*": { - react: ["build/react-components/index.d.ts"], playwright: ["build/playwright/index.d.ts"], }, }; @@ -41,7 +35,6 @@ const expectedTypesVersions = { const allowedPackageRootFiles = new Set(["LICENSE", "README.md", "package.json"]); const allowedRuntimeFiles = new Set([ "build/index.js", - "build/react-components/index.js", "build/playwright/index.js", "build/playwright/index.cjs", "build/playwright/index.d.cts", @@ -60,8 +53,8 @@ function getPackMetadata(output) { return Array.isArray(metadata) ? metadata[0] : metadata; } -function assertPackedFiles(metadata) { - assert.equal(metadata.name, "@gravity-ui/graph"); +function assertPackedFiles(metadata, react) { + assert.equal(metadata.name, react ? "@gravity-ui/graph-react" : "@gravity-ui/graph"); assert.ok(Array.isArray(metadata.files), "pnpm pack did not report the packed file list."); const packedFiles = metadata.files.map(({ path: packedPath }) => packedPath).sort(); @@ -76,8 +69,12 @@ function assertPackedFiles(metadata) { !/^build\/docs\/.+\.md$/.test(packedPath) ); }); - const forbiddenFiles = packedFiles.filter((packedPath) => - forbiddenPackedPathPatterns.some((pattern) => pattern.test(packedPath)) + const forbiddenFiles = packedFiles.filter( + (packedPath) => + forbiddenPackedPathPatterns.some((pattern) => pattern.test(packedPath)) || + (react + ? /(?:^|\/)(?:playwright|docs)(?:\/|$)/.test(packedPath) + : /(?:^|\/)react-components(?:\/|$)/.test(packedPath)) ); assert.deepEqual(unexpectedFiles, [], `Tarball contains files outside the public allowlist: ${unexpectedFiles}`); @@ -87,14 +84,16 @@ function assertPackedFiles(metadata) { ...allowedPackageRootFiles, "build/index.js", "build/index.d.ts", - "build/react-components/index.js", - "build/react-components/index.d.ts", - "build/playwright/index.js", - "build/playwright/index.d.ts", - "build/playwright/index.cjs", - "build/playwright/index.d.cts", "build/styles.css", - "build/docs/INDEX.md", + ...(react + ? [] + : [ + "build/playwright/index.js", + "build/playwright/index.d.ts", + "build/playwright/index.cjs", + "build/playwright/index.d.cts", + "build/docs/INDEX.md", + ]), ]) { assert.ok(packedFiles.includes(requiredFile), `Tarball is missing required file ${requiredFile}.`); } @@ -143,7 +142,7 @@ async function assertNoPrivateSchedulerSpecifiers(packageRoot) { } } -export async function buildAndPackArtifact({ packageRoot, staleBuildSentinelPath, tarballPath }) { +export async function buildAndPackArtifact({ packageRoot, staleBuildSentinelPath, tarballPath, react = false }) { console.log("\n[package-contract] Building published files..."); await mkdir(path.dirname(staleBuildSentinelPath), { recursive: true }); await writeFile(staleBuildSentinelPath, "The production build must remove this stale artifact.\n"); @@ -151,24 +150,24 @@ export async function buildAndPackArtifact({ packageRoot, staleBuildSentinelPath await assertPathDoesNotExist(staleBuildSentinelPath, "The production build did not clean its output directory."); await assertNoPrivateSchedulerSpecifiers(packageRoot); - console.log("\n[package-contract] Packing @gravity-ui/graph and checking the tarball allowlist..."); + console.log("\n[package-contract] Packing the package and checking the tarball allowlist..."); const packMetadata = getPackMetadata( await run("pnpm", ["pack", "--out", tarballPath, "--json"], { cwd: packageRoot, printStdout: false, }) ); - assertPackedFiles(packMetadata); + assertPackedFiles(packMetadata, react); console.log("\n[package-contract] Linting the packed package metadata..."); await run("pnpm", ["exec", "publint", tarballPath, "--strict"], { cwd: packageRoot }); } -export async function checkInstalledArtifact(consumerDirectory, expectedVersion) { - const packageRoot = path.join(consumerDirectory, "node_modules", "@gravity-ui", "graph"); +export async function checkInstalledArtifact(consumerDirectory, expectedVersion, react = false) { + const packageRoot = path.join(consumerDirectory, "node_modules", "@gravity-ui", react ? "graph-react" : "graph"); const manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8")); - assert.equal(manifest.name, "@gravity-ui/graph"); + assert.equal(manifest.name, react ? "@gravity-ui/graph-react" : "@gravity-ui/graph"); assert.equal(manifest.version, expectedVersion, "The installed package version does not match the tested artifact."); assert.notEqual(manifest.private, true); assert.equal(manifest.type, "module"); @@ -176,14 +175,41 @@ export async function checkInstalledArtifact(consumerDirectory, expectedVersion) assert.equal(manifest.module.replace(/^\.\//, ""), "build/index.js"); assert.equal(manifest.types.replace(/^\.\//, ""), "build/index.d.ts"); assert.deepEqual(manifest.files, ["build"]); - assert.deepEqual(manifest.exports, expectedExports); - assert.deepEqual(manifest.typesVersions, expectedTypesVersions); - assert.equal(manifest.peerDependencies?.["@playwright/test"], ">=1.58.0"); - assert.equal(manifest.peerDependencies?.react, "^18.0.0"); - assert.equal(manifest.peerDependencies?.["react-dom"], "^18.0.0"); - assert.equal(manifest.peerDependenciesMeta?.["@playwright/test"]?.optional, true); - assert.equal(manifest.peerDependenciesMeta?.react?.optional, true); - assert.equal(manifest.peerDependenciesMeta?.["react-dom"]?.optional, true); + assert.deepEqual( + manifest.exports, + react ? { ".": expectedExports["."], "./styles.css": expectedExports["./styles.css"] } : expectedExports + ); + assert.deepEqual(manifest.typesVersions, react ? undefined : expectedTypesVersions); + if (react) { + assert.equal(manifest.peerDependencies?.react, "^18.0.0"); + assert.equal(manifest.peerDependencies?.["react-dom"], "^18.0.0"); + assert.equal(manifest.peerDependenciesMeta?.react?.optional, undefined); + assert.equal(manifest.peerDependenciesMeta?.["react-dom"]?.optional, undefined); + const coreManifest = JSON.parse(await readFile(path.join(packageRoot, "../graph/package.json"), "utf8")); + assert.equal(manifest.peerDependencies?.["@gravity-ui/graph"], `^${coreManifest.version}`); + assert.equal(manifest.dependencies?.["@gravity-ui/graph"], undefined); + } else { + assert.equal(manifest.peerDependencies?.["@playwright/test"], ">=1.58.0"); + assert.equal(manifest.peerDependenciesMeta?.["@playwright/test"]?.optional, true); + for (const field of [ + "dependencies", + "devDependencies", + "peerDependencies", + "peerDependenciesMeta", + "optionalDependencies", + ]) { + for (const name of [ + "react", + "react-dom", + "@types/react", + "@types/react-dom", + "@gravity-ui/graph-react", + "elkjs", + ]) { + assert.equal(manifest[field]?.[name], undefined, `Core must not depend on ${name} through ${field}.`); + } + } + } for (const dependencyField of ["dependencies", "optionalDependencies", "peerDependencies"]) { assert.equal( @@ -207,14 +233,16 @@ export async function checkInstalledArtifact(consumerDirectory, expectedVersion) [ "build/index.js", "build/index.d.ts", - "build/react-components/index.js", - "build/react-components/index.d.ts", - "build/playwright/index.js", - "build/playwright/index.d.ts", - "build/playwright/index.cjs", - "build/playwright/index.d.cts", "build/styles.css", - "build/docs/INDEX.md", + ...(react + ? [] + : [ + "build/playwright/index.js", + "build/playwright/index.d.ts", + "build/playwright/index.cjs", + "build/playwright/index.d.cts", + "build/docs/INDEX.md", + ]), "README.md", "LICENSE", ].map((relativePath) => access(path.join(packageRoot, relativePath))) @@ -227,9 +255,27 @@ export async function checkInstalledArtifact(consumerDirectory, expectedVersion) await assertNoPrivateSchedulerSpecifiers(packageRoot); const publicStyles = await readFile(path.join(packageRoot, "build", "styles.css"), "utf8"); - assert.match(publicStyles, /\.layer\b/, "Public styles do not include the vanilla canvas contract."); - assert.match(publicStyles, /\.graph-wrapper\b/, "Public styles do not include the React canvas contract."); - assert.match(publicStyles, /\.graph-block-container\b/, "Public styles do not include the React block contract."); - assert.match(publicStyles, /\.graph-block-anchor\b/, "Public styles do not include the React anchor contract."); - assert.match(publicStyles, /\.devtools-ruler-bg\b/, "Public styles do not include the devtools contract."); + if (react) { + assert.match(publicStyles, /\.graph-wrapper\b/); + for (const file of await collectGeneratedContractFiles(path.join(packageRoot, "build"))) { + assert.doesNotMatch( + await readFile(file, "utf8"), + /["']@gravity-ui\/graph\//, + `React artifact ${file} must reference the public core entrypoint, including inferred declaration types.` + ); + } + assert.match(publicStyles, /\.graph-block-container\b/); + assert.match(publicStyles, /\.graph-block-anchor\b/); + assert.doesNotMatch(publicStyles, /\.devtools-ruler-bg\b/); + } else { + assert.match(publicStyles, /\.layer\b/); + assert.match(publicStyles, /\.devtools-ruler-bg\b/); + assert.doesNotMatch(publicStyles, /\.(?:graph-wrapper|graph-block-container|graph-block-anchor)\b/); + for (const file of await collectGeneratedContractFiles(path.join(packageRoot, "build"))) { + assert.doesNotMatch( + await readFile(file, "utf8"), + /(?:from\s+|import\s*\(|require\s*\()["'](?:react(?:-dom)?(?:\/[^"']*)?|@gravity-ui\/graph-react)["']/ + ); + } + } } diff --git a/packages/graph/tests/package-contract/checks/browser.mjs b/tests/package-contract/checks/browser.mjs similarity index 100% rename from packages/graph/tests/package-contract/checks/browser.mjs rename to tests/package-contract/checks/browser.mjs diff --git a/packages/graph/tests/package-contract/checks/runtime.mjs b/tests/package-contract/checks/runtime.mjs similarity index 81% rename from packages/graph/tests/package-contract/checks/runtime.mjs rename to tests/package-contract/checks/runtime.mjs index cfe2f005..3c886f71 100644 --- a/packages/graph/tests/package-contract/checks/runtime.mjs +++ b/tests/package-contract/checks/runtime.mjs @@ -14,7 +14,9 @@ const nativeImportProbes = { } `, react: ` - const react = await import("@gravity-ui/graph/react"); + const react = await import("@gravity-ui/graph-react"); + const core = await import("@gravity-ui/graph"); + if (react.MultipointConnection !== core.MultipointConnection) throw new Error("React has a second core connection class."); if (typeof react.GraphCanvas !== "function") throw new Error("React entrypoint does not export GraphCanvas."); `, playwright: ` @@ -75,13 +77,21 @@ async function checkSignalInterop(consumerDirectory) { async function checkReactIsAbsent(consumerDirectory) { const probe = ` + for (const specifier of ["react", "react-dom", "@gravity-ui/graph-react", "elkjs"]) { + try { + await import(specifier); + } catch (error) { + if (error?.code === "ERR_MODULE_NOT_FOUND") continue; + throw error; + } + throw new Error("The vanilla consumer unexpectedly resolves " + specifier); + } try { - await import("react"); + await import("@gravity-ui/graph/react"); + throw new Error("The removed React subpath is still exported."); } catch (error) { - if (error?.code === "ERR_MODULE_NOT_FOUND") process.exit(0); - throw error; + if (error?.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") throw error; } - throw new Error("The vanilla consumer unexpectedly resolves React."); `; console.log("\n[package-contract] Confirming React is absent from the vanilla project..."); diff --git a/packages/graph/tests/package-contract/checks/types.mjs b/tests/package-contract/checks/types.mjs similarity index 69% rename from packages/graph/tests/package-contract/checks/types.mjs rename to tests/package-contract/checks/types.mjs index 022f5749..824e4c2a 100644 --- a/packages/graph/tests/package-contract/checks/types.mjs +++ b/tests/package-contract/checks/types.mjs @@ -1,12 +1,14 @@ import { run } from "../utils.mjs"; -export async function checkTarballTypes({ packageRoot, tarballPath }) { - console.log("\n[package-contract] Checking the root and React ESM type contracts with ATTW..."); - await run( - "pnpm", - ["exec", "attw", tarballPath, "--profile", "esm-only", "--entrypoints", ".", "./react", "--no-emoji", "--no-color"], - { cwd: packageRoot } - ); +export async function checkTarballTypes({ packageRoot, tarballPath, reactTarballPath }) { + console.log("\n[package-contract] Checking the core and React ESM type contracts with ATTW..."); + for (const artifact of [tarballPath, reactTarballPath]) { + await run( + "pnpm", + ["exec", "attw", artifact, "--profile", "esm-only", "--entrypoints", ".", "--no-emoji", "--no-color"], + { cwd: packageRoot } + ); + } console.log("\n[package-contract] Checking the Playwright ESM and CommonJS type contracts with ATTW..."); await run( diff --git a/packages/graph/tests/package-contract/fixtures/apps/react/app.css b/tests/package-contract/fixtures/apps/react/app.css similarity index 100% rename from packages/graph/tests/package-contract/fixtures/apps/react/app.css rename to tests/package-contract/fixtures/apps/react/app.css diff --git a/packages/graph/tests/package-contract/fixtures/apps/react/app.tsx b/tests/package-contract/fixtures/apps/react/app.tsx similarity index 62% rename from packages/graph/tests/package-contract/fixtures/apps/react/app.tsx rename to tests/package-contract/fixtures/apps/react/app.tsx index 74503725..6474f9cb 100644 --- a/packages/graph/tests/package-contract/fixtures/apps/react/app.tsx +++ b/tests/package-contract/fixtures/apps/react/app.tsx @@ -1,9 +1,10 @@ import React, { useLayoutEffect } from "react"; import { createRoot } from "react-dom/client"; -import { Graph, GraphState, type TBlock } from "@gravity-ui/graph"; -import { GraphBlock, GraphCanvas, useGraph, useGraphEvent } from "@gravity-ui/graph/react"; +import { Graph, GraphState, Layer, type TBlock } from "@gravity-ui/graph"; +import { GraphBlock, GraphCanvas, GraphPortal, useGraph, useGraphEvent } from "@gravity-ui/graph-react"; import "@gravity-ui/graph/styles.css"; +import "@gravity-ui/graph-react/styles.css"; import "../../shared/base.css"; import "./app.css"; @@ -23,6 +24,7 @@ const blocks = [ function ReactGraph() { const { graph, setEntities, start } = useGraph({ settings: {} }); + if (!(graph instanceof Graph)) throw new Error("useGraph created a duplicate core runtime."); useLayoutEffect(() => { setEntities({ blocks, connections: [] }); @@ -45,7 +47,18 @@ function ReactGraph() { ); - return ; + return ( + + + {(layer, portalGraph) => { + if (!(layer instanceof Layer) || portalGraph !== graph) { + throw new Error("The React portal does not share the application's core runtime."); + } + return
Shared core layer
; + }} +
+
+ ); } const root = document.querySelector("#react-root"); diff --git a/packages/graph/tests/package-contract/fixtures/apps/react/graph.pw.ts b/tests/package-contract/fixtures/apps/react/graph.pw.ts similarity index 87% rename from packages/graph/tests/package-contract/fixtures/apps/react/graph.pw.ts rename to tests/package-contract/fixtures/apps/react/graph.pw.ts index f97814b0..2cd13895 100644 --- a/packages/graph/tests/package-contract/fixtures/apps/react/graph.pw.ts +++ b/tests/package-contract/fixtures/apps/react/graph.pw.ts @@ -12,4 +12,5 @@ test("installed React entrypoint renders with public styles", async ({ page }) = await graph.waitForReady(); await expect(page.getByTestId("react-block-react-source")).toHaveText("React source"); + await expect(page.getByTestId("react-portal")).toHaveText("Shared core layer"); }); diff --git a/packages/graph/tests/package-contract/fixtures/apps/react/index.html b/tests/package-contract/fixtures/apps/react/index.html similarity index 100% rename from packages/graph/tests/package-contract/fixtures/apps/react/index.html rename to tests/package-contract/fixtures/apps/react/index.html diff --git a/packages/graph/tests/package-contract/fixtures/apps/react/tsconfig.json b/tests/package-contract/fixtures/apps/react/tsconfig.json similarity index 100% rename from packages/graph/tests/package-contract/fixtures/apps/react/tsconfig.json rename to tests/package-contract/fixtures/apps/react/tsconfig.json diff --git a/packages/graph/tests/package-contract/fixtures/apps/vanilla/app.ts b/tests/package-contract/fixtures/apps/vanilla/app.ts similarity index 100% rename from packages/graph/tests/package-contract/fixtures/apps/vanilla/app.ts rename to tests/package-contract/fixtures/apps/vanilla/app.ts diff --git a/packages/graph/tests/package-contract/fixtures/apps/vanilla/graph.pw.ts b/tests/package-contract/fixtures/apps/vanilla/graph.pw.ts similarity index 100% rename from packages/graph/tests/package-contract/fixtures/apps/vanilla/graph.pw.ts rename to tests/package-contract/fixtures/apps/vanilla/graph.pw.ts diff --git a/packages/graph/tests/package-contract/fixtures/apps/vanilla/index.html b/tests/package-contract/fixtures/apps/vanilla/index.html similarity index 100% rename from packages/graph/tests/package-contract/fixtures/apps/vanilla/index.html rename to tests/package-contract/fixtures/apps/vanilla/index.html diff --git a/packages/graph/tests/package-contract/fixtures/apps/vanilla/tsconfig.json b/tests/package-contract/fixtures/apps/vanilla/tsconfig.json similarity index 100% rename from packages/graph/tests/package-contract/fixtures/apps/vanilla/tsconfig.json rename to tests/package-contract/fixtures/apps/vanilla/tsconfig.json diff --git a/packages/graph/tests/package-contract/fixtures/shared/base.css b/tests/package-contract/fixtures/shared/base.css similarity index 100% rename from packages/graph/tests/package-contract/fixtures/shared/base.css rename to tests/package-contract/fixtures/shared/base.css diff --git a/packages/graph/tests/package-contract/fixtures/shared/playwright.config.ts b/tests/package-contract/fixtures/shared/playwright.config.ts similarity index 100% rename from packages/graph/tests/package-contract/fixtures/shared/playwright.config.ts rename to tests/package-contract/fixtures/shared/playwright.config.ts diff --git a/packages/graph/tests/package-contract/fixtures/shared/server.mjs b/tests/package-contract/fixtures/shared/server.mjs similarity index 100% rename from packages/graph/tests/package-contract/fixtures/shared/server.mjs rename to tests/package-contract/fixtures/shared/server.mjs diff --git a/packages/graph/tests/package-contract/fixtures/shared/tsconfig.json b/tests/package-contract/fixtures/shared/tsconfig.json similarity index 100% rename from packages/graph/tests/package-contract/fixtures/shared/tsconfig.json rename to tests/package-contract/fixtures/shared/tsconfig.json diff --git a/packages/graph/tests/package-contract/fixtures/types/node-cjs-playwright/index.cts b/tests/package-contract/fixtures/types/node-cjs-playwright/index.cts similarity index 100% rename from packages/graph/tests/package-contract/fixtures/types/node-cjs-playwright/index.cts rename to tests/package-contract/fixtures/types/node-cjs-playwright/index.cts diff --git a/packages/graph/tests/package-contract/fixtures/types/node-cjs-playwright/tsconfig.json b/tests/package-contract/fixtures/types/node-cjs-playwright/tsconfig.json similarity index 100% rename from packages/graph/tests/package-contract/fixtures/types/node-cjs-playwright/tsconfig.json rename to tests/package-contract/fixtures/types/node-cjs-playwright/tsconfig.json diff --git a/packages/graph/tests/package-contract/fixtures/types/node-esm/index.mts b/tests/package-contract/fixtures/types/node-esm/index.mts similarity index 84% rename from packages/graph/tests/package-contract/fixtures/types/node-esm/index.mts rename to tests/package-contract/fixtures/types/node-esm/index.mts index d6781d50..3b97e836 100644 --- a/packages/graph/tests/package-contract/fixtures/types/node-esm/index.mts +++ b/tests/package-contract/fixtures/types/node-esm/index.mts @@ -1,5 +1,5 @@ import { ESchedulerPriority, Graph, debounce, schedule, throttle } from "@gravity-ui/graph"; -import { GraphCanvas, useElk } from "@gravity-ui/graph/react"; +import { GraphCanvas, useElk, useLayeredLayout, GraphLayer, GraphPortal } from "@gravity-ui/graph-react"; const removeScheduledTask = schedule(() => undefined, { priority: ESchedulerPriority.LOWEST, diff --git a/packages/graph/tests/package-contract/fixtures/types/node-esm/tsconfig.json b/tests/package-contract/fixtures/types/node-esm/tsconfig.json similarity index 100% rename from packages/graph/tests/package-contract/fixtures/types/node-esm/tsconfig.json rename to tests/package-contract/fixtures/types/node-esm/tsconfig.json diff --git a/packages/graph/tests/package-contract/fixtures/types/playwright-bundler/index.ts b/tests/package-contract/fixtures/types/playwright-bundler/index.ts similarity index 100% rename from packages/graph/tests/package-contract/fixtures/types/playwright-bundler/index.ts rename to tests/package-contract/fixtures/types/playwright-bundler/index.ts diff --git a/packages/graph/tests/package-contract/fixtures/types/playwright-bundler/tsconfig.json b/tests/package-contract/fixtures/types/playwright-bundler/tsconfig.json similarity index 100% rename from packages/graph/tests/package-contract/fixtures/types/playwright-bundler/tsconfig.json rename to tests/package-contract/fixtures/types/playwright-bundler/tsconfig.json diff --git a/packages/graph/tests/package-contract/run.mjs b/tests/package-contract/run.mjs similarity index 69% rename from packages/graph/tests/package-contract/run.mjs rename to tests/package-contract/run.mjs index 0ff22664..517b6a88 100644 --- a/packages/graph/tests/package-contract/run.mjs +++ b/tests/package-contract/run.mjs @@ -10,17 +10,35 @@ import { checkConsumerTypes, checkTarballTypes } from "./checks/types.mjs"; import { run } from "./utils.mjs"; const fixturesDirectory = fileURLToPath(new URL("./fixtures", import.meta.url)); -const packageRoot = fileURLToPath(new URL("../../", import.meta.url)); -const workspaceRoot = fileURLToPath(new URL("../../../../", import.meta.url)); +const workspaceRoot = fileURLToPath(new URL("../../", import.meta.url)); +const packageRoot = path.join(workspaceRoot, "packages/graph"); +const reactPackageRoot = path.join(workspaceRoot, "packages/graph-react"); const temporaryDirectory = await mkdtemp(path.join(tmpdir(), "gravity-graph-package-contract-")); const requestedTarballPath = process.env.PACKAGE_CONTRACT_TARBALL_PATH; const defaultTarballPath = path.join(temporaryDirectory, "tarballs", "gravity-ui-graph.tgz"); -const tarballPath = requestedTarballPath ? path.resolve(requestedTarballPath) : defaultTarballPath; +const testingReactPackage = process.argv[2] === "graph-react"; +const workspaceTarballs = JSON.parse(process.env.PACKAGE_CONTRACT_WORKSPACE_TARBALLS || "{}"); +const suppliedCoreTarball = testingReactPackage ? workspaceTarballs["@gravity-ui/graph"] : undefined; +const tarballPath = suppliedCoreTarball + ? path.resolve(suppliedCoreTarball) + : requestedTarballPath && !testingReactPackage + ? path.resolve(requestedTarballPath) + : defaultTarballPath; +const reactTarballPath = + requestedTarballPath && testingReactPackage + ? path.resolve(requestedTarballPath) + : path.join(temporaryDirectory, "tarballs", "gravity-ui-graph-react.tgz"); +const reactStaleBuildSentinelPath = path.join(reactPackageRoot, "build", "package-contract-stale-sentinel.txt"); const staleBuildSentinelPath = path.join(packageRoot, "build", "package-contract-stale-sentinel.txt"); const consumerNames = ["vanilla", "react"]; async function getInstalledVersion(packageName) { - const manifestPath = path.join(packageRoot, "node_modules", ...packageName.split("/"), "package.json"); + const owner = ["react", "react-dom", "@types/react", "@types/react-dom"].includes(packageName) + ? reactPackageRoot + : packageName === "@preact/signals-core" + ? packageRoot + : workspaceRoot; + const manifestPath = path.join(owner, "node_modules", ...packageName.split("/"), "package.json"); const manifest = JSON.parse(await readFile(manifestPath, "utf8")); if (!manifest.version) { @@ -34,6 +52,7 @@ async function runConsumer({ name, manifest, expectedVersion, + expectedReactVersion, typecheckConfigs, entryPoint, nativeImports, @@ -51,6 +70,7 @@ async function runConsumer({ }); await checkInstalledArtifact(consumerDirectory, expectedVersion); + if (expectedReactVersion) await checkInstalledArtifact(consumerDirectory, expectedReactVersion, true); await checkRuntimeConsumer({ consumerDirectory, entrypoints: nativeImports, @@ -73,9 +93,22 @@ async function runConsumer({ try { const graphManifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8")); const expectedVersion = graphManifest.version; + const reactManifest = JSON.parse(await readFile(path.join(reactPackageRoot, "package.json"), "utf8")); await mkdir(path.dirname(tarballPath), { recursive: true }); - await buildAndPackArtifact({ packageRoot, staleBuildSentinelPath, tarballPath }); - await checkTarballTypes({ packageRoot, tarballPath }); + await mkdir(path.dirname(reactTarballPath), { recursive: true }); + if (suppliedCoreTarball) { + // React declarations compile against the workspace core build; consumers use the supplied exact tarball. + await run("pnpm", ["run", "build"], { cwd: packageRoot }); + } else { + await buildAndPackArtifact({ packageRoot, staleBuildSentinelPath, tarballPath }); + } + await buildAndPackArtifact({ + packageRoot: reactPackageRoot, + staleBuildSentinelPath: reactStaleBuildSentinelPath, + tarballPath: reactTarballPath, + react: true, + }); + await checkTarballTypes({ packageRoot, tarballPath, reactTarballPath }); const workspaceManifest = JSON.parse(await readFile(path.join(workspaceRoot, "package.json"), "utf8")); if (!workspaceManifest.packageManager) { @@ -142,11 +175,13 @@ try { await runConsumer({ name: "react", expectedVersion, + expectedReactVersion: reactManifest.version, manifest: { ...commonManifest, name: "gravity-graph-installed-react-consumer", dependencies: { ...commonManifest.dependencies, + "@gravity-ui/graph-react": `file:${reactTarballPath}`, react: reactVersion, "react-dom": reactDomVersion, }, @@ -163,10 +198,11 @@ try { console.log("\n[package-contract] Packed package contract passed."); } catch (error) { - await preserveBrowserArtifacts({ consumerNames, packageRoot, temporaryDirectory }); + await preserveBrowserArtifacts({ consumerNames, packageRoot: workspaceRoot, temporaryDirectory }); throw error; } finally { await rm(staleBuildSentinelPath, { force: true }); + await rm(reactStaleBuildSentinelPath, { force: true }); if (process.env.KEEP_PACKAGE_CONTRACT_TMP === "1") { console.log(`\n[package-contract] Preserved temporary projects at ${temporaryDirectory}`); diff --git a/packages/graph/tests/package-contract/utils.mjs b/tests/package-contract/utils.mjs similarity index 100% rename from packages/graph/tests/package-contract/utils.mjs rename to tests/package-contract/utils.mjs