Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new @scalar/api-reference-editor package #2091

Merged
merged 4 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/curvy-pumpkins-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@scalar/use-codemirror': patch
'@scalar/api-reference': patch
'@scalar/client-app': patch
'@scalar/components': patch
'@scalar/use-toasts': patch
'@scalar/nuxt': patch
---

chore: introduce the new @scalar/api-reference-editor
5 changes: 5 additions & 0 deletions .changeset/pink-bananas-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@scalar/api-reference-editor': minor
---

init
2 changes: 1 addition & 1 deletion examples/hono-api-reference/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ app.openapi(
},
}),
(c) => {
// @ts-expect-error invalid type depth
// @ts-expect-error type depth error
return c.json({
message: 'hello',
})
Expand Down
71 changes: 71 additions & 0 deletions packages/api-reference-editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# API Reference Editor

Scalar API references with an integrated editor. The editor can be used in two ways:

## Internal State Management

You (optionally) provide an initial value and the editor will manage any changes internally.

```html
<!doctype html>
<html>
<head>
<title>API Reference Editor</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
<!-- TODO: Update import script -->
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference-editor"></script>
</head>
<body>
<div id="scalar-api-reference" />
<script>
import { mountApiReferenceEditable } from '@scalar/api-reference-editor'

mountApiReferenceEditable('#scalar-api-reference')
</script>
</body>
</html>
```

## External State Management

To have additional control over when the references are updated you can provide a `configuration.spec.content` value and then handle the custom event that is emitted from the Editor component. A handler can be passed directly to the mountApiReferenceEditable function or you can attach an event listener for `scalar-update` to the mounted div.

If you wish to have external UI that updates the spec then `updateSpecValue` can be used to force update the content.

```html
<!doctype html>
<html>
<head>
<title>API Reference Editor</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
<!-- TODO: Update import script -->
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference-editor"></script>
</head>
<body>
<div id="scalar-api-reference" />
<script>
import { mountApiReferenceEditable } from '@scalar/api-reference-editor'

const externalState = {
value: ''
}

const { updateSpecValue} = mountApiReferenceEditable(
'#scalar-api-reference',
{ spec: {content: ''}},
(v: string) => {
console.log('The value is updated!')
updateSpecValue(v) // Updates the rendered spec
externalState.value = v
}
)
</script>
</body>
</html>
```
16 changes: 16 additions & 0 deletions packages/api-reference-editor/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<title>Scalar API Reference Editor</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
<script
src="playground/main.ts"
type="module"></script>
</head>
<body>
<div id="editor"></div>
</body>
</html>
67 changes: 67 additions & 0 deletions packages/api-reference-editor/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"name": "@scalar/api-reference-editor",
"description": "Editer interface for OpenAPI specs",
"license": "MIT",
"author": "Scalar (https://github.com/scalar)",
"homepage": "https://github.com/scalar/scalar",
"bugs": "https://github.com/scalar/scalar/issues/new/choose",
"repository": {
"type": "git",
"url": "https://github.com/scalar/scalar.git",
"directory": "packages/api-reference-editor"
},
"keywords": [
"editor openapi swagger api-reference"
],
"version": "0.0.0",
"engines": {
"node": ">=18"
},
"scripts": {
"build": "vite build && pnpm types:build && tsc-alias -p tsconfig.build.json",
"dev": "vite",
"lint:check": "eslint .",
"lint:fix": "eslint . --fix",
"preview": "vite preview",
"test": "vitest",
"types:build": "vue-tsc -p tsconfig.build.json",
"types:check": "vue-tsc --noEmit --skipLibCheck --composite false"
},
"type": "module",
"main": "dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./css/*.css": {
"import": "./dist/css/*.css",
"require": "./dist/css/*.css"
},
"./*.css": {
"import": "./dist/*.css",
"require": "./dist/*.css"
}
},
"files": [
"dist",
"CHANGELOG.md"
],
"module": "dist/index.js",
"dependencies": {
"@scalar/api-client": "workspace:*",
"@scalar/api-reference": "workspace:*",
"@scalar/oas-utils": "workspace:*",
"@scalar/use-codemirror": "workspace:*",
"unhead": "^1.8.3",
"vue": "^3.4.22"
},
"devDependencies": {
"@scalar/build-tooling": "workspace:*",
"@vitejs/plugin-vue": "^5.0.4",
"tsc-alias": "^1.8.8",
"vite": "^5.2.10",
"vite-svg-loader": "^5.1.0",
"vue-tsc": "^2.0.13"
}
}
5 changes: 5 additions & 0 deletions packages/api-reference-editor/playground/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { mountApiReferenceEditable } from '@/api-reference-editor'

const el = document.getElementById('editor')

const { mount } = mountApiReferenceEditable(el, {})
73 changes: 73 additions & 0 deletions packages/api-reference-editor/src/api-reference-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import ApiReferenceEditor from '@/components/ApiReferenceEditor.vue'
import { UPDATE_EVENT } from '@/constants'
import type {
ReferenceConfiguration,
ReferenceProps,
} from '@scalar/api-reference'
import { objectMerge } from '@scalar/oas-utils/helpers'
import { createApp, h, reactive } from 'vue'

/** Attach the Editable API Reference to the DOM */
export function mountApiReferenceEditable(
/** Element to mount the references to */
elementOrSelector: string | HTMLElement | null,
/** Base configuration */
configuration: ReferenceProps['configuration'] & {
useExternalState?: boolean
} = {},
/** Optional event handler to trigger when the editor input changes */
onUpdate?: (v: string) => void,
) {
// Reactive props that can be set
const props = reactive({ configuration })

// We use a render wrapper to enable reactive props on root
const app = createApp({
render: () => h(ApiReferenceEditor, props),
})

// If an event handler is provided we capture the event.

function mount(el: string | HTMLElement) {
const mountEl = typeof el === 'string' ? document.querySelector(el) : el

if (!mountEl) {
console.error(
'INVALID HTML ELEMENT PROVIDED: Can not mount Scalar API References',
)
} else {
app.mount(mountEl)

if (onUpdate) {
mountEl.addEventListener(UPDATE_EVENT, (evt) => {
const event = evt as CustomEvent<{ value: string }>
onUpdate(event?.detail?.value || '')
})
}
}
}

// Attempt to mount if an element is provided
if (elementOrSelector) {
const el =
typeof elementOrSelector === 'string'
? document.querySelector(elementOrSelector)
: elementOrSelector
if (el) mount(elementOrSelector)
}

return {
/** Attach the ApiReferenceEditor to a given DOM node */
mount,
/** Update the complete config (replaces original config) */
updateConfig: (newConfig: ReferenceConfiguration) => {
objectMerge(props, { configuration: newConfig })
},
/** Update only the spec value - used for external state mode */
updateSpecValue: (specString: string) => {
props.configuration.spec = { content: specString }
},
}
}

export { ApiReferenceEditor }
Loading