diff --git a/.github/workflows/ci-sync.yml b/.github/workflows/ci-sync.yml index 06e2bf5a5..bea7a04f8 100644 --- a/.github/workflows/ci-sync.yml +++ b/.github/workflows/ci-sync.yml @@ -1,21 +1,14 @@ -name: CI - Surface Sync +name: CI - Cross-Repo Sync on: pull_request: branches: - main - development - paths: - - 'src/frontend/**' - - 'src/middleware/shared/**' - - 'src/backend/shared/**' - - 'src/__architecture__/**' - - 'scripts/compare-surfaces.py' - - '.github/workflows/ci-sync.yml' jobs: surface-sync: - name: Shared Surface Sync Check + name: Shared Surface Sync runs-on: ubuntu-latest steps: @@ -37,8 +30,22 @@ jobs: with: path: editor - - name: Checkout web repo (target branch) + - name: Check for shared surface changes if: steps.token-check.outputs.skip == 'false' + id: filter + run: | + cd editor + git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1 + CHANGED=$(git diff --name-only FETCH_HEAD HEAD) + if echo "$CHANGED" | grep -qE '^(src/frontend/|src/middleware/shared/|src/backend/shared/|src/__architecture__/)'; then + echo "shared=true" >> "$GITHUB_OUTPUT" + else + echo "shared=false" >> "$GITHUB_OUTPUT" + echo "No shared surface changes detected. Skipping sync check." + fi + + - name: Checkout web repo (target branch) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' uses: actions/checkout@v4 with: repository: Autonomy-Logic/openplc-web @@ -49,7 +56,7 @@ jobs: id: checkout-web-target - name: Checkout web repo (main fallback) - if: steps.token-check.outputs.skip == 'false' && steps.checkout-web-target.outcome == 'failure' + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' && steps.checkout-web-target.outcome == 'failure' uses: actions/checkout@v4 with: repository: Autonomy-Logic/openplc-web @@ -58,12 +65,12 @@ jobs: token: ${{ secrets.CROSS_REPO_TOKEN }} - name: Warn about branch fallback - if: steps.token-check.outputs.skip == 'false' && steps.checkout-web-target.outcome == 'failure' + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' && steps.checkout-web-target.outcome == 'failure' run: | echo "::warning::Web repo does not have branch '${{ github.event.pull_request.base.ref }}'. Fell back to 'main'." - name: Compare surfaces against target branch - if: steps.token-check.outputs.skip == 'false' + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' id: compare-target run: | set +e @@ -91,7 +98,7 @@ jobs: fi - name: Find matching web PRs - if: steps.token-check.outputs.skip == 'false' && steps.compare-target.outputs.match == 'False' + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' && steps.compare-target.outputs.match == 'False' id: find-prs env: GH_TOKEN: ${{ secrets.CROSS_REPO_TOKEN }} @@ -118,7 +125,7 @@ jobs: fi - name: Check web PRs for sync - if: steps.token-check.outputs.skip == 'false' && steps.compare-target.outputs.match == 'False' && steps.find-prs.outputs.pr_count != '0' + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' && steps.compare-target.outputs.match == 'False' && steps.find-prs.outputs.pr_count != '0' id: check-prs run: | PR_JSON=$(cat /tmp/web-prs.json) @@ -164,7 +171,7 @@ jobs: done - name: Report result - if: always() && steps.token-check.outputs.skip == 'false' + if: always() && steps.token-check.outputs.skip == 'false' && steps.filter.outputs.shared == 'true' run: | MATCH="${{ steps.compare-target.outputs.match }}" MATCHED_PR="${{ steps.check-prs.outputs.matched_pr }}" @@ -192,3 +199,219 @@ jobs: " exit 1 fi + + deps-sync: + name: Shared Dependencies Sync + runs-on: ubuntu-latest + + steps: + - name: Check for cross-repo token + id: token-check + run: | + if [ -z "$TOKEN" ]; then + echo "::warning::CROSS_REPO_TOKEN is not set. Skipping sync check (expected on fork PRs)." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + env: + TOKEN: ${{ secrets.CROSS_REPO_TOKEN }} + + - name: Checkout editor repo + if: steps.token-check.outputs.skip == 'false' + uses: actions/checkout@v4 + with: + path: editor + + - name: Check for package.json changes + if: steps.token-check.outputs.skip == 'false' + id: filter + run: | + cd editor + git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1 + CHANGED=$(git diff --name-only FETCH_HEAD HEAD) + if echo "$CHANGED" | grep -qE '^package\.json$'; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No package.json changes detected. Skipping dependency sync check." + fi + + - name: Checkout web repo (head branch) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' + uses: actions/checkout@v4 + with: + repository: Autonomy-Logic/openplc-web + ref: ${{ github.head_ref }} + path: web + token: ${{ secrets.CROSS_REPO_TOKEN }} + continue-on-error: true + id: checkout-web-head + + - name: Checkout web repo (base branch) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: Autonomy-Logic/openplc-web + ref: ${{ github.event.pull_request.base.ref }} + path: web + token: ${{ secrets.CROSS_REPO_TOKEN }} + continue-on-error: true + id: checkout-web-base + + - name: Checkout web repo (main fallback) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure' && steps.checkout-web-base.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: Autonomy-Logic/openplc-web + ref: main + path: web + token: ${{ secrets.CROSS_REPO_TOKEN }} + + - name: Compare shared dependencies + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' + run: python3 editor/scripts/compare-dependencies.py --web-root web --editor-root editor + + script-sync: + name: Comparison Script Sync + runs-on: ubuntu-latest + + steps: + - name: Check for cross-repo token + id: token-check + run: | + if [ -z "$TOKEN" ]; then + echo "::warning::CROSS_REPO_TOKEN is not set. Skipping sync check (expected on fork PRs)." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + env: + TOKEN: ${{ secrets.CROSS_REPO_TOKEN }} + + - name: Checkout editor repo + if: steps.token-check.outputs.skip == 'false' + uses: actions/checkout@v4 + with: + path: editor + + - name: Check for compare script changes + if: steps.token-check.outputs.skip == 'false' + id: filter + run: | + cd editor + git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1 + CHANGED=$(git diff --name-only FETCH_HEAD HEAD) + if echo "$CHANGED" | grep -qE '^scripts/compare-surfaces\.py$'; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No compare script changes detected. Skipping script sync check." + fi + + - name: Checkout web repo (head branch) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' + uses: actions/checkout@v4 + with: + repository: Autonomy-Logic/openplc-web + ref: ${{ github.head_ref }} + path: web + token: ${{ secrets.CROSS_REPO_TOKEN }} + continue-on-error: true + id: checkout-web-head + + - name: Checkout web repo (base branch) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: Autonomy-Logic/openplc-web + ref: ${{ github.event.pull_request.base.ref }} + path: web + token: ${{ secrets.CROSS_REPO_TOKEN }} + continue-on-error: true + id: checkout-web-base + + - name: Checkout web repo (main fallback) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure' && steps.checkout-web-base.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: Autonomy-Logic/openplc-web + ref: main + path: web + token: ${{ secrets.CROSS_REPO_TOKEN }} + + - name: Compare SURFACES definitions + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' + run: python3 editor/scripts/compare-surface-definitions.py --web-root web --editor-root editor + + tooling-sync: + name: Tooling Configuration Sync + runs-on: ubuntu-latest + + steps: + - name: Check for cross-repo token + id: token-check + run: | + if [ -z "$TOKEN" ]; then + echo "::warning::CROSS_REPO_TOKEN is not set. Skipping sync check (expected on fork PRs)." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + env: + TOKEN: ${{ secrets.CROSS_REPO_TOKEN }} + + - name: Checkout editor repo + if: steps.token-check.outputs.skip == 'false' + uses: actions/checkout@v4 + with: + path: editor + + - name: Check for tooling changes + if: steps.token-check.outputs.skip == 'false' + id: filter + run: | + cd editor + git fetch origin "${{ github.event.pull_request.base.ref }}" --depth=1 + CHANGED=$(git diff --name-only FETCH_HEAD HEAD) + if echo "$CHANGED" | grep -qE '^(\.prettierrc$|eslint\.config\.|tsconfig|scripts/)'; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No tooling changes detected. Skipping tooling sync check." + fi + + - name: Checkout web repo (head branch) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' + uses: actions/checkout@v4 + with: + repository: Autonomy-Logic/openplc-web + ref: ${{ github.head_ref }} + path: web + token: ${{ secrets.CROSS_REPO_TOKEN }} + continue-on-error: true + id: checkout-web-head + + - name: Checkout web repo (base branch) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: Autonomy-Logic/openplc-web + ref: ${{ github.event.pull_request.base.ref }} + path: web + token: ${{ secrets.CROSS_REPO_TOKEN }} + continue-on-error: true + id: checkout-web-base + + - name: Checkout web repo (main fallback) + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' && steps.checkout-web-head.outcome == 'failure' && steps.checkout-web-base.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: Autonomy-Logic/openplc-web + ref: main + path: web + token: ${{ secrets.CROSS_REPO_TOKEN }} + + - name: Compare tooling configuration + if: steps.token-check.outputs.skip == 'false' && steps.filter.outputs.changed == 'true' + run: python3 editor/scripts/compare-tooling.py --web-root web --editor-root editor --github-annotations diff --git a/CLAUDE.md b/CLAUDE.md index 9dc815965..4c65cb6b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,130 +1,296 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code when working with the OpenPLC Editor codebase. + +## Project Overview + +OpenPLC Editor is an **Electron + React** desktop IDE for programming PLCs using IEC 61131-3 languages (Structured Text, Ladder Diagram, Function Block Diagram, Instruction List) plus Python and C++ extensions. ## Build & Development Commands +**Package manager:** `npm` (not pnpm) + ```bash -# Install dependencies -npm install +npm install # Install deps + download binaries + build DLL cache +npm run dev # Full dev mode (main + renderer + Electron, port 1313) +npm run build # Production build (main + renderer) +npm run build:main # Electron main process only +npm run build:renderer # React renderer only +npm run build:dll # Webpack DLL cache for faster dev rebuilds +npm run package # Build + create distributable (electron-builder) + +npm run lint # ESLint check +npm run lint:fix # Auto-fix lint issues +npm run format # Prettier formatting + +npm run test # Jest with coverage (enforced thresholds) +npm run test:watch # Jest watch mode (no coverage) +npm run test:e2e # Playwright E2E tests + +npm run validate:arch # Architecture layer dependency validation +``` + +## Architecture + +### Layer Overview + +``` +src/ +├── main/ # Electron main process (Node.js) +├── frontend/ # React UI layer (renderer process) +│ ├── components/ # Atomic Design: _atoms, _molecules, _organisms, _features, _templates +│ ├── store/ # Zustand store (19 slices) +│ ├── hooks/ # Custom React hooks +│ ├── services/ # Business logic and side effects +│ ├── utils/ # Domain utilities (PLC, graphical, debug, formatters) +│ ├── data/ # Static data (function libraries, block definitions) +│ ├── locales/ # i18next translations +│ └── assets/ # Images, icons +├── backend/ +│ ├── editor/ # Main process modules (compiler, hardware, modbus, websocket, services) +│ └── shared/ # Platform-agnostic utilities (XML generation, project parsing, simulator) +├── middleware/ # Ports & Adapters layer +│ ├── shared/ +│ │ ├── ports/ # Port interfaces (platform-agnostic contracts) +│ │ └── providers/ # PlatformContext (React Context for dependency injection) +│ └── adapters/ +│ └── editor/ # Electron-specific port implementations (IPC bridge) +├── types/ # Shared IPC type contracts +└── __architecture__/ # Layer dependency validation script +``` + +### Ports & Adapters Pattern + +The codebase uses **dependency inversion** via port interfaces. Frontend code never imports backend or Electron APIs directly. All platform-specific behavior flows through ports. + +**Port interfaces** (`src/middleware/shared/ports/`): + +| Port | Responsibility | +|------|---------------| +| `CompilerPort` | PLC compilation pipeline | +| `RuntimePort` | Remote PLC runtime control (login, start/stop, status) | +| `DebuggerPort` | Debug protocol (read/write variables, MD5 verification) | +| `SimulatorPort` | Built-in AVR simulator | +| `ProjectPort` | Project CRUD operations | +| `DevicePort` | Board discovery, serial ports | +| `OrchestratorPort` | Device fleet management (web-only) | +| `SystemPort` | Platform services (store, logging, external links) | +| `WindowPort` | Native window management | +| `AcceleratorPort` | Keyboard shortcuts | +| `ThemePort` | Theme detection and switching | +| `VersionControlPort` | Git operations | +| `AIPort` | AI assistant (optional) | + +**Consuming ports** in components: +```typescript +import { useCompiler, useRuntime, useCapabilities } from '@root/middleware/shared/providers' + +function MyComponent() { + const compiler = useCompiler() + const capabilities = useCapabilities() + + if (capabilities.hasLocalSerialPorts) { /* ... */ } + await compiler.compileProgram(args, onProgress) +} +``` + +**Wiring** happens at the app root (`src/App.tsx`): +```typescript +import { editorPorts } from './middleware/editor-platform' +... +``` + +**Editor adapters** (`src/middleware/adapters/editor/`) implement ports by calling `window.bridge.*` (Electron IPC). The web repo has its own adapters using HTTP/WebRTC instead. + +### Architecture Layer Rules + +Enforced by `npm run validate:arch`. Source dependencies point inward only: -# Development mode (hot reload for both main and renderer) -npm run dev +``` +assets -> utils, data +utils -> utils, ports, data, assets +data -> ports, utils, data, assets +types -> store, utils +ports -> utils, ports +provider -> ports, utils +adapters -> ports, provider, utils, backend-shared, backend-web, store, assets +backend-shared -> ports, utils, types +store -> ports, provider, store, utils, assets +services -> ports, provider, store, services, utils, assets +hooks -> ports, provider, store, hooks, services, utils, assets +components -> ports, provider, store, hooks, services, components, data, utils, assets +``` + +### IPC Communication -# Build for production -npm run build +Main and renderer processes communicate through typed IPC bridges: -# Build individual processes -npm run build:main # Electron main process -npm run build:renderer # React renderer process -npm run build:dll # Webpack DLL for faster dev builds +- **Main bridge:** `src/main/modules/ipc/main.ts` — `MainProcessBridge` registers 50+ `ipcMain.handle()` handlers +- **Renderer bridge:** `src/main/modules/ipc/renderer.ts` — async wrappers calling `ipcRenderer.invoke()` +- **Preload:** `src/main/modules/preload/preload.ts` — exposes `window.bridge` via `contextBridge` -# Package for distribution -npm run package +### State Management (Zustand) -# Linting and formatting -npm run lint # ESLint check -npm run lint:fix # Auto-fix lint issues -npm run format # Prettier formatting +Single store composed of 19 slices (`src/frontend/store/`), accessed via auto-generated selector hooks: -# Testing -npm run test # Jest with coverage -npm run test:unit # Jest watch mode -npm run test:e2e # Playwright E2E tests +```typescript +import { useOpenPLCStore } from '@root/frontend/store' -# Native module rebuild (after node version changes) -npm run rebuild +const pous = useOpenPLCStore((s) => s.project.data.pous) +const createPou = useOpenPLCStore((s) => s.projectActions.createPou) ``` -## Architecture Overview +**Slice pattern** — each slice has three files: -This is an Electron + React application for PLC (Programmable Logic Controller) programming using IEC 61131-3 standards. +- `types.ts` — state shape + action signatures +- `slice.ts` — implementation using Immer's `produce()` for immutable updates +- `index.ts` — re-exports -### Process Architecture +**Key slices:** + +| Slice | Purpose | +|-------|---------| +| `project` | PLC project structure (POUs, data types, servers, devices) | +| `device` | Board config, pin mappings, runtime connection | +| `editor` | Editor models (discriminated union: textual, graphical, device, etc.) | +| `tabs` | Open file tabs | +| `workspace` | UI viewport state, debug values | +| `ladder` | Ladder diagram rungs per POU | +| `fbd` | FBD flow graphs per POU | +| `console` | Log output | +| `library` | System + user function block libraries | +| `file` | File save states (dirty tracking) | +| `ai`, `clipboard`, `history`, `modal`, `search`, `shared`, `version-control`, `webrtc` | Supporting features | + +**Conventions:** +- Actions are grouped under a `*Actions` namespace (e.g., `projectActions`, `deviceActions`) +- Complex actions return `{ ok: boolean; message?: string }` response objects +- State is never mutated directly — always use `produce()` from Immer +- Direct state access outside React: `openPLCStoreBase.getState()` + +### Component Organization (Atomic Design) ``` -Main Process (Node.js) Renderer Process (React) -├── src/main/main.ts ├── src/renderer/index.tsx -├── modules/ ├── components/ -│ ├── compiler/ ├── screens/ -│ ├── ipc/ │ ├── StartScreen/ -│ ├── modbus/ │ └── WorkspaceScreen/ -│ ├── preload/ ├── store/ (Zustand slices) -│ └── websocket/ └── hooks/ -├── services/ -│ ├── project-service/ -│ ├── pou-service/ -│ └── user-service/ +src/frontend/components/ +├── _atoms/ # Primitive UI elements (buttons, inputs, select, checkbox, table) +├── _molecules/ # Composed patterns (menu-bar, modal, variables-table, tabs) +├── _organisms/ # Complex sections (explorer, panel, console, debugger, navigation) +├── _features/ # Context-specific feature bundles +│ ├── [app]/ # App-level (loading overlay, toast) +│ ├── [start]/ # Start screen (menu, new-project modal) +│ └── [workspace]/ # Workspace features +│ └── editor/ # Monaco, graphical (LD/FBD/SFC), device, server editors +├── _templates/ # Layout wrappers (app-layout, workspace-layout) +└── ui/ # Radix UI primitive wrappers ``` -### IPC Communication +### Navigation -Communication between main and renderer uses typed IPC bridges: -- **Main bridge:** `src/main/modules/ipc/main.ts` - Handler definitions -- **Renderer bridge:** `src/main/modules/ipc/renderer.ts` - Invocation wrappers -- **Preload:** `src/main/modules/preload/preload.ts` - Exposes `window.bridge` +There is **no URL-based router**. Navigation is tab-driven via the Zustand `tabs` + `editor` slices: -### State Management +1. `App.tsx` renders `StartScreen` (no project) or `WorkspaceScreen` (project loaded) +2. Opening a POU/resource creates a tab entry in the store +3. Clicking a tab sets the active `EditorModel` (discriminated union determines which editor renders) -Zustand store with 14 domain slices in `src/renderer/store/slices/`: -- `WorkspaceSlice`, `EditorSlice`, `TabsSlice` - UI state -- `FBDFlowSlice`, `LadderFlowSlice` - Visual programming editors -- `ProjectSlice`, `FileSlice`, `DeviceSlice` - Project data -- `ConsoleSlice`, `ModalSlice`, `SearchSlice` - Utilities +### Graphical Editors -### PLC Compilation Pipeline +- **Ladder Diagram (LD):** DnD Kit-based, rung structure with contacts/coils/blocks +- **Function Block Diagram (FBD):** @xyflow/react flow graph with custom node types (block, variable, connector, comment) +- **SFC:** @xyflow/react graph (sequential function charts) -The `CompilerModule` (`src/main/modules/compiler/`) orchestrates: -1. IEC 61131-3 XML parsing -2. Device/pin configuration generation -3. C/C++ code generation via `xml2st` and `iec2c` binaries -4. Arduino CLI integration for embedded targets -5. Modbus TCP/RTU configuration +Flow state is stored per-POU in dedicated slices (`ladder`, `fbd`). Flows must be relinked to current variables after variable table changes. -Platform-specific binaries in `/resources/bin/[platform]/[arch]/`. +### Compilation Pipeline -### Key Path Aliases (tsconfig.json) +Orchestrated by `CompilerModule` (`src/backend/editor/compiler/compiler-module.ts`): ``` -@root/* → ./src/* -@process:main/* → ./src/main/* -@process:renderer/* → ./src/renderer/* -@components/* → ./src/renderer/components/* -@utils/* → ./src/utils/* -@shared/* → ./src/shared/* -@hooks/* → ./src/renderer/hooks/* +PLCProjectData -> Preprocess POUs -> XML Generation -> xml2st -> iec2c -> C code + | + defines.h (pins, Modbus, MD5) + | + Arduino CLI / openplc-compiler -> firmware ``` +Platform-specific binaries in `/resources/bin/[platform]/[arch]/`. Board configs in `/resources/sources/boards/hals.json`. + +### Debugging + +- **Protocol:** Custom Modbus PDU (function codes 0x41-0x45) for variable read/write +- **Transports:** Modbus TCP, Modbus RTU, WebSocket, or virtual serial (simulator) +- **Simulator:** AVR8JS emulator (`src/backend/shared/simulator/`) emulates ATmega2560 +- **Flow:** Compile with debug symbols (.dbg file + MD5) -> connect debugger -> poll variables + ## Testing -- **Unit tests:** Jest with jsdom environment, test files use `*.test.ts(x)` or `*.spec.ts(x)` -- **E2E tests:** Playwright in `/e2e` directory, Chromium only -- **Mocks:** `configs/mocks/` for file stubs, `identity-obj-proxy` for CSS modules +- **Framework:** Jest + jsdom +- **Test files:** `*.test.ts(x)`, `*.spec.ts(x)`, or `__tests__/` directories +- **E2E:** Playwright (`/e2e`), Chromium only +- **Coverage thresholds** (100% functions/lines/statements required): + - `src/frontend/store/slices/` + - `src/frontend/utils/` + - `src/backend/shared/` + - `src/middleware/adapters/editor/` +- **Mocks:** `configs/mocks/` for file stubs; `identity-obj-proxy` for CSS modules + +When adding new code to covered directories, you must add corresponding tests to maintain 100% coverage. ## Code Style -- This is a TypeScript-first codebase. Use strict TypeScript patterns, proper typing, and avoid `any` types. +- TypeScript strict mode, avoid `any` types - ESLint flat config (`eslint.config.mjs`) with TypeScript strict type checking - Prettier: 120 char width, no semicolons, single quotes, trailing commas - Import sorting enforced via `simple-import-sort` plugin - Pre-commit hooks via Husky run lint-staged on `./src/**/*` +- Path alias: `@root/*` -> `./src/*` ## Key Technologies -- **Electron 35** / **React 18** / **TypeScript 5.4** -- **Monaco Editor** for code editing (Python LSP support) -- **Tailwind CSS** + **Radix UI** for styling -- **DND Kit** for drag-and-drop in visual editors +- **Electron 35** / **React 18** / **TypeScript** (target ES2022) +- **Webpack** (not Vite) with separate main/renderer/preload configs +- **Zustand 5** + **Immer** for state management +- **Monaco Editor** for code editing (ST, IL, Python, C++) +- **@xyflow/react 12** for FBD/SFC graphical editors +- **@dnd-kit** for drag-and-drop (tabs, ladder rungs) +- **Tailwind CSS 3** + **Radix UI** for styling +- **Zod** for schema validation +- **i18next** for internationalization +- **avr8js** for Arduino simulation +- **Axios** for HTTP requests - **Socket.io** for real-time communication -- **Modbus TCP/RTU** clients for industrial protocols - -## Debugging - -- When fixing UI flickering or rendering issues, always check for multiple potential causes: component re-renders, data changes, AND layout/sizing recalculations. Test fixes with the specific user-reported conditions (e.g., specific chart sizes, time ranges) before marking complete. -- For multi-step debugging tasks, verify each fix is working before moving to the next issue. Run the app and confirm the specific behavior is resolved. - -## Environment Requirements - -- Node.js >= 20.x < 24 -- npm >= 10.x -- Supported platforms: macOS, Windows, Linux (x64 & ARM64) +- **Winston** for structured logging (main process) +- **serialport** for serial communication + +## Important Patterns + +### When adding a new port: +1. Define the interface in `src/middleware/shared/ports/` +2. Add it to `PlatformPorts` in `src/middleware/shared/providers/types.ts` +3. Add a convenience hook in `src/middleware/shared/providers/platform-context.tsx` +4. Implement the editor adapter in `src/middleware/adapters/editor/` +5. Wire it in `src/middleware/editor-platform.ts` + +### When adding a new store slice: +1. Create `types.ts`, `slice.ts`, `index.ts` in `src/frontend/store/slices//` +2. Add the slice type to `RootState` union in `src/frontend/store/index.ts` +3. Spread the slice creator in `createOpenPLCStore()` +4. Add tests to maintain 100% coverage + +### When adding a new POU language or type: +1. Update project parser (`src/backend/shared/utils/parse-project-files.ts`) +2. Update serializer for save flow +3. Add editor component if graphical +4. Register in library system and project actions + +### When modifying graphical editors: +1. Flow state is stored separately from POU body during editing +2. Sync flows back to POU on save +3. Relink variables after variable table changes +4. Node IDs must be unique per flow + +## Environment + +- **Node.js:** >= 20.x < 24 +- **Dev server port:** 1313 +- **Supported platforms:** macOS, Windows, Linux (x64 & ARM64) +- **Binaries:** Auto-downloaded via `scripts/download-binaries.ts` during `npm install` diff --git a/README.md b/README.md index 1d6ead46c..8861e05c6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ In order to run the development version, clone the repository, and install depen You'll need the following tools: - [Git](https://git-scm.com/) -- [NodeJS](https://nodejs.org/en/download/), **x64**, version `>=20` +- [NodeJS](https://nodejs.org/en/download/), version `>=20 <24` ### Step by step diff --git a/docs/migration-state.md b/docs/migration-state.md deleted file mode 100644 index 07ca5e84a..000000000 --- a/docs/migration-state.md +++ /dev/null @@ -1,52 +0,0 @@ -# Migration State - -Tracks progress of the shared UI migration. Updated by the `/migrate` skill after each step. - -## Current Phase - -integration - -## Current Step - -31 - -## Step Log - -| Step | Phase | Description | Status | Date | -|------|-------|-------------|--------|------| -| 0 | planning | Migration tracker, port interfaces, PlatformProvider scaffolding | done | 2026-03-10 | -| 1 | architecture | Define clean architecture layers and create validation tests | done | 2026-03-10 | -| 2 | domain | Migrate shared domain types and pure utilities | done | 2026-03-10 | -| 3 | adapters | ThemePort adapter implementation | done | 2026-03-10 | -| 4 | adapters | SystemPort adapter implementation | done | 2026-03-10 | -| 5 | adapters | WindowPort adapter implementation | done | 2026-03-10 | -| 6 | adapters | AcceleratorPort adapter implementation | done | 2026-03-10 | -| 7 | adapters | DevicePort adapter implementation | done | 2026-03-10 | -| 8 | adapters | ProjectPort adapter implementation | done | 2026-03-10 | -| 9 | adapters | CompilerPort adapter implementation | done | 2026-03-10 | -| 10 | adapters | RuntimePort adapter implementation | done | 2026-03-10 | -| 11 | adapters | DebuggerPort adapter implementation | done | 2026-03-10 | -| 12 | adapters | SimulatorPort adapter implementation | done | 2026-03-10 | -| 13 | store | UI state slices (Workspace, Editor, Tabs, Modal, Search) | done | 2026-03-10 | -| 14 | store | Data state slices (Project, File, Library, Console, Shared) | done | 2026-03-10 | -| 15 | store | Visual editor slices (FBDFlow, LadderFlow) | done | 2026-03-11 | -| 16 | store | Platform state slices (Device, History, web-only) | done | 2026-03-11 | -| 17 | resources | Copy shared resources (styles, assets, locales, declarations) to src2/ | done | 2026-03-11 | -| 18 | components | Atoms batch 1 — shared identical components (revised: 23 kept, 17 divergent moved to step 22) | done | 2026-03-11 | -| 19 | architecture-rework | Restructure src2/ into frontend/middleware/backend three-layer architecture | done | 2026-03-11 | -| 20 | architecture-rework | Extract application logic from Zustand stores into backend/shared/ | done | 2026-03-11 | -| 21 | architecture-rework | Update comparison script to validate all byte-identical surfaces | done | 2026-03-11 | -| 22 | components | Atoms batch 2a — simple divergent atoms, generic-table-inputs, UI scroll-area | done | 2026-03-11 | -| 22b | components | Atoms batch 2b — graphical editor divergent atoms + platform-specific atoms | done | 2026-03-11 | -| 23 | components | Molecules batch 1 — shared identical (10 modules migrated, divergent deferred to step 24) | done | 2026-03-11 | -| 24 | components | Molecules batch 2a — divergent non-graphical-editor molecules | done | 2026-03-12 | -| 24b | components | Molecules batch 2b — graphical-editor divergent molecules (fbd/ladder utils, fbd-utils, rung, index) | done | 2026-03-12 | -| 25 | components | Organisms — shared (15 files: console, explorer, global-variables-editor, graphical-editor/ladder, navigation, panel, plc-logs, variables-code-editor, workspace-activity-bar) | done | 2026-03-12 | -| 26 | components | Organisms — platform-specific (port-dependent) | done | 2026-03-12 | -| 27 | components | Features — shared (45 files: toast, menu, new-project store/interval-model/first-step, graphical editor routing/SFC, device configuration + pin-mapping-table, monaco completion/configs/languages/themes/drag-and-drop, server barrel + opcua-server, create-element data-type-element, arrow-button-group, fbd/ladder block library, ladder coil/contact) | done | 2026-03-12 | -| 28 | components | Features — platform-specific (97 files: divergent reconciliation of search, create-element, device/board, monaco editor, data-type editor; web-only AI chat, debug-manager, loading-overlay, orchestrators, device elements; editor-only device-aware gating; new utils device.ts, formatters/POU.ts, data sources data-type.tsx) | done | 2026-03-13 | -| 29 | components | Templates, screens, and shared hooks | done | 2026-03-13 | -| 30 | components | Hooks migration — debug/AI/WebRTC hooks, services, utilities, simulator facade, architecture validator updates (637 byte-identical files) | done | 2026-03-13 | -| 31 | integration | App shell and routing — editor store-based routing, web TanStack Router with project loading | done | 2026-03-13 | -| 32 | integration | Production build configs | pending | | -| 33 | integration | Switchover and cleanup — remove src/ | pending | | diff --git a/docs/migration-tracker.md b/docs/migration-tracker.md deleted file mode 100644 index 0690d026a..000000000 --- a/docs/migration-tracker.md +++ /dev/null @@ -1,484 +0,0 @@ -# Shared UI Migration Tracker - -Base branches: `refactor/shared-ui-migration` on both repos -- Editor: `4a9f4001` (from `origin/development`) -- Web: `5130d60` (from `origin/development`) - -Legend: -- **S** = Shared (identical or <5% diff, pure UI) -- **D** = Divergent (>5% diff, needs reconciliation) -- **E** = Editor only -- **W** = Web only -- **P** = Platform-specific (needs port interface) -- Status: `[ ]` pending, `[~]` in progress, `[x]` migrated - ---- - -## 1. ATOMS (`_atoms/`) - -### Identical (copy as-is) -| # | Component | Status | -|---|-----------|--------| -| 1 | `accordion/index.tsx` | [x] S | -| 2 | `buttons/index.ts` | [x] S | -| 3 | `buttons/default/index.tsx` | [x] S | -| 4 | `buttons/window-control/index.tsx` | [x] S | -| 5 | `card/index.tsx` | [x] S | -| 6 | `dimensions-modal/array-dimensions-input/index.tsx` | [x] S | -| 7 | `graphical-editor/fbd/svg/connector-svg.tsx` | [x] S | -| 8 | `graphical-editor/fbd/svg/continuation-svg.tsx` | [x] S | -| 9 | `graphical-editor/fbd/svg/index.ts` | [x] S | -| 10 | `input/index.tsx` | [x] S | -| 11 | `label/index.tsx` | [x] S | -| 12 | `tab-list/index.tsx` | [x] S | -| 13 | `tooltip/index.tsx` | [x] S | -| 14 | `workspace-activity-bar/divider.tsx` | [x] S | - -### Near-identical (<5% diff, import paths and cosmetic only) -| # | Component | Status | -|---|-----------|--------| -| 15 | `buttons/activity-bar/index.tsx` | [x] S | -| 16 | `buttons/console/clear-console.tsx` | [x] S | -| 17 | `buttons/tables-actions/index.tsx` | [x] S | -| 18 | `file/index.tsx` | [x] S | -| 19 | `generic-table/index.tsx` | [x] S | -| 20 | `graphical-editor/fbd/handle.tsx` | [x] S | -| 21 | `react-flow/index.tsx` | [x] S | -| 22 | `table/index.tsx` | [x] S | -| 23 | `type-dropdown-selector/index.tsx` | [x] S | - -### Divergent (needs reconciliation) -| # | Component | Diff | Status | Notes | -|---|-----------|------|--------|-------| -| 24 | `checkbox/index.tsx` | 456B | [x] D | Reconciled: web superset (label, disabled, checked border) | -| 25 | `debug-tree-node/index.tsx` | 14 | [x] D | Reconciled: unified imports | -| 26 | `dimensions-modal/index.tsx` | 28 | [x] D | Reconciled: unified imports, PLCBaseType | -| 27 | `generic-data-type-table/index.tsx` | 258B | [x] D | Reconciled: web scroll wrapper | -| 28 | `generic-table-inputs/*` | varies | [x] D | Reconciled: editor superset (5 files), search via extractSearchQuery | -| 29 | `graphical-editor/autocomplete/index.tsx` | 63 | [ ] D | Reclassified: editor has triggerSubmit imperative handle | -| 30 | `graphical-editor/fbd/autocomplete/index.tsx` | 43 | [ ] D | Reclassified: different ID gen, error handling | -| 31 | `graphical-editor/fbd/block.tsx` | 7.4KB | [ ] D | Editor much larger | -| 32 | `graphical-editor/fbd/comment.tsx` | 1.4KB | [ ] D | | -| 33 | `graphical-editor/fbd/connection.tsx` | 2KB | [ ] D | | -| 34 | `graphical-editor/fbd/index.ts` | 14 | [ ] D | Reclassified: web uses buildNodes module | -| 35 | `graphical-editor/fbd/utils/index.ts` | - | [ ] D | Barrel for types.ts + utils.ts | -| 36 | `graphical-editor/fbd/utils/types.ts` | 1.2KB | [ ] D | Web much larger | -| 37 | `graphical-editor/fbd/utils/utils.ts` | 3.6KB | [ ] D | Web much larger | -| 38 | `graphical-editor/fbd/variable.tsx` | 2.9KB | [ ] D | | -| 39 | `graphical-editor/ladder/autocomplete/index.tsx` | 44 | [ ] D | Reclassified: different ID gen, error handling | -| 40 | `graphical-editor/ladder/block.tsx` | 10.6KB | [ ] D | Editor much larger | -| 41 | `graphical-editor/ladder/coil.tsx` | 5KB | [ ] D | | -| 42 | `graphical-editor/ladder/contact.tsx` | 4.3KB | [ ] D | | -| 43 | `graphical-editor/ladder/handle.tsx` | 16 | [ ] D | Reclassified: editor has extra props | -| 44 | `graphical-editor/ladder/index.ts` | 152 | [ ] D | Reclassified: web uses constants/buildNodes modules | -| 45 | `graphical-editor/ladder/mock-node.tsx` | 2.9KB | [ ] D | Editor much larger | -| 46 | `graphical-editor/ladder/parallel.tsx` | 3.8KB | [ ] D | Editor much larger | -| 47 | `graphical-editor/ladder/placeholder.tsx` | 2.4KB | [ ] D | Editor much larger | -| 48 | `graphical-editor/ladder/power-rail.tsx` | 2.6KB | [ ] D | Editor much larger | -| 49 | `graphical-editor/ladder/utils/index.ts` | - | [ ] D | Barrel for types.ts + utils.ts | -| 50 | `graphical-editor/ladder/utils/types.ts` | 3.4KB | [ ] D | Web much larger | -| 51 | `graphical-editor/ladder/utils/utils.ts` | 8KB | [ ] D | Web much larger | -| 52 | `graphical-editor/ladder/variable.tsx` | 2.6KB | [ ] D | | -| 53 | `graphical-editor/types/block.ts` | 9 | [ ] D | Reclassified: depends on divergent Zod schemas | -| 54 | `graphical-editor/utils/index.ts` | 14 | [ ] D | Reclassified: different type/import sources | -| 55 | `highlighted-textarea/index.tsx` | 19 | [x] D | Reconciled: web approach (extractSearchQuery, no HighlightedText dep) | -| 56 | `react-flow/style.css` | 24 | [x] D | Reconciled: web class-based dark mode | -| 57 | `select/index.tsx` | 405B | [x] D | Reconciled: editor superset (forwardRef, viewportRef) | -| 58 | `tab/index.tsx` | 74 | [x] D | Reconciled: merged web icons + editor safe rendering | -| 59 | `table-actions/index.tsx` | 14 | [x] D | Reconciled: editor superset (className prop) | - -### Platform-specific -| # | Component | Repo | Status | Notes | -|---|-----------|------|--------|-------| -| 60 | `highlighted-text/index.tsx` | E | [ ] E | Editor only | -| 61 | `graphical-editor/debug-value-badge.tsx` | E | [ ] E | Editor only | -| 62 | `graphical-editor/block-output-debug-badges.tsx` | E | [ ] E | Editor only | -| 63 | `resolution-warning-message/index.tsx` | W | [ ] W | Web only | -| 64 | `react-flow/custom-nodes/coil.tsx` | W | [ ] W | Web only | -| 65 | `react-flow/custom-nodes/contact.tsx` | W | [ ] W | Web only | -| 66 | `graphical-editor/fbd/buildNodes.tsx` | W | [ ] W | Web only | -| 67 | `graphical-editor/ladder/buildNodes.tsx` | W | [ ] W | Web only | -| 68 | `graphical-editor/fbd/utils/constants.tsx` | W | [ ] W | Web only | -| 69 | `graphical-editor/ladder/utils/constants.tsx` | W | [ ] W | Web only | - ---- - -## 2. MOLECULES (`_molecules/`) - -### Identical / near-identical -| # | Component | Status | -|---|-----------|--------| -| 70 | `file/*` (4 files) | [x] S | -| 71 | `pin-mapping-table/*` (4 files) | [x] S | -| 72 | `select-field/index.tsx` | [x] S | -| 73 | `input-field/index.tsx` | [x] S | -| 74 | `toast/index.tsx` | [x] S | -| 75 | `window-controls/index.tsx` | [ ] E | Reclassified: editor-only (uses window.bridge) | -| 76 | `menu-bar/index.tsx` | [x] D | Reconciled step 24: shell + stub menus | -| 77 | `modal/index.tsx` | [x] S | -| 78 | `rename-impact-modal/index.tsx` | [x] S | -| 79 | `type-change-modal/index.tsx` | [x] D | Reconciled step 24: validation extracted to backend/shared | -| 80 | `breadcrumbs/index.tsx` | [x] S | -| 81 | `search/index.tsx` | [x] S | -| 82 | `variables-panel/index.tsx` | [x] D | Reconciled step 24 | -| 83 | `data-types/array/* (header + table)` | [x] D | Reconciled step 24 | -| 84 | `data-types/enumerated/*` | [x] D | Reconciled step 24 | -| 85 | `data-types/structure/* (table + elements)` | [x] D | Reconciled step 24 | -| 86 | `graphical-editor/fbd/fbd-utils/*` | [ ] D | Reclassified: useCopyPaste.ts divergent | -| 87 | `graphical-editor/fbd/index.tsx` | [ ] D | Reclassified: heavily divergent (ID gen, node sync, delete logic) | -| 88 | `graphical-editor/ladder/rung/index.tsx` | [ ] D | Reclassified: rung children divergent | -| 89 | `graphical-editor/ladder/index.tsx` | [ ] D | Reclassified: depends on divergent children | -| 90 | `graphical-editor/ladder/* utils` | [ ] D | Reclassified: ID generation differs (newGraphicalEditorNodeID vs uuidv4) | -| 91 | `instances-table/*` | [x] D | Reconciled step 24 | -| 92 | `library-tree/index.tsx` | [x] S | -| 93 | `task-table/*` | [x] D | Reconciled step 24 | -| 94 | `global-variables-table/elements/*` | [x] D | Reconciled step 24 | -| 95 | `variables-table/elements/*` | [x] D | Reconciled step 24 | - -### Divergent -| # | Component | Diff | Status | Notes | -|---|-----------|------|--------|-------| -| 96 | `charts/line-chart.tsx` | 50% | [x] D | Reconciled step 24 | -| 97 | `global-variables-table/editable-cell.tsx` | 21% | [x] D | Reconciled step 24 | -| 98 | `global-variables-table/selectable-cell.tsx` | 30% | [x] D | Reconciled step 24 | -| 99 | `variables-table/editable-cell.tsx` | 48% | [x] D | Reconciled step 24 | -| 100 | `tabs/index.tsx` | 23% | [x] D | Reconciled step 24 | -| 101 | `graphical-editor/ladder/rung/body.tsx` | 13% | [ ] D | Deferred to step 24b | -| 102 | `workspace-activity-bar/download.tsx` | 47% | [x] D | Reconciled step 24 | - -### Platform-specific (menus) -| # | Component | Status | Notes | -|---|-----------|--------|-------| -| 103 | `menu-bar/menus/display.tsx` | [ ] P | 25% diff, platform behavior | -| 104 | `menu-bar/menus/file.tsx` | [ ] P | 23% diff, Electron file ops vs web | -| 105 | `menu-bar/menus/recent.tsx` | [ ] P | 78% diff, Electron recent files | -| 106 | `project-tree/utils/index.ts` | [ ] W | Web only (extracted util) | -| 107 | `workspace-activity-bar/default/chat.tsx` | [ ] W | Web only (AI chat button) | - ---- - -## 3. ORGANISMS (`_organisms/`) - -### Identical / near-identical -| # | Component | Status | -|---|-----------|--------| -| 107 | `console/* (3 files)` | [x] S | -| 108 | `explorer/index.tsx` | [x] S | -| 109 | `explorer/info.tsx` | [x] S | -| 110 | `global-variables-editor/index.tsx` | [x] S | -| 111 | `graphical-editor/ladder/index.ts` | [x] S | -| 112 | `graphical-editor/ladder/rung/index.tsx` | [x] S | -| 113 | `navigation/index.tsx` | [x] S | -| 114 | `panel/index.tsx` | [x] S | -| 115 | `plc-logs/* (2 files)` | [x] S | -| 116 | `variables-code-editor/index.tsx` | [x] S | -| 117 | `workspace-activity-bar/fbd-toolbox.tsx` | [x] S | -| 118 | `workspace-activity-bar/index.tsx` | [x] S | - -### Divergent -| # | Component | Diff | Status | Notes | -|---|-----------|------|--------|-------| -| 119 | `debugger/index.tsx` | 400B | [ ] D | Web slightly larger | -| 120 | `explorer/library.tsx` | 400B | [ ] D | Web slightly larger | -| 121 | `explorer/project.tsx` | 1.3KB | [ ] D | Editor larger | -| 122 | `instances-editor/index.tsx` | 1KB | [ ] D | Editor larger | -| 123 | `task-editor/index.tsx` | 1KB | [ ] D | Editor larger | -| 124 | `variables-editor/index.tsx` | 11KB | [ ] D | Editor 27% larger | -| 125 | `workspace-activity-bar/default.tsx` | 3KB | [ ] P | Business logic mixed in | -| 126 | `workspace-activity-bar/ladder-toolbox.tsx` | 2.4KB | [ ] D | Web 68% larger | -| 127 | `title-bar/index.tsx` | 112B | [ ] D | | -| 128 | `title-bar/slots/*` | N/A | [ ] P | Completely different structure | - -### Platform-specific modals -| # | Component | Repo | Status | Notes | -|---|-----------|------|--------|-------| -| 129 | `modals/debugger-message-modal.tsx` | Both | [ ] S | | -| 130 | `modals/delete-confirmation-modal.tsx` | Both | [ ] D | Editor 1.3KB larger | -| 131 | `modals/runtime-connection-lost-modal.tsx` | Both | [ ] S | | -| 132 | `modals/runtime-create-user-modal.tsx` | Both | [ ] D | Web 1.3KB larger | -| 133 | `modals/runtime-login-modal.tsx` | Both | [ ] P | Direct IPC/API calls | -| 134 | `modals/save-changes-file-modal.tsx` | Both | [ ] S | | -| 135 | `modals/save-changes-modal.tsx` | Both | [ ] D | Editor 1.4KB larger | -| 136 | `modals/confirm-device-switch-modal.tsx` | E | [ ] E | | -| 137 | `modals/debugger-ip-input-modal.tsx` | E | [ ] E | | -| 138 | `modals/quit-application-modal.tsx` | E | [ ] E | Electron only | -| 139 | `modals/server-ip-mismatch-modal.tsx` | W | [ ] W | | -| 140 | `about-modal/index.tsx` | E | [ ] E | | -| 141 | `display-recent-projects/index.tsx` | E | [ ] E | | -| 142 | `project-filter-bar/index.tsx` | E | [ ] E | | -| 143 | `pin-mapping-editor/index.tsx` | W | [ ] W | | - ---- - -## 4. FEATURES (`_features/`) - -### [app] -| # | Component | Repo | Status | Notes | -|---|-----------|------|--------|-------| -| 144 | `toast/*` (3 files) | Both | [ ] S | Near-identical | -| 145 | `debug-manager/index.tsx` | W | [ ] W | 16KB, simulator/debugger UI | -| 146 | `loading-overlay/index.tsx` | W | [ ] W | | - -### [start] -| # | Component | Status | Notes | -|---|-----------|--------|-------| -| 147 | `menu/index.tsx` | [ ] S | Identical | -| 148 | `new-project/interval-model.tsx` | [ ] S | | -| 149 | `new-project/project-modal.tsx` | [ ] S | | -| 150 | `new-project/steps/first-step.tsx` | [ ] S | | -| 151 | `new-project/steps/second-step.tsx` | [ ] D | Different implementations | -| 152 | `new-project/steps/third-step.tsx` | [ ] D | Different implementations | -| 153 | `new-project/store/index.ts` | [ ] S | | - -### [workspace]/editor -| # | Component | Status | Notes | -|---|-----------|--------|-------| -| 154 | `editor/device/index.tsx` | [ ] S | | -| 155 | `editor/device/configuration/index.tsx` | [ ] S | | -| 156 | `editor/device/configuration/communication.tsx` | [ ] S | | -| 157 | `editor/device/configuration/board.tsx` | [ ] D | Editor 702 lines vs Web 294 lines | -| 158 | `editor/device/configuration/components/*` | [ ] S | modbus-rtu, modbus-tcp, pin-mapping, static-host | -| 159 | `editor/device/remote-device/index.tsx` | [ ] S | ~45KB both | -| 160 | `editor/device/orchestrators/*` | W | [ ] W | Web only (32KB orchestrator list) | -| 161 | `editor/device/components/*` | W | [ ] W | Web only (extracted components) | -| 162 | `editor/device/elements/*` | W | [ ] W | Web only (board-config, tcp-settings, rtu-settings) | -| 163 | `editor/graphical/index.tsx` | [ ] S | | -| 164 | `editor/graphical/FBD/index.tsx` | [ ] S | | -| 165 | `editor/graphical/SFC/index.tsx` | [ ] S | | -| 166 | `editor/graphical/elements/*` | [ ] S | Block, coil, contact, arrow-button-group | -| 167 | `editor/graphical/ladder/index.tsx` | E | [ ] E | 12KB monolithic (web refactored out) | -| 168 | `editor/monaco/index.tsx` | [ ] D | Editor 1167 lines vs Web 823 lines | -| 169 | `editor/monaco/completion/*` | [ ] S | All completion files identical | -| 170 | `editor/monaco/configs/*` | [ ] S | All language/theme configs identical | -| 171 | `editor/monaco/drag-and-drop/*` | [ ] S | | -| 172 | `editor/monaco/python-lsp/*` | [ ] S | | -| 173 | `editor/monaco/theme-utils.ts` | W | [ ] W | Web only | -| 174 | `editor/resource-editor/index.tsx` | [ ] S | | -| 175 | `editor/search-in-project/index.tsx` | [ ] S | | -| 176 | `editor/server/index.ts` | [ ] S | | -| 177 | `editor/server/modbus-server/index.tsx` | [ ] S | | -| 178 | `editor/server/opcua-server/*` | [ ] S | All identical | -| 179 | `editor/server/s7comm-server/index.tsx` | [ ] S | | -| 180 | `editor/server/modbus-server/address-mapping-reference.tsx` | W | [ ] W | Web only | -| 181 | `editor/data-type/index.tsx` | [ ] D | Web 180% larger | -| 182 | `create-element/*` | [ ] S | | -| 183 | `create-element/hooks/use-name-validation.ts` | W | [ ] W | Web only | -| 184 | `search/*` | [ ] S | | -| 185 | `ai-chat/*` (5 files) | W | [ ] W | AI chat panel, input, messages, code block (web only) | -| 186 | `editor/monaco/ai-completion/*` (3 files) | W | [ ] W | AI inline completion provider, context builder | -| 187 | `editor/monaco/ai-consent-modal.tsx` | W | [ ] W | AI consent dialog | -| 188 | `editor/monaco/ai-status-indicator.tsx` | W | [ ] W | AI status indicator | - ---- - -## 5. TEMPLATES (`_templates/`) - -| # | Component | Status | Notes | -|---|-----------|--------|-------| -| 185 | `[start]/index.ts` | [ ] S | | -| 186 | `[start]/main-content.tsx` | [ ] S | | -| 187 | `[start]/side-content.tsx` | [ ] S | | -| 188 | `[workspace]/index.ts` | [ ] S | | -| 189 | `[workspace]/side-content.tsx` | [ ] S | | -| 190 | `[workspace]/main-content.tsx` | [ ] D | Web 29% smaller | -| 191 | `[editors]/index.ts` | [ ] S | | -| 192 | `[editors]/device-editor-slot.tsx` | [ ] S | | -| 193 | `[editors]/device-editor-template.tsx` | [ ] S | | -| 194 | `app-layout.tsx` | [ ] D | Minor styling diff | -| 195 | `accelerator-handler.tsx` | E | [ ] E | Electron keyboard shortcuts | - ---- - -## 6. SCREENS / PAGES - -| # | Component | Repo | Status | Notes | -|---|-----------|------|--------|-------| -| 196 | `workspace-screen.tsx` | Both | [ ] P | Editor 2072 lines vs Web 604 lines. Heaviest business logic. | -| 197 | `start-screen.tsx` | E | [ ] E | | -| 198 | `welcome-page.tsx` | W | [ ] W | | -| 199 | `unauthorized-page.tsx` | W | [ ] W | Web auth page | - ---- - -## 7. HOOKS - -### Shared -| # | Hook | Status | Notes | -|---|------|--------|-------| -| 200 | `use-debug-composite-key.ts` | [ ] S | Identical | -| 201 | `use-remove-tab.tsx` | [ ] S | Near-identical | -| 202 | `use-store-selectors.ts` | [ ] D | Editor 258 lines vs Web 119 lines | - -### Platform-specific -| # | Hook | Repo | Status | Notes | -|---|------|------|--------|-------| -| 203 | `use-compiler.ts` | E | [ ] P | Calls `window.bridge.exportProjectXml()` | -| 204 | `use-quit-app.tsx` | E | [ ] E | Electron only | -| 205 | `use-runtime-polling.ts` | E | [ ] P | Calls `window.bridge.*` | -| 206 | `useDebugPolling.ts` | W | [ ] P | Calls web debug bridge | -| 207 | `useDebugSession.ts` | W | [ ] P | Debug session lifecycle | -| 208 | `useDebuggerLauncher.ts` | W | [ ] P | 790 lines, compiler pipeline | -| 209 | `useRuntimePolling.ts` | W | [ ] P | Calls web APIs | -| 210 | `useSaveShortcut.ts` | W | [ ] W | | -| 211 | `useUndoRedoShortcut.ts` | W | [ ] W | | -| 212 | `useWebRTCConnection.ts` | W | [ ] W | WebRTC specific | -| 213 | `useAI.ts` | W | [ ] W | AI chat and completion hook | - ---- - -## 8. STORE SLICES - -### Shared slices (in both repos) -| # | Slice | Status | Notes | -|---|-------|--------|-------| -| 213 | `console/` | [ ] S | | -| 214 | `editor/` | [ ] S | | -| 215 | `fbd/` | [ ] S | | -| 216 | `files/` | [ ] S | | -| 217 | `ladder/` | [ ] D | Editor 4 files vs Web 3 | -| 218 | `library/` | [ ] S | | -| 219 | `modal/` | [ ] S | | -| 220 | `project/` | [ ] D | Both 8 files, but complex business logic inside | -| 221 | `react-flow/` | [ ] S | | -| 222 | `search/` | [ ] S | | -| 223 | `shared/` | [ ] S | | -| 224 | `tabs/` | [ ] S | | -| 225 | `workspace/` | [ ] D | Both 6 files, may mix platform state | -| 226 | `device/` | [ ] P | Editor 7 files vs Web 5, runtime connection state differs | - -### Platform-specific slices -| # | Slice | Repo | Status | Notes | -|---|-------|------|--------|-------| -| 227 | `history/` | E | [ ] E | Undo/redo | -| 228 | `webrtc/` | W | [ ] W | WebRTC state | -| 229 | `ai/` (3 files) | W | [ ] W | AI state (chat messages, completions, consent, telemetry) | - ---- - -## 9. UTILS - -### Shared utils -| # | Utility | Status | Notes | -|---|---------|--------|-------| -| 229 | `debug-tree-builder.ts` | [ ] S | | -| 230 | `parse-debug-file.ts` | [ ] S | | -| 231 | `sync-nodes-with-variables.ts` | [ ] S | | -| 232 | `validate-variable-reference.ts` | [ ] S | | -| 233 | `variable-references.ts` | [ ] D | Editor 532 vs Web 562 lines | -| 234 | `variable-sizes.ts` | [ ] D | Editor 217 vs Web 278 lines | -| 235 | `debug-tree-traversal.ts` | [ ] D | Editor 394 vs Web 366 lines | -| 236 | `debug-variable-finder.ts` | [ ] S | | -| 237 | `keywords.ts` | [ ] S | | -| 238 | `pou-helpers.ts` | [ ] S | | -| 239 | `generate-iec-string-to-variables.ts` | [ ] D | Editor 190 vs Web 88 lines | -| 240 | `remote-device-options.ts` | [ ] S | | - -### Utils subdirectories -| # | Directory | Status | Notes | -|---|-----------|--------|-------| -| 241 | `PLC/` | [ ] D | Editor 29 files (codesys + old-editor), Web 14 files (consolidated) | -| 242 | `cpp/` (5 files) | [ ] S | Same structure | -| 243 | `python/` (5 files) | [ ] S | Same structure | -| 244 | `modbus/` | [ ] D | Editor 2 files, Web 4 files | -| 245 | `opcua/` | [ ] D | Editor 5 files, Web 4 files | -| 246 | `s7comm/` (2 files) | [ ] S | Same structure | -| 247 | `formatters/` | [ ] D | Editor 3 files, Web 2 files | - -### Editor-only utils -| # | Utility | Status | Notes | -|---|---------|--------|-------| -| 248 | `debugger-session.ts` | [ ] P | 294 lines, IPC-coupled | -| 249 | `PLC/pou-text-parser.ts` | [ ] E | 517 lines | -| 250 | `PLC/pou-text-serializer.ts` | [ ] E | 148 lines | -| 251 | `PLC/pou-file-extensions.ts` | [ ] E | 110 lines | -| 252 | `PLC/preprocess-pous.ts` | [ ] E | 182 lines | -| 253 | `PLC/array-codegen-helpers.ts` | [ ] E | 110 lines | -| 254 | `PLC/codesys/*` | [ ] E | Separate XML format | -| 255 | `PLC/old-editor/*` | [ ] E | Separate XML format | - -### Web-only utils -| # | Utility | Status | Notes | -|---|---------|--------|-------| -| 256 | `cookies.ts` | [ ] W | | -| 257 | `download-file.ts` | [ ] W | | -| 258 | `hex.ts` | [ ] W | | -| 259 | `library.ts` | [ ] W | | -| 260 | `project-parser.ts` | [ ] W | 601 lines | -| 261 | `project-serializer.ts` | [ ] W | 187 lines | -| 262 | `project-summary.ts` | [ ] W | | -| 263 | `server-ip-validation.ts` | [ ] W | | -| 264 | `theme.ts` | [ ] W | | -| 265 | `graphical/drag-detection.ts` | [ ] W | | -| 266 | `graphical/relink-variables.ts` | [ ] W | | - ---- - -## 10. SHARED MODULES - -| # | Module | Repo | Status | Notes | -|---|--------|------|--------|-------| -| 267 | `data/index.ts` | Both | [ ] S | | -| 268 | `data/constants.ts` | Both | [ ] S | | -| 269 | `data/common.ts` | Both | [ ] S | | -| 270 | `contracts/types/*` | E | [ ] E | pou.ts, xml-project.ts | -| 271 | `contracts/validations/*` | E | [ ] E | pou-validation.ts, project-validation.ts | -| 272 | `data/mock/*` | E | [ ] E | Test data | - ---- - -## 11. SERVICES (Web only) - -| # | Service | Status | Notes | -|---|---------|--------|-------| -| 273 | `api/axios.ts` | [ ] W | HTTP client config | -| 274 | `api/compiler-api.ts` | [ ] W | Compiler endpoints | -| 275 | `api/debug-transport.ts` | [ ] W | Debug protocol | -| 276 | `api/project-api.ts` | [ ] W | Project CRUD | -| 277 | `api/runtime-api.ts` | [ ] W | Runtime communication | -| 278 | `api/webrtc/*` (5 files) | [ ] W | WebRTC signaling/connection | -| 279 | `debug/debug-bridge.ts` | [ ] W | Debug protocol adapter | -| 280 | `debug/types.ts` | [ ] W | | -| 281 | `debug/transports/*` (2 files) | [ ] W | Modbus RTU + WebRTC transports | -| 282 | `simulator/*` (6 files) | [ ] W | In-browser AVR simulator | -| 283 | `debug-session-controls.ts` | [ ] W | Session lifecycle | -| 284 | `ai/*` (6 files) | W | [ ] W | AI api-client, completion-cache, context-collector, telemetry, types | - ---- - -## 12. IPC BRIDGE (Editor only — to be replaced by port interfaces) - -The editor's `window.bridge.*` with 350+ methods is the main coupling point. -These will be replaced by port interfaces that both repos implement differently. - -### Port interfaces needed (derived from IPC categories) -| # | Port | Methods | Status | -|---|------|---------|--------| -| P1 | `CompilerPort` | compileProject, exportXml, getCompilationStatus | [ ] | -| P2 | `RuntimePort` | login, getStatus, startPlc, stopPlc, getLogs, getSerialPorts, createUser | [ ] | -| P3 | `DebuggerPort` | connect, disconnect, getVariablesList, setVariable, verifyMd5 | [ ] | -| P4 | `SimulatorPort` | loadFirmware, stop, isRunning, onStopped | [ ] | -| P5 | `ProjectPort` | openProject, saveProject, createPou, deletePou, renamePou, pickPath | [ ] | -| P6 | `DevicePort` | getAvailableBoards, getCommunicationPorts, getPreviewImage | [ ] | -| P7 | `SystemPort` | getSystemInfo, setStoreValue, getStoreValue, retrieveRecent | [ ] | -| P8 | `WindowPort` | minimize, maximize, close, hide, reload, quit | [ ] | -| P9 | `AcceleratorPort` | onSaveProject, onOpenProject, onUndo, onRedo, ... | [ ] | -| P10 | `ThemePort` | getCurrentTheme, setTheme, onThemeChanged | [ ] | - ---- - -## STATISTICS SUMMARY - -| Category | Total Items | Shared (S) | Divergent (D) | Editor Only (E) | Web Only (W) | Platform (P) | -|----------|-------------|------------|---------------|------------------|--------------|--------------| -| Atoms | 69 | 37 | 22 | 3 | 7 | 0 | -| Molecules | 38 | 26 | 7 | 0 | 2 | 3 | -| Organisms | 37 | 12 | 10 | 5 | 1 | 9 | -| Features | 45 | 22 | 5 | 1 | 13 | 4 | -| Templates | 11 | 8 | 2 | 1 | 0 | 0 | -| Screens | 4 | 0 | 0 | 1 | 2 | 1 | -| Hooks | 14 | 2 | 1 | 2 | 4 | 5 | -| Store | 17 | 10 | 3 | 1 | 2 | 1 | -| Utils | 38 | 10 | 8 | 8 | 11 | 1 | -| Shared | 6 | 3 | 0 | 3 | 0 | 0 | -| Services | 12 | 0 | 0 | 0 | 12 | 0 | -| **TOTAL** | **291** | **130** | **58** | **25** | **54** | **24** | diff --git a/docs/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md b/docs/outdated/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md similarity index 100% rename from docs/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md rename to docs/outdated/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md diff --git a/docs/HEADLESS_SETUP.md b/docs/outdated/HEADLESS_SETUP.md similarity index 100% rename from docs/HEADLESS_SETUP.md rename to docs/outdated/HEADLESS_SETUP.md diff --git a/docs/dead-code-inventory.md b/docs/outdated/dead-code-inventory.md similarity index 100% rename from docs/dead-code-inventory.md rename to docs/outdated/dead-code-inventory.md diff --git a/docs/debugger-opcua-shared-utilities.md b/docs/outdated/debugger-opcua-shared-utilities.md similarity index 100% rename from docs/debugger-opcua-shared-utilities.md rename to docs/outdated/debugger-opcua-shared-utilities.md diff --git a/docs/external-binaries-strategy.md b/docs/outdated/external-binaries-strategy.md similarity index 100% rename from docs/external-binaries-strategy.md rename to docs/outdated/external-binaries-strategy.md diff --git a/docs/name-type-linking-design.md b/docs/outdated/name-type-linking-design.md similarity index 100% rename from docs/name-type-linking-design.md rename to docs/outdated/name-type-linking-design.md diff --git a/docs/opcua-server-configuration/01-design-overview.md b/docs/outdated/opcua-server-configuration/01-design-overview.md similarity index 100% rename from docs/opcua-server-configuration/01-design-overview.md rename to docs/outdated/opcua-server-configuration/01-design-overview.md diff --git a/docs/opcua-server-configuration/02-ui-screen-specifications.md b/docs/outdated/opcua-server-configuration/02-ui-screen-specifications.md similarity index 100% rename from docs/opcua-server-configuration/02-ui-screen-specifications.md rename to docs/outdated/opcua-server-configuration/02-ui-screen-specifications.md diff --git a/docs/opcua-server-configuration/03-json-configuration-mapping.md b/docs/outdated/opcua-server-configuration/03-json-configuration-mapping.md similarity index 100% rename from docs/opcua-server-configuration/03-json-configuration-mapping.md rename to docs/outdated/opcua-server-configuration/03-json-configuration-mapping.md diff --git a/docs/opcua-server-configuration/04-implementation-phases.md b/docs/outdated/opcua-server-configuration/04-implementation-phases.md similarity index 100% rename from docs/opcua-server-configuration/04-implementation-phases.md rename to docs/outdated/opcua-server-configuration/04-implementation-phases.md diff --git a/docs/opcua-server-configuration/README.md b/docs/outdated/opcua-server-configuration/README.md similarity index 100% rename from docs/opcua-server-configuration/README.md rename to docs/outdated/opcua-server-configuration/README.md diff --git a/docs/s7comm-server-implementation.md b/docs/outdated/s7comm-server-implementation.md similarity index 100% rename from docs/s7comm-server-implementation.md rename to docs/outdated/s7comm-server-implementation.md diff --git a/docs/unified-frontend-serialization.md b/docs/outdated/unified-frontend-serialization.md similarity index 100% rename from docs/unified-frontend-serialization.md rename to docs/outdated/unified-frontend-serialization.md diff --git a/docs/variable-id-audit.md b/docs/outdated/variable-id-audit.md similarity index 100% rename from docs/variable-id-audit.md rename to docs/outdated/variable-id-audit.md diff --git a/docs/ports/WIRING.md b/docs/ports/WIRING.md deleted file mode 100644 index c33381721..000000000 --- a/docs/ports/WIRING.md +++ /dev/null @@ -1,150 +0,0 @@ -# PlatformProvider Wiring Guide - -How to wire the PlatformProvider into each application root. - -## Strategy - -The migration lives in `src2/` — a parallel source directory that will eventually -replace `src/` once all components are migrated and tested. The original `src/` -remains untouched and fully functional throughout the migration. - -## Directory Structure - -``` -src/ <-- ORIGINAL, untouched during migration - renderer/ (editor) or . (web) - components/ - hooks/ - store/ - ... - -src2/ <-- MIGRATION target, new architecture - providers/ - platform/ - index.ts <-- exports PlatformProvider, usePlatform, convenience hooks - platform-context.tsx <-- React context + hooks (SHARED, identical in both repos) - types.ts <-- PlatformPorts aggregate type (SHARED) - ports/ <-- Port interfaces (SHARED, identical in both repos) - types.ts - compiler-port.ts - runtime-port.ts - debugger-port.ts - simulator-port.ts - project-port.ts - device-port.ts - system-port.ts - window-port.ts - accelerator-port.ts - theme-port.ts - platform-capabilities.ts - index.ts - adapters/ - editor-platform.ts <-- (editor only) Electron IPC implementations - web-platform.ts <-- (web only) HTTP/WebRTC implementations - components/ <-- migrated components (added over time) - hooks/ <-- migrated hooks (added over time) - store/ <-- migrated store slices (added over time) -``` - -## Editor Wiring (src2 App root) - -```tsx -import { PlatformProvider } from '@src2/providers/platform' -import { editorPorts } from '@src2/adapters/editor-platform' - -export default function App() { - const { project: { meta: { path } } } = useOpenPLCStore() - - return ( - - - {path === '' ? : } - - - ) -} -``` - -## Web Wiring (src2 App root) - -```tsx -import { PlatformProvider } from '@src2/providers/platform' -import { webPorts } from '@src2/adapters/web-platform' - -createRoot(document.getElementById('root')!).render( - - - - - , -) -``` - -## Using Ports in Components - -```tsx -import { usePlatform, useCapabilities, useRuntime } from '@src2/providers/platform' - -// Full access to all ports -function WorkspaceActivityBar() { - const { compiler, runtime, debugger: dbg, capabilities } = usePlatform() - - const handleCompile = async () => { - await compiler.compileProgram(args, (event) => { - console.log(event.stage, event.message) - }) - } - - return ( - <> - - {capabilities.hasNativeWindowControls && } - {capabilities.hasAIAssistant && } - - ) -} - -// Single port access -function RuntimeLoginModal() { - const runtime = useRuntime() - - const handleLogin = async (username: string, password: string) => { - const result = await runtime.login({ username, password }) - if (result.success) { /* ... */ } - } -} - -// Feature toggle -function TitleBar() { - const caps = useCapabilities() - - return ( -
- {caps.hasNativeMenu ? : } - {caps.hasNativeWindowControls && } -
- ) -} -``` - -## Migration Workflow - -1. Pick a component from `src/` that calls `window.bridge.*` or a service directly -2. Identify which port(s) it needs (e.g., RuntimePort for login) -3. Implement the adapter method in `editor-platform.ts` / `web-platform.ts` -4. Copy the component to `src2/`, refactoring it to use `usePlatform()` hooks -5. Verify the migrated component works identically to the original -6. Mark the item as migrated in `migration-tracker.md` - -The original `src/` component remains untouched until `src2/` is fully tested. -Each stub port uses a Proxy that throws a descriptive error when called, -so you'll immediately know if a component uses an unmigrated port. - -## Cutover - -Once all components are migrated and tested in `src2/`: -1. Rename `src/` to `src-legacy/` (keep as backup) -2. Rename `src2/` to `src/` -3. Update build configs and path aliases -4. Run full test suite -5. Remove `src-legacy/` once confident diff --git a/scripts/compare-dependencies.py b/scripts/compare-dependencies.py new file mode 100644 index 000000000..4fc9566e6 --- /dev/null +++ b/scripts/compare-dependencies.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +""" +Compare shared production dependencies between openplc-web and openplc-editor. + +Reads package.json from both repos and checks that every dependency present +in both has the same version specifier. +Exit code 0 = all match, 1 = mismatches found. +""" + +import argparse +import json +import sys +from pathlib import Path + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Compare shared production dependency versions between repos." + ) + parser.add_argument( + "--web-root", + required=True, + type=Path, + help="Path to the web repo root (containing package.json)", + ) + parser.add_argument( + "--editor-root", + required=True, + type=Path, + help="Path to the editor repo root (containing package.json)", + ) + args = parser.parse_args() + + with open(args.web_root / "package.json") as f: + web_deps = json.load(f).get("dependencies", {}) + with open(args.editor_root / "package.json") as f: + editor_deps = json.load(f).get("dependencies", {}) + + shared = sorted(set(web_deps) & set(editor_deps)) + mismatches = [] + + for pkg in shared: + if web_deps[pkg] != editor_deps[pkg]: + mismatches.append((pkg, web_deps[pkg], editor_deps[pkg])) + + if mismatches: + print(f"::error::Found {len(mismatches)} shared dependency version mismatch(es):") + for pkg, web_ver, editor_ver in mismatches: + print(f" {pkg}: web={web_ver} editor={editor_ver}") + return 1 + + print(f"All {len(shared)} shared dependencies match.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/compare-surface-definitions.py b/scripts/compare-surface-definitions.py new file mode 100644 index 000000000..a8a2079d5 --- /dev/null +++ b/scripts/compare-surface-definitions.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +""" +Compare SURFACES definitions between the web and editor compare-surfaces.py scripts. + +Parses the SURFACES variable from both scripts using Python's AST module +and checks that they define the same set of surfaces. +Exit code 0 = match, 1 = mismatch or parse failure. +""" + +from __future__ import annotations + +import argparse +import ast +import sys +from pathlib import Path + +SCRIPT_RELATIVE_PATH = "scripts/compare-surfaces.py" + + +def extract_surfaces(filepath: Path) -> list[str] | None: + with open(filepath) as f: + tree = ast.parse(f.read()) + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name) and target.id == "SURFACES": + return sorted(ast.literal_eval(node.value)) + return None + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Compare SURFACES definitions between repo compare-surfaces.py scripts." + ) + parser.add_argument( + "--web-root", + required=True, + type=Path, + help="Path to the web repo root", + ) + parser.add_argument( + "--editor-root", + required=True, + type=Path, + help="Path to the editor repo root", + ) + args = parser.parse_args() + + web_script = args.web_root / SCRIPT_RELATIVE_PATH + editor_script = args.editor_root / SCRIPT_RELATIVE_PATH + + web = extract_surfaces(web_script) + editor = extract_surfaces(editor_script) + + if web is None or editor is None: + missing = [] + if web is None: + missing.append(f"web ({web_script})") + if editor is None: + missing.append(f"editor ({editor_script})") + print(f"::error::Could not extract SURFACES from: {', '.join(missing)}") + return 1 + + if web != editor: + web_only = set(web) - set(editor) + editor_only = set(editor) - set(web) + print("::error::SURFACES definitions do not match:") + if web_only: + print(f" Only in web: {', '.join(sorted(web_only))}") + if editor_only: + print(f" Only in editor: {', '.join(sorted(editor_only))}") + return 1 + + print(f"SURFACES definitions match: {web}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/compare-tooling.py b/scripts/compare-tooling.py new file mode 100644 index 000000000..a48f5858f --- /dev/null +++ b/scripts/compare-tooling.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +""" +Compare tooling configuration between openplc-web and openplc-editor. + +Checks that lint, formatting, and TypeScript configurations produce +equivalent output on shared surfaces. Byte-identical files are hash- +compared; config files with legitimate platform differences are +structurally compared. + +Exit code 0 = all in sync, 1 = differences found. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +import sys +from pathlib import Path + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +# Files that must be byte-identical (same relative path in both repos) +IDENTICAL_FILES = [ + ".prettierrc", + "scripts/compare-surfaces.py", + "scripts/compare-dependencies.py", + "scripts/compare-surface-definitions.py", + "scripts/compare-tooling.py", +] + +# ESLint config (different filenames, rules must match) +ESLINT_FILES = { + "web": "eslint.config.js", + "editor": "eslint.config.mjs", +} + +# TypeScript config (different structure, key options must match) +TSCONFIG_FILES = { + "web": "tsconfig.app.json", + "editor": "tsconfig.json", +} + +# Compiler options that must match for shared surface type-checking consistency +TSCONFIG_SHARED_OPTIONS = [ + "strict", + "jsx", + "noFallthroughCasesInSwitch", + "noUnusedLocals", + "noUnusedParameters", + "paths", +] + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def hash_file(path: Path) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return h.hexdigest() + + +def strip_eslint_ignores(text: str) -> str: + """Remove the { ignores: [...] } config block from ESLint config text.""" + return re.sub( + r"\{\s*ignores:\s*\[.*?\],?\s*\}", + "", + text, + flags=re.DOTALL, + ) + + +def normalize_whitespace(text: str) -> str: + """Collapse runs of whitespace and strip for comparison.""" + return re.sub(r"\s+", " ", text).strip() + + +def parse_jsonc(text: str) -> dict: + """Parse JSON with comments (// and /* */) and trailing commas.""" + # Strip comments while preserving strings containing // + result = [] + i = 0 + while i < len(text): + if text[i] == '"': + j = i + 1 + while j < len(text): + if text[j] == '\\': + j += 2 + continue + if text[j] == '"': + j += 1 + break + j += 1 + result.append(text[i:j]) + i = j + elif text[i:i+2] == '//': + i = text.index('\n', i) if '\n' in text[i:] else len(text) + elif text[i:i+2] == '/*': + end = text.index('*/', i) + i = end + 2 + else: + result.append(text[i]) + i += 1 + text = "".join(result) + # Strip trailing commas before } or ] + text = re.sub(r",\s*([}\]])", r"\1", text) + return json.loads(text) + + +# --------------------------------------------------------------------------- +# Checks +# --------------------------------------------------------------------------- + +def check_identical_files( + web_root: Path, editor_root: Path +) -> list[dict]: + results = [] + for relpath in IDENTICAL_FILES: + web_file = web_root / relpath + editor_file = editor_root / relpath + + entry: dict = {"file": relpath, "check": "byte-identical"} + + if not web_file.exists() and not editor_file.exists(): + entry["status"] = "skip" + entry["reason"] = "missing in both repos" + elif not web_file.exists(): + entry["status"] = "fail" + entry["reason"] = "missing in web repo" + elif not editor_file.exists(): + entry["status"] = "fail" + entry["reason"] = "missing in editor repo" + elif hash_file(web_file) == hash_file(editor_file): + entry["status"] = "pass" + else: + entry["status"] = "fail" + entry["reason"] = "files differ" + + results.append(entry) + return results + + +def check_eslint_rules( + web_root: Path, editor_root: Path +) -> list[dict]: + web_file = web_root / ESLINT_FILES["web"] + editor_file = editor_root / ESLINT_FILES["editor"] + + entry: dict = { + "file": f"{ESLINT_FILES['web']} <-> {ESLINT_FILES['editor']}", + "check": "eslint-rules", + } + + if not web_file.exists() or not editor_file.exists(): + missing = [] + if not web_file.exists(): + missing.append(f"web ({web_file})") + if not editor_file.exists(): + missing.append(f"editor ({editor_file})") + entry["status"] = "fail" + entry["reason"] = f"missing: {', '.join(missing)}" + return [entry] + + web_text = web_file.read_text() + editor_text = editor_file.read_text() + + web_stripped = normalize_whitespace(strip_eslint_ignores(web_text)) + editor_stripped = normalize_whitespace(strip_eslint_ignores(editor_text)) + + if web_stripped == editor_stripped: + entry["status"] = "pass" + else: + entry["status"] = "fail" + entry["reason"] = "ESLint rules/plugins/config differ (ignoring ignores block)" + + return [entry] + + +def check_tsconfig_options( + web_root: Path, editor_root: Path +) -> list[dict]: + web_file = web_root / TSCONFIG_FILES["web"] + editor_file = editor_root / TSCONFIG_FILES["editor"] + + entry: dict = { + "file": f"{TSCONFIG_FILES['web']} <-> {TSCONFIG_FILES['editor']}", + "check": "tsconfig-shared-options", + } + + if not web_file.exists() or not editor_file.exists(): + missing = [] + if not web_file.exists(): + missing.append(f"web ({web_file})") + if not editor_file.exists(): + missing.append(f"editor ({editor_file})") + entry["status"] = "fail" + entry["reason"] = f"missing: {', '.join(missing)}" + return [entry] + + with open(web_file) as f: + web_config = parse_jsonc(f.read()) + with open(editor_file) as f: + editor_config = parse_jsonc(f.read()) + + web_opts = web_config.get("compilerOptions", {}) + editor_opts = editor_config.get("compilerOptions", {}) + + mismatches = [] + for opt in TSCONFIG_SHARED_OPTIONS: + web_val = web_opts.get(opt) + editor_val = editor_opts.get(opt) + if web_val != editor_val: + mismatches.append( + f"{opt}: web={json.dumps(web_val)} editor={json.dumps(editor_val)}" + ) + + if mismatches: + entry["status"] = "fail" + entry["reason"] = "; ".join(mismatches) + else: + entry["status"] = "pass" + + return [entry] + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main() -> int: + parser = argparse.ArgumentParser( + description="Compare tooling configuration between web and editor repos." + ) + parser.add_argument( + "--web-root", required=True, type=Path, + help="Path to the web repo root", + ) + parser.add_argument( + "--editor-root", required=True, type=Path, + help="Path to the editor repo root", + ) + parser.add_argument( + "--github-annotations", action="store_true", + help="Emit GitHub Actions error annotations", + ) + args = parser.parse_args() + + all_results: list[dict] = [] + all_results.extend(check_identical_files(args.web_root, args.editor_root)) + all_results.extend(check_eslint_rules(args.web_root, args.editor_root)) + all_results.extend(check_tsconfig_options(args.web_root, args.editor_root)) + + failures = [r for r in all_results if r["status"] == "fail"] + passes = [r for r in all_results if r["status"] == "pass"] + skips = [r for r in all_results if r["status"] == "skip"] + + for r in passes: + print(f" PASS {r['file']} ({r['check']})") + for r in skips: + print(f" SKIP {r['file']} -- {r.get('reason', '')}") + for r in failures: + msg = f" FAIL {r['file']} ({r['check']}): {r.get('reason', '')}" + print(msg) + if args.github_annotations: + print(f"::error::{r['file']}: {r.get('reason', 'mismatch')}", file=sys.stderr) + + print() + print(f"{len(passes)} passed, {len(failures)} failed, {len(skips)} skipped") + + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tsconfig.json b/tsconfig.json index ab5adf630..31900d6d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,9 @@ "lib": ["dom", "es2022"], "jsx": "react-jsx", "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, "skipLibCheck": true, "sourceMap": true, "moduleResolution": "node",