{"owner":"swagger-api","repo":"swagger-editor","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md"],"skills":{"CLAUDE.md":"# CLAUDE.md - SwaggerEditor Codebase Guide for AI Assistants\n\n**Version:** 5.0.6 | **Last Updated:** 2026-05-16\n\n---\n\n## Project Overview\n\nSwaggerEditor is a browser-based editor for API specifications supporting **OpenAPI 2.0/3.0/3.1/3.2**, **AsyncAPI 2.x/3.0**, **API Design Systems**, and **JSON Schema**. Built as a React app on SwaggerUI's plugin architecture with a split-pane Monaco Editor interface.\n\n**Core Technologies:** React 17+/18, SwaggerUI React, TypeScript (gradual), Immutable.js, Monaco Editor, ApiDOM, Vite 8, Vitest + Playwright\n\n**Philosophy:** Plugin-based architecture, minimal over-engineering, heavy E2E testing, gradual TypeScript adoption, app/ESM/UMD build artifacts.\n\n---\n\n## Codebase Structure\n\n```\nswagger-editor/\n├── vite/                 # Vite plugins, configs, and build scripts\n├── docs/                 # architecture.md, customization guides, migration guides\n├── public/               # Static assets, HTML template\n├── src/\n│   ├── App.tsx           # Main app component & plugin composition\n│   ├── index.tsx         # Browser entry point\n│   ├── plugins/          # 26 plugins (see Architecture section)\n│   ├── presets/\n│   │   ├── monaco/       # Full-featured preset (default)\n│   │   └── textarea/     # Lightweight fallback\n│   ├── styles/           # Global SCSS (index.scss)\n│   └── types/            # TypeScript declarations (*.d.ts)\n├── test/\n│   ├── playwright/\n│   │   ├── e2e/          # Test specs (*.spec.ts)\n│   │   ├── fixtures/     # Test data\n│   │   ├── helpers/      # Helper functions\n│   │   └── tsconfig.json\n│   └── setupTests.js     # Vitest setup (jest-dom/vitest, canvas-mock)\n├── build/                # Standalone app (generated)\n└── dist/esm|umd|types/   # Library bundles (generated)\n```\n\n---\n\n## Architecture & Design Patterns\n\n### Plugin Categories (26 total)\n\n**Editor implementations:**\n- `editor-textarea` — HTML `<textarea>` fallback\n- `editor-monaco` — Monaco Editor (advanced)\n- `editor-monaco-language-apidom` — ApiDOM language support\n- `editor-monaco-yaml-paste` — YAML paste transformations\n\n**Preview plugins:**\n- `editor-preview` — Base preview component\n- `editor-preview-swagger-ui` — OpenAPI rendering\n- `editor-preview-asyncapi` — AsyncAPI rendering\n- `editor-preview-api-design-systems` — ADS rendering\n\n**Editor support plugins:**\n- `editor-content-type` — Auto-detect content type (OpenAPI/AsyncAPI/JSON Schema)\n- `editor-content-persistence` — LocalStorage persistence\n- `editor-content-read-only` — Read-only mode\n- `editor-content-origin` — Track content source (URL, file, user)\n- `editor-content-fixtures` — Load example/fixture files\n- `editor-content-from-file` — File import\n\n**Generic feature plugins:**\n- `layout`, `top-bar`, `modals`, `dialogs`, `dropdown-menu`, `dropzone`, `splash-screen`, `editor-safe-render`, `swagger-ui-adapter`, `util`, `versions`, `props-change-watcher`\n\n### Component Hierarchy\n\n```\nApp.tsx (SwaggerUI wrapper)\n└── SwaggerEditorLayout\n    ├── SplashScreen\n    ├── TopBar (File/Edit/Generate menus)\n    └── Container\n        └── Dropzone\n            └── SplitPane (resizable)\n                ├── EditorPane\n                │   ├── EditorPaneBarTop\n                │   ├── MonacoEditor / TextareaEditor\n                │   └── ValidationPane (errors/warnings)\n                └── EditorPreviewPane\n                    └── EditorPreviewSwaggerUI / AsyncAPI / ApiDesignSystems\n```\n\n### Design Patterns\n\n- **Container/Presenter:** Containers connect to Redux (e.g., `MonacoEditorContainer.jsx`), presenters handle rendering (e.g., `MonacoEditor.jsx`)\n- **HOC via `wrapComponents`:** Plugins wrap existing components to enhance without forking\n- **`getComponent`:** Dynamically resolve components — `const C = getComponent('MonacoEditor')`\n- **FSM for async:** `idle → loading → success/failure` with request ID tracking to prevent race conditions\n\n---\n\n## Plugin System\n\n### Plugin Structure\n\n```javascript\n// src/plugins/plugin-name/index.js\nconst PluginName = ({ getSystem }) => ({\n  afterLoad: function,                   // Runs after plugin loads\n  components: {\n    ComponentName: Component,            // Register new components\n  },\n  wrapComponents: {\n    ComponentName: WrapperFn,            // Wrap/enhance existing components\n  },\n  rootInjects: {\n    utilityName: function,               // Inject utilities into system\n  },\n  statePlugins: {\n    pluginStateKey: {\n      actions: {},                       // Action creators\n      reducers: {},                      // Immutable.js reducers\n      selectors: {},                     // Reselect selectors\n      wrapActions: {},                   // Action middleware\n    },\n  },\n  fn: { utilityFunction: function },\n});\nexport default PluginName;\n```\n\n### Typical Plugin File Structure\n\n```\nplugin-name/\n├── index.js\n├── actions/index.js\n├── reducers.js\n├── selectors.js\n├── components/ComponentName.jsx\n├── components/ComponentName.scss\n├── extensions/other-plugin/wrap-components/ComponentWrapper.jsx\n├── after-load.js\n└── fn.js\n```\n\n### Component Wrapping Pattern\n\n```javascript\n// extensions/editor-preview/wrap-components/EditorPreviewWrapper.jsx\nconst EditorPreviewWrapper = (Original, system) => {\n  const EnhancedComponent = (props) => {\n    const isOpenAPI = system.editorSelectors.selectIsContentTypeOpenAPI();\n    if (isOpenAPI) return <EditorPreviewSwaggerUI />;\n    return <Original {...props} />;\n  };\n  return EnhancedComponent; // must return new component, not Original directly\n};\nexport default EditorPreviewWrapper;\n```\n\n### System Access in Plugins\n\n```javascript\nconst MyPlugin = (system) => {\n  const { getComponent, editorActions, editorSelectors, fn } = system;\n  const content = editorSelectors.selectEditorContent();\n  editorActions.setEditorContent('new content');\n  const MonacoEditor = getComponent('MonacoEditor');\n};\n```\n\n---\n\n## Development Workflows\n\n### Prerequisites\n- **Node.js** `>=24.19.0`, **npm** `>=11.7.0`, **Python 3.x** (node-gyp), **GLIBC** `>=2.29`\n- Optional: Docker or emscripten (for WASM builds)\n\n### npm Scripts\n\n| Script | Description |\n|--------|-------------|\n| `npm start` | Dev server on port 3000 (hot reload) |\n| `npm test` | Vitest unit tests (watch mode) |\n| `npm run test:run` | Vitest unit tests (single run, no watch) |\n| `npm run test:coverage` | Vitest unit tests with coverage report |\n| `npm run lint` | ESLint on all files |\n| `npm run lint:fix` | Auto-fix ESLint errors |\n| `npm run build` | Build all artifacts (app + bundles + types) |\n| `npm run build:app` | Standalone app → `/build` |\n| `npm run build:app:serve` | Serve built app on port 3050 |\n| `npm run build:bundle:esm` | ESM bundle → `/dist/esm` |\n| `npm run build:bundle:umd` | UMD bundle → `/dist/umd` |\n| `npm run build:definitions` | TypeScript definitions → `/dist/types` |\n| `npm run pw:test` | E2E tests (headless) |\n| `npm run pw:test:headed` | E2E with browser visible |\n| `npm run pw:test:ui` | Interactive Playwright UI mode |\n| `npm run pw:test:debug` | Playwright debug mode |\n| `npm run pw:report` | View test report |\n| `npm run clean` | Remove `/build` and `/dist` |\n\n### Environment Variables (`.env`, baked into build)\n\n| Variable | Description |\n|----------|-------------|\n| `VITE_VERSION` | App version displayed in the splash screen (defaults to `$npm_package_version`) |\n\n### Web Workers\n\nTwo workers handle background processing: `apidom.worker.js` (parsing/validation) and `editor.worker.js` (Monaco ops). Configure Monaco env **before** rendering:\n\n```javascript\nself.MonacoEnvironment = { baseUrl: `${document.baseURI || location.href}dist/` };\n```\n\nWorkers must be accessible at runtime — either build them separately via webpack entry points, or copy pre-built files from `node_modules/swagger-editor/dist/umd/` using CopyWebpackPlugin.\n\n### OOM Fix for Large Builds\n\n```bash\nexport NODE_OPTIONS=\"--max_old_space_size=4096\"\nnpm run build\n```\n\n---\n\n## Testing Strategy\n\n### Unit Testing (Vitest)\n- **Location:** `src/**/*.{spec,test}.{js,jsx,ts,tsx}`, run with `npm test` (watch) or `npm run test:run` (CI)\n- Config: `vitest.config.ts` — jsdom environment, globals enabled, `@codingame/monaco-vscode-api` inlined\n- Use `vi.fn()` for mocks; `@testing-library/jest-dom/vitest` for DOM matchers\n- ⚠️ Only 1 unit test exists: `ValidationPane.test.jsx` — heavy reliance on E2E\n\n### E2E Testing (Playwright)\n- **Location:** `test/playwright/e2e/*.spec.ts`, base URL `http://localhost:3000`\n- All tests written in TypeScript with full `@playwright/test` type safety\n\n**Existing test files:** `app.spec.ts`, `plugin.top-bar.spec.ts`, `plugin.editor-monaco.spec.ts`, `plugin.editor-monaco-yaml-paste.spec.ts`, `plugin.dropzone.spec.ts`, `plugin.validation-pane.spec.ts`, `plugin.editor-content-from-file.spec.ts`, `plugin.editor-persistence.spec.ts`, `plugin.editor-preview-*.spec.ts`, and more.\n\n**Helper functions** (`test/playwright/helpers/`):\n- Setup: `visitBlankPage()`, `waitForSplashScreen()`, `prepareAsyncAPI()`\n- Editor: `typeInEditor()`, `getAllEditorText()`, `selectAllEditorText()`\n- Menu: `clickMenu()`, `loadExample()`, `generateServer()`\n\n**E2E test template:**\n\n```typescript\nimport { test, expect } from '@playwright/test';\nimport { visitBlankPage, waitForSplashScreen } from '../helpers';\n\ntest.describe('Feature Name', () => {\n  test.beforeEach(async ({ page }) => {\n    await visitBlankPage(page);\n    await waitForSplashScreen(page);\n  });\n\n  test('should do something', async ({ page }) => {\n    await page.locator('[data-testid=\"some-element\"]').click();\n    await expect(page.locator('text=Expected Text')).toBeVisible();\n  });\n});\n```\n\n---\n\n## Code Style & Conventions\n\n### ESLint (Airbnb + Prettier + jsx-a11y + @typescript-eslint)\n\nKey rules:\n- **Arrow functions** for named components — not `function` declarations\n- **File extensions required** on all JS/JSX imports: `./Component.jsx` ✅ `./Component` ❌ (`.ts`/`.tsx` exempt)\n- **JSX only in `.jsx`/`.tsx` files**\n- **Import groups:** external/builtin first (blank line), then internal\n\nFix violations: `npm run lint:fix`\n\n### Prettier\n`printWidth: 100`, `tabWidth: 2`, `semi: true`, `singleQuote: true`, `trailingComma: 'es5'`, `endOfLine: 'lf'`\n\n### Commit Messages (Conventional Commits)\n\n```\n<type>(<scope>): <subject>      ← max 69 characters\n```\n\n**Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `perf`, `ci`\n\n**Scopes:** plugin name (`editor-monaco`, `top-bar`, `validation`) or area (`deps`, `build`, `release`)\n\nEnforced by commitlint via Husky pre-commit hook.\n\n### File Naming\n\n| Type | Convention | Example |\n|------|-----------|---------|\n| React Components | `PascalCase.jsx/tsx` | `MonacoEditor.jsx` |\n| Utilities | `kebab-case.js` | `import-url.js` |\n| Types | `kebab-case.d.ts` | `system.d.ts` |\n| Styles (partials) | `_kebab-case.scss` | `_monaco-editor.scss` |\n| Unit Tests | `ComponentName.test.jsx` | `ValidationPane.test.jsx` |\n| E2E Tests | `feature.spec.ts` | `plugin.editor-monaco.spec.ts` |\n\n### Styling\n- **SCSS** for all component styles; BEM-like naming (`.editor-pane__title`)\n- Partial files prefixed with `_`; global styles in `/src/styles/index.scss`\n\n### TypeScript\n- Strict mode **off** globally; gradual adoption via `typescript-strict-plugin`\n- Legacy files may use `// @ts-strict-ignore` at top; new files should aim for strictness\n- `allowJs: false` — TypeScript files only in tsconfig scope\n- Path aliases work: `import X from 'plugins/editor-monaco'` ✅\n\n---\n\n## State Management\n\nSwaggerUI's plugin-based Redux-like system with Immutable.js.\n\n### Actions\n\n```javascript\nexport const SET_EDITOR_CONTENT = 'editor_set_content';\nexport const setEditorContent = (content) => ({ type: SET_EDITOR_CONTENT, payload: content });\n\n// Async thunk\nexport const loadDefinition = (url) => async (system) => {\n  const { editorActions, fn } = system;\n  const requestId = generateRequestId();\n  editorActions.loadDefinitionRequest({ url, requestId });\n  try {\n    const content = await fn.fetchUrl(url);\n    editorActions.loadDefinitionSuccess({ content, requestId });\n  } catch (error) {\n    editorActions.loadDefinitionFailure({ error, requestId });\n  }\n};\n```\n\n### Reducers (Immutable.js)\n\n```javascript\nimport { Map } from 'immutable';\n\nexport const initialState = Map({ content: '', status: 'idle', error: null, requestId: null });\n\nconst loadSuccessReducer = (state, action) => {\n  if (state.get('requestId') !== action.payload.requestId) return state; // ignore stale\n  return state.merge({ status: 'success', content: action.payload.content, error: null });\n};\n\nexport default {\n  [SET_EDITOR_CONTENT]: (state, action) => state.set('content', action.payload),\n  [LOAD_DEFINITION_SUCCESS]: loadSuccessReducer,\n};\n```\n\n### Selectors (Reselect)\n\n```javascript\nimport { createSelector } from 'reselect';\n\nexport const selectEditorState = (state) => state.get('editor');\nexport const selectEditorContent = (state) => selectEditorState(state).get('content');\nexport const selectStatus = (state) => selectEditorState(state).get('status');\n\n// Always memoize derived state\nexport const selectValidationErrors = createSelector(\n  selectValidationResults,\n  (results) => results.filter((r) => r.severity === 'error')\n);\n```\n\n### State Access in Components\n\n```javascript\nconst MyComponent = () => {\n  const { editorSelectors, editorActions } = useSystem();\n  const content = editorSelectors.selectEditorContent();\n  const isLoading = editorSelectors.selectIsLoading();\n\n  return <div>{isLoading ? 'Loading...' : content}</div>;\n};\n```\n\n### Known Issue: Editor Content Storage\n\n> Editor content is stored in SwaggerUI `spec` plugin, causing parse/resolve/store on every keystroke → typing lag with large specs. **Future fix:** store content in editor plugin with FSM pattern in preview plugins.\n\n- Avoid unnecessary state updates in the editor\n- Debounce expensive validation triggers\n\n---\n\n## Common Tasks\n\n### Creating a New Plugin\n\n```javascript\n// src/plugins/my-plugin/index.js\nconst MyPlugin = () => ({\n  components: { MyComponent: () => <div>Hello from MyPlugin</div> },\n  statePlugins: {\n    myPlugin: {\n      initialState: Map({ data: null }),\n      actions: { myAction: (payload) => ({ type: 'MY_ACTION', payload }) },\n      reducers: { MY_ACTION: (state, action) => state.set('data', action.payload) },\n      selectors: { selectData: createSelector((s) => s.get('myPlugin'), (s) => s.get('data')) },\n    },\n  },\n});\nexport default MyPlugin;\n```\n\nThen import and add to `App.tsx` or your preset's plugin array.\n\n### Adding a Component Wrapper\n\n```javascript\n// src/plugins/my-plugin/extensions/top-bar/wrap-components/TopBarWrapper.jsx\nconst TopBarWrapper = (Original, system) => {\n  const Enhanced = (props) => {\n    const showBanner = system.myPluginSelectors.selectShowBanner();\n    return (\n      <>\n        {showBanner && <div className=\"banner\">Important Notice</div>}\n        <Original {...props} />\n      </>\n    );\n  };\n  return Enhanced;\n};\n// Register in plugin: wrapComponents: { TopBar: TopBarWrapper }\n```\n\n### Adding a New Content Type\n\n```javascript\n// Extend detection in editor-content-type plugin (order matters — specific first)\nconst detectContentType = (content) => {\n  if (/^openapi:\\s*[\"']?3\\.1/.test(content)) return 'openapi-3-1';\n  if (/^openapi:\\s*[\"']?3\\.0/.test(content)) return 'openapi-3-0';\n  if (/^swagger:\\s*[\"']?2\\.0/.test(content)) return 'openapi-2-0';\n  if (/^asyncapi:\\s*[\"']?2\\./.test(content)) return 'asyncapi-2';\n  if (/^myspec:\\s*[\"']?1\\.0/.test(content)) return 'myspec-1-0'; // custom\n  return 'unknown';\n};\n```\n\nThen create a preview plugin that wraps `EditorPreview` and conditionally renders based on `editorSelectors.selectEditorContentType()`.\n\n### Adding E2E Tests\n\n```typescript\n// test/playwright/e2e/plugin.my-feature.spec.ts\nimport { test, expect } from '@playwright/test';\nimport { visitBlankPage, waitForSplashScreen } from '../helpers';\n\ntest.describe('My Feature', () => {\n  test.beforeEach(async ({ page }) => {\n    await visitBlankPage(page);\n    await waitForSplashScreen(page);\n  });\n  test('should perform action', async ({ page }) => {\n    await page.locator('[data-testid=\"my-button\"]').click();\n    await expect(page.locator('text=Expected Result')).toBeVisible();\n  });\n});\n```\n\n### Debugging Validation\n\n```javascript\nconst { editorSelectors } = useSystem();\nconsole.log('markers:', editorSelectors.selectEditorMarkers());\nconsole.log('diagnostics:', editorSelectors.selectDiagnostics());\nconsole.log('content type:', editorSelectors.selectEditorContentType());\nconsole.log('is OpenAPI:', editorSelectors.selectIsContentTypeOpenAPI());\n```\n\n---\n\n## Important Gotchas\n\n### 1. File Extensions Required\n\n```javascript\nimport Component from './Component.jsx';  // ✅\nimport Component from './Component';      // ❌ fails linting\n```\nException: `.ts`/`.tsx` files may omit extensions due to ESLint overrides.\n\n### 2. Immutable.js State Updates\n\n```javascript\nstate.data = newValue;           // ❌ mutates state\nreturn state.set('data', val);   // ✅\nreturn state.merge({ a, b });    // ✅ multiple fields\n```\n\n### 3. Component Wrapping Return Value\n\n```javascript\nconst Wrapper = (Original, system) => Original;              // ❌ no enhancement\nconst Wrapper = (Original, system) => (props) => <Original {...props} />; // ✅\n```\n\n### 4. Request ID Race Conditions\n\nAlways include `requestId` in async actions and guard in reducers:\n```javascript\nif (state.get('requestId') !== action.payload.requestId) return state;\n```\n\n### 5. Monaco Environment Configuration\n\nMust be set **before** rendering SwaggerEditor:\n```javascript\nself.MonacoEnvironment = { baseUrl: `${document.baseURI || location.href}dist/` };\nReactDOM.render(<SwaggerEditor />, document.getElementById('root'));\n```\n\n### 6. Web Worker Path Issues\n\nWorkers must be accessible at runtime — build separately via webpack entries or copy pre-built files with CopyWebpackPlugin.\n\n### 7. Large Bundle / OOM Errors\n\n```bash\nexport NODE_OPTIONS=\"--max_old_space_size=4096\"\n```\n\n### 8. Content Type Detection Order\n\nMore specific patterns must come first — detect `3.1` before `3.0`, `3.0` before `2.0`.\n\n### 9. Selector Memoization\n\n```javascript\n// ❌ recalculates every render\nexport const selectErrors = (state) => selectResults(state).filter(r => r.severity === 'error');\n\n// ✅ memoized with createSelector\nexport const selectErrors = createSelector(selectResults, (r) => r.filter(...));\n```\n\n### 10. TypeScript Strict Mode\n\nOff globally. Use `typescript-strict-plugin` per file. New files should aim for strictness (no `@ts-strict-ignore`). Legacy files may have it.\n\n---\n\n## Key Files Reference\n\n**Core:**\n| File | Purpose |\n|------|---------|\n| `/src/App.tsx` | Main component, plugin composition |\n| `/src/index.tsx` | Browser entry point |\n| `/public/index.html` | HTML template, MonacoEnvironment setup |\n\n**Config:**\n| File | Purpose |\n|------|---------|\n| `/package.json` | Dependencies and scripts |\n| `/tsconfig.json` | TypeScript compiler options |\n| `/.eslintrc` | ESLint rules (Airbnb + Prettier + @vitest) |\n| `/.prettierrc` | Formatting rules |\n| `/.commitlintrc.json` | Commit message linting |\n| `/vite.config.js` | Vite dev server config |\n| `/vite.config.app.js` | Vite app production build config |\n| `/vitest.config.ts` | Vitest unit test config |\n| `/playwright.config.ts` | Playwright E2E config |\n\n**Docs:**\n| File | Purpose |\n|------|---------|\n| `/docs/architecture.md` | High-level architecture overview |\n| `/docs/customization/plug-points/` | Plugin customization guides |\n| `/docs/migration*.md` | Migration guides from legacy version |\n\n**Testing:**\n| File | Purpose |\n|------|---------|\n| `/test/setupTests.js` | Vitest setup (jest-dom/vitest, vitest-canvas-mock) |\n| `/test/playwright/e2e/*.spec.ts` | E2E test specs |\n| `/test/playwright/helpers/` | Playwright helper functions |\n\n**Plugin Paths:**\n- Editor: `/src/plugins/editor-textarea/`, `/src/plugins/editor-monaco/`, `/src/plugins/editor-monaco-language-apidom/`, `/src/plugins/editor-monaco-yaml-paste/`\n- Preview: `/src/plugins/editor-preview*/`\n- Support: `/src/plugins/editor-content-*/`\n- Generic: `/src/plugins/layout/`, `/src/plugins/top-bar/`, `/src/plugins/modals/`, etc.\n- Presets: `/src/presets/monaco/` (default), `/src/presets/textarea/`\n\n---\n\n## Quick Reference for AI Assistants\n\n### Fixing a Bug\n1. Identify the plugin — most bugs are plugin-specific\n2. Check `test/playwright/e2e/plugin.<name>.spec.ts` for existing coverage\n3. Review actions/reducers/selectors in that plugin\n4. Add an E2E test to prevent regression\n\n### Adding a Feature\n1. Identify affected plugins; decide new vs. extend existing\n2. Plan state management needs (new actions/reducers?)\n3. Use component wrapping to enhance without forking\n4. Write E2E test first (TDD preferred); update README if user-facing\n\n### Refactoring\n1. Follow established patterns — don't tightly couple plugins\n2. Maintain Immutable.js correctness and selector memoization\n3. Run: `npm run lint:fix && npm test && npx playwright test`\n\n### When Stuck\n1. Read the relevant plugin source — most logic lives there\n2. Check `docs/architecture.md` for high-level overview\n3. Look at similar plugins for patterns to copy\n4. Check Playwright tests to see how features are exercised\n\n### Before Committing\n- [ ] `npm run lint:fix`\n- [ ] `npm test`\n- [ ] `npx playwright test`\n- [ ] Commit message: Conventional Commits format, max 69 chars header\n- [ ] `npm run build` succeeds\n\n---\n\n**Last Updated:** 2026-05-16 | **Version:** 5.0.6 | Update this file when architecture changes significantly\n"},"files":{"CLAUDE.md":"# CLAUDE.md - SwaggerEditor Codebase Guide for AI Assistants\n\n**Version:** 5.0.6 | **Last Updated:** 2026-05-16\n\n---\n\n## Project Overview\n\nSwaggerEditor is a browser-based editor for API specifications supporting **OpenAPI 2.0/3.0/3.1/3.2**, **AsyncAPI 2.x/3.0**, **API Design Systems**, and **JSON Schema**. Built as a React app on SwaggerUI's plugin architecture with a split-pane Monaco Editor interface.\n\n**Core Technologies:** React 17+/18, SwaggerUI React, TypeScript (gradual), Immutable.js, Monaco Editor, ApiDOM, Vite 8, Vitest + Playwright\n\n**Philosophy:** Plugin-based architecture, minimal over-engineering, heavy E2E testing, gradual TypeScript adoption, app/ESM/UMD build artifacts.\n\n---\n\n## Codebase Structure\n\n```\nswagger-editor/\n├── vite/                 # Vite plugins, configs, and build scripts\n├── docs/                 # architecture.md, customization guides, migration guides\n├── public/               # Static assets, HTML template\n├── src/\n│   ├── App.tsx           # Main app component & plugin composition\n│   ├── index.tsx         # Browser entry point\n│   ├── plugins/          # 26 plugins (see Architecture section)\n│   ├── presets/\n│   │   ├── monaco/       # Full-featured preset (default)\n│   │   └── textarea/     # Lightweight fallback\n│   ├── styles/           # Global SCSS (index.scss)\n│   └── types/            # TypeScript declarations (*.d.ts)\n├── test/\n│   ├── playwright/\n│   │   ├── e2e/          # Test specs (*.spec.ts)\n│   │   ├── fixtures/     # Test data\n│   │   ├── helpers/      # Helper functions\n│   │   └── tsconfig.json\n│   └── setupTests.js     # Vitest setup (jest-dom/vitest, canvas-mock)\n├── build/                # Standalone app (generated)\n└── dist/esm|umd|types/   # Library bundles (generated)\n```\n\n---\n\n## Architecture & Design Patterns\n\n### Plugin Categories (26 total)\n\n**Editor implementations:**\n- `editor-textarea` — HTML `<textarea>` fallback\n- `editor-monaco` — Monaco Editor (advanced)\n- `editor-monaco-language-apidom` — ApiDOM language support\n- `editor-monaco-yaml-paste` — YAML paste transformations\n\n**Preview plugins:**\n- `editor-preview` — Base preview component\n- `editor-preview-swagger-ui` — OpenAPI rendering\n- `editor-preview-asyncapi` — AsyncAPI rendering\n- `editor-preview-api-design-systems` — ADS rendering\n\n**Editor support plugins:**\n- `editor-content-type` — Auto-detect content type (OpenAPI/AsyncAPI/JSON Schema)\n- `editor-content-persistence` — LocalStorage persistence\n- `editor-content-read-only` — Read-only mode\n- `editor-content-origin` — Track content source (URL, file, user)\n- `editor-content-fixtures` — Load example/fixture files\n- `editor-content-from-file` — File import\n\n**Generic feature plugins:**\n- `layout`, `top-bar`, `modals`, `dialogs`, `dropdown-menu`, `dropzone`, `splash-screen`, `editor-safe-render`, `swagger-ui-adapter`, `util`, `versions`, `props-change-watcher`\n\n### Component Hierarchy\n\n```\nApp.tsx (SwaggerUI wrapper)\n└── SwaggerEditorLayout\n    ├── SplashScreen\n    ├── TopBar (File/Edit/Generate menus)\n    └── Container\n        └── Dropzone\n            └── SplitPane (resizable)\n                ├── EditorPane\n                │   ├── EditorPaneBarTop\n                │   ├── MonacoEditor / TextareaEditor\n                │   └── ValidationPane (errors/warnings)\n                └── EditorPreviewPane\n                    └── EditorPreviewSwaggerUI / AsyncAPI / ApiDesignSystems\n```\n\n### Design Patterns\n\n- **Container/Presenter:** Containers connect to Redux (e.g., `MonacoEditorContainer.jsx`), presenters handle rendering (e.g., `MonacoEditor.jsx`)\n- **HOC via `wrapComponents`:** Plugins wrap existing components to enhance without forking\n- **`getComponent`:** Dynamically resolve components — `const C = getComponent('MonacoEditor')`\n- **FSM for async:** `idle → loading → success/failure` with request ID tracking to prevent race conditions\n\n---\n\n## Plugin System\n\n### Plugin Structure\n\n```javascript\n// src/plugins/plugin-name/index.js\nconst PluginName = ({ getSystem }) => ({\n  afterLoad: function,                   // Runs after plugin loads\n  components: {\n    ComponentName: Component,            // Register new components\n  },\n  wrapComponents: {\n    ComponentName: WrapperFn,            // Wrap/enhance existing components\n  },\n  rootInjects: {\n    utilityName: function,               // Inject utilities into system\n  },\n  statePlugins: {\n    pluginStateKey: {\n      actions: {},                       // Action creators\n      reducers: {},                      // Immutable.js reducers\n      selectors: {},                     // Reselect selectors\n      wrapActions: {},                   // Action middleware\n    },\n  },\n  fn: { utilityFunction: function },\n});\nexport default PluginName;\n```\n\n### Typical Plugin File Structure\n\n```\nplugin-name/\n├── index.js\n├── actions/index.js\n├── reducers.js\n├── selectors.js\n├── components/ComponentName.jsx\n├── components/ComponentName.scss\n├── extensions/other-plugin/wrap-components/ComponentWrapper.jsx\n├── after-load.js\n└── fn.js\n```\n\n### Component Wrapping Pattern\n\n```javascript\n// extensions/editor-preview/wrap-components/EditorPreviewWrapper.jsx\nconst EditorPreviewWrapper = (Original, system) => {\n  const EnhancedComponent = (props) => {\n    const isOpenAPI = system.editorSelectors.selectIsContentTypeOpenAPI();\n    if (isOpenAPI) return <EditorPreviewSwaggerUI />;\n    return <Original {...props} />;\n  };\n  return EnhancedComponent; // must return new component, not Original directly\n};\nexport default EditorPreviewWrapper;\n```\n\n### System Access in Plugins\n\n```javascript\nconst MyPlugin = (system) => {\n  const { getComponent, editorActions, editorSelectors, fn } = system;\n  const content = editorSelectors.selectEditorContent();\n  editorActions.setEditorContent('new content');\n  const MonacoEditor = getComponent('MonacoEditor');\n};\n```\n\n---\n\n## Development Workflows\n\n### Prerequisites\n- **Node.js** `>=24.19.0`, **npm** `>=11.7.0`, **Python 3.x** (node-gyp), **GLIBC** `>=2.29`\n- Optional: Docker or emscripten (for WASM builds)\n\n### npm Scripts\n\n| Script | Description |\n|--------|-------------|\n| `npm start` | Dev server on port 3000 (hot reload) |\n| `npm test` | Vitest unit tests (watch mode) |\n| `npm run test:run` | Vitest unit tests (single run, no watch) |\n| `npm run test:coverage` | Vitest unit tests with coverage report |\n| `npm run lint` | ESLint on all files |\n| `npm run lint:fix` | Auto-fix ESLint errors |\n| `npm run build` | Build all artifacts (app + bundles + types) |\n| `npm run build:app` | Standalone app → `/build` |\n| `npm run build:app:serve` | Serve built app on port 3050 |\n| `npm run build:bundle:esm` | ESM bundle → `/dist/esm` |\n| `npm run build:bundle:umd` | UMD bundle → `/dist/umd` |\n| `npm run build:definitions` | TypeScript definitions → `/dist/types` |\n| `npm run pw:test` | E2E tests (headless) |\n| `npm run pw:test:headed` | E2E with browser visible |\n| `npm run pw:test:ui` | Interactive Playwright UI mode |\n| `npm run pw:test:debug` | Playwright debug mode |\n| `npm run pw:report` | View test report |\n| `npm run clean` | Remove `/build` and `/dist` |\n\n### Environment Variables (`.env`, baked into build)\n\n| Variable | Description |\n|----------|-------------|\n| `VITE_VERSION` | App version displayed in the splash screen (defaults to `$npm_package_version`) |\n\n### Web Workers\n\nTwo workers handle background processing: `apidom.worker.js` (parsing/validation) and `editor.worker.js` (Monaco ops). Configure Monaco env **before** rendering:\n\n```javascript\nself.MonacoEnvironment = { baseUrl: `${document.baseURI || location.href}dist/` };\n```\n\nWorkers must be accessible at runtime — either build them separately via webpack entry points, or copy pre-built files from `node_modules/swagger-editor/dist/umd/` using CopyWebpackPlugin.\n\n### OOM Fix for Large Builds\n\n```bash\nexport NODE_OPTIONS=\"--max_old_space_size=4096\"\nnpm run build\n```\n\n---\n\n## Testing Strategy\n\n### Unit Testing (Vitest)\n- **Location:** `src/**/*.{spec,test}.{js,jsx,ts,tsx}`, run with `npm test` (watch) or `npm run test:run` (CI)\n- Config: `vitest.config.ts` — jsdom environment, globals enabled, `@codingame/monaco-vscode-api` inlined\n- Use `vi.fn()` for mocks; `@testing-library/jest-dom/vitest` for DOM matchers\n- ⚠️ Only 1 unit test exists: `ValidationPane.test.jsx` — heavy reliance on E2E\n\n### E2E Testing (Playwright)\n- **Location:** `test/playwright/e2e/*.spec.ts`, base URL `http://localhost:3000`\n- All tests written in TypeScript with full `@playwright/test` type safety\n\n**Existing test files:** `app.spec.ts`, `plugin.top-bar.spec.ts`, `plugin.editor-monaco.spec.ts`, `plugin.editor-monaco-yaml-paste.spec.ts`, `plugin.dropzone.spec.ts`, `plugin.validation-pane.spec.ts`, `plugin.editor-content-from-file.spec.ts`, `plugin.editor-persistence.spec.ts`, `plugin.editor-preview-*.spec.ts`, and more.\n\n**Helper functions** (`test/playwright/helpers/`):\n- Setup: `visitBlankPage()`, `waitForSplashScreen()`, `prepareAsyncAPI()`\n- Editor: `typeInEditor()`, `getAllEditorText()`, `selectAllEditorText()`\n- Menu: `clickMenu()`, `loadExample()`, `generateServer()`\n\n**E2E test template:**\n\n```typescript\nimport { test, expect } from '@playwright/test';\nimport { visitBlankPage, waitForSplashScreen } from '../helpers';\n\ntest.describe('Feature Name', () => {\n  test.beforeEach(async ({ page }) => {\n    await visitBlankPage(page);\n    await waitForSplashScreen(page);\n  });\n\n  test('should do something', async ({ page }) => {\n    await page.locator('[data-testid=\"some-element\"]').click();\n    await expect(page.locator('text=Expected Text')).toBeVisible();\n  });\n});\n```\n\n---\n\n## Code Style & Conventions\n\n### ESLint (Airbnb + Prettier + jsx-a11y + @typescript-eslint)\n\nKey rules:\n- **Arrow functions** for named components — not `function` declarations\n- **File extensions required** on all JS/JSX imports: `./Component.jsx` ✅ `./Component` ❌ (`.ts`/`.tsx` exempt)\n- **JSX only in `.jsx`/`.tsx` files**\n- **Import groups:** external/builtin first (blank line), then internal\n\nFix violations: `npm run lint:fix`\n\n### Prettier\n`printWidth: 100`, `tabWidth: 2`, `semi: true`, `singleQuote: true`, `trailingComma: 'es5'`, `endOfLine: 'lf'`\n\n### Commit Messages (Conventional Commits)\n\n```\n<type>(<scope>): <subject>      ← max 69 characters\n```\n\n**Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `perf`, `ci`\n\n**Scopes:** plugin name (`editor-monaco`, `top-bar`, `validation`) or area (`deps`, `build`, `release`)\n\nEnforced by commitlint via Husky pre-commit hook.\n\n### File Naming\n\n| Type | Convention | Example |\n|------|-----------|---------|\n| React Components | `PascalCase.jsx/tsx` | `MonacoEditor.jsx` |\n| Utilities | `kebab-case.js` | `import-url.js` |\n| Types | `kebab-case.d.ts` | `system.d.ts` |\n| Styles (partials) | `_kebab-case.scss` | `_monaco-editor.scss` |\n| Unit Tests | `ComponentName.test.jsx` | `ValidationPane.test.jsx` |\n| E2E Tests | `feature.spec.ts` | `plugin.editor-monaco.spec.ts` |\n\n### Styling\n- **SCSS** for all component styles; BEM-like naming (`.editor-pane__title`)\n- Partial files prefixed with `_`; global styles in `/src/styles/index.scss`\n\n### TypeScript\n- Strict mode **off** globally; gradual adoption via `typescript-strict-plugin`\n- Legacy files may use `// @ts-strict-ignore` at top; new files should aim for strictness\n- `allowJs: false` — TypeScript files only in tsconfig scope\n- Path aliases work: `import X from 'plugins/editor-monaco'` ✅\n\n---\n\n## State Management\n\nSwaggerUI's plugin-based Redux-like system with Immutable.js.\n\n### Actions\n\n```javascript\nexport const SET_EDITOR_CONTENT = 'editor_set_content';\nexport const setEditorContent = (content) => ({ type: SET_EDITOR_CONTENT, payload: content });\n\n// Async thunk\nexport const loadDefinition = (url) => async (system) => {\n  const { editorActions, fn } = system;\n  const requestId = generateRequestId();\n  editorActions.loadDefinitionRequest({ url, requestId });\n  try {\n    const content = await fn.fetchUrl(url);\n    editorActions.loadDefinitionSuccess({ content, requestId });\n  } catch (error) {\n    editorActions.loadDefinitionFailure({ error, requestId });\n  }\n};\n```\n\n### Reducers (Immutable.js)\n\n```javascript\nimport { Map } from 'immutable';\n\nexport const initialState = Map({ content: '', status: 'idle', error: null, requestId: null });\n\nconst loadSuccessReducer = (state, action) => {\n  if (state.get('requestId') !== action.payload.requestId) return state; // ignore stale\n  return state.merge({ status: 'success', content: action.payload.content, error: null });\n};\n\nexport default {\n  [SET_EDITOR_CONTENT]: (state, action) => state.set('content', action.payload),\n  [LOAD_DEFINITION_SUCCESS]: loadSuccessReducer,\n};\n```\n\n### Selectors (Reselect)\n\n```javascript\nimport { createSelector } from 'reselect';\n\nexport const selectEditorState = (state) => state.get('editor');\nexport const selectEditorContent = (state) => selectEditorState(state).get('content');\nexport const selectStatus = (state) => selectEditorState(state).get('status');\n\n// Always memoize derived state\nexport const selectValidationErrors = createSelector(\n  selectValidationResults,\n  (results) => results.filter((r) => r.severity === 'error')\n);\n```\n\n### State Access in Components\n\n```javascript\nconst MyComponent = () => {\n  const { editorSelectors, editorActions } = useSystem();\n  const content = editorSelectors.selectEditorContent();\n  const isLoading = editorSelectors.selectIsLoading();\n\n  return <div>{isLoading ? 'Loading...' : content}</div>;\n};\n```\n\n### Known Issue: Editor Content Storage\n\n> Editor content is stored in SwaggerUI `spec` plugin, causing parse/resolve/store on every keystroke → typing lag with large specs. **Future fix:** store content in editor plugin with FSM pattern in preview plugins.\n\n- Avoid unnecessary state updates in the editor\n- Debounce expensive validation triggers\n\n---\n\n## Common Tasks\n\n### Creating a New Plugin\n\n```javascript\n// src/plugins/my-plugin/index.js\nconst MyPlugin = () => ({\n  components: { MyComponent: () => <div>Hello from MyPlugin</div> },\n  statePlugins: {\n    myPlugin: {\n      initialState: Map({ data: null }),\n      actions: { myAction: (payload) => ({ type: 'MY_ACTION', payload }) },\n      reducers: { MY_ACTION: (state, action) => state.set('data', action.payload) },\n      selectors: { selectData: createSelector((s) => s.get('myPlugin'), (s) => s.get('data')) },\n    },\n  },\n});\nexport default MyPlugin;\n```\n\nThen import and add to `App.tsx` or your preset's plugin array.\n\n### Adding a Component Wrapper\n\n```javascript\n// src/plugins/my-plugin/extensions/top-bar/wrap-components/TopBarWrapper.jsx\nconst TopBarWrapper = (Original, system) => {\n  const Enhanced = (props) => {\n    const showBanner = system.myPluginSelectors.selectShowBanner();\n    return (\n      <>\n        {showBanner && <div className=\"banner\">Important Notice</div>}\n        <Original {...props} />\n      </>\n    );\n  };\n  return Enhanced;\n};\n// Register in plugin: wrapComponents: { TopBar: TopBarWrapper }\n```\n\n### Adding a New Content Type\n\n```javascript\n// Extend detection in editor-content-type plugin (order matters — specific first)\nconst detectContentType = (content) => {\n  if (/^openapi:\\s*[\"']?3\\.1/.test(content)) return 'openapi-3-1';\n  if (/^openapi:\\s*[\"']?3\\.0/.test(content)) return 'openapi-3-0';\n  if (/^swagger:\\s*[\"']?2\\.0/.test(content)) return 'openapi-2-0';\n  if (/^asyncapi:\\s*[\"']?2\\./.test(content)) return 'asyncapi-2';\n  if (/^myspec:\\s*[\"']?1\\.0/.test(content)) return 'myspec-1-0'; // custom\n  return 'unknown';\n};\n```\n\nThen create a preview plugin that wraps `EditorPreview` and conditionally renders based on `editorSelectors.selectEditorContentType()`.\n\n### Adding E2E Tests\n\n```typescript\n// test/playwright/e2e/plugin.my-feature.spec.ts\nimport { test, expect } from '@playwright/test';\nimport { visitBlankPage, waitForSplashScreen } from '../helpers';\n\ntest.describe('My Feature', () => {\n  test.beforeEach(async ({ page }) => {\n    await visitBlankPage(page);\n    await waitForSplashScreen(page);\n  });\n  test('should perform action', async ({ page }) => {\n    await page.locator('[data-testid=\"my-button\"]').click();\n    await expect(page.locator('text=Expected Result')).toBeVisible();\n  });\n});\n```\n\n### Debugging Validation\n\n```javascript\nconst { editorSelectors } = useSystem();\nconsole.log('markers:', editorSelectors.selectEditorMarkers());\nconsole.log('diagnostics:', editorSelectors.selectDiagnostics());\nconsole.log('content type:', editorSelectors.selectEditorContentType());\nconsole.log('is OpenAPI:', editorSelectors.selectIsContentTypeOpenAPI());\n```\n\n---\n\n## Important Gotchas\n\n### 1. File Extensions Required\n\n```javascript\nimport Component from './Component.jsx';  // ✅\nimport Component from './Component';      // ❌ fails linting\n```\nException: `.ts`/`.tsx` files may omit extensions due to ESLint overrides.\n\n### 2. Immutable.js State Updates\n\n```javascript\nstate.data = newValue;           // ❌ mutates state\nreturn state.set('data', val);   // ✅\nreturn state.merge({ a, b });    // ✅ multiple fields\n```\n\n### 3. Component Wrapping Return Value\n\n```javascript\nconst Wrapper = (Original, system) => Original;              // ❌ no enhancement\nconst Wrapper = (Original, system) => (props) => <Original {...props} />; // ✅\n```\n\n### 4. Request ID Race Conditions\n\nAlways include `requestId` in async actions and guard in reducers:\n```javascript\nif (state.get('requestId') !== action.payload.requestId) return state;\n```\n\n### 5. Monaco Environment Configuration\n\nMust be set **before** rendering SwaggerEditor:\n```javascript\nself.MonacoEnvironment = { baseUrl: `${document.baseURI || location.href}dist/` };\nReactDOM.render(<SwaggerEditor />, document.getElementById('root'));\n```\n\n### 6. Web Worker Path Issues\n\nWorkers must be accessible at runtime — build separately via webpack entries or copy pre-built files with CopyWebpackPlugin.\n\n### 7. Large Bundle / OOM Errors\n\n```bash\nexport NODE_OPTIONS=\"--max_old_space_size=4096\"\n```\n\n### 8. Content Type Detection Order\n\nMore specific patterns must come first — detect `3.1` before `3.0`, `3.0` before `2.0`.\n\n### 9. Selector Memoization\n\n```javascript\n// ❌ recalculates every render\nexport const selectErrors = (state) => selectResults(state).filter(r => r.severity === 'error');\n\n// ✅ memoized with createSelector\nexport const selectErrors = createSelector(selectResults, (r) => r.filter(...));\n```\n\n### 10. TypeScript Strict Mode\n\nOff globally. Use `typescript-strict-plugin` per file. New files should aim for strictness (no `@ts-strict-ignore`). Legacy files may have it.\n\n---\n\n## Key Files Reference\n\n**Core:**\n| File | Purpose |\n|------|---------|\n| `/src/App.tsx` | Main component, plugin composition |\n| `/src/index.tsx` | Browser entry point |\n| `/public/index.html` | HTML template, MonacoEnvironment setup |\n\n**Config:**\n| File | Purpose |\n|------|---------|\n| `/package.json` | Dependencies and scripts |\n| `/tsconfig.json` | TypeScript compiler options |\n| `/.eslintrc` | ESLint rules (Airbnb + Prettier + @vitest) |\n| `/.prettierrc` | Formatting rules |\n| `/.commitlintrc.json` | Commit message linting |\n| `/vite.config.js` | Vite dev server config |\n| `/vite.config.app.js` | Vite app production build config |\n| `/vitest.config.ts` | Vitest unit test config |\n| `/playwright.config.ts` | Playwright E2E config |\n\n**Docs:**\n| File | Purpose |\n|------|---------|\n| `/docs/architecture.md` | High-level architecture overview |\n| `/docs/customization/plug-points/` | Plugin customization guides |\n| `/docs/migration*.md` | Migration guides from legacy version |\n\n**Testing:**\n| File | Purpose |\n|------|---------|\n| `/test/setupTests.js` | Vitest setup (jest-dom/vitest, vitest-canvas-mock) |\n| `/test/playwright/e2e/*.spec.ts` | E2E test specs |\n| `/test/playwright/helpers/` | Playwright helper functions |\n\n**Plugin Paths:**\n- Editor: `/src/plugins/editor-textarea/`, `/src/plugins/editor-monaco/`, `/src/plugins/editor-monaco-language-apidom/`, `/src/plugins/editor-monaco-yaml-paste/`\n- Preview: `/src/plugins/editor-preview*/`\n- Support: `/src/plugins/editor-content-*/`\n- Generic: `/src/plugins/layout/`, `/src/plugins/top-bar/`, `/src/plugins/modals/`, etc.\n- Presets: `/src/presets/monaco/` (default), `/src/presets/textarea/`\n\n---\n\n## Quick Reference for AI Assistants\n\n### Fixing a Bug\n1. Identify the plugin — most bugs are plugin-specific\n2. Check `test/playwright/e2e/plugin.<name>.spec.ts` for existing coverage\n3. Review actions/reducers/selectors in that plugin\n4. Add an E2E test to prevent regression\n\n### Adding a Feature\n1. Identify affected plugins; decide new vs. extend existing\n2. Plan state management needs (new actions/reducers?)\n3. Use component wrapping to enhance without forking\n4. Write E2E test first (TDD preferred); update README if user-facing\n\n### Refactoring\n1. Follow established patterns — don't tightly couple plugins\n2. Maintain Immutable.js correctness and selector memoization\n3. Run: `npm run lint:fix && npm test && npx playwright test`\n\n### When Stuck\n1. Read the relevant plugin source — most logic lives there\n2. Check `docs/architecture.md` for high-level overview\n3. Look at similar plugins for patterns to copy\n4. Check Playwright tests to see how features are exercised\n\n### Before Committing\n- [ ] `npm run lint:fix`\n- [ ] `npm test`\n- [ ] `npx playwright test`\n- [ ] Commit message: Conventional Commits format, max 69 chars header\n- [ ] `npm run build` succeeds\n\n---\n\n**Last Updated:** 2026-05-16 | **Version:** 5.0.6 | Update this file when architecture changes significantly\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md - SwaggerEditor Codebase Guide for AI Assistants\n\n**Version:** 5.0.6 | **Last Updated:** 2026-05-16\n\n---\n\n## Project Overview\n\nSwaggerEditor is a browser-based editor for API specifications supporting **OpenAPI 2.0/3.0/3.1/3.2**, **AsyncAPI 2.x/3.0**, **API Design Systems**, and **JSON Schema**. Built as a React app on SwaggerUI's plugin architecture with a split-pane Monaco Editor interface.\n\n**Core Technologies:** React 17+/18, SwaggerUI React, TypeScript (gradual), Immutable.js, Monaco Editor, ApiDOM, Vite 8, Vitest + Playwright\n\n**Philosophy:** Plugin-based architecture, minimal over-engineering, heavy E2E testing, gradual TypeScript adoption, app/ESM/UMD build artifacts.\n\n---\n\n## Codebase Structure\n\n```\nswagger-editor/\n├── vite/                 # Vite plugins, configs, and build scripts\n├── docs/                 # architecture.md, customization guides, migration guides\n├── public/               # Static assets, HTML template\n├── src/\n│   ├── App.tsx           # Main app component & plugin composition\n│   ├── index.tsx         # Browser entry point\n│   ├── plugins/          # 26 plugins (see Architecture section)\n│   ├── presets/\n│   │   ├── monaco/       # Full-featured preset (default)\n│   │   └── textarea/     # Lightweight fallback\n│   ├── styles/           # Global SCSS (index.scss)\n│   └── types/            # TypeScript declarations (*.d.ts)\n├── test/\n│   ├── playwright/\n│   │   ├── e2e/          # Test specs (*.spec.ts)\n│   │   ├── fixtures/     # Test data\n│   │   ├── helpers/      # Helper functions\n│   │   └── tsconfig.json\n│   └── setupTests.js     # Vitest setup (jest-dom/vitest, canvas-mock)\n├── build/                # Standalone app (generated)\n└── dist/esm|umd|types/   # Library bundles (generated)\n```\n\n---\n\n## Architecture & Design Patterns\n\n### Plugin Categories (26 total)\n\n**Editor implementations:**\n- `editor-textarea` — HTML `<textarea>` fallback\n- `editor-monaco` — Monaco Editor (advanced)\n- `editor-monaco-language-apidom` — ApiDOM language support\n- `editor-monaco-yaml-paste` — YAML paste transformations\n\n**Preview plugins:**\n- `editor-preview` — Base preview component\n- `editor-preview-swagger-ui` — OpenAPI rendering\n- `editor-preview-asyncapi` — AsyncAPI rendering\n- `editor-preview-api-design-systems` — ADS rendering\n\n**Editor support plugins:**\n- `editor-content-type` — Auto-detect content type (OpenAPI/AsyncAPI/JSON Schema)\n- `editor-content-persistence` — LocalStorage persistence\n- `editor-content-read-only` — Read-only mode\n- `editor-content-origin` — Track content source (URL, file, user)\n- `editor-content-fixtures` — Load example/fixture files\n- `editor-content-from-file` — File import\n\n**Generic feature plugins:**\n- `layout`, `top-bar`, `modals`, `dialogs`, `dropdown-menu`, `dropzone`, `splash-screen`, `editor-safe-render`, `swagger-ui-adapter`, `util`, `versions`, `props-change-watcher`\n\n### Component Hierarchy\n\n```\nApp.tsx (SwaggerUI wrapper)\n└── SwaggerEditorLayout\n    ├── SplashScreen\n    ├── TopBar (File/Edit/Generate menus)\n    └── Container\n        └── Dropzone\n            └── SplitPane (resizable)\n                ├── EditorPane\n                │   ├── EditorPaneBarTop\n                │   ├── MonacoEditor / TextareaEditor\n                │   └── ValidationPane (errors/warnings)\n                └── EditorPreviewPane\n                    └── EditorPreviewSwaggerUI / AsyncAPI / ApiDesignSystems\n```\n\n### Design Patterns\n\n- **Container/Presenter:** Containers connect to Redux (e.g., `MonacoEditorContainer.jsx`), presenters handle rendering (e.g., `MonacoEditor.jsx`)\n- **HOC via `wrapComponents`:** Plugins wrap existing components to enhance without forking\n- **`getComponent`:** Dynamically resolve components — `const C = getComponent('MonacoEditor')`\n- **FSM for async:** `idle → loading → success/failure` with request ID tracking to prevent race conditions\n\n---\n\n## Plugin System\n\n### Plugin Structure\n\n```javascript\n// src/plugins/plugin-name/index.js\nconst PluginName = ({ getSystem }) => ({\n  afterLoad: function,                   // Runs after plugin loads\n  components: {\n    ComponentName: Component,            // Register new components\n  },\n  wrapComponents: {\n    ComponentName: WrapperFn,            // Wrap/enhance existing components\n  },\n  rootInjects: {\n    utilityName: function,               // Inject utilities into system\n  },\n  statePlugins: {\n    pluginStateKey: {\n      actions: {},                       // Action creators\n      reducers: {},                      // Immutable.js reducers\n      selectors: {},                     // Reselect selectors\n      wrapActions: {},                   // Action middleware\n    },\n  },\n  fn: { utilityFunction: function },\n});\nexport default PluginName;\n```\n\n### Typical Plugin File Structure\n\n```\nplugin-name/\n├── index.js\n├── actions/index.js\n├── reducers.js\n├── selectors.js\n├── components/ComponentName.jsx\n├── components/ComponentName.scss\n├── extensions/other-plugin/wrap-components/ComponentWrapper.jsx\n├── after-load.js\n└── fn.js\n```\n\n### Component Wrapping Pattern\n\n```javascript\n// extensions/editor-preview/wrap-components/EditorPreviewWrapper.jsx\nconst EditorPreviewWrapper = (Original, system) => {\n  const EnhancedComponent = (props) => {\n    const isOpenAPI = system.editorSelectors.selectIsContentTypeOpenAPI();\n    if (isOpenAPI) return <EditorPreviewSwaggerUI />;\n    return <Original {...props} />;\n  };\n  return EnhancedComponent; // must return new component, not Original directly\n};\nexport default EditorPreviewWrapper;\n```\n\n### System Access in Plugins\n\n```javascript\nconst MyPlugin = (system) => {\n  const { getComponent, editorActions, editorSelectors, fn } = system;\n  const content = editorSelectors.selectEditorContent();\n  editorActions.setEditorContent('new content');\n  const MonacoEditor = getComponent('MonacoEditor');\n};\n```\n\n---\n\n## Development Workflows\n\n### Prerequisites\n- **Node.js** `>=24.19.0`, **npm** `>=11.7.0`, **Python 3.x** (node-gyp), **GLIBC** `>=2.29`\n- Optional: Docker or emscripten (for WASM builds)\n\n### npm Scripts\n\n| Script | Description |\n|--------|-------------|\n| `npm start` | Dev server on port 3000 (hot reload) |\n| `npm test` | Vitest unit tests (watch mode) |\n| `npm run test:run` | Vitest unit tests (single run, no watch) |\n| `npm run test:coverage` | Vitest unit tests with coverage report |\n| `npm run lint` | ESLint on all files |\n| `npm run lint:fix` | Auto-fix ESLint errors |\n| `npm run build` | Build all artifacts (app + bundles + types) |\n| `npm run build:app` | Standalone app → `/build` |\n| `npm run build:app:serve` | Serve built app on port 3050 |\n| `npm run build:bundle:esm` | ESM bundle → `/dist/esm` |\n| `npm run build:bundle:umd` | UMD bundle → `/dist/umd` |\n| `npm run build:definitions` | TypeScript definitions → `/dist/types` |\n| `npm run pw:test` | E2E tests (headless) |\n| `npm run pw:test:headed` | E2E with browser visible |\n| `npm run pw:test:ui` | Interactive Playwright UI mode |\n| `npm run pw:test:debug` | Playwright debug mode |\n| `npm run pw:report` | View test report |\n| `npm run clean` | Remove `/build` and `/dist` |\n\n### Environment Variables (`.env`, baked into build)\n\n| Variable | Description |\n|----------|-------------|\n| `VITE_VERSION` | App version displayed in the splash screen (defaults to `$npm_package_version`) |\n\n### Web Workers\n\nTwo workers handle background processing: `apidom.worker.js` (parsing/validation) and `editor.worker.js` (Monaco ops). Configure Monaco env **before** rendering:\n\n```javascript\nself.MonacoEnvironment = { baseUrl: `${document.baseURI || location.href}dist/` };\n```\n\nWorkers must be accessible at runtime — either build them separately via webpack entry points, or copy pre-built files from `node_modules/swagger-editor/dist/umd/` using CopyWebpackPlugin.\n\n### OOM Fix for Large Builds\n\n```bash\nexport NODE_OPTIONS=\"--max_old_space_size=4096\"\nnpm run build\n```\n\n---\n\n## Testing Strategy\n\n### Unit Testing (Vitest)\n- **Location:** `src/**/*.{spec,test}.{js,jsx,ts,tsx}`, run with `npm test` (watch) or `npm run test:run` (CI)\n- Config: `vitest.config.ts` — jsdom environment, globals enabled, `@codingame/monaco-vscode-api` inlined\n- Use `vi.fn()` for mocks; `@testing-library/jest-dom/vitest` for DOM matchers\n- ⚠️ Only 1 unit test exists: `ValidationPane.test.jsx` — heavy reliance on E2E\n\n### E2E Testing (Playwright)\n- **Location:** `test/playwright/e2e/*.spec.ts`, base URL `http://localhost:3000`\n- All tests written in TypeScript with full `@playwright/test` type safety\n\n**Existing test files:** `app.spec.ts`, `plugin.top-bar.spec.ts`, `plugin.editor-monaco.spec.ts`, `plugin.editor-monaco-yaml-paste.spec.ts`, `plugin.dropzone.spec.ts`, `plugin.validation-pane.spec.ts`, `plugin.editor-content-from-file.spec.ts`, `plugin.editor-persistence.spec.ts`, `plugin.editor-preview-*.spec.ts`, and more.\n\n**Helper functions** (`test/playwright/helpers/`):\n- Setup: `visitBlankPage()`, `waitForSplashScreen()`, `prepareAsyncAPI()`\n- Editor: `typeInEditor()`, `getAllEditorText()`, `selectAllEditorText()`\n- Menu: `clickMenu()`, `loadExample()`, `generateServer()`\n\n**E2E test template:**\n\n```typescript\nimport { test, expect } from '@playwright/test';\nimport { visitBlankPage, waitForSplashScreen } from '../helpers';\n\ntest.describe('Feature Name', () => {\n  test.beforeEach(async ({ page }) => {\n    await visitBlankPage(page);\n    await waitForSplashScreen(page);\n  });\n\n  test('should do something', async ({ page }) => {\n    await page.locator('[data-testid=\"some-element\"]').click();\n    await expect(page.locator('text=Expected Text')).toBeVisible();\n  });\n});\n```\n\n---\n\n## Code Style & Conventions\n\n### ESLint (Airbnb + Prettier + jsx-a11y + @typescript-eslint)\n\nKey rules:\n- **Arrow functions** for named components — not `function` declarations\n- **File extensions required** on all JS/JSX imports: `./Component.jsx` ✅ `./Component` ❌ (`.ts`/`.tsx` exempt)\n- **JSX only in `.jsx`/`.tsx` files**\n- **Import groups:** external/builtin first (blank line), then internal\n\nFix violations: `npm run lint:fix`\n\n### Prettier\n`printWidth: 100`, `tabWidth: 2`, `semi: true`, `singleQuote: true`, `trailingComma: 'es5'`, `endOfLine: 'lf'`\n\n### Commit Messages (Conventional Commits)\n\n```\n<type>(<scope>): <subject>      ← max 69 characters\n```\n\n**Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `perf`, `ci`\n\n**Scopes:** plugin name (`editor-monaco`, `top-bar`, `validation`) or area (`deps`, `build`, `release`)\n\nEnforced by commitlint via Husky pre-commit hook.\n\n### File Naming\n\n| Type | Convention | Example |\n|------|-----------|---------|\n| React Components | `PascalCase.jsx/tsx` | `MonacoEditor.jsx` |\n| Utilities | `kebab-case.js` | `import-url.js` |\n| Types | `kebab-case.d.ts` | `system.d.ts` |\n| Styles (partials) | `_kebab-case.scss` | `_monaco-editor.scss` |\n| Unit Tests | `ComponentName.test.jsx` | `ValidationPane.test.jsx` |\n| E2E Tests | `feature.spec.ts` | `plugin.editor-monaco.spec.ts` |\n\n### Styling\n- **SCSS** for all component styles; BEM-like naming (`.editor-pane__title`)\n- Partial files prefixed with `_`; global styles in `/src/styles/index.scss`\n\n### TypeScript\n- Strict mode **off** globally; gradual adoption via `typescript-strict-plugin`\n- Legacy files may use `// @ts-strict-ignore` at top; new files should aim for strictness\n- `allowJs: false` — TypeScript files only in tsconfig scope\n- Path aliases work: `import X from 'plugins/editor-monaco'` ✅\n\n---\n\n## State Management\n\nSwaggerUI's plugin-based Redux-like system with Immutable.js.\n\n### Actions\n\n```javascript\nexport const SET_EDITOR_CONTENT = 'editor_set_content';\nexport const setEditorContent = (content) => ({ type: SET_EDITOR_CONTENT, payload: content });\n\n// Async thunk\nexport const loadDefinition = (url) => async (system) => {\n  const { editorActions, fn } = system;\n  const requestId = generateRequestId();\n  editorActions.loadDefinitionRequest({ url, requestId });\n  try {\n    const content = await fn.fetchUrl(url);\n    editorActions.loadDefinitionSuccess({ content, requestId });\n  } catch (error) {\n    editorActions.loadDefinitionFailure({ error, requestId });\n  }\n};\n```\n\n### Reducers (Immutable.js)\n\n```javascript\nimport { Map } from 'immutable';\n\nexport const initialState = Map({ content: '', status: 'idle', error: null, requestId: null });\n\nconst loadSuccessReducer = (state, action) => {\n  if (state.get('requestId') !== action.payload.requestId) return state; // ignore stale\n  return state.merge({ status: 'success', content: action.payload.content, error: null });\n};\n\nexport default {\n  [SET_EDITOR_CONTENT]: (state, action) => state.set('content', action.payload),\n  [LOAD_DEFINITION_SUCCESS]: loadSuccessReducer,\n};\n```\n\n### Selectors (Reselect)\n\n```javascript\nimport { createSelector } from 'reselect';\n\nexport const selectEditorState = (state) => state.get('editor');\nexport const selectEditorContent = (state) => selectEditorState(state).get('content');\nexport const selectStatus = (state) => selectEditorState(state).get('status');\n\n// Always memoize derived state\nexport const selectValidationErrors = createSelector(\n  selectValidationResults,\n  (results) => results.filter((r) => r.severity === 'error')\n);\n```\n\n### State Access in Components\n\n```javascript\nconst MyComponent = () => {\n  const { editorSelectors, editorActions } = useSystem();\n  const content = editorSelectors.selectEditorContent();\n  const isLoading = editorSelectors.selectIsLoading();\n\n  return <div>{isLoading ? 'Loading...' : content}</div>;\n};\n```\n\n### Known Issue: Editor Content Storage\n\n> Editor content is stored in SwaggerUI `spec` plugin, causing parse/resolve/store on every keystroke → typing lag with large specs. **Future fix:** store content in editor plugin with FSM pattern in preview plugins.\n\n- Avoid unnecessary state updates in the editor\n- Debounce expensive validation triggers\n\n---\n\n## Common Tasks\n\n### Creating a New Plugin\n\n```javascript\n// src/plugins/my-plugin/index.js\nconst MyPlugin = () => ({\n  components: { MyComponent: () => <div>Hello from MyPlugin</div> },\n  statePlugins: {\n    myPlugin: {\n      initialState: Map({ data: null }),\n      actions: { myAction: (payload) => ({ type: 'MY_ACTION', payload }) },\n      reducers: { MY_ACTION: (state, action) => state.set('data', action.payload) },\n      selectors: { selectData: createSelector((s) => s.get('myPlugin'), (s) => s.get('data')) },\n    },\n  },\n});\nexport default MyPlugin;\n```\n\nThen import and add to `App.tsx` or your preset's plugin array.\n\n### Adding a Component Wrapper\n\n```javascript\n// src/plugins/my-plugin/extensions/top-bar/wrap-components/TopBarWrapper.jsx\nconst TopBarWrapper = (Original, system) => {\n  const Enhanced = (props) => {\n    const showBanner = system.myPluginSelectors.selectShowBanner();\n    return (\n      <>\n        {showBanner && <div className=\"banner\">Important Notice</div>}\n        <Original {...props} />\n      </>\n    );\n  };\n  return Enhanced;\n};\n// Register in plugin: wrapComponents: { TopBar: TopBarWrapper }\n```\n\n### Adding a New Content Type\n\n```javascript\n// Extend detection in editor-content-type plugin (order matters — specific first)\nconst detectContentType = (content) => {\n  if (/^openapi:\\s*[\"']?3\\.1/.test(content)) return 'openapi-3-1';\n  if (/^openapi:\\s*[\"']?3\\.0/.test(content)) return 'openapi-3-0';\n  if (/^swagger:\\s*[\"']?2\\.0/.test(content)) return 'openapi-2-0';\n  if (/^asyncapi:\\s*[\"']?2\\./.test(content)) return 'asyncapi-2';\n  if (/^myspec:\\s*[\"']?1\\.0/.test(content)) return 'myspec-1-0'; // custom\n  return 'unknown';\n};\n```\n\nThen create a preview plugin that wraps `EditorPreview` and conditionally renders based on `editorSelectors.selectEditorContentType()`.\n\n### Adding E2E Tests\n\n```typescript\n// test/playwright/e2e/plugin.my-feature.spec.ts\nimport { test, expect } from '@playwright/test';\nimport { visitBlankPage, waitForSplashScreen } from '../helpers';\n\ntest.describe('My Feature', () => {\n  test.beforeEach(async ({ page }) => {\n    await visitBlankPage(page);\n    await waitForSplashScreen(page);\n  });\n  test('should perform action', async ({ page }) => {\n    await page.locator('[data-testid=\"my-button\"]').click();\n    await expect(page.locator('text=Expected Result')).toBeVisible();\n  });\n});\n```\n\n### Debugging Validation\n\n```javascript\nconst { editorSelectors } = useSystem();\nconsole.log('markers:', editorSelectors.selectEditorMarkers());\nconsole.log('diagnostics:', editorSelectors.selectDiagnostics());\nconsole.log('content type:', editorSelectors.selectEditorContentType());\nconsole.log('is OpenAPI:', editorSelectors.selectIsContentTypeOpenAPI());\n```\n\n---\n\n## Important Gotchas\n\n### 1. File Extensions Required\n\n```javascript\nimport Component from './Component.jsx';  // ✅\nimport Component from './Component';      // ❌ fails linting\n```\nException: `.ts`/`.tsx` files may omit extensions due to ESLint overrides.\n\n### 2. Immutable.js State Updates\n\n```javascript\nstate.data = newValue;           // ❌ mutates state\nreturn state.set('data', val);   // ✅\nreturn state.merge({ a, b });    // ✅ multiple fields\n```\n\n### 3. Component Wrapping Return Value\n\n```javascript\nconst Wrapper = (Original, system) => Original;              // ❌ no enhancement\nconst Wrapper = (Original, system) => (props) => <Original {...props} />; // ✅\n```\n\n### 4. Request ID Race Conditions\n\nAlways include `requestId` in async actions and guard in reducers:\n```javascript\nif (state.get('requestId') !== action.payload.requestId) return state;\n```\n\n### 5. Monaco Environment Configuration\n\nMust be set **before** rendering SwaggerEditor:\n```javascript\nself.MonacoEnvironment = { baseUrl: `${document.baseURI || location.href}dist/` };\nReactDOM.render(<SwaggerEditor />, document.getElementById('root'));\n```\n\n### 6. Web Worker Path Issues\n\nWorkers must be accessible at runtime — build separately via webpack entries or copy pre-built files with CopyWebpackPlugin.\n\n### 7. Large Bundle / OOM Errors\n\n```bash\nexport NODE_OPTIONS=\"--max_old_space_size=4096\"\n```\n\n### 8. Content Type Detection Order\n\nMore specific patterns must come first — detect `3.1` before `3.0`, `3.0` before `2.0`.\n\n### 9. Selector Memoization\n\n```javascript\n// ❌ recalculates every render\nexport const selectErrors = (state) => selectResults(state).filter(r => r.severity === 'error');\n\n// ✅ memoized with createSelector\nexport const selectErrors = createSelector(selectResults, (r) => r.filter(...));\n```\n\n### 10. TypeScript Strict Mode\n\nOff globally. Use `typescript-strict-plugin` per file. New files should aim for strictness (no `@ts-strict-ignore`). Legacy files may have it.\n\n---\n\n## Key Files Reference\n\n**Core:**\n| File | Purpose |\n|------|---------|\n| `/src/App.tsx` | Main component, plugin composition |\n| `/src/index.tsx` | Browser entry point |\n| `/public/index.html` | HTML template, MonacoEnvironment setup |\n\n**Config:**\n| File | Purpose |\n|------|---------|\n| `/package.json` | Dependencies and scripts |\n| `/tsconfig.json` | TypeScript compiler options |\n| `/.eslintrc` | ESLint rules (Airbnb + Prettier + @vitest) |\n| `/.prettierrc` | Formatting rules |\n| `/.commitlintrc.json` | Commit message linting |\n| `/vite.config.js` | Vite dev server config |\n| `/vite.config.app.js` | Vite app production build config |\n| `/vitest.config.ts` | Vitest unit test config |\n| `/playwright.config.ts` | Playwright E2E config |\n\n**Docs:**\n| File | Purpose |\n|------|---------|\n| `/docs/architecture.md` | High-level architecture overview |\n| `/docs/customization/plug-points/` | Plugin customization guides |\n| `/docs/migration*.md` | Migration guides from legacy version |\n\n**Testing:**\n| File | Purpose |\n|------|---------|\n| `/test/setupTests.js` | Vitest setup (jest-dom/vitest, vitest-canvas-mock) |\n| `/test/playwright/e2e/*.spec.ts` | E2E test specs |\n| `/test/playwright/helpers/` | Playwright helper functions |\n\n**Plugin Paths:**\n- Editor: `/src/plugins/editor-textarea/`, `/src/plugins/editor-monaco/`, `/src/plugins/editor-monaco-language-apidom/`, `/src/plugins/editor-monaco-yaml-paste/`\n- Preview: `/src/plugins/editor-preview*/`\n- Support: `/src/plugins/editor-content-*/`\n- Generic: `/src/plugins/layout/`, `/src/plugins/top-bar/`, `/src/plugins/modals/`, etc.\n- Presets: `/src/presets/monaco/` (default), `/src/presets/textarea/`\n\n---\n\n## Quick Reference for AI Assistants\n\n### Fixing a Bug\n1. Identify the plugin — most bugs are plugin-specific\n2. Check `test/playwright/e2e/plugin.<name>.spec.ts` for existing coverage\n3. Review actions/reducers/selectors in that plugin\n4. Add an E2E test to prevent regression\n\n### Adding a Feature\n1. Identify affected plugins; decide new vs. extend existing\n2. Plan state management needs (new actions/reducers?)\n3. Use component wrapping to enhance without forking\n4. Write E2E test first (TDD preferred); update README if user-facing\n\n### Refactoring\n1. Follow established patterns — don't tightly couple plugins\n2. Maintain Immutable.js correctness and selector memoization\n3. Run: `npm run lint:fix && npm test && npx playwright test`\n\n### When Stuck\n1. Read the relevant plugin source — most logic lives there\n2. Check `docs/architecture.md` for high-level overview\n3. Look at similar plugins for patterns to copy\n4. Check Playwright tests to see how features are exercised\n\n### Before Committing\n- [ ] `npm run lint:fix`\n- [ ] `npm test`\n- [ ] `npx playwright test`\n- [ ] Commit message: Conventional Commits format, max 69 chars header\n- [ ] `npm run build` succeeds\n\n---\n\n**Last Updated:** 2026-05-16 | **Version:** 5.0.6 | Update this file when architecture changes significantly\n","category":"root","tokens":5384}]}