@skriptfabrik/elements-cli 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yaml +39 -0
- package/.github/pull_request_template.md +12 -0
- package/.github/workflows/ci.yml +2 -2
- package/Dockerfile +2 -2
- package/README.md +1 -1
- package/elements-cli.mjs +414 -0
- package/package.json +8 -8
- package/elements-cli.js +0 -392
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
registries:
|
|
4
|
+
dockerhub:
|
|
5
|
+
type: 'docker-registry'
|
|
6
|
+
url: 'https://registry.hub.docker.com'
|
|
7
|
+
username: '${{ secrets.DOCKERHUB_USERNAME }}'
|
|
8
|
+
password: '${{ secrets.DOCKERHUB_TOKEN }}'
|
|
9
|
+
replaces-base: true
|
|
10
|
+
|
|
11
|
+
updates:
|
|
12
|
+
- package-ecosystem: 'github-actions'
|
|
13
|
+
directory: '/'
|
|
14
|
+
reviewers:
|
|
15
|
+
- 'skriptfabrik/developers'
|
|
16
|
+
schedule:
|
|
17
|
+
interval: 'weekly'
|
|
18
|
+
time: '08:00'
|
|
19
|
+
timezone: 'Europe/Berlin'
|
|
20
|
+
|
|
21
|
+
- package-ecosystem: 'npm'
|
|
22
|
+
directory: '/'
|
|
23
|
+
reviewers:
|
|
24
|
+
- 'skriptfabrik/developers'
|
|
25
|
+
schedule:
|
|
26
|
+
interval: 'weekly'
|
|
27
|
+
time: '08:00'
|
|
28
|
+
timezone: 'Europe/Berlin'
|
|
29
|
+
|
|
30
|
+
- package-ecosystem: 'docker'
|
|
31
|
+
directory: '/'
|
|
32
|
+
reviewers:
|
|
33
|
+
- 'skriptfabrik/developers'
|
|
34
|
+
registries:
|
|
35
|
+
- 'dockerhub'
|
|
36
|
+
schedule:
|
|
37
|
+
interval: 'weekly'
|
|
38
|
+
time: '08:00'
|
|
39
|
+
timezone: 'Europe/Berlin'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## Describe your changes
|
|
2
|
+
|
|
3
|
+
- [ ] Bug fix (non-breaking change which fixes an issue)
|
|
4
|
+
- [ ] New feature (non-breaking change which adds functionality)
|
|
5
|
+
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
|
6
|
+
|
|
7
|
+
## Checklist before requesting a review
|
|
8
|
+
|
|
9
|
+
- [ ] I have included the ticket number of the issue at the beginning of the title of this pull request.
|
|
10
|
+
- [ ] I have updated the docs and/or specs to reflect this change.
|
|
11
|
+
- [ ] I have performed a self-review of my code.
|
|
12
|
+
- [ ] I have added thorough tests.
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -68,7 +68,7 @@ jobs:
|
|
|
68
68
|
|
|
69
69
|
- name: Build and Push latest Docker image
|
|
70
70
|
if: github.ref_name == 'main'
|
|
71
|
-
uses: docker/build-push-action@
|
|
71
|
+
uses: docker/build-push-action@v4
|
|
72
72
|
with:
|
|
73
73
|
build-args: |-
|
|
74
74
|
BUILDKIT_INLINE_CACHE=${{ env.DOCKER_BUILDKIT_INLINE_CACHE }}
|
|
@@ -90,7 +90,7 @@ jobs:
|
|
|
90
90
|
|
|
91
91
|
- name: Build and Push Docker image release version
|
|
92
92
|
if: github.ref_type == 'tag'
|
|
93
|
-
uses: docker/build-push-action@
|
|
93
|
+
uses: docker/build-push-action@v4
|
|
94
94
|
with:
|
|
95
95
|
build-args: |-
|
|
96
96
|
BUILDKIT_INLINE_CACHE=${{ env.DOCKER_BUILDKIT_INLINE_CACHE }}
|
package/Dockerfile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM node:18.
|
|
1
|
+
FROM node:18.14.0-alpine
|
|
2
2
|
|
|
3
3
|
LABEL maintainer="Daniel Schröder <daniel.schroeder@skriptfabrik.com>"
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@ COPY . /opt/elements-cli-${ELEMENTS_CLI_VERSION}
|
|
|
12
12
|
RUN set -eux; \
|
|
13
13
|
npm --prefix /opt/elements-cli-${ELEMENTS_CLI_VERSION} install; \
|
|
14
14
|
rm -Rf ~/.npm; \
|
|
15
|
-
ln -s /opt/elements-cli-${ELEMENTS_CLI_VERSION}/elements-cli.
|
|
15
|
+
ln -s /opt/elements-cli-${ELEMENTS_CLI_VERSION}/elements-cli.mjs /usr/local/bin/elements
|
|
16
16
|
|
|
17
17
|
WORKDIR /data
|
|
18
18
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[](https://www.npmjs.com/package/@skriptfabrik/elements-cli)
|
|
2
2
|
[](https://www.npmjs.com/package/@skriptfabrik/elements-cli)
|
|
3
|
-
[](https://github.com/skriptfabrik/elements-cli/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
5
|
# Elements CLI
|
|
6
6
|
|
package/elements-cli.mjs
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import chokidar from 'chokidar';
|
|
5
|
+
import corsAnywhere from 'cors-anywhere';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { engine } from 'express-handlebars';
|
|
8
|
+
import { readFile } from 'fs/promises';
|
|
9
|
+
import handlebars from 'handlebars';
|
|
10
|
+
import gracefulShutdown from 'http-graceful-shutdown';
|
|
11
|
+
import minimist from 'minimist';
|
|
12
|
+
import { createRequire } from 'module';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import send from 'send';
|
|
15
|
+
import { WebSocketServer } from 'ws';
|
|
16
|
+
import { fileURLToPath, URL } from 'url';
|
|
17
|
+
|
|
18
|
+
// Compat
|
|
19
|
+
|
|
20
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
21
|
+
const __dirname = path.dirname(__filename);
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
|
|
24
|
+
// Package info
|
|
25
|
+
|
|
26
|
+
const pkg = JSON.parse(await readFile(path.join(__dirname, 'package.json')));
|
|
27
|
+
|
|
28
|
+
// Argument defaults
|
|
29
|
+
|
|
30
|
+
const argd = {
|
|
31
|
+
'base-path': process.env.ELEMENTS_BASE_PATH || process.env.BASE_PATH || '/',
|
|
32
|
+
'credentials-policy': process.env.ELEMENTS_CREDENTIALS_POLICY || process.env.CREDENTIALS_POLICY || 'omit',
|
|
33
|
+
hostname: process.env.ELEMENTS_HOSTNAME || 'localhost',
|
|
34
|
+
layout: process.env.ELEMENTS_LAYOUT || process.env.LAYOUT || 'sidebar',
|
|
35
|
+
logo: process.env.ELEMENTS_LOGO || process.env.LOGO,
|
|
36
|
+
port: parseInt(process.env.ELEMENTS_PORT || '8000'),
|
|
37
|
+
router: process.env.ELEMENTS_ROUTER || process.env.ROUTER || 'history',
|
|
38
|
+
style: process.env.ELEMENTS_STYLE || process.env.STYLE || 'flex: 1 0 0; overflow: hidden;',
|
|
39
|
+
title: process.env.ELEMENTS_TITLE || process.env.TITLE || 'My API Docs',
|
|
40
|
+
variable: (process.env.ELEMENTS_VARIABLE || process.env.VARIABLE || '').split('\n').map(variable => variable.trim()),
|
|
41
|
+
'virtual-host': process.env.ELEMENTS_VIRTUAL_HOST || 'localhost',
|
|
42
|
+
'virtual-port': process.env.ELEMENTS_VIRTUAL_PORT || '8000',
|
|
43
|
+
'working-dir': process.cwd(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Parse arguments
|
|
47
|
+
|
|
48
|
+
const argv = minimist(process.argv.slice(2), {
|
|
49
|
+
boolean: ['c', 'f', 'h', 'n', 'p', 'v', 'w'],
|
|
50
|
+
alias: {
|
|
51
|
+
c: 'with-cors-proxy',
|
|
52
|
+
f: 'filter-internal',
|
|
53
|
+
h: 'help',
|
|
54
|
+
n: 'no-try-it',
|
|
55
|
+
p: 'poll',
|
|
56
|
+
v: 'version',
|
|
57
|
+
w: 'watch',
|
|
58
|
+
},
|
|
59
|
+
default: argd,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Print version number
|
|
63
|
+
|
|
64
|
+
if (argv.version) {
|
|
65
|
+
console.log(pkg.version);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Display help message
|
|
70
|
+
|
|
71
|
+
if (argv.help || argv._.length < 2 || !['export', 'preview'].includes(argv._[0])) {
|
|
72
|
+
if (argv._[0] === 'export') {
|
|
73
|
+
console.error(
|
|
74
|
+
`Elements CLI\n\n${chalk.yellow('Usage:')}\n%s\n\n${chalk.yellow('Arguments:')}\n%s\n\n${chalk.yellow('Options:')}\n%s\n\n${chalk.yellow('Examples:')}\n%s`,
|
|
75
|
+
` ${path.basename(process.argv[1])} export [options] <openapi_json>`,
|
|
76
|
+
` ${chalk.green('openapi_json')} The path or URL of the OpenAPI JSON file`,
|
|
77
|
+
[
|
|
78
|
+
` ${chalk.green(' --base-path=BASE_PATH')} Use the given base path ${chalk.yellow('[default: "' + argd['base-path'] + '"]')}`,
|
|
79
|
+
` ${chalk.green(' --credentials-policy=CREDENTIALS_POLICY')} Credentials policy for "Try It" feature: omit, include, same-origin ${chalk.yellow('[default: "' + argd['credentials-policy'] + '"]')}`,
|
|
80
|
+
` ${chalk.green(' --cors-proxy=CORS_PROXY')} Provide CORS proxy`,
|
|
81
|
+
` ${chalk.green('-f, --filter-internal')} Filter out any content which has been marked as internal with x-internal`,
|
|
82
|
+
` ${chalk.green('-h, --help')} Display this help message`,
|
|
83
|
+
` ${chalk.green(' --layout=LAYOUT')} Layout for Elements: sidebar, stacked ${chalk.yellow('[default: "' + argd.layout + '"]')}`,
|
|
84
|
+
` ${chalk.green(' --logo=LOGO')} URL of an image that will show as a small square logo next to the title`,
|
|
85
|
+
` ${chalk.green('-n --no-try-it')} Hide the "Try It" panel (the interactive API console)`,
|
|
86
|
+
` ${chalk.green(' --router=ROUTER')} Determines how navigation should work: history, hash, memory, static ${chalk.yellow('[default: "' + argd.router + '"]')}`,
|
|
87
|
+
` ${chalk.green(' --style=STYLE')} Additional style for Elements ${chalk.yellow('[default: "' + argd.style + '"]')}`,
|
|
88
|
+
` ${chalk.green(' --title=TITLE')} API docs title ${chalk.yellow('[default: "' + argd.title + '"]')}`,
|
|
89
|
+
` ${chalk.green(' --variable=VARIABLE')} Variable to be replaced in the OpenAPI document`,
|
|
90
|
+
` ${chalk.green('-v, --version')} Print version number`,
|
|
91
|
+
].join('\n'),
|
|
92
|
+
[
|
|
93
|
+
` Export rendered API docs based on local ${chalk.magenta('openapi.json')} path as ${chalk.magenta('index.html')}:`,
|
|
94
|
+
``,
|
|
95
|
+
` ${chalk.green(path.basename(process.argv[1]) + ' export openapi.json > index.html')}`,
|
|
96
|
+
``,
|
|
97
|
+
` Export rendered Swagger Petstore docs based on remote ${chalk.magenta('https://petstore.swagger.io/v2/swagger.json')} URL as ${chalk.magenta('index.html')}:`,
|
|
98
|
+
``,
|
|
99
|
+
` ${chalk.green(path.basename(process.argv[1]) + ' export --title="Swagger Petstore" https://petstore.swagger.io/v2/swagger.json > index.html')}`,
|
|
100
|
+
].join('\n'),
|
|
101
|
+
);
|
|
102
|
+
} else if (argv._[0] === 'preview') {
|
|
103
|
+
console.error(
|
|
104
|
+
`Elements CLI\n\n${chalk.yellow('Usage:')}\n%s\n\n${chalk.yellow('Arguments:')}\n%s\n\n${chalk.yellow('Options:')}\n%s\n\n${chalk.yellow('Examples:')}\n%s`,
|
|
105
|
+
` ${path.basename(process.argv[1])} preview [options] <openapi_json>`,
|
|
106
|
+
` ${chalk.green('openapi_json')} The path or URL of the OpenAPI JSON file`,
|
|
107
|
+
[
|
|
108
|
+
` ${chalk.green(' --base-path=BASE_PATH')} Use the given base path ${chalk.yellow('[default: "' + argd['base-path'] + '"]')}`,
|
|
109
|
+
` ${chalk.green(' --credentials-policy=CREDENTIALS_POLICY')} Credentials policy for "Try It" feature: omit, include, same-origin ${chalk.yellow('[default: "' + argd['credentials-policy'] + '"]')}`,
|
|
110
|
+
` ${chalk.green('-c --with-cors-proxy')} Enable CORS proxy capabilities`,
|
|
111
|
+
` ${chalk.green('-f, --filter-internal')} Filter out any content which has been marked as internal with x-internal`,
|
|
112
|
+
` ${chalk.green('-h, --help')} Display this help message`,
|
|
113
|
+
` ${chalk.green(' --hostname=HOSTNAME')} Server hostname ${chalk.yellow('[default: "' + argd.hostname + '"]')}`,
|
|
114
|
+
` ${chalk.green(' --layout=LAYOUT')} Layout for Elements: sidebar, stacked ${chalk.yellow('[default: "' + argd.layout + '"]')}`,
|
|
115
|
+
` ${chalk.green(' --logo=LOGO')} URL of an image that will show as a small square logo next to the title`,
|
|
116
|
+
` ${chalk.green('-n --no-try-it')} Hide the "Try It" panel (the interactive API console)`,
|
|
117
|
+
` ${chalk.green('-p, --poll')} Use polling instead of file system events`,
|
|
118
|
+
` ${chalk.green(' --port=PORT')} Server port ${chalk.yellow('[default: ' + argd.port + ']')}`,
|
|
119
|
+
` ${chalk.green(' --router=ROUTER')} Determines how navigation should work: history, hash, memory, static ${chalk.yellow('[default: "' + argd.router + '"]')}`,
|
|
120
|
+
` ${chalk.green(' --style=STYLE')} Additional style for Elements ${chalk.yellow('[default: "' + argd.style + '"]')}`,
|
|
121
|
+
` ${chalk.green(' --title=TITLE')} API docs title ${chalk.yellow('[default: "' + argd.title + '"]')}`,
|
|
122
|
+
` ${chalk.green(' --variable=VARIABLE')} Variable to be replaced in the OpenAPI document`,
|
|
123
|
+
` ${chalk.green('-v, --version')} Print version number`,
|
|
124
|
+
` ${chalk.green('-w --watch')} Watch for changes and reload (only for local files)`,
|
|
125
|
+
` ${chalk.green(' --virtual-host=VIRTUAL_HOST')} Reported hostname ${chalk.yellow('[default: ' + argd['virtual-host'] + ']')}`,
|
|
126
|
+
` ${chalk.green(' --virtual-port=VIRTUAL_PORT')} Reported port ${chalk.yellow('[default: ' + argd['virtual-port'] + ']')}`,
|
|
127
|
+
` ${chalk.green(' --working-dir=PWD')} Use the given directory as working directory`,
|
|
128
|
+
].join('\n'),
|
|
129
|
+
[
|
|
130
|
+
` Preview rendered API docs based on local ${chalk.magenta('openapi.json')} path:`,
|
|
131
|
+
``,
|
|
132
|
+
` ${chalk.green(path.basename(process.argv[1]) + ' preview openapi.json')}`,
|
|
133
|
+
``,
|
|
134
|
+
` Preview rendered Swagger Petstore docs based on remote ${chalk.magenta('https://petstore.swagger.io/v2/swagger.json')} URL:`,
|
|
135
|
+
``,
|
|
136
|
+
` ${chalk.green(path.basename(process.argv[1]) + ' preview --title="Swagger Petstore" https://petstore.swagger.io/v2/swagger.json')}`,
|
|
137
|
+
'',
|
|
138
|
+
` Preview local API docs, enable CORS proxy and watch/reload on data changes:`,
|
|
139
|
+
``,
|
|
140
|
+
` ${chalk.green(path.basename(process.argv[1]) + ' preview -cw openapi.json')}`,
|
|
141
|
+
].join('\n'),
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
console.error(
|
|
145
|
+
`Elements CLI\n\n${chalk.yellow('Usage:')}\n%s\n\n${chalk.yellow('Options:')}\n%s\n\n${chalk.yellow('Commands:')}\n%s`,
|
|
146
|
+
` ${path.basename(process.argv[1])} command [options] [arguments]`,
|
|
147
|
+
[
|
|
148
|
+
` ${chalk.green('-h, --help')} Display this help message`,
|
|
149
|
+
` ${chalk.green('-v, --version')} Print version number`,
|
|
150
|
+
].join('\n'),
|
|
151
|
+
[
|
|
152
|
+
` ${chalk.green('export')} Export rendered API docs`,
|
|
153
|
+
` ${chalk.green('preview')} Preview rendered API docs`,
|
|
154
|
+
].join('\n'),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
process.exit(argv.help ? 0 : 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Watching remote files is not supported
|
|
162
|
+
|
|
163
|
+
if (/^http(s)?:\/\//i.test(argv._[1])) {
|
|
164
|
+
argv.watch = false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Replace double forward slashes, removes trailing slashes and optionally appends suffix
|
|
169
|
+
*
|
|
170
|
+
* @param {string} str The input string
|
|
171
|
+
* @param {string} suffix The optional suffix
|
|
172
|
+
*
|
|
173
|
+
* @returns {string}
|
|
174
|
+
*/
|
|
175
|
+
function sanitize(str, suffix = '') {
|
|
176
|
+
return str.replace(/\/+/g, '/').replace(/\/$/, '') + suffix;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Upgrade HTTP server with web socket server capabilities
|
|
181
|
+
*
|
|
182
|
+
* @param {http.Server} server The HTTP server instance
|
|
183
|
+
*
|
|
184
|
+
* @returns {ws.WebSocketServer}
|
|
185
|
+
*/
|
|
186
|
+
function upgrade(server) {
|
|
187
|
+
const wss = new WebSocketServer({ server });
|
|
188
|
+
|
|
189
|
+
return wss.on('connection', (socket) => {
|
|
190
|
+
socket.on('message', (message) => {
|
|
191
|
+
const request = JSON.parse(message);
|
|
192
|
+
|
|
193
|
+
if (request.command === 'hello') {
|
|
194
|
+
const data = JSON.stringify({
|
|
195
|
+
command: 'hello',
|
|
196
|
+
protocols: [
|
|
197
|
+
'http://livereload.com/protocols/official-7',
|
|
198
|
+
'http://livereload.com/protocols/official-8',
|
|
199
|
+
'http://livereload.com/protocols/official-9',
|
|
200
|
+
'http://livereload.com/protocols/2.x-origin-version-negotiation',
|
|
201
|
+
'http://livereload.com/protocols/2.x-remote-control',
|
|
202
|
+
],
|
|
203
|
+
serverName: 'elements-server',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
socket.send(data);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Create file system watcher and broatcast all file changes to every client
|
|
214
|
+
*
|
|
215
|
+
* @param {string} filePath The file path to watch
|
|
216
|
+
* @param {ws.WebSocketServer} server The web socket server instance
|
|
217
|
+
*
|
|
218
|
+
* @returns {chokidar.FSWatcher}
|
|
219
|
+
*/
|
|
220
|
+
function watch(filePath, server) {
|
|
221
|
+
const watcher = chokidar.watch(filePath, {
|
|
222
|
+
ignoreInitial: true,
|
|
223
|
+
usePolling: argv.poll,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return watcher.on('all', (filePath) => {
|
|
227
|
+
const data = JSON.stringify({
|
|
228
|
+
command: 'reload',
|
|
229
|
+
path: filePath,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
server.clients.forEach((socket) => socket.send(data));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Define base href
|
|
237
|
+
|
|
238
|
+
const baseHref = sanitize(`/${argv['base-path']}`, '/');
|
|
239
|
+
|
|
240
|
+
// Define delimiters and variables
|
|
241
|
+
|
|
242
|
+
const delimiters = { open: '{{', close: '}}' },
|
|
243
|
+
variables = [argv.variable]
|
|
244
|
+
.flat()
|
|
245
|
+
.filter((variable) => !!variable)
|
|
246
|
+
.reduce((variables, variable) => {
|
|
247
|
+
const [name, value] = variable.split('=');
|
|
248
|
+
variables[name] = value;
|
|
249
|
+
return variables;
|
|
250
|
+
}, {});
|
|
251
|
+
|
|
252
|
+
// Export rendered API docs
|
|
253
|
+
|
|
254
|
+
if (argv._[0] === 'export') {
|
|
255
|
+
const input = await readFile(path.resolve(__dirname, 'views', 'index.handlebars'));
|
|
256
|
+
const template = handlebars.compile(input.toString('utf8'));
|
|
257
|
+
const version = pkg.dependencies['@stoplight/elements'];
|
|
258
|
+
|
|
259
|
+
let tryItCorsProxy;
|
|
260
|
+
|
|
261
|
+
if (argv['cors-proxy'] && !argv['no-try-it']) {
|
|
262
|
+
tryItCorsProxy = argv['cors-proxy'];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log(
|
|
266
|
+
template({
|
|
267
|
+
baseHref,
|
|
268
|
+
delimiters,
|
|
269
|
+
elements: {
|
|
270
|
+
apiDescriptionUrl: argv._[1],
|
|
271
|
+
basePath: baseHref,
|
|
272
|
+
hideInternal: argv['filter-internal'] ? 'true' : undefined,
|
|
273
|
+
hideTryIt: argv['no-try-it'] ? 'true' : undefined,
|
|
274
|
+
tryItCorsProxy,
|
|
275
|
+
tryItCredentialsPolicy: argv['credentials-policy'],
|
|
276
|
+
layout: argv.layout,
|
|
277
|
+
logo: argv.logo,
|
|
278
|
+
router: argv.router,
|
|
279
|
+
style: argv.style,
|
|
280
|
+
},
|
|
281
|
+
'elements-css': `https://unpkg.com/@stoplight/elements@${version}/styles.min.css`,
|
|
282
|
+
'elements-js': `https://unpkg.com/@stoplight/elements@${version}/web-components.min.js`,
|
|
283
|
+
layout: false,
|
|
284
|
+
livereload: false,
|
|
285
|
+
title: argv.title,
|
|
286
|
+
variables,
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
process.exit(0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Create express app
|
|
294
|
+
|
|
295
|
+
const app = express();
|
|
296
|
+
|
|
297
|
+
// Enable Handlebars view engine
|
|
298
|
+
|
|
299
|
+
app.engine('handlebars', engine());
|
|
300
|
+
app.set('view engine', 'handlebars');
|
|
301
|
+
app.set('views', path.join(__dirname, 'views'));
|
|
302
|
+
|
|
303
|
+
// Serve assets from node_modules
|
|
304
|
+
|
|
305
|
+
const assets = {
|
|
306
|
+
'livereload.js': require.resolve('livereload-js/dist/livereload.min.js'),
|
|
307
|
+
'styles.min.css': require.resolve('@stoplight/elements/styles.min.css'),
|
|
308
|
+
'web-components.min.js': require.resolve('@stoplight/elements/web-components.min.js'),
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
app.get(
|
|
312
|
+
Object.keys(assets).map((asset) =>
|
|
313
|
+
sanitize(`/${argv['base-path']}/${asset}`)
|
|
314
|
+
),
|
|
315
|
+
(req, res) => {
|
|
316
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
317
|
+
|
|
318
|
+
send(req, assets[path.basename(url.pathname)]).pipe(res);
|
|
319
|
+
}
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Serve static files from working directory
|
|
323
|
+
|
|
324
|
+
app.use(
|
|
325
|
+
sanitize(`/${argv['base-path']}`),
|
|
326
|
+
express.static(argv['working-dir'], { index: false })
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// Handle CORS proxy requests
|
|
330
|
+
|
|
331
|
+
if (argv['with-cors-proxy'] && !argv['no-try-it']) {
|
|
332
|
+
const proxy = corsAnywhere.createServer({
|
|
333
|
+
originWhitelist: [], // Allow all origins
|
|
334
|
+
requireHeaders: [], // Do not require any headers
|
|
335
|
+
removeHeaders: [], // Do not remove any headers
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
app.all(sanitize(`/${argv['base-path']}/_/*`), (req, res) => {
|
|
339
|
+
const pos = req.originalUrl.indexOf('?');
|
|
340
|
+
const queryString = pos === -1 ? '' : req.originalUrl.substring(pos);
|
|
341
|
+
|
|
342
|
+
req.url = `/${req.params['0']}${queryString}`;
|
|
343
|
+
|
|
344
|
+
proxy.emit('request', req, res);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Render and serve index template
|
|
349
|
+
|
|
350
|
+
app.get(
|
|
351
|
+
[sanitize(`/${argv['base-path']}`, '*'), sanitize(`/${argv['base-path']}`)],
|
|
352
|
+
(req, res) => {
|
|
353
|
+
let tryItCorsProxy;
|
|
354
|
+
|
|
355
|
+
if (argv['with-cors-proxy'] && !argv['no-try-it']) {
|
|
356
|
+
tryItCorsProxy = `http://${req.headers.host}${baseHref}_/`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
res.render('index', {
|
|
360
|
+
baseHref,
|
|
361
|
+
delimiters,
|
|
362
|
+
elements: {
|
|
363
|
+
apiDescriptionUrl: argv._[1],
|
|
364
|
+
basePath: baseHref,
|
|
365
|
+
hideInternal: argv['filter-internal'] ? 'true' : undefined,
|
|
366
|
+
hideTryIt: argv['no-try-it'] ? 'true' : undefined,
|
|
367
|
+
tryItCorsProxy,
|
|
368
|
+
tryItCredentialsPolicy: argv['credentials-policy'],
|
|
369
|
+
layout: argv.layout,
|
|
370
|
+
logo: argv.logo,
|
|
371
|
+
router: argv.router,
|
|
372
|
+
style: argv.style,
|
|
373
|
+
},
|
|
374
|
+
'elements-css': 'styles.min.css',
|
|
375
|
+
'elements-js': 'web-components.min.js',
|
|
376
|
+
layout: false,
|
|
377
|
+
'livereload-js': argv.watch ? 'livereload.js' : undefined,
|
|
378
|
+
title: argv.title,
|
|
379
|
+
variables,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Listen for HTTP connections
|
|
385
|
+
|
|
386
|
+
const server = app.listen(argv.port, argv.hostname, () => {
|
|
387
|
+
console.error(`Elements server listening on ${argv.hostname}:${argv.port}`);
|
|
388
|
+
console.error(`Visit http://${argv['virtual-host']}:${argv['virtual-port']}${baseHref}`);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Watch files in working directory and launch web socket server
|
|
392
|
+
|
|
393
|
+
const watcher = argv.watch
|
|
394
|
+
? watch(
|
|
395
|
+
argv['working-dir'],
|
|
396
|
+
upgrade(server).on('error', (err) => console.error(err))
|
|
397
|
+
)
|
|
398
|
+
.once('ready', () =>
|
|
399
|
+
console.error(`Watching ${path.resolve(argv['working-dir'])}`)
|
|
400
|
+
)
|
|
401
|
+
.on('error', (err) => console.error(err))
|
|
402
|
+
: undefined;
|
|
403
|
+
|
|
404
|
+
// Enable the graceful shutdown
|
|
405
|
+
|
|
406
|
+
gracefulShutdown(server, {
|
|
407
|
+
onShutdown: () =>
|
|
408
|
+
new Promise((resolve) => {
|
|
409
|
+
if (watcher) {
|
|
410
|
+
watcher.close();
|
|
411
|
+
}
|
|
412
|
+
resolve();
|
|
413
|
+
}),
|
|
414
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skriptfabrik/elements-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "The missing CLI for beautiful, interactive API docs powered by with Stoplight Elements",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"stoplight",
|
|
@@ -21,20 +21,20 @@
|
|
|
21
21
|
},
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@stoplight/elements": "7.7.
|
|
25
|
-
"chalk": "^
|
|
24
|
+
"@stoplight/elements": "7.7.13",
|
|
25
|
+
"chalk": "^5.2.0",
|
|
26
26
|
"chokidar": "^3.5.3",
|
|
27
27
|
"cors-anywhere": "~0.4.4",
|
|
28
28
|
"express": "^4.18.2",
|
|
29
|
-
"express-handlebars": "^
|
|
29
|
+
"express-handlebars": "^7.0.4",
|
|
30
30
|
"handlebars": "^4.7.7",
|
|
31
|
-
"http-graceful-shutdown": "^3.1.
|
|
32
|
-
"livereload-js": "
|
|
31
|
+
"http-graceful-shutdown": "^3.1.12",
|
|
32
|
+
"livereload-js": "^4.0.1",
|
|
33
33
|
"minimist": "^1.2.7",
|
|
34
34
|
"send": "~0.18.0",
|
|
35
|
-
"ws": "^8.
|
|
35
|
+
"ws": "^8.13.0"
|
|
36
36
|
},
|
|
37
37
|
"bin": {
|
|
38
|
-
"elements": "./elements-cli.
|
|
38
|
+
"elements": "./elements-cli.mjs"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/elements-cli.js
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const chalk = require('chalk');
|
|
4
|
-
const chokidar = require('chokidar');
|
|
5
|
-
const { compile } = require('handlebars');
|
|
6
|
-
const corsAnywhere = require('cors-anywhere');
|
|
7
|
-
const express = require('express');
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const gracefulShutdown = require('http-graceful-shutdown');
|
|
10
|
-
const handlebars = require('express-handlebars');
|
|
11
|
-
const minimist = require('minimist');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const pkg = require('./package.json');
|
|
14
|
-
const send = require('send');
|
|
15
|
-
const { Server } = require('ws');
|
|
16
|
-
const { URL } = require('url');
|
|
17
|
-
|
|
18
|
-
// Argument defaults
|
|
19
|
-
|
|
20
|
-
const argd = {
|
|
21
|
-
'base-path': process.env.ELEMENTS_BASE_PATH || process.env.BASE_PATH || '/',
|
|
22
|
-
'credentials-policy': process.env.ELEMENTS_CREDENTIALS_POLICY || process.env.CREDENTIALS_POLICY || 'omit',
|
|
23
|
-
hostname: process.env.ELEMENTS_HOSTNAME || 'localhost',
|
|
24
|
-
layout: process.env.ELEMENTS_LAYOUT || process.env.LAYOUT || 'sidebar',
|
|
25
|
-
logo: process.env.ELEMENTS_LOGO || process.env.LOGO,
|
|
26
|
-
port: parseInt(process.env.ELEMENTS_PORT || '8000'),
|
|
27
|
-
router: process.env.ELEMENTS_ROUTER || process.env.ROUTER || 'history',
|
|
28
|
-
style: process.env.ELEMENTS_STYLE || process.env.STYLE || 'flex: 1 0 0; overflow: hidden;',
|
|
29
|
-
title: process.env.ELEMENTS_TITLE || process.env.TITLE || 'My API Docs',
|
|
30
|
-
variable: (process.env.ELEMENTS_VARIABLE || process.env.VARIABLE || '').split('\n').map(variable => variable.trim()),
|
|
31
|
-
'virtual-host': process.env.ELEMENTS_VIRTUAL_HOST || 'localhost',
|
|
32
|
-
'virtual-port': process.env.ELEMENTS_VIRTUAL_PORT || '8000',
|
|
33
|
-
'working-dir': process.cwd(),
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Parse arguments
|
|
37
|
-
|
|
38
|
-
const argv = minimist(process.argv.slice(2), {
|
|
39
|
-
boolean: ['c', 'f', 'h', 'n', 'p', 'v', 'w'],
|
|
40
|
-
alias: {
|
|
41
|
-
c: 'with-cors-proxy',
|
|
42
|
-
f: 'filter-internal',
|
|
43
|
-
h: 'help',
|
|
44
|
-
n: 'no-try-it',
|
|
45
|
-
p: 'poll',
|
|
46
|
-
v: 'version',
|
|
47
|
-
w: 'watch',
|
|
48
|
-
},
|
|
49
|
-
default: argd,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Print version number
|
|
53
|
-
|
|
54
|
-
if (argv.version) {
|
|
55
|
-
console.log(pkg.version);
|
|
56
|
-
process.exit(0);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Display help message
|
|
60
|
-
|
|
61
|
-
if (argv.help || argv._.length < 2 || !['export', 'preview'].includes(argv._[0])) {
|
|
62
|
-
if (argv._[0] === 'export') {
|
|
63
|
-
console.error(
|
|
64
|
-
`Elements CLI\n\n${chalk.yellow('Usage:')}\n%s\n\n${chalk.yellow('Arguments:')}\n%s\n\n${chalk.yellow('Options:')}\n%s\n\n${chalk.yellow('Examples:')}\n%s`,
|
|
65
|
-
` ${path.basename(process.argv[1])} export [options] <openapi_json>`,
|
|
66
|
-
` ${chalk.green('openapi_json')} The path or URL of the OpenAPI JSON file`,
|
|
67
|
-
[
|
|
68
|
-
` ${chalk.green(' --base-path=BASE_PATH')} Use the given base path ${chalk.yellow('[default: "' + argd['base-path'] + '"]')}`,
|
|
69
|
-
` ${chalk.green(' --credentials-policy=CREDENTIALS_POLICY')} Credentials policy for "Try It" feature: omit, include, same-origin ${chalk.yellow('[default: "' + argd['credentials-policy'] + '"]')}`,
|
|
70
|
-
` ${chalk.green(' --cors-proxy=CORS_PROXY')} Provide CORS proxy`,
|
|
71
|
-
` ${chalk.green('-f, --filter-internal')} Filter out any content which has been marked as internal with x-internal`,
|
|
72
|
-
` ${chalk.green('-h, --help')} Display this help message`,
|
|
73
|
-
` ${chalk.green(' --layout=LAYOUT')} Layout for Elements: sidebar, stacked ${chalk.yellow('[default: "' + argd.layout + '"]')}`,
|
|
74
|
-
` ${chalk.green(' --logo=LOGO')} URL of an image that will show as a small square logo next to the title`,
|
|
75
|
-
` ${chalk.green('-n --no-try-it')} Hide the "Try It" panel (the interactive API console)`,
|
|
76
|
-
` ${chalk.green(' --router=ROUTER')} Determines how navigation should work: history, hash, memory, static ${chalk.yellow('[default: "' + argd.router + '"]')}`,
|
|
77
|
-
` ${chalk.green(' --style=STYLE')} Additional style for Elements ${chalk.yellow('[default: "' + argd.style + '"]')}`,
|
|
78
|
-
` ${chalk.green(' --title=TITLE')} API docs title ${chalk.yellow('[default: "' + argd.title + '"]')}`,
|
|
79
|
-
` ${chalk.green(' --variable=VARIABLE')} Variable to be replaced in the OpenAPI document`,
|
|
80
|
-
` ${chalk.green('-v, --version')} Print version number`,
|
|
81
|
-
].join('\n'),
|
|
82
|
-
[
|
|
83
|
-
` Export rendered API docs based on local ${chalk.magenta('openapi.json')} path as ${chalk.magenta('index.html')}:`,
|
|
84
|
-
``,
|
|
85
|
-
` ${chalk.green(path.basename(process.argv[1]) + ' export openapi.json > index.html')}`,
|
|
86
|
-
``,
|
|
87
|
-
` Export rendered Swagger Petstore docs based on remote ${chalk.magenta('https://petstore.swagger.io/v2/swagger.json')} URL as ${chalk.magenta('index.html')}:`,
|
|
88
|
-
``,
|
|
89
|
-
` ${chalk.green(path.basename(process.argv[1]) + ' export --title="Swagger Petstore" https://petstore.swagger.io/v2/swagger.json > index.html')}`,
|
|
90
|
-
].join('\n'),
|
|
91
|
-
);
|
|
92
|
-
} else if (argv._[0] === 'preview') {
|
|
93
|
-
console.error(
|
|
94
|
-
`Elements CLI\n\n${chalk.yellow('Usage:')}\n%s\n\n${chalk.yellow('Arguments:')}\n%s\n\n${chalk.yellow('Options:')}\n%s\n\n${chalk.yellow('Examples:')}\n%s`,
|
|
95
|
-
` ${path.basename(process.argv[1])} preview [options] <openapi_json>`,
|
|
96
|
-
` ${chalk.green('openapi_json')} The path or URL of the OpenAPI JSON file`,
|
|
97
|
-
[
|
|
98
|
-
` ${chalk.green(' --base-path=BASE_PATH')} Use the given base path ${chalk.yellow('[default: "' + argd['base-path'] + '"]')}`,
|
|
99
|
-
` ${chalk.green(' --credentials-policy=CREDENTIALS_POLICY')} Credentials policy for "Try It" feature: omit, include, same-origin ${chalk.yellow('[default: "' + argd['credentials-policy'] + '"]')}`,
|
|
100
|
-
` ${chalk.green('-c --with-cors-proxy')} Enable CORS proxy capabilities`,
|
|
101
|
-
` ${chalk.green('-f, --filter-internal')} Filter out any content which has been marked as internal with x-internal`,
|
|
102
|
-
` ${chalk.green('-h, --help')} Display this help message`,
|
|
103
|
-
` ${chalk.green(' --hostname=HOSTNAME')} Server hostname ${chalk.yellow('[default: "' + argd.hostname + '"]')}`,
|
|
104
|
-
` ${chalk.green(' --layout=LAYOUT')} Layout for Elements: sidebar, stacked ${chalk.yellow('[default: "' + argd.layout + '"]')}`,
|
|
105
|
-
` ${chalk.green(' --logo=LOGO')} URL of an image that will show as a small square logo next to the title`,
|
|
106
|
-
` ${chalk.green('-n --no-try-it')} Hide the "Try It" panel (the interactive API console)`,
|
|
107
|
-
` ${chalk.green('-p, --poll')} Use polling instead of file system events`,
|
|
108
|
-
` ${chalk.green(' --port=PORT')} Server port ${chalk.yellow('[default: ' + argd.port + ']')}`,
|
|
109
|
-
` ${chalk.green(' --router=ROUTER')} Determines how navigation should work: history, hash, memory, static ${chalk.yellow('[default: "' + argd.router + '"]')}`,
|
|
110
|
-
` ${chalk.green(' --style=STYLE')} Additional style for Elements ${chalk.yellow('[default: "' + argd.style + '"]')}`,
|
|
111
|
-
` ${chalk.green(' --title=TITLE')} API docs title ${chalk.yellow('[default: "' + argd.title + '"]')}`,
|
|
112
|
-
` ${chalk.green(' --variable=VARIABLE')} Variable to be replaced in the OpenAPI document`,
|
|
113
|
-
` ${chalk.green('-v, --version')} Print version number`,
|
|
114
|
-
` ${chalk.green('-w --watch')} Watch for changes and reload (only for local files)`,
|
|
115
|
-
` ${chalk.green(' --virtual-host=VIRTUAL_HOST')} Reported hostname ${chalk.yellow('[default: ' + argd['virtual-host'] + ']')}`,
|
|
116
|
-
` ${chalk.green(' --virtual-port=VIRTUAL_PORT')} Reported port ${chalk.yellow('[default: ' + argd['virtual-port'] + ']')}`,
|
|
117
|
-
` ${chalk.green(' --working-dir=PWD')} Use the given directory as working directory`,
|
|
118
|
-
].join('\n'),
|
|
119
|
-
[
|
|
120
|
-
` Preview rendered API docs based on local ${chalk.magenta('openapi.json')} path:`,
|
|
121
|
-
``,
|
|
122
|
-
` ${chalk.green(path.basename(process.argv[1]) + ' preview openapi.json')}`,
|
|
123
|
-
``,
|
|
124
|
-
` Preview rendered Swagger Petstore docs based on remote ${chalk.magenta('https://petstore.swagger.io/v2/swagger.json')} URL:`,
|
|
125
|
-
``,
|
|
126
|
-
` ${chalk.green(path.basename(process.argv[1]) + ' preview --title="Swagger Petstore" https://petstore.swagger.io/v2/swagger.json')}`,
|
|
127
|
-
'',
|
|
128
|
-
` Preview local API docs, enable CORS proxy and watch/reload on data changes:`,
|
|
129
|
-
``,
|
|
130
|
-
` ${chalk.green(path.basename(process.argv[1]) + ' preview -cw openapi.json')}`,
|
|
131
|
-
].join('\n'),
|
|
132
|
-
);
|
|
133
|
-
} else {
|
|
134
|
-
console.error(
|
|
135
|
-
`Elements CLI\n\n${chalk.yellow('Usage:')}\n%s\n\n${chalk.yellow('Options:')}\n%s\n\n${chalk.yellow('Commands:')}\n%s`,
|
|
136
|
-
` ${path.basename(process.argv[1])} command [options] [arguments]`,
|
|
137
|
-
[
|
|
138
|
-
` ${chalk.green('-h, --help')} Display this help message`,
|
|
139
|
-
` ${chalk.green('-v, --version')} Print version number`,
|
|
140
|
-
].join('\n'),
|
|
141
|
-
[
|
|
142
|
-
` ${chalk.green('export')} Export rendered API docs`,
|
|
143
|
-
` ${chalk.green('preview')} Preview rendered API docs`,
|
|
144
|
-
].join('\n'),
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
process.exit(argv.help ? 0 : 1);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Watching remote files is not supported
|
|
152
|
-
|
|
153
|
-
if (/^http(s)?:\/\//i.test(argv._[1])) {
|
|
154
|
-
argv.watch = false;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Replace double forward slashes, removes trailing slashes and optionally appends suffix
|
|
159
|
-
*
|
|
160
|
-
* @param {string} str The input string
|
|
161
|
-
* @param {string} suffix The optional suffix
|
|
162
|
-
*
|
|
163
|
-
* @returns {string}
|
|
164
|
-
*/
|
|
165
|
-
function sanitize(str, suffix = '') {
|
|
166
|
-
return str.replace(/\/+/g, '/').replace(/\/$/, '') + suffix;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Upgrade HTTP server with web socket server capabilities
|
|
171
|
-
*
|
|
172
|
-
* @param {http.Server} server The HTTP server instance
|
|
173
|
-
*
|
|
174
|
-
* @returns {ws.Server}
|
|
175
|
-
*/
|
|
176
|
-
function upgrade(server) {
|
|
177
|
-
const wss = new Server({ server });
|
|
178
|
-
|
|
179
|
-
return wss.on('connection', (socket) => {
|
|
180
|
-
socket.on('message', (message) => {
|
|
181
|
-
const request = JSON.parse(message);
|
|
182
|
-
|
|
183
|
-
if (request.command === 'hello') {
|
|
184
|
-
const data = JSON.stringify({
|
|
185
|
-
command: 'hello',
|
|
186
|
-
protocols: [
|
|
187
|
-
'http://livereload.com/protocols/official-7',
|
|
188
|
-
'http://livereload.com/protocols/official-8',
|
|
189
|
-
'http://livereload.com/protocols/official-9',
|
|
190
|
-
'http://livereload.com/protocols/2.x-origin-version-negotiation',
|
|
191
|
-
'http://livereload.com/protocols/2.x-remote-control',
|
|
192
|
-
],
|
|
193
|
-
serverName: 'elements-server',
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
socket.send(data);
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Create file system watcher and broatcast all file changes to every client
|
|
204
|
-
*
|
|
205
|
-
* @param {sting} filePath The file path to watch
|
|
206
|
-
* @param {ws.Server} server The web socket server instance
|
|
207
|
-
*
|
|
208
|
-
* @returns {chokidar.FSWatcher}
|
|
209
|
-
*/
|
|
210
|
-
function watch(filePath, server) {
|
|
211
|
-
const watcher = chokidar.watch(filePath, {
|
|
212
|
-
ignoreInitial: true,
|
|
213
|
-
usePolling: argv.poll,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
return watcher.on('all', (filePath) => {
|
|
217
|
-
const data = JSON.stringify({
|
|
218
|
-
command: 'reload',
|
|
219
|
-
path: filePath,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
server.clients.forEach((socket) => socket.send(data));
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Define base href
|
|
227
|
-
|
|
228
|
-
const baseHref = sanitize(`/${argv['base-path']}`, '/');
|
|
229
|
-
|
|
230
|
-
// Define delimiters and variables
|
|
231
|
-
|
|
232
|
-
const delimiters = { open: '{{', close: '}}' },
|
|
233
|
-
variables = [argv.variable].flat().filter((variable) => !!variable).reduce((variables, variable) => {
|
|
234
|
-
const [name, value] = variable.split('=');
|
|
235
|
-
variables[name] = value;
|
|
236
|
-
return variables;
|
|
237
|
-
}, {});
|
|
238
|
-
|
|
239
|
-
// Export rendered API docs
|
|
240
|
-
|
|
241
|
-
if (argv._[0] === 'export') {
|
|
242
|
-
const input = fs.readFileSync(path.resolve(__dirname, 'views', 'index.handlebars')).toString('utf8');
|
|
243
|
-
const template = compile(input);
|
|
244
|
-
const version = pkg.dependencies['@stoplight/elements'];
|
|
245
|
-
|
|
246
|
-
let tryItCorsProxy;
|
|
247
|
-
|
|
248
|
-
if (argv['cors-proxy'] && !argv['no-try-it']) {
|
|
249
|
-
tryItCorsProxy = argv['cors-proxy'];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
console.log(template({
|
|
253
|
-
baseHref,
|
|
254
|
-
delimiters,
|
|
255
|
-
elements: {
|
|
256
|
-
apiDescriptionUrl: argv._[1],
|
|
257
|
-
basePath: baseHref,
|
|
258
|
-
hideInternal: argv['filter-internal'] ? 'true' : undefined,
|
|
259
|
-
hideTryIt: argv['no-try-it'] ? 'true' : undefined,
|
|
260
|
-
tryItCorsProxy,
|
|
261
|
-
tryItCredentialsPolicy: argv['credentials-policy'],
|
|
262
|
-
layout: argv.layout,
|
|
263
|
-
logo: argv.logo,
|
|
264
|
-
router: argv.router,
|
|
265
|
-
style: argv.style,
|
|
266
|
-
},
|
|
267
|
-
'elements-css': `https://unpkg.com/@stoplight/elements@${version}/styles.min.css`,
|
|
268
|
-
'elements-js': `https://unpkg.com/@stoplight/elements@${version}/web-components.min.js`,
|
|
269
|
-
layout: false,
|
|
270
|
-
livereload: false,
|
|
271
|
-
title: argv.title,
|
|
272
|
-
variables,
|
|
273
|
-
}));
|
|
274
|
-
|
|
275
|
-
process.exit(0);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Create express app
|
|
279
|
-
|
|
280
|
-
const app = express();
|
|
281
|
-
|
|
282
|
-
// Enable Handlebars view engine
|
|
283
|
-
|
|
284
|
-
app.engine('handlebars', handlebars.engine());
|
|
285
|
-
app.set('view engine', 'handlebars');
|
|
286
|
-
app.set('views', path.join(__dirname, 'views'));
|
|
287
|
-
|
|
288
|
-
// Serve assets from node_modules
|
|
289
|
-
|
|
290
|
-
const assets = {
|
|
291
|
-
'livereload.js': require.resolve('livereload-js/dist/livereload.min.js'),
|
|
292
|
-
'styles.min.css': require.resolve('@stoplight/elements/styles.min.css'),
|
|
293
|
-
'web-components.min.js': require.resolve('@stoplight/elements/web-components.min.js'),
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
app.get(
|
|
297
|
-
Object.keys(assets).map((asset) =>
|
|
298
|
-
sanitize(`/${argv['base-path']}/${asset}`)
|
|
299
|
-
),
|
|
300
|
-
(req, res) => {
|
|
301
|
-
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
302
|
-
|
|
303
|
-
send(req, assets[path.basename(url.pathname)]).pipe(
|
|
304
|
-
res
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
// Serve static files from working directory
|
|
310
|
-
|
|
311
|
-
app.use(sanitize(`/${argv['base-path']}`), express.static(argv['working-dir'], { index: false }));
|
|
312
|
-
|
|
313
|
-
// Handle CORS proxy requests
|
|
314
|
-
|
|
315
|
-
if (argv['with-cors-proxy'] && !argv['no-try-it']) {
|
|
316
|
-
const proxy = corsAnywhere.createServer({
|
|
317
|
-
originWhitelist: [], // Allow all origins
|
|
318
|
-
requireHeaders: [], // Do not require any headers
|
|
319
|
-
removeHeaders: [] // Do not remove any headers
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
app.all(sanitize(`/${argv['base-path']}/_/*`), (req, res) => {
|
|
323
|
-
const pos = req.originalUrl.indexOf('?');
|
|
324
|
-
const queryString = pos === -1 ? '' : req.originalUrl.substring(pos);
|
|
325
|
-
|
|
326
|
-
req.url = `/${req.params['0']}${queryString}`;
|
|
327
|
-
|
|
328
|
-
proxy.emit('request', req, res);
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Render and serve index template
|
|
333
|
-
|
|
334
|
-
app.get(
|
|
335
|
-
[sanitize(`/${argv['base-path']}`, '*'), sanitize(`/${argv['base-path']}`)],
|
|
336
|
-
(req, res) => {
|
|
337
|
-
let tryItCorsProxy;
|
|
338
|
-
|
|
339
|
-
if (argv['with-cors-proxy'] && !argv['no-try-it']) {
|
|
340
|
-
tryItCorsProxy = `http://${req.headers.host}${baseHref}_/`;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
res.render('index', {
|
|
344
|
-
baseHref,
|
|
345
|
-
delimiters,
|
|
346
|
-
elements: {
|
|
347
|
-
apiDescriptionUrl: argv._[1],
|
|
348
|
-
basePath: baseHref,
|
|
349
|
-
hideInternal: argv['filter-internal'] ? 'true' : undefined,
|
|
350
|
-
hideTryIt: argv['no-try-it'] ? 'true' : undefined,
|
|
351
|
-
tryItCorsProxy,
|
|
352
|
-
tryItCredentialsPolicy: argv['credentials-policy'],
|
|
353
|
-
layout: argv.layout,
|
|
354
|
-
logo: argv.logo,
|
|
355
|
-
router: argv.router,
|
|
356
|
-
style: argv.style,
|
|
357
|
-
},
|
|
358
|
-
'elements-css': 'styles.min.css',
|
|
359
|
-
'elements-js': 'web-components.min.js',
|
|
360
|
-
layout: false,
|
|
361
|
-
'livereload-js': argv.watch ? 'livereload.js' : undefined,
|
|
362
|
-
title: argv.title,
|
|
363
|
-
variables,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
// Listen for HTTP connections
|
|
369
|
-
|
|
370
|
-
const server = app.listen(argv.port, argv.hostname, () => {
|
|
371
|
-
console.error(
|
|
372
|
-
`Elements server listening on ${argv.hostname}:${argv.port}`
|
|
373
|
-
);
|
|
374
|
-
console.error(
|
|
375
|
-
`Visit http://${argv['virtual-host']}:${argv['virtual-port']}${baseHref}`
|
|
376
|
-
);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// Watch files in working directory and launch web socket server
|
|
380
|
-
|
|
381
|
-
if (argv.watch) {
|
|
382
|
-
watch(
|
|
383
|
-
argv['working-dir'],
|
|
384
|
-
upgrade(server).on('error', (err) => console.error(err))
|
|
385
|
-
)
|
|
386
|
-
.once('ready', () => console.error(`Watching ${path.resolve(argv['working-dir'])}`))
|
|
387
|
-
.on('error', (err) => console.error(err));
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Enable the graceful shutdown
|
|
391
|
-
|
|
392
|
-
gracefulShutdown(server);
|