Version: 5.0.6 | Last Updated: 2026-02-27
SwaggerEditor is a browser-based editor for API specifications supporting OpenAPI 2.0/3.0/3.1, AsyncAPI 2.x, API Design Systems, and JSON Schema. Built as a React app on SwaggerUI's plugin architecture with a split-pane Monaco Editor interface.
Core Technologies: React 17+/18, SwaggerUI React, TypeScript (gradual), Immutable.js, Monaco Editor, ApiDOM, Webpack 5, Jest + Playwright
Philosophy: Plugin-based architecture, minimal over-engineering, heavy E2E testing, gradual TypeScript adoption, app/ESM/UMD build artifacts.
swagger-editor/
├── config/ # Webpack configs (dev, prod, bundle), Jest transforms
├── docs/ # architecture.md, customization guides, migration guides
├── public/ # Static assets, HTML template
├── scripts/ # Build scripts (start, build, test)
├── src/
│ ├── App.tsx # Main app component & plugin composition
│ ├── index.tsx # Browser entry point
│ ├── plugins/ # 26 plugins (see Architecture section)
│ ├── presets/
│ │ ├── monaco/ # Full-featured preset (default)
│ │ └── textarea/ # Lightweight fallback
│ ├── styles/ # Global SCSS (index.scss)
│ └── types/ # TypeScript declarations (*.d.ts)
├── test/
│ ├── playwright/
│ │ ├── e2e/ # Test specs (*.spec.ts)
│ │ ├── fixtures/ # Test data
│ │ ├── helpers/ # Helper functions
│ │ └── tsconfig.json
│ └── setupTests.js # Jest setup (jest-dom, canvas-mock)
├── build/ # Standalone app (generated)
└── dist/esm|umd|types/ # Library bundles (generated)
Editor implementations:
editor-textarea— HTML<textarea>fallbackeditor-monaco— Monaco Editor (advanced)editor-monaco-language-apidom— ApiDOM language supporteditor-monaco-yaml-paste— YAML paste transformations
Preview plugins:
editor-preview— Base preview componenteditor-preview-swagger-ui— OpenAPI renderingeditor-preview-asyncapi— AsyncAPI renderingeditor-preview-api-design-systems— ADS rendering
Editor support plugins:
editor-content-type— Auto-detect content type (OpenAPI/AsyncAPI/JSON Schema)editor-content-persistence— LocalStorage persistenceeditor-content-read-only— Read-only modeeditor-content-origin— Track content source (URL, file, user)editor-content-fixtures— Load example/fixture fileseditor-content-from-file— File import
Generic feature plugins:
layout,top-bar,modals,dialogs,dropdown-menu,dropzone,splash-screen,editor-safe-render,swagger-ui-adapter,util,versions,props-change-watcher
App.tsx (SwaggerUI wrapper)
└── SwaggerEditorLayout
├── SplashScreen
├── TopBar (File/Edit/Generate menus)
└── Container
└── Dropzone
└── SplitPane (resizable)
├── EditorPane
│ ├── EditorPaneBarTop
│ ├── MonacoEditor / TextareaEditor
│ └── ValidationPane (errors/warnings)
└── EditorPreviewPane
└── EditorPreviewSwaggerUI / AsyncAPI / ApiDesignSystems
- Container/Presenter: Containers connect to Redux (e.g.,
MonacoEditorContainer.jsx), presenters handle rendering (e.g.,MonacoEditor.jsx) - HOC via
wrapComponents: Plugins wrap existing components to enhance without forking getComponent: Dynamically resolve components —const C = getComponent('MonacoEditor')- FSM for async:
idle → loading → success/failurewith request ID tracking to prevent race conditions
// src/plugins/plugin-name/index.js
const PluginName = ({ getSystem }) => ({
afterLoad: function, // Runs after plugin loads
components: {
ComponentName: Component, // Register new components
},
wrapComponents: {
ComponentName: WrapperFn, // Wrap/enhance existing components
},
rootInjects: {
utilityName: function, // Inject utilities into system
},
statePlugins: {
pluginStateKey: {
actions: {}, // Action creators
reducers: {}, // Immutable.js reducers
selectors: {}, // Reselect selectors
wrapActions: {}, // Action middleware
},
},
fn: { utilityFunction: function },
});
export default PluginName;plugin-name/
├── index.js
├── actions/index.js
├── reducers.js
├── selectors.js
├── components/ComponentName.jsx
├── components/ComponentName.scss
├── extensions/other-plugin/wrap-components/ComponentWrapper.jsx
├── after-load.js
└── fn.js
// extensions/editor-preview/wrap-components/EditorPreviewWrapper.jsx
const EditorPreviewWrapper = (Original, system) => {
const EnhancedComponent = (props) => {
const isOpenAPI = system.editorSelectors.selectIsContentTypeOpenAPI();
if (isOpenAPI) return <EditorPreviewSwaggerUI />;
return <Original {...props} />;
};
return EnhancedComponent; // must return new component, not Original directly
};
export default EditorPreviewWrapper;const MyPlugin = (system) => {
const { getComponent, editorActions, editorSelectors, fn } = system;
const content = editorSelectors.selectEditorContent();
editorActions.setEditorContent('new content');
const MonacoEditor = getComponent('MonacoEditor');
};- Node.js
>=22.11.0, npm>=10.9.0, Python 3.x (node-gyp), GLIBC>=2.29 - Optional: Docker or emscripten (for WASM builds)
| Script | Description |
|---|---|
npm start |
Dev server on port 3000 (hot reload) |
npm test |
Jest unit tests (watch mode) |
npm run lint |
ESLint on all files |
npm run lint:fix |
Auto-fix ESLint errors |
npm run build |
Build all artifacts (app + bundles + types) |
npm run build:app |
Standalone app → /build |
npm run build:app:serve |
Serve built app on port 3050 |
npm run build:bundle:esm |
ESM bundle → /dist/esm |
npm run build:bundle:umd |
UMD bundle → /dist/umd |
npm run build:definitions |
TypeScript definitions → /dist/types |
npx playwright test |
E2E tests (headless) |
npx playwright test --headed |
E2E with browser visible |
npx playwright test --ui |
Interactive UI mode |
npx playwright test --debug |
Debug mode |
npx playwright show-report |
View test report |
npm run clean |
Remove /build and /dist |
| Variable | Description |
|---|---|
REACT_APP_DEFINITION_FILE |
Local file path (must be in /public/static) |
REACT_APP_DEFINITION_URL |
Remote URL (takes precedence over file) |
REACT_APP_VERSION |
App version (from package.json) |
REACT_APP_APIDOM_WORKER_FILENAME |
ApiDOM worker filename |
REACT_APP_EDITOR_WORKER_FILENAME |
Monaco editor worker filename |
Two workers handle background processing: apidom.worker.js (parsing/validation) and editor.worker.js (Monaco ops). Configure Monaco env before rendering:
self.MonacoEnvironment = { baseUrl: `${document.baseURI || location.href}dist/` };Workers 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.
export NODE_OPTIONS="--max_old_space_size=4096"
npm run build- Location:
src/**/*.{spec,test}.{js,jsx,ts,tsx}, run withnpm test ⚠️ Only 1 unit test exists:ValidationPane.test.jsx— heavy reliance on E2E
- Location:
test/playwright/e2e/*.spec.ts, base URLhttp://localhost:3000 - All tests written in TypeScript with full
@playwright/testtype safety
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.
Helper functions (test/playwright/helpers/):
- Setup:
visitBlankPage(),waitForSplashScreen(),prepareAsyncAPI() - Editor:
typeInEditor(),getAllEditorText(),selectAllEditorText() - Menu:
clickMenu(),loadExample(),generateServer()
E2E test template:
import { test, expect } from '@playwright/test';
import { visitBlankPage, waitForSplashScreen } from '../helpers';
test.describe('Feature Name', () => {
test.beforeEach(async ({ page }) => {
await visitBlankPage(page);
await waitForSplashScreen(page);
});
test('should do something', async ({ page }) => {
await page.locator('[data-testid="some-element"]').click();
await expect(page.locator('text=Expected Text')).toBeVisible();
});
});Key rules:
- Arrow functions for named components — not
functiondeclarations - File extensions required on all JS/JSX imports:
./Component.jsx✅./Component❌ (.ts/.tsxexempt) - JSX only in
.jsx/.tsxfiles - Import groups: external/builtin first (blank line), then internal
Fix violations: npm run lint:fix
printWidth: 100, tabWidth: 2, semi: true, singleQuote: true, trailingComma: 'es5', endOfLine: 'lf'
<type>(<scope>): <subject> ← max 69 characters
Types: feat, fix, docs, style, refactor, test, chore, perf, ci
Scopes: plugin name (editor-monaco, top-bar, validation) or area (deps, build, release)
Enforced by commitlint via Husky pre-commit hook.
| Type | Convention | Example |
|---|---|---|
| React Components | PascalCase.jsx/tsx |
MonacoEditor.jsx |
| Utilities | kebab-case.js |
import-url.js |
| Types | kebab-case.d.ts |
system.d.ts |
| Styles (partials) | _kebab-case.scss |
_monaco-editor.scss |
| Unit Tests | ComponentName.test.jsx |
ValidationPane.test.jsx |
| E2E Tests | feature.spec.ts |
plugin.editor-monaco.spec.ts |
- SCSS for all component styles; BEM-like naming (
.editor-pane__title) - Partial files prefixed with
_; global styles in/src/styles/index.scss
- Strict mode off globally; gradual adoption via
typescript-strict-plugin - Legacy files may use
// @ts-strict-ignoreat top; new files should aim for strictness allowJs: false— TypeScript files only in tsconfig scope- Path aliases work:
import X from 'plugins/editor-monaco'✅
SwaggerUI's plugin-based Redux-like system with Immutable.js.
export const SET_EDITOR_CONTENT = 'editor_set_content';
export const setEditorContent = (content) => ({ type: SET_EDITOR_CONTENT, payload: content });
// Async thunk
export const loadDefinition = (url) => async (system) => {
const { editorActions, fn } = system;
const requestId = generateRequestId();
editorActions.loadDefinitionRequest({ url, requestId });
try {
const content = await fn.fetchUrl(url);
editorActions.loadDefinitionSuccess({ content, requestId });
} catch (error) {
editorActions.loadDefinitionFailure({ error, requestId });
}
};import { Map } from 'immutable';
export const initialState = Map({ content: '', status: 'idle', error: null, requestId: null });
const loadSuccessReducer = (state, action) => {
if (state.get('requestId') !== action.payload.requestId) return state; // ignore stale
return state.merge({ status: 'success', content: action.payload.content, error: null });
};
export default {
[SET_EDITOR_CONTENT]: (state, action) => state.set('content', action.payload),
[LOAD_DEFINITION_SUCCESS]: loadSuccessReducer,
};import { createSelector } from 'reselect';
export const selectEditorState = (state) => state.get('editor');
export const selectEditorContent = (state) => selectEditorState(state).get('content');
export const selectStatus = (state) => selectEditorState(state).get('status');
// Always memoize derived state
export const selectValidationErrors = createSelector(
selectValidationResults,
(results) => results.filter((r) => r.severity === 'error')
);const MyComponent = () => {
const { editorSelectors, editorActions } = useSystem();
const content = editorSelectors.selectEditorContent();
const isLoading = editorSelectors.selectIsLoading();
return <div>{isLoading ? 'Loading...' : content}</div>;
};Editor content is stored in SwaggerUI
specplugin, 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.
- Avoid unnecessary state updates in the editor
- Debounce expensive validation triggers
// src/plugins/my-plugin/index.js
const MyPlugin = () => ({
components: { MyComponent: () => <div>Hello from MyPlugin</div> },
statePlugins: {
myPlugin: {
initialState: Map({ data: null }),
actions: { myAction: (payload) => ({ type: 'MY_ACTION', payload }) },
reducers: { MY_ACTION: (state, action) => state.set('data', action.payload) },
selectors: { selectData: createSelector((s) => s.get('myPlugin'), (s) => s.get('data')) },
},
},
});
export default MyPlugin;Then import and add to App.tsx or your preset's plugin array.
// src/plugins/my-plugin/extensions/top-bar/wrap-components/TopBarWrapper.jsx
const TopBarWrapper = (Original, system) => {
const Enhanced = (props) => {
const showBanner = system.myPluginSelectors.selectShowBanner();
return (
<>
{showBanner && <div className="banner">Important Notice</div>}
<Original {...props} />
</>
);
};
return Enhanced;
};
// Register in plugin: wrapComponents: { TopBar: TopBarWrapper }// Extend detection in editor-content-type plugin (order matters — specific first)
const detectContentType = (content) => {
if (/^openapi:\s*["']?3\.1/.test(content)) return 'openapi-3-1';
if (/^openapi:\s*["']?3\.0/.test(content)) return 'openapi-3-0';
if (/^swagger:\s*["']?2\.0/.test(content)) return 'openapi-2-0';
if (/^asyncapi:\s*["']?2\./.test(content)) return 'asyncapi-2';
if (/^myspec:\s*["']?1\.0/.test(content)) return 'myspec-1-0'; // custom
return 'unknown';
};Then create a preview plugin that wraps EditorPreview and conditionally renders based on editorSelectors.selectEditorContentType().
// test/playwright/e2e/plugin.my-feature.spec.ts
import { test, expect } from '@playwright/test';
import { visitBlankPage, waitForSplashScreen } from '../helpers';
test.describe('My Feature', () => {
test.beforeEach(async ({ page }) => {
await visitBlankPage(page);
await waitForSplashScreen(page);
});
test('should perform action', async ({ page }) => {
await page.locator('[data-testid="my-button"]').click();
await expect(page.locator('text=Expected Result')).toBeVisible();
});
});const { editorSelectors } = useSystem();
console.log('markers:', editorSelectors.selectEditorMarkers());
console.log('diagnostics:', editorSelectors.selectDiagnostics());
console.log('content type:', editorSelectors.selectEditorContentType());
console.log('is OpenAPI:', editorSelectors.selectIsContentTypeOpenAPI());import Component from './Component.jsx'; // ✅
import Component from './Component'; // ❌ fails lintingException: .ts/.tsx files may omit extensions due to ESLint overrides.
state.data = newValue; // ❌ mutates state
return state.set('data', val); // ✅
return state.merge({ a, b }); // ✅ multiple fieldsconst Wrapper = (Original, system) => Original; // ❌ no enhancement
const Wrapper = (Original, system) => (props) => <Original {...props} />; // ✅Always include requestId in async actions and guard in reducers:
if (state.get('requestId') !== action.payload.requestId) return state;Must be set before rendering SwaggerEditor:
self.MonacoEnvironment = { baseUrl: `${document.baseURI || location.href}dist/` };
ReactDOM.render(<SwaggerEditor />, document.getElementById('root'));Workers must be accessible at runtime — build separately via webpack entries or copy pre-built files with CopyWebpackPlugin.
export NODE_OPTIONS="--max_old_space_size=4096"More specific patterns must come first — detect 3.1 before 3.0, 3.0 before 2.0.
// ❌ recalculates every render
export const selectErrors = (state) => selectResults(state).filter(r => r.severity === 'error');
// ✅ memoized with createSelector
export const selectErrors = createSelector(selectResults, (r) => r.filter(...));Off globally. Use typescript-strict-plugin per file. New files should aim for strictness (no @ts-strict-ignore). Legacy files may have it.
Core:
| File | Purpose |
|---|---|
/src/App.tsx |
Main component, plugin composition |
/src/index.tsx |
Browser entry point |
/public/index.html |
HTML template, MonacoEnvironment setup |
Config:
| File | Purpose |
|---|---|
/package.json |
Dependencies, scripts, Jest config |
/tsconfig.json |
TypeScript compiler options |
/.eslintrc |
ESLint rules (Airbnb + Prettier) |
/.prettierrc |
Formatting rules |
/.commitlintrc.json |
Commit message linting |
/config/webpack.config.js |
Webpack configuration |
/playwright.config.ts |
Playwright configuration |
Docs:
| File | Purpose |
|---|---|
/docs/architecture.md |
High-level architecture overview |
/docs/customization/plug-points/ |
Plugin customization guides |
/docs/migration*.md |
Migration guides from legacy version |
Testing:
| File | Purpose |
|---|---|
/test/setupTests.js |
Jest test setup |
/test/playwright/e2e/*.spec.ts |
E2E test specs |
/test/playwright/helpers/ |
Playwright helper functions |
Plugin Paths:
- Editor:
/src/plugins/editor-textarea/,/src/plugins/editor-monaco/,/src/plugins/editor-monaco-language-apidom/,/src/plugins/editor-monaco-yaml-paste/ - Preview:
/src/plugins/editor-preview*/ - Support:
/src/plugins/editor-content-*/ - Generic:
/src/plugins/layout/,/src/plugins/top-bar/,/src/plugins/modals/, etc. - Presets:
/src/presets/monaco/(default),/src/presets/textarea/
- Identify the plugin — most bugs are plugin-specific
- Check
test/playwright/e2e/plugin.<name>.spec.tsfor existing coverage - Review actions/reducers/selectors in that plugin
- Add an E2E test to prevent regression
- Identify affected plugins; decide new vs. extend existing
- Plan state management needs (new actions/reducers?)
- Use component wrapping to enhance without forking
- Write E2E test first (TDD preferred); update README if user-facing
- Follow established patterns — don't tightly couple plugins
- Maintain Immutable.js correctness and selector memoization
- Run:
npm run lint:fix && npm test && npx playwright test
- Read the relevant plugin source — most logic lives there
- Check
docs/architecture.mdfor high-level overview - Look at similar plugins for patterns to copy
- Check Playwright tests to see how features are exercised
-
npm run lint:fix -
npm test -
npx playwright test - Commit message: Conventional Commits format, max 69 chars header
-
npm run buildsucceeds
Last Updated: 2026-02-27 | Version: 5.0.6 | Update this file when architecture changes significantly