File: README.md
โฐ๏ธ Looking for the next generation version of Swagger Editor?
Only SwaggerEditor@5 supports OpenAPI 3.1.0.
SwaggerEditor@4 will not receive OpenAPI 3.1.0 support and is considered deprecated at this point.
๐ฐ๏ธ Looking for the older version of Swagger Editor? Refer to the 2.x or 3.x branches.
Swagger Editor lets you edit OpenAPI API definitions (OpenAPI 2.0 and OpenAPI 3.0.3)
in JSON or YAML format inside your browser and to preview documentations in real time.
Valid OpenAPI definitions can then be generated and used with the full Swagger tooling (code generation, documentation, etc).
As a brand-new version, written from the ground up, there are some known issues and unimplemented features. Check out the Known Issues section for more details.
This repository publishes to two different NPM modules:
- swagger-editor is a traditional npm module intended for use in single-page applications that are capable of resolving dependencies (via Webpack, Browserify, etc).
- swagger-editor-dist is a dependency-free module that includes everything you need to serve Swagger Editor in a server-side project, or a web project that can't resolve npm module dependencies.
If you're building a single-page application, using swagger-editor is strongly recommended, since swagger-editor-dist is significantly larger.
Anonymized analytics
Swagger Editor uses Scarf to collect anonymized installation analytics. These analytics help support the maintainers of this library and ONLY run during installation. To opt out, you can set the scarfSettings.enabled field to false in your project's package.json:
// package.json
{
// ...
"scarfSettings": {
"enabled": false
}
// ...
}Alternatively, you can set the environment variable SCARF_ANALYTICS to false as part of the environment that installs your npm packages, e.g., SCARF_ANALYTICS=false npm install.
Helpful scripts
Any of the scripts below can be run by typing npm run <script name> in the project's root directory.
Developing
| Script name | Description |
|---|---|
dev |
Spawn a hot-reloading dev server on port 3200. |
deps-check |
Generate a size and licensing report on Swagger Editors's dependencies. |
lint |
Report ESLint style errors and warnings. |
lint-errors |
Report ESLint style errors, without warnings. |
lint-fix |
Attempt to fix style errors automatically. |
watch |
Rebuild the core files in /dist when the source code changes. Useful for npm link. |
Building
| Script name | Description |
|---|---|
build |
Build a new set of JS and CSS assets, and output them to /dist. |
build:bundle |
Build swagger-editor-bundle.js only (commonJS). |
build:core |
Build swagger-editor.(js|css) only (commonJS). |
build:standalone |
Build swagger-editor-standalone-preset.js only (commonJS). |
build:stylesheets |
Build swagger-editor.css only. |
build:es:bundle |
Build swagger-editor-es-bundle.js only (es2015). |
build:es:bundle:core |
Build swagger-editor-es-bundle-core.js only (es2015). |
Testing
| Script name | Description |
|---|---|
test |
Run unit tests in Node, run Cypress end-to-end tests, and run ESLint in errors-only mode. |
test:unit-mocha |
Run Mocha-based unit tests in Node. |
test:unit-jest |
Run Jest-based unit tests in Node. |
e2e |
Run end-to-end browser tests with Cypress. |
lint |
Run ESLint test |
test:artifact |
Run list of bundle artifact tests in Jest |
test:artifact:umd:bundle |
Run unit test that confirms swagger-editor-bundle exports as a Function |
test:artifact:es:bundle |
Run unit test that confirms swagger-editor-es-bundle exports as a Function |
test:artifact:es:bundle:core |
Run unit test that confirms swagger-editor-es-bundle-core exports as a Function |
Running locally
Prerequisites
- git, any version
- Node.js >=20.3.0 and npm >=9.6.7 are the minimum required versions that this repo runs on, but we always recommend using the latest version of Node.js.
$ npm i --legacy-peer-depsIf you have Node.js and npm installed, you can run npm start to spin up a static server.
Otherwise, you can open index.html directly from your filesystem in your browser.
If you'd like to make code changes to Swagger Editor, you can start up a Webpack hot-reloading dev server via npm run dev.
Browser support
Swagger Editor works in the latest versions of Chrome, Safari, Firefox, and Edge.
Known Issues
To help with the migration, here are the currently known issues with 3.X. This list will update regularly, and will not include features that were not implemented in previous versions.
- Everything listed in Swagger UI's Known Issues.
- The integration with the codegen is still missing.
Docker
Running the image from DockerHub
There is a docker image published in docker.swagger.io registry.
To use this, run the following:
docker pull docker.swagger.io/swaggerapi/swagger-editor
docker run -d -p 80:8080 docker.swagger.io/swaggerapi/swagger-editorThis will run Swagger Editor (in detached mode) on port 80 on your machine, so you can open it by navigating to http://localhost in your browser.
- You can provide a URL pointing to an API definition (may not be available if some security policies such as CSP or CORS are enforced):
docker run -d -p 80:8080 -e URL="https://petstore3.swagger.io/api/v3/openapi.json" docker.swagger.io/swaggerapi/swagger-editor- You can provide your own
jsonoryamldefinition file from your local host:
docker run -d -p 80:8080 -v $(pwd):/tmp -e SWAGGER_FILE=/tmp/swagger.json docker.swagger.io/swaggerapi/swagger-editorNote: When both URL and SWAGGER_FILE environment variables are set, URL has priority and SWAGGER_FILE is ignored.
- You can specify a different base url via
BASE_URLvariable for accessing the application - for example if you want the application to be available athttp://localhost/swagger-editor/:
docker run -d -p 80:8080 -e BASE_URL=/swagger-editor docker.swagger.io/swaggerapi/swagger-editor- You can specify a different port via
PORTvariable for accessing the application, default is8080.
docker run -d -p 80:80 -e PORT=80 docker.swagger.io/swaggerapi/swagger-editor- You can specify Google Tag Manager ID via
GTMvariable for tracking the usage of the swagger-editor.
docker run -d -p 80:8080 -e GTM=GTM-XXXXXX docker.swagger.io/swaggerapi/swagger-editorYou can also customize the different endpoints used by the Swagger Editor with the following environment variables. For instance, this can be useful if you have your own Swagger generator server:
| Environment variable | Default value |
|---|---|
URL_SWAGGER2_GENERATOR |
https://generator.swagger.io/api/swagger.json |
URL_OAS3_GENERATOR |
https://generator3.swagger.io/openapi.json |
URL_SWAGGER2_CONVERTER |
https://converter.swagger.io/api/convert |
If you want to run the Swagger Editor locally without the Codegen features (Generate Server and Generate Client) you can set the above environment variables to null (URL_SWAGGER2_CONVERTER=null).
Building and running an image locally
To build and run a docker image with the code checked out on your machine, run the following from the root directory of the project:
# Install npm packages (if needed)
npm install
# Build the app
npm run build
# Build an image
docker build -t swagger-editor .
# Run the container
docker run -d -p 80:8080 swagger-editorYou can then view the app by navigating to http://localhost in your browser.
Documentation
Using older version of React
[!IMPORTANT]
By older versions we specifically refer toReact >=17 <18.
By default swagger-editor@4 npm package comes with latest version of React@18.
It's possible to use swagger-editor@4 npm package with older version of React.
Let's say my application integrates with swagger-editor@4 npm package and uses [email protected].
npm
In order to inform swagger-editor@4 npm package that I require it to use my React version, I need to use npm overrides.
{
"dependencies": {
"react": "=17.0.2",
"react-dom": "=17.0.2"
},
"overrides": {
"swagger-editor": {
"react": "$react",
"react": "$react-dom",
"react-redux": "^8"
}
}
}[!NOTE]
The React and ReactDOM override are defined as a reference to the dependency. Since react-redux@9 only supportsReact >= 18, we need to use react-redux@8.
yarn
In order to inform swagger-editor@4 npm package that I require it to use my specific React version, I need to use yarn resolutions.
{
"dependencies": {
"react": "17.0.2",
"react-dom": "17.0.2"
},
"resolutions": {
"swagger-editor/react": "17.0.2",
"swagger-editor/react-dom": "17.0.2",
"swagger-editor/react-redux": "^8"
}
}[!NOTE]
The React and ReactDOM resolution cannot be defined as a reference to the dependency. Unfortunately yarn does not support aliasing like$reactor$react-domas npm does. You'll need to specify the exact versions.
Security contact
Please disclose any security-related issues or vulnerabilities by emailing [email protected], instead of using the public issue tracker.
File: docs/customization/plug-points/editor-monaco-language-apidom.md
editor-monaco plug points
Configuring web worker capabilities
Worker accepts configuration for a language service via ApiDOM Context configuration object.
By default, this configuration looks like this:
{
validatorProviders: [],
completionProviders: [],
performanceLogs: false,
logLevel: apidomLS.LogLevel.WARN,
completionContext: {
maxNumberOfItems: 100,
enableLSPFilter: false,
},
}If you want to override the default ApiDOM Context configuration object, you need to pass apiDOMContext option to the EditorMonacoLanguageApiDOM plugin.
EditorMonacoLanguageApiDOM({
createData: {
apiDOMContext: {
completionContext: {
enableLSPFilter: true, // enables "strict" word filtering (instead of default Monaco fuzzy matching; https://github.com/swagger-api/apidom/pull/2954)
},
},
},
});NOTE: note that the provided ApiDOM Context configuration object is merged with default ApiDOM Context configuration object
using deep-extend npm package.
Extending web worker capabilities
editor-monaco-language-apidom comes with implementation of apidom language.
The plugin comes with apidom.worker utilizing ApiDOM capabilities.apidom.worker can be extended in two ways: dynamic and static.
Dynamic extension
Dynamic extension happens during runtime, and we recommend to use it only for simple use-cases.
First thing we need to do is to pass a customApiDOMWorkerPath option to the EditorMonacoLanguageApiDOM plugin.
EditorMonacoLanguageApiDOM({
createData: {
customApiDOMWorkerPath: 'https://example.com/index.js',
},
});customApiDOMWorkerPath is a URL (absolute or relative) of the extending module. The apidom.worker
loads it via dynamic import() at runtime.
The module must export a customApiDOMWorkerFactory function:
export const customApiDOMWorkerFactory = (ApiDOMWorkerClass, toolbelt) => {
return ApiDOMWorkerClass;
};The function receives two arguments:
ApiDOMWorkerClass- the class that implements the editor capabilitiestoolbelt- an object containing various library exports
Here is a simple example demonstrating changing the log level of language service:
export const customApiDOMWorkerFactory = (ApiDOMWorkerClass, toolbelt) => {
const { apidomLS } = toolbelt;
class ApiDOMWorkerLogLevelErrorClass extends ApiDOMWorkerClass {
static apiDOMContext = {
...ApiDOMWorkerClass.apiDOMContext,
logLevel: apidomLS.LogLevel.ERROR,
};
}
return ApiDOMWorkerLogLevelErrorClass;
};Note: Classic (non-module) worker consumers may still use the
globalThis.customApiDOMWorkerFactory
assignment pattern โ it is supported as a fallback when a named export is absent.
Static extension
Static extension requires a bundler (Vite, webpack, etc.) to build a custom worker entry point.
my-custom-apidom.worker.js
import { initialize, makeCreate, ApiDOMWorker } from 'swagger-editor/apidom.worker';
class ApiDOMWorkerExtended extends ApiDOMWorker {
// implementation of extensions
}
const create = makeCreate(ApiDOMWorkerExtended);
initialize((ctx, createData) => create(ctx, createData));
export { initialize, create, makeCreate, ApiDOMWorkerExtended as ApiDOMWorker };Point your bundler at my-custom-apidom.worker.js as the apidom.worker entry and configureMonacoEnvironment.getWorker to serve the built file. For example, with webpack:
entry: {
app: './index.js',
'apidom.worker': './my-custom-apidom.worker.js',
'editor.worker': 'swagger-editor/editor.worker',
}Passing data to web workers
Often when extending web worker capabilities it is the case that we need to pass additional
data to web worker. These data may include any arbitrary data compatible with
the structured clone algorithm.
Let's consider the following use-case. We need to extend the apidom.worker in a way that
it will be fetching data on demand from authorized REST endpoint.
Dynamic extension
EditorMonacoLanguageApiDOM plugin configuration.
EditorMonacoLanguageApiDOM({
createData: {
authToken: 'c32d8b45-92fe-44f6-8b61-42c2107dfe87',
customApiDOMWorkerPath: 'https://example.com/index.js',
},
});export const customApiDOMWorkerFactory = (ApiDOMWorkerClass, toolbelt) => {
const { apidomLS } = toolbelt;
class ApiDOMWorkerLogLevelErrorClass extends ApiDOMWorkerClass {
static apiDOMContext = {
...ApiDOMWorkerClass.apiDOMContext,
logLevel: apidomLS.LogLevel.ERROR,
};
async loadData() {
// createData passed as plugin option is available in worker as this._createData
const { authToken } = this._createData;
return await fetch(`https://example.com/data?authToken=${authToken}`)
}
}
return ApiDOMWorkerLogLevelErrorClass;
};Static extension
Whenever you extend the ApiDOMWorker class you will have _createData public property available.
my-custom-apidom.worker.js
import { initialize, makeCreate, ApiDOMWorker } from 'swagger-editor/apidom.worker';
class ApiDOMWorkerExtended extends ApiDOMWorker {
async loadData() {
// createData passed as plugin option is available in worker as this._createData
const { authToken } = this._createData;
return await fetch(`https://example.com/data?authToken=${authToken}`)
}
}
const create = makeCreate(ApiDOMWorkerExtended);
initialize((ctx, createData) => create(ctx, createData));
export { initialize, create, makeCreate, ApiDOMWorkerExtended as ApiDOMWorker };File: docs/customization/plug-points/README.md
Plug points
File: docs/architecture.md
High level architecture of SwaggerEditor@5
This document briefly describes high level architecture of SwaggerEditor@5.
Plugin architecture
SwaggerEditor@5 is composed of number of plugins. These plugins are divided into four
distinct categories
- Plugins providing editor implementations
- Plugins providing preview of editor content
- Editor implementation support plugins
- Generic features plugins
Plugins providing editor implementations
These plugins include editor-textarea and editor-moanco.
editor-textarea is a base plugin that other editor implementations build on.
This plugin provides basic editing experience using <textarea /> HTML tag.
editor-monaco builds on top of editor-textarea and provide advanced editing
experience using Monaco Editor.
Plugins providing preview of editor content
These plugins include editor-preview-swagger-ui, editor-preview-asyncapi and others.
The single responsibility of editor preview plugin is to render the editor content (text)
to a set of UI components.
Editor implementation support plugins
These plugins include editor-persistence, editor-read-only and others.
Plugins in this category are responsible to support plugins providing editor implementation
with additional enhanced capabilities in generic way.
Generic features plugins
These plugins include dialogs, modals, layout and others. Each plugin in this
category provide features or enhancements to plugins in previous three categories.
Future architectural changes
This section describes opportunities to make the architecture of SwaggerEditor@5 better.
State management
Currently, editor content (text) is stored in SwaggerUI spec plugin. This causes various issues
which manifests in decreased user experience of editing text. Every time the editor content
changes the spec plugin will try to parse it as JSON or YAML, resolve it and store it in
redux state. This is absolutely not necessary. We can store editor content in editor implementation plugin
and have preview plugins work as finite state machines and detect if they should process the editor content in any way.
When this architectural change is implemented, the typing lag on big amount of editor content (text)
will significantly decrease.
File: docs/migration.md
Migrate from SwaggerEditor@4 to SwaggerEditor@5
Table of Contents
Introduction
SwaggerEditor@5 is a ground-up rewrite of SwaggerEditor@4. However, SwaggerEditor@5 is still fundamentally based on the SwaggerUI plugin system, and continues to be developed within the React ecosystem. For users without custom plugins or other forked customizations, SwaggerEditor@5 is intended to be mostly drop-in compatible with the existing SwaggerEditor@4.
SwaggerEditor@5 introduces integration with ApiDOM and Microsoft's Monaco Editor. ApiDOM replaces the parser from SwaggerClient, as well as existing custom validation from SwaggerEditor@4. Monaco Editor replaces Ace Editor. Additional documentation on features and usage of ApiDOM and Monaco Editor can be found within their respective documention. Also available are the ApiDOM Playground and Monaco Editor Playground
This migration guide is intended to highlight setup changes and key customization features for SwaggerEditor@5.
Development
SwaggerEditor@5 is using Create React App as it's building infrastructure. Therefore there are a few minor differences
to develop SwaggerEditor@5
Start script with hot-reloading
new:
$ npm startold:
$ npm run devStatic build script
new:
$ npm run build:app
$ npm run build:app:serveold:
$ npm startLocal & remote definitions
Only files inside public can be used from public/index.html. JS files must be put inside src.
new:
<SwaggerEditor url={url} />old:
In dev-helpers/index.html, add a url key like this:
const editor = SwaggerEditorBundle({
dom_id: '#swagger-editor',
url: "some-local-or-remote-path-to-definition.yaml" # define path here
layout: 'StandaloneLayout',
presets: [
SwaggerEditorStandalonePreset
]
})Linting
Automatic linting checks remain part of the commit process. SwaggerEditor@5 now generally follows the eslint recommendations from the AirBnb and React teams.
Dependencies
SwaggerEditor@5 is now based on swagger-ui-react instead of swagger-ui. In addition, unlike SwaggerEditor@4, SwaggerEditor@5 does not have a dependency on SwaggerClient.
Testing
Unit tests are now covered by jest and @testing-library. Unit tests are now co-located next to their component. Component tests follow the @testing-library philosophy to avoid including implementation details, so that changes to implementation but not functionality doesn't break existing tests.
E2E tests are now covered by Playwright and are located in the test/playwright directory.
Design
Plugins
SwaggerEditor@5 maintains its core as an extension of SwaggerUI's plugin system. SwaggerEditor@5 exports itself as a fully realized set of plugins, each of which can be extended and wrapped as needed. This even includes the new Monaco Editor feature as a plugin!
Compared to SwaggerEditor@4, there is also no change to precedence with regards to the order of loading plugins.
SwaggerEditor@5 provides a modals plugin for a unified modal system. The modals plugin is further extended via the dialogs plugin which provides a set of alert and confirm modal dialogs.
Any existing custom plugins for SwaggerEditor@4 that modified the behavior of, or directly interfaced with, the Ace Editor will likely need to be heavily refactored. Custom validation rules should be migrated to ApiDOM as needed.
plugins available at release
dialogsdropzoneeditor-monacoeditor-persistenceeditor-preview-asyncapieditor-preview-swagger-uieditor-read-onlyeditor-spec-origineditor-textarealayoutmodalstopbar
plugins not yet migrated
At the time of this writing there are two plugins from SwaggerEditor@4 that have not been migrated:
jump-to-path. Note: expect to leverage ApiDOM andeditor-monacoplugin to replace AST and Ace Editor.topbar-insert. Note: expect to leverage and wrap components of various existing plugins:topbar,editor-monaco,modals, anddialogs.
Layout
SwaggerEditor@5 comes with a plugin for a "core" layout. The "core" layout provides plug points that are analogous to "panes" and/or "bars" in other IDEs. These plug points make it easier for developers to create plugins that customize their UX with as minimal changes to the "core" layout as possible.
The "core" layout includes contains EditorPane, EditorPreviewPane, and Topbar. These three components represent the basic UX wireframe for SwaggerEditor@5. In addition, the EditorPane is provided with four surrounding bars (top, bottom, left, right) that can each be customized with their own wrapped implementations.
Using itself as a reference design, SwaggerEditor@5 provides implementation of two different EditorPane (editor-monaco and editor-textarea), and two different EditorPreviewPane (editor-preview-swagger-ui and editor-preview-asyncapi). The editor-monacoplugin also further extends the baseEditorPaneBarTopandEditorPaneBarBottom` with its own wrapped version of each.
File: docs/migration-legacy-summary.md
Swagger Editor Legacy (v3/v4) Migration Summary
Topbar Features
[x] Import from URL
[x] Import from File
[x] Save As Json
[x] Save As Yaml
[x] Convert from Json to Yaml, then download
[x] Convert from Yaml to Json, then download
[x] Convert from Json to Yaml, in editor only
[x] Clear Editor (moved from File Menu to Edit Menu)
[x] Convert from OAS2 to OAS3
[x] Generate Servers list - OAS2
[x] Generate Servers list - OAS3.0
[x] Generate Clients list - OAS2
[x] Generate Clients list - OAS3.0
[x] Display/Hide menu items based on combination of json/yaml, oas2/oas3
[x] NEW: consistent use of styled Modal components throughout (react-modal)
[x] NEW: Load default OAS2/OAS3/OAS3.1/AsyncApi2 definition; w/keyboard shortcuts
[x] Removed: topbar-insert (plugin); no replacement planned
Editor Features
[x] NEW: Monaco Editor
[x] NEW: validation from apidom-ls library
[x] Removed: Ace Editor
[x] Removed: internal validation from swagger-editor v3/v4
[x] Drag-n-drop local file to editor (react-dropzone)
[x] Persist definition on browser refresh/reload (localStorage)
[x] Display validation pane results
Visual UI Features
[x] SwaggerUI v4; React 17
Implementation Changes
[x] React Hooks support, e.g. menu item display toggle, modal system
[x] Behavioral testing of components, e.g. not the implementation details
[x] Simplify and reorganize presets, layouts, and plugins
[x] Extract menu action methods from Components to Actions
[x] Extract menu logic helpers to Actions
[x] Extract shared menu logic helpers to utils/editor-converter
[x] Replace fetch with axios http helpers to utils/topbar-http
[x] deprecate plugins with direct implementation: Convert to OAS3, Import File
[x] Removed: unnecessary state where possible
[x] Removed: window.alert and window.confirm popups
[x] Removed: swagger-client library dependency
[x] Tests: testing-library + jest (unit), playwright (e2e)
Possible additional/expanded docs/contributing topics
[x] Separation of actions from components. tldr: easier to test, possibly more re-usable
[x] React Hooks. tldr: modern practices
[x] testing-library. tldr: focus on user behaviors, more durable/maintainable tests
[x] reorganizing the presets. e.g. what happened to standalone and other plugins? The new recommended structure. tldr: multi-spec, multi-layouts
List of migrated legacy methods
Note: likely to remove this section from final doc
Extract menu action methods from React to Actions
[x] importFromURL
[x] saveAsYaml
[x] saveAsJson
[x] convertToYaml
[x] downloadGeneratedFile
[x] importFile
[x] onDocumentLoad prop - removed. handled now in actions. Theoretically, we could expose as a user-overwritable function. Maybe SH needed it?
[x] updateEditorContent prop - will be removed, and handled in actions.
[x] clearEditor
Deprecate methods from React
[x] saveAsText
[x] handleResponse
Extract menu logic helpers to Actions
[x] getGeneratorUrl
[x] instantiateGeneratorClient
[x] shouldReInstantiateGeneratorClient (new)
Extract menu logic helpers to utils-converter
[x] hasParserErrors
[x] getFileName
[x] getDefinitionLanguage
[x] getDefinitionVersion
Create (axios) http method to utils-http
[x] getDefinitionFromUrl
[x] getGenerator2Definition
[x] postPerformOasConversion
Remove unnecessary state:
[x] swaggerClient
[x] definitionVersion
Remove use of alert and confirm via new modal system
[x] alert
[x] confirm
Additional notes (remove from final doc)
[x] swagger-ui redux state
[x] connect monaco state to swagger-ui redux state
[x] connect actions with monaco state, e.g. updateEditorContent
[x] init oas spec via swagger-ui
[x] connect & sync generic-editor updates to swagger-ui
[x] init oas spec via import File (finish action) - Json
[x] init oas spec via import File (finish action) - Yaml
[x] init oas spec via import Url (finish action)
[x] CSS/Less/Saas styling
[x] remove use of mock data in topbarActions. (mock fixtures not removed yet)
[x] try react-modal lib instead of creating internal version
[x] modify generic-editor plugin to have a default editor placeholder (instead of calling GenericEditorContainer directly)
[x] add disposables array, and ability to dispose()
[x] make generator servers/clients toggleable; enable oas3.1/others when supported
[x] File Menu dropdown - add json/yaml detection to display appropriate link
[x] Edit menu dropdown - render convert to OAS3 only if currently 'isSwagger2`
[x] Edit menu dropdown - render convert to Yaml only if currently 'json'
[x] onboard - default definition should be in YAML format
Resolved Issues (remove from final doc)
[x] fix exploding styling/rendering when using in-browser (ctrl+f) find text within monaco; monaco's find+replace feature USING P2M/M2P FIXES THIS ISSUE
[x] warning: overlapping semantic tokens THIS IS APIDOM-LS ISSUE
[x] fix editor configuration/onChange to always word-wrap.
[x] remove "dev mode" case when editor content is undefined
[x] remove mock data/fixtures/configuration from actions.js
[x] in codeActionUi, sometimes recieve an Uncaught promise TypeError: d.dispose is not a funtion
[x] asyncapi support: anywhere we should be detecting async api (or any other supported spec); e.g. clearEditor
[x] handle apidom parser throw, when unable to detect langugage. e.g. empty string, or oas2
[x] load default (oas3). note the topbar generate server/client exists. -> clear all. user may type random string. note that the topbar generate server/client did NOT disappear
[x] load default -> import OAS2. note the topbar generate server/client exists. -> clear all. user may type random string. note that the topbar generate server/client did NOT disappear
File: docs/migration-tasks-todo.md
Migration Status
Known Issues
[ ] handle case(s) when specSelectors.method returns undefined
Integration Tasks
[ ] match and extend configurability options for editor & monaco
Backlog
[ ] update "minimal" specs/fixtures for clearEditor; oas2, oas3, oas3_1, asycapi2
[ ] memory performance benchmarks
[ ] if refresh empty monaco, should MAY load a default definition
[ ] jump-to-path (plugin)
Ideas
[ ] topbar: topbar-insert (plugin); this should be integrated into the Topbar plugin
[ ] topbar: update generator (plugin?) for oas3.1, asyncapi, etc. support when available
[ ] monaco: update syntax highlighting as needed
[ ] monaco - additional initial style and configuration of monaco editor, as needed
Test Coverage Ideas
[ ] should add unit tests with bad/invalid urls; generator, import Url
[ ] should add unit tests when both swagger2 and oas3 flags set to same value (both true, both false)
[ ] should add unit tests allowing exclusion of both swagger2 and oas3, e.g. future asynapi, graphql, etc.
[ ] e2e: monaco-editor features
File: docs/monaco-worker.md
Monaco Client (React)
- setup a Monaco instance
- setup monaco environment use of language worker(s) to use
- instantiate web workers
- register providers via Adapters
- create a Monaco instance
Adapter - an adapter for each service or provider
- generally expect a
provide[Description]method that matches monaco interface - offloads language service functions to language web worker
- run and/or return a monaco editor "effect". e.g. hover, completion, based on web worker result
- not required to use Typescript
WebWorker - called via an adapter
- call a language service method in separate thread
- recommended use by Monaco
LanguageService - called via a web worker
- any lib, in our case, apidom
- examples include typescript/javascript, json, css
notes:
- some minor naming differences across libs
File: docs/swagger-client.md
Swagger-client is used as middleware to transform generator responses,
which also includes function methods alongside data objects.
fetch GET call to SwaggerClient(url, [options]) will return response.apis methods.
- reminder to attach
.catch()to anyresponse.apis.[method]call
response.apis.clients:
- clientOptions: ฦ(parameters)
- downloadFile: ฦ(parameters)
- generateClient: ฦ(parameters)
- getClientOptions: ฦ(parameters)
response.apis.servers:
- downloadFile: ฦ(parameters)
- generateServerForLanguage: ฦ(parameters)
- getServerOptions: ฦ(parameters)
- serverOptions: ฦ(parameters)Summary
In order to download a generated definition, legacy swagger-editor made two calls to swagger-client.
- Given OAS3 or Swagger2/OAS2 definition, swagger-editor makes a request to Generator3 or Generator2 respectively.
Generator3/Generator2 returns a response that includes it's own definition, which swagger-client, as middleware, translates the defintion included in Generator's response into a more usable response.apis.clients format. This format also includes new execute methods for swagger-editor to call.
swagger-editorcalls the appropriateexecutemethod that was created byswagger-clientto download the generated definition. Note, Generator3 returns the Blob directly, while Generator2 returns a json object containing an URI link to the Blob.
Additionally, legacy swagger-editor saved these methods to React state for general purpose use, e.g. downloadFile vs clientOptions.
In this new version of swagger-editor, use of swagger-client has been deprecated, so these methods are no longer available or saved. Equivalent functionality to directly interface with Generator3/Generator2 now exists in topbarActions. This change also should now always yield a deterministic final result.
File: docs/wasm-bundling.md
WASM Bundling in ESM/UMD Library Builds
The apidom.worker.js bundle contains three WASM binaries that must be fully
self-contained โ no runtime HTTP fetches โ for the worker to function inside a
Webpack-bundled consumer (swagger-editor-plus and similar).
All three WASMs are handled by the inlineAllWasms Vite plugin defined invite.config.shared.js and used by both vite.config.esm.js and vite.config.umd.js.
See the Plugin ordering section for how the three hooks work together.
The three binaries
| Binary | Source | How it is loaded at runtime |
|---|---|---|
tree-sitter.wasm |
web-tree-sitter (emscripten) |
Emscripten's own locateFile() / fetch() machinery |
tree-sitter-yaml.wasm |
@swagger-api/apidom-parser-adapter-yaml-1-2 |
web-tree-sitter Language.load(arg) |
tree-sitter-json.wasm |
@swagger-api/apidom-parser-adapter-json |
web-tree-sitter Language.load(arg) |
`tree-sitter.wasm` โ emscripten runtime binary
The problem
Naively inlining tree-sitter.wasm as a data URI (e.g. by replacing the'tree-sitter.wasm' string literal in the bundle) breaks emscripten's fetch
interception. Tools like @rollup/plugin-wasm patch globalThis.fetch with anendsWith('tree-sitter.wasm') guard:
globalThis.fetch = (...args) =>
isString(args[0]) && args[0].endsWith('tree-sitter.wasm')
? realFetch(tree_sitter_default, ...)
: realFetch(...args);If the literal is replaced with a ~250 KB data URI, the endsWith condition
becomes url.endsWith('<250KB data URI>'). A string always ends with itself, so
when emscripten then calls fetch(dataUri), the broken branch fires and passes
the loader function as the URL argument to realFetch, producing a request tohttp://โฆ/function%20tree_sitter_default(Sn)%20{โฆ}.
The fix โ `renderChunk` hook in `inlineAllWasms`
Instead of replacing the 'tree-sitter.wasm' literal, inject the binary into
emscripten's Module object before any fetch is attempted. Emscripten readsModule.wasmBinary into a local variable during module init, andgetBinaryPromise() checks that variable first:
function getBinaryPromise(path) {
return wasmBinary // โ truthy โ fetch is never called
? Promise.resolve().then(() => getBinarySync(path))
: readAsync(path)...;
}The renderChunk hook in inlineAllWasms matches the emscripten module
init line (present in Rollup's output before esbuild minifies it):
var Module = typeof Module != "undefined" ? Module : {};and injects Module['wasmBinary'] = new Uint8Array(atob("โฆ").split("").map(โฆ))
immediately after it. By the time emscripten reaches getBinaryPromise, the
local wasmBinary is already set and fetch() is never called.
Grammar WASMs โ `Language.load()` binaries
The problem
The apidom yaml/json adapter packages import their grammar WASMs as ES modules:
// @swagger-api/apidom-parser-adapter-yaml-1-2/src/lexical-analysis/browser.mjs
import treeSitterYaml from '../../wasm/tree-sitter-yaml.wasm';
// ...
await Language.load(treeSitterYaml);Without intervention, a WASM plugin such as @rollup/plugin-wasm would convert
each .wasm import into an async loader function:
function tree_sitter_yaml_default(imports) {
return _loadWasmModule(0, null, ``, imports);
}Language.load() in web-tree-sitter accepts either a URL string or aUint8Array. It does NOT accept a function. When given a function it callsfetch(fn), which stringifies the function and tries to fetch:
http://โฆ/function%20tree_sitter_yaml_default(Sn)%20{โฆ}Why a plain `load` hook does not work (historical context)
@rollup/plugin-wasm has both a load hook and a transform hook:
// @rollup/plugin-wasm โ simplified
load(id) {
if (!/\.wasm$/.test(id)) return null;
// reads file, emits asset
},
transform(code, id) {
if (code && /\.wasm$/.test(id)) {
// replaces module content with the loader function code
}
}If a preceding plugin's load hook returns valid JavaScript for a .wasm id,@rollup/plugin-wasm's transform hook still fires (the id still ends with.wasm) and overwrites the JS with the loader function. The fix must prevent
the transform hook from seeing the .wasm id at all.
The fix โ `inlineAllWasms` in `vite.config.shared.js`
All three WASMs are handled by a single inlineAllWasms Vite plugin (shared
between vite.config.esm.js and vite.config.umd.js via vite.config.shared.js).
It combines three hooks:
const inlineAllWasms = () => {
const treeSitterBase64 = readFileSync('node_modules/web-tree-sitter/tree-sitter.wasm').toString('base64');
return {
name: 'inline-all-wasms',
enforce: 'pre',
// Redirect every .wasm import to a virtual id that does NOT end with '.wasm',
// bypassing any downstream plugin that checks id.endsWith('.wasm').
async resolveId(id, importer) {
if (!id.endsWith('.wasm')) return null;
const resolved = await this.resolve(id, importer, { skipSelf: true });
if (!resolved) return null;
return '\0wasm-inline:' + resolved.id + ':inline';
},
// Export the raw bytes as a Uint8Array. Language.load(bytes) accepts this directly.
// For tree-sitter.wasm the exported value is unused โ emscripten reads Module.wasmBinary.
load(id) {
if (!id.startsWith('\0wasm-inline:')) return null;
const filePath = id.slice('\0wasm-inline:'.length, -':inline'.length);
const base64 = readFileSync(filePath).toString('base64');
return `const bytes=new Uint8Array(atob("${base64}").split("").map(function(c){return c.charCodeAt(0)}));export default bytes;`;
},
// Inject Module['wasmBinary'] into the final bundle at emscripten's module
// init line so getBinaryPromise() short-circuits before fetch() is called.
renderChunk(code) {
const updated = code.replace(
/var Module\s*=\s*typeof Module\s*!=\s*["']undefined["']\s*\?\s*Module\s*:\s*\{\}/,
(match) => `${match};Module['wasmBinary']=new Uint8Array(atob("${treeSitterBase64}").split("").map(function(c){return c.charCodeAt(0)}))`
);
return updated !== code ? { code: updated } : null;
},
};
};inlineAllWasms must be a Vite plugin (in the plugins array), not a
Rollup plugin (in rollupOptions.plugins). Rollup plugins run after Vite's
transform pipeline, so placing it in rollupOptions.plugins would not give it
priority over other WASM-handling plugins.
Verification after a build
# Check tree-sitter.wasm is inlined (Module.wasmBinary injection present)
grep -c "wasmBinary" dist/esm/apidom.worker.js # โ โฅ1
grep -c "wasmBinary" dist/umd/apidom.worker.js # โ โฅ1
# Check grammar WASMs are Uint8Arrays, not loader functions
grep -c "tree_sitter_yaml_default\|tree_sitter_json_default" dist/esm/apidom.worker.js # โ 0
grep -c "tree_sitter_yaml_default\|tree_sitter_json_default" dist/umd/apidom.worker.js # โ 0
# Language.load should receive bytes$N (Uint8Array), not a function
grep "Language.load(" dist/esm/apidom.worker.js # โ Language.load(bytes$1)