@redocly/cli 1.0.0-beta.96
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/README.md +39 -0
- package/bin/cli.js +3 -0
- package/lib/__mocks__/utils.d.ts +17 -0
- package/lib/__mocks__/utils.js +14 -0
- package/lib/__tests__/commands/bundle.test.d.ts +1 -0
- package/lib/__tests__/commands/bundle.test.js +92 -0
- package/lib/__tests__/commands/push-region.test.d.ts +1 -0
- package/lib/__tests__/commands/push-region.test.js +55 -0
- package/lib/__tests__/commands/push.test.d.ts +1 -0
- package/lib/__tests__/commands/push.test.js +153 -0
- package/lib/__tests__/utils.test.d.ts +1 -0
- package/lib/__tests__/utils.test.js +41 -0
- package/lib/assert-node-version.d.ts +1 -0
- package/lib/assert-node-version.js +10 -0
- package/lib/commands/bundle.d.ts +19 -0
- package/lib/commands/bundle.js +128 -0
- package/lib/commands/join.d.ts +7 -0
- package/lib/commands/join.js +421 -0
- package/lib/commands/lint.d.ts +11 -0
- package/lib/commands/lint.js +80 -0
- package/lib/commands/login.d.ts +6 -0
- package/lib/commands/login.js +28 -0
- package/lib/commands/preview-docs/index.d.ts +12 -0
- package/lib/commands/preview-docs/index.js +141 -0
- package/lib/commands/preview-docs/preview-server/default.hbs +24 -0
- package/lib/commands/preview-docs/preview-server/hot.js +42 -0
- package/lib/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
- package/lib/commands/preview-docs/preview-server/preview-server.d.ts +5 -0
- package/lib/commands/preview-docs/preview-server/preview-server.js +120 -0
- package/lib/commands/preview-docs/preview-server/server.d.ts +23 -0
- package/lib/commands/preview-docs/preview-server/server.js +85 -0
- package/lib/commands/push.d.ts +25 -0
- package/lib/commands/push.js +247 -0
- package/lib/commands/split/__tests__/index.test.d.ts +1 -0
- package/lib/commands/split/__tests__/index.test.js +70 -0
- package/lib/commands/split/index.d.ts +8 -0
- package/lib/commands/split/index.js +279 -0
- package/lib/commands/split/types.d.ts +37 -0
- package/lib/commands/split/types.js +52 -0
- package/lib/commands/stats.d.ts +5 -0
- package/lib/commands/stats.js +92 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +269 -0
- package/lib/js-utils.d.ts +3 -0
- package/lib/js-utils.js +16 -0
- package/lib/types.d.ts +13 -0
- package/lib/types.js +5 -0
- package/lib/utils.d.ts +28 -0
- package/lib/utils.js +260 -0
- package/package.json +54 -0
- package/src/__mocks__/utils.ts +11 -0
- package/src/__tests__/commands/bundle.test.ts +120 -0
- package/src/__tests__/commands/push-region.test.ts +51 -0
- package/src/__tests__/commands/push.test.ts +156 -0
- package/src/__tests__/utils.test.ts +50 -0
- package/src/assert-node-version.ts +8 -0
- package/src/commands/bundle.ts +178 -0
- package/src/commands/join.ts +488 -0
- package/src/commands/lint.ts +110 -0
- package/src/commands/login.ts +19 -0
- package/src/commands/preview-docs/index.ts +188 -0
- package/src/commands/preview-docs/preview-server/default.hbs +24 -0
- package/src/commands/preview-docs/preview-server/hot.js +42 -0
- package/src/commands/preview-docs/preview-server/oauth2-redirect.html +21 -0
- package/src/commands/preview-docs/preview-server/preview-server.ts +150 -0
- package/src/commands/preview-docs/preview-server/server.ts +91 -0
- package/src/commands/push.ts +355 -0
- package/src/commands/split/__tests__/fixtures/spec.json +70 -0
- package/src/commands/split/__tests__/fixtures/webhooks.json +88 -0
- package/src/commands/split/__tests__/index.test.ts +96 -0
- package/src/commands/split/index.ts +349 -0
- package/src/commands/split/types.ts +73 -0
- package/src/commands/stats.ts +115 -0
- package/src/index.ts +311 -0
- package/src/js-utils.ts +12 -0
- package/src/types.ts +13 -0
- package/src/utils.ts +300 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import * as colorette from 'colorette';
|
|
2
|
+
import * as chockidar from 'chokidar';
|
|
3
|
+
import {
|
|
4
|
+
bundle,
|
|
5
|
+
loadConfig,
|
|
6
|
+
ResolveError,
|
|
7
|
+
YamlParseError,
|
|
8
|
+
RedoclyClient,
|
|
9
|
+
getTotals,
|
|
10
|
+
getMergedConfig,
|
|
11
|
+
} from '@redocly/openapi-core';
|
|
12
|
+
import { getFallbackEntryPointsOrExit } from '../../utils';
|
|
13
|
+
import startPreviewServer from './preview-server/preview-server';
|
|
14
|
+
|
|
15
|
+
export async function previewDocs(argv: {
|
|
16
|
+
port: number;
|
|
17
|
+
host: string;
|
|
18
|
+
'use-community-edition'?: boolean;
|
|
19
|
+
config?: string;
|
|
20
|
+
entrypoint?: string;
|
|
21
|
+
'skip-rule'?: string[];
|
|
22
|
+
'skip-decorator'?: string[];
|
|
23
|
+
'skip-preprocessor'?: string[];
|
|
24
|
+
force?: boolean;
|
|
25
|
+
}) {
|
|
26
|
+
let isAuthorizedWithRedocly: boolean = false;
|
|
27
|
+
let redocOptions: any = {};
|
|
28
|
+
let config = await reloadConfig();
|
|
29
|
+
|
|
30
|
+
const entrypoints = await getFallbackEntryPointsOrExit(
|
|
31
|
+
argv.entrypoint ? [argv.entrypoint] : [],
|
|
32
|
+
config,
|
|
33
|
+
);
|
|
34
|
+
const entrypoint = entrypoints[0];
|
|
35
|
+
|
|
36
|
+
let cachedBundle: any;
|
|
37
|
+
const deps = new Set<string>();
|
|
38
|
+
|
|
39
|
+
async function getBundle() {
|
|
40
|
+
return cachedBundle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function updateBundle() {
|
|
44
|
+
process.stdout.write('\nBundling...\n\n');
|
|
45
|
+
try {
|
|
46
|
+
const {
|
|
47
|
+
bundle: openapiBundle,
|
|
48
|
+
problems,
|
|
49
|
+
fileDependencies,
|
|
50
|
+
} = await bundle({
|
|
51
|
+
ref: entrypoint.path,
|
|
52
|
+
config,
|
|
53
|
+
});
|
|
54
|
+
const removed = [...deps].filter((x) => !fileDependencies.has(x));
|
|
55
|
+
watcher.unwatch(removed);
|
|
56
|
+
watcher.add([...fileDependencies]);
|
|
57
|
+
deps.clear();
|
|
58
|
+
fileDependencies.forEach(deps.add, deps);
|
|
59
|
+
|
|
60
|
+
const fileTotals = getTotals(problems);
|
|
61
|
+
|
|
62
|
+
if (fileTotals.errors === 0) {
|
|
63
|
+
process.stdout.write(
|
|
64
|
+
fileTotals.errors === 0
|
|
65
|
+
? `Created a bundle for ${entrypoint.alias || entrypoint.path} ${
|
|
66
|
+
fileTotals.warnings > 0 ? 'with warnings' : 'successfully'
|
|
67
|
+
}\n`
|
|
68
|
+
: colorette.yellow(
|
|
69
|
+
`Created a bundle for ${
|
|
70
|
+
entrypoint.alias || entrypoint.path
|
|
71
|
+
} with errors. Docs may be broken or not accurate\n`,
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return openapiBundle.parsed;
|
|
77
|
+
} catch (e) {
|
|
78
|
+
handleError(e, entrypoint.path);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
setImmediate(() => {
|
|
83
|
+
cachedBundle = updateBundle();
|
|
84
|
+
}); // initial cache
|
|
85
|
+
|
|
86
|
+
const isAuthorized = isAuthorizedWithRedocly || redocOptions.licenseKey;
|
|
87
|
+
if (!isAuthorized) {
|
|
88
|
+
process.stderr.write(
|
|
89
|
+
`Using Redoc community edition.\nLogin with redocly ${colorette.blue(
|
|
90
|
+
'login',
|
|
91
|
+
)} or use an enterprise license key to preview with the premium docs.\n\n`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const hotClients = await startPreviewServer(argv.port, argv.host, {
|
|
96
|
+
getBundle,
|
|
97
|
+
getOptions: () => redocOptions,
|
|
98
|
+
useRedocPro: isAuthorized && !redocOptions.useCommunityEdition,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const watchPaths = [entrypoint.path, config.configFile!].filter((e) => !!e);
|
|
102
|
+
const watcher = chockidar.watch(watchPaths, {
|
|
103
|
+
disableGlobbing: true,
|
|
104
|
+
ignoreInitial: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const debouncedUpdatedBundle = debounce(async () => {
|
|
108
|
+
cachedBundle = updateBundle();
|
|
109
|
+
await cachedBundle;
|
|
110
|
+
hotClients.broadcast('{"type": "reload", "bundle": true}');
|
|
111
|
+
}, 2000);
|
|
112
|
+
|
|
113
|
+
const changeHandler = async (type: string, file: string) => {
|
|
114
|
+
process.stdout.write(`${colorette.green('watch')} ${type} ${colorette.blue(file)}\n`);
|
|
115
|
+
if (file === config.configFile) {
|
|
116
|
+
config = await reloadConfig();
|
|
117
|
+
hotClients.broadcast(JSON.stringify({ type: 'reload' }));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
debouncedUpdatedBundle();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
watcher.on('change', changeHandler.bind(undefined, 'changed'));
|
|
125
|
+
watcher.on('add', changeHandler.bind(undefined, 'added'));
|
|
126
|
+
watcher.on('unlink', changeHandler.bind(undefined, 'removed'));
|
|
127
|
+
|
|
128
|
+
watcher.on('ready', () => {
|
|
129
|
+
process.stdout.write(
|
|
130
|
+
`\n 👀 Watching ${colorette.blue(
|
|
131
|
+
entrypoint.path,
|
|
132
|
+
)} and all related resources for changes\n\n`,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
async function reloadConfig() {
|
|
137
|
+
let config = await loadConfig(argv.config);
|
|
138
|
+
const redoclyClient = new RedoclyClient();
|
|
139
|
+
isAuthorizedWithRedocly = await redoclyClient.isAuthorizedWithRedocly();
|
|
140
|
+
const resolvedConfig = getMergedConfig(config, argv.entrypoint);
|
|
141
|
+
resolvedConfig.lint.skipRules(argv['skip-rule']);
|
|
142
|
+
resolvedConfig.lint.skipPreprocessors(argv['skip-preprocessor']);
|
|
143
|
+
resolvedConfig.lint.skipDecorators(argv['skip-decorator']);
|
|
144
|
+
const referenceDocs = resolvedConfig['features.openapi'];
|
|
145
|
+
redocOptions = {
|
|
146
|
+
...referenceDocs,
|
|
147
|
+
useCommunityEdition: argv['use-community-edition'] || referenceDocs.useCommunityEdition,
|
|
148
|
+
licenseKey: process.env.REDOCLY_LICENSE_KEY || referenceDocs.licenseKey,
|
|
149
|
+
};
|
|
150
|
+
return resolvedConfig;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function debounce(func: Function, wait: number, immediate?: boolean) {
|
|
155
|
+
let timeout: NodeJS.Timeout | null;
|
|
156
|
+
|
|
157
|
+
return function executedFunction(...args: any[]) {
|
|
158
|
+
// @ts-ignore
|
|
159
|
+
const context = this;
|
|
160
|
+
|
|
161
|
+
const later = () => {
|
|
162
|
+
timeout = null;
|
|
163
|
+
if (!immediate) func.apply(context, args);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const callNow = immediate && !timeout;
|
|
167
|
+
|
|
168
|
+
if (timeout) clearTimeout(timeout);
|
|
169
|
+
|
|
170
|
+
timeout = setTimeout(later, wait);
|
|
171
|
+
|
|
172
|
+
if (callNow) func.apply(context, args);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function handleError(e: Error, ref: string) {
|
|
177
|
+
if (e instanceof ResolveError) {
|
|
178
|
+
process.stderr.write(
|
|
179
|
+
`Failed to resolve entrypoint definition at ${ref}:\n\n - ${e.message}.\n\n`,
|
|
180
|
+
);
|
|
181
|
+
} else if (e instanceof YamlParseError) {
|
|
182
|
+
process.stderr.write(
|
|
183
|
+
`Failed to parse entrypoint definition at ${ref}:\n\n - ${e.message}.\n\n`,
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
process.stderr.write(`Something went wrong when processing ${ref}:\n\n - ${e.message}.\n\n`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html>
|
|
4
|
+
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf8" />
|
|
7
|
+
<title>{{title}}</title>
|
|
8
|
+
<!-- needed for adaptive design -->
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
|
+
<style>
|
|
11
|
+
body {
|
|
12
|
+
padding: 0;
|
|
13
|
+
margin: 0;
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
16
|
+
{{{redocHead}}}
|
|
17
|
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
|
18
|
+
</head>
|
|
19
|
+
|
|
20
|
+
<body>
|
|
21
|
+
{{{redocHTML}}}
|
|
22
|
+
</body>
|
|
23
|
+
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
(function run() {
|
|
2
|
+
const Socket = window.SimpleWebsocket;
|
|
3
|
+
const port = window.__OPENAPI_CLI_WS_PORT;
|
|
4
|
+
|
|
5
|
+
let socket;
|
|
6
|
+
|
|
7
|
+
reconnect();
|
|
8
|
+
|
|
9
|
+
function reconnect() {
|
|
10
|
+
socket = new Socket(`ws://127.0.0.1:${port}`);
|
|
11
|
+
socket.on('connect', () => {
|
|
12
|
+
socket.send('{"type": "ping"}');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
socket.on('data', (data) => {
|
|
16
|
+
const message = JSON.parse(data);
|
|
17
|
+
switch (message.type) {
|
|
18
|
+
case 'pong':
|
|
19
|
+
console.log('[hot] hot reloading connected');
|
|
20
|
+
break;
|
|
21
|
+
case 'reload':
|
|
22
|
+
console.log('[hot] full page reload');
|
|
23
|
+
window.location.reload();
|
|
24
|
+
break;
|
|
25
|
+
default:
|
|
26
|
+
console.log(`[hot] ${message.type} received`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
socket.on('close', () => {
|
|
31
|
+
socket.destroy();
|
|
32
|
+
console.log('Connection lost, trying to reconnect in 4s');
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
reconnect();
|
|
35
|
+
}, 4000);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
socket.on('error', () => {
|
|
39
|
+
socket.destroy();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" />
|
|
6
|
+
<title>Redocly Reference Docs: OAuth2 Redirect</title>
|
|
7
|
+
<style>
|
|
8
|
+
.user-info {
|
|
9
|
+
margin: 150px auto auto;
|
|
10
|
+
width: 50%;
|
|
11
|
+
background-color: whitesmoke;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
text-align: center;
|
|
14
|
+
}
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<script src="https://cdn.jsdelivr.net/npm/@redocly/reference-docs@latest/dist/oauth2-redirect.js"></script>
|
|
19
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { compile } from 'handlebars';
|
|
2
|
+
import * as colorette from 'colorette';
|
|
3
|
+
import * as portfinder from 'portfinder';
|
|
4
|
+
import { readFileSync, promises as fsPromises } from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
|
|
7
|
+
import { startHttpServer, startWsServer, respondWithGzip, mimeTypes } from './server';
|
|
8
|
+
import type { IncomingMessage } from 'http';
|
|
9
|
+
import { isSubdir } from '../../../utils';
|
|
10
|
+
|
|
11
|
+
function getPageHTML(
|
|
12
|
+
htmlTemplate: string,
|
|
13
|
+
redocOptions: object = {},
|
|
14
|
+
useRedocPro: boolean,
|
|
15
|
+
wsPort: number,
|
|
16
|
+
) {
|
|
17
|
+
let templateSrc = readFileSync(htmlTemplate, 'utf-8');
|
|
18
|
+
|
|
19
|
+
// fix template for backward compatibility
|
|
20
|
+
templateSrc = templateSrc
|
|
21
|
+
.replace(/{?{{redocHead}}}?/, '{{{redocHead}}}')
|
|
22
|
+
.replace('{{redocBody}}', '{{{redocHTML}}}');
|
|
23
|
+
|
|
24
|
+
const template = compile(templateSrc);
|
|
25
|
+
|
|
26
|
+
return template({
|
|
27
|
+
redocHead: `
|
|
28
|
+
<script>
|
|
29
|
+
window.__REDOC_EXPORT = '${useRedocPro ? 'RedoclyReferenceDocs' : 'Redoc'}';
|
|
30
|
+
window.__OPENAPI_CLI_WS_PORT = ${wsPort};
|
|
31
|
+
</script>
|
|
32
|
+
<script src="/simplewebsocket.min.js"></script>
|
|
33
|
+
<script src="/hot.js"></script>
|
|
34
|
+
<script src="${
|
|
35
|
+
useRedocPro
|
|
36
|
+
? 'https://cdn.jsdelivr.net/npm/@redocly/reference-docs@latest/dist/redocly-reference-docs.min.js'
|
|
37
|
+
: 'https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js'
|
|
38
|
+
}"></script>
|
|
39
|
+
`,
|
|
40
|
+
redocHTML: `
|
|
41
|
+
<div id="redoc"></div>
|
|
42
|
+
<script>
|
|
43
|
+
var container = document.getElementById('redoc');
|
|
44
|
+
${
|
|
45
|
+
useRedocPro
|
|
46
|
+
? "window[window.__REDOC_EXPORT].setPublicPath('https://cdn.jsdelivr.net/npm/@redocly/reference-docs@latest/dist/');"
|
|
47
|
+
: ''
|
|
48
|
+
}
|
|
49
|
+
window[window.__REDOC_EXPORT].init("/openapi.json", ${JSON.stringify(redocOptions)}, container)
|
|
50
|
+
</script>`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default async function startPreviewServer(
|
|
55
|
+
port: number,
|
|
56
|
+
host: string,
|
|
57
|
+
{
|
|
58
|
+
getBundle,
|
|
59
|
+
getOptions,
|
|
60
|
+
useRedocPro,
|
|
61
|
+
}: { getBundle: Function; getOptions: Function; useRedocPro: boolean },
|
|
62
|
+
) {
|
|
63
|
+
const defaultTemplate = path.join(__dirname, 'default.hbs');
|
|
64
|
+
const handler = async (request: IncomingMessage, response: any) => {
|
|
65
|
+
console.time(colorette.dim(`GET ${request.url}`));
|
|
66
|
+
const { htmlTemplate } = getOptions() || {};
|
|
67
|
+
|
|
68
|
+
if (request.url?.endsWith('/') || path.extname(request.url!) === '') {
|
|
69
|
+
respondWithGzip(
|
|
70
|
+
getPageHTML(htmlTemplate || defaultTemplate, getOptions(), useRedocPro, wsPort),
|
|
71
|
+
request,
|
|
72
|
+
response,
|
|
73
|
+
{
|
|
74
|
+
'Content-Type': 'text/html',
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
} else if (request.url === '/openapi.json') {
|
|
78
|
+
const bundle = await getBundle();
|
|
79
|
+
if (bundle === undefined) {
|
|
80
|
+
respondWithGzip(
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
openapi: '3.0.0',
|
|
83
|
+
info: {
|
|
84
|
+
description:
|
|
85
|
+
'<code> Failed to generate bundle: check out console output for more details </code>',
|
|
86
|
+
},
|
|
87
|
+
paths: {},
|
|
88
|
+
}),
|
|
89
|
+
request,
|
|
90
|
+
response,
|
|
91
|
+
{
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
respondWithGzip(JSON.stringify(bundle), request, response, {
|
|
97
|
+
'Content-Type': 'application/json',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
const filePath =
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
{
|
|
104
|
+
'/hot.js': path.join(__dirname, 'hot.js'),
|
|
105
|
+
'/oauth2-redirect.html': path.join(__dirname, 'oauth2-redirect.html'),
|
|
106
|
+
'/simplewebsocket.min.js': require.resolve('simple-websocket/simplewebsocket.min.js'),
|
|
107
|
+
}[request.url || ''] ||
|
|
108
|
+
path.resolve(htmlTemplate ? path.dirname(htmlTemplate) : process.cwd(), `.${request.url}`);
|
|
109
|
+
|
|
110
|
+
if (!isSubdir(process.cwd(), filePath)) {
|
|
111
|
+
respondWithGzip('404 Not Found', request, response, { 'Content-Type': 'text/html' }, 404);
|
|
112
|
+
console.timeEnd(colorette.dim(`GET ${request.url}`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const extname = String(path.extname(filePath)).toLowerCase() as keyof typeof mimeTypes;
|
|
117
|
+
|
|
118
|
+
const contentType = mimeTypes[extname] || 'application/octet-stream';
|
|
119
|
+
try {
|
|
120
|
+
respondWithGzip(await fsPromises.readFile(filePath), request, response, {
|
|
121
|
+
'Content-Type': contentType,
|
|
122
|
+
});
|
|
123
|
+
} catch (e) {
|
|
124
|
+
if (e.code === 'ENOENT') {
|
|
125
|
+
respondWithGzip('404 Not Found', request, response, { 'Content-Type': 'text/html' }, 404);
|
|
126
|
+
} else {
|
|
127
|
+
respondWithGzip(
|
|
128
|
+
`Something went wrong: ${e.code || e.message}...\n`,
|
|
129
|
+
request,
|
|
130
|
+
response,
|
|
131
|
+
{},
|
|
132
|
+
500,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
console.timeEnd(colorette.dim(`GET ${request.url}`));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let wsPort = await portfinder.getPortPromise({ port: 32201 });
|
|
141
|
+
|
|
142
|
+
const server = startHttpServer(port, host, handler);
|
|
143
|
+
server.on('listening', () => {
|
|
144
|
+
process.stdout.write(
|
|
145
|
+
`\n 🔎 Preview server running at ${colorette.blue(`http://${host}:${port}\n`)}`,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return startWsServer(wsPort);
|
|
150
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as zlib from 'zlib';
|
|
3
|
+
import { ReadStream } from 'fs';
|
|
4
|
+
|
|
5
|
+
const SocketServer = require('simple-websocket/server.js');
|
|
6
|
+
|
|
7
|
+
export const mimeTypes = {
|
|
8
|
+
'.html': 'text/html',
|
|
9
|
+
'.js': 'text/javascript',
|
|
10
|
+
'.css': 'text/css',
|
|
11
|
+
'.json': 'application/json',
|
|
12
|
+
'.png': 'image/png',
|
|
13
|
+
'.jpg': 'image/jpg',
|
|
14
|
+
'.gif': 'image/gif',
|
|
15
|
+
'.svg': 'image/svg+xml',
|
|
16
|
+
'.wav': 'audio/wav',
|
|
17
|
+
'.mp4': 'video/mp4',
|
|
18
|
+
'.woff': 'application/font-woff',
|
|
19
|
+
'.ttf': 'application/font-ttf',
|
|
20
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
21
|
+
'.otf': 'application/font-otf',
|
|
22
|
+
'.wasm': 'application/wasm',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// credits: https://stackoverflow.com/a/9238214/1749888
|
|
26
|
+
export function respondWithGzip(
|
|
27
|
+
contents: string | Buffer | ReadStream,
|
|
28
|
+
request: http.IncomingMessage,
|
|
29
|
+
response: http.ServerResponse,
|
|
30
|
+
headers = {},
|
|
31
|
+
code = 200,
|
|
32
|
+
) {
|
|
33
|
+
let compressedStream;
|
|
34
|
+
const acceptEncoding = (request.headers['accept-encoding'] as string) || '';
|
|
35
|
+
if (acceptEncoding.match(/\bdeflate\b/)) {
|
|
36
|
+
response.writeHead(code, { ...headers, 'content-encoding': 'deflate' });
|
|
37
|
+
compressedStream = zlib.createDeflate();
|
|
38
|
+
} else if (acceptEncoding.match(/\bgzip\b/)) {
|
|
39
|
+
response.writeHead(code, { ...headers, 'content-encoding': 'gzip' });
|
|
40
|
+
compressedStream = zlib.createGzip();
|
|
41
|
+
} else {
|
|
42
|
+
response.writeHead(code, headers);
|
|
43
|
+
if (typeof contents === 'string' || Buffer.isBuffer(contents)) {
|
|
44
|
+
response.write(contents);
|
|
45
|
+
response.end();
|
|
46
|
+
} else if (response !== undefined) {
|
|
47
|
+
contents.pipe(response);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof contents === 'string' || Buffer.isBuffer(contents)) {
|
|
53
|
+
compressedStream.write(contents);
|
|
54
|
+
compressedStream.pipe(response);
|
|
55
|
+
compressedStream.end();
|
|
56
|
+
} else {
|
|
57
|
+
contents.pipe(compressedStream).pipe(response);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function startHttpServer(port: number, host: string, handler: http.RequestListener) {
|
|
62
|
+
return http.createServer(handler).listen(port, host);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function startWsServer(port: number) {
|
|
66
|
+
const socketServer = new SocketServer({ port, clientTracking: true });
|
|
67
|
+
|
|
68
|
+
socketServer.on('connection', (socket: any) => {
|
|
69
|
+
socket.on('data', (data: string) => {
|
|
70
|
+
const message = JSON.parse(data);
|
|
71
|
+
switch (message.type) {
|
|
72
|
+
case 'ping':
|
|
73
|
+
socket.send('{"type": "pong"}');
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
// nope
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
socketServer.broadcast = (message: string) => {
|
|
82
|
+
socketServer._server.clients.forEach((client: any) => {
|
|
83
|
+
if (client.readyState === 1) {
|
|
84
|
+
// OPEN
|
|
85
|
+
client.send(message);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return socketServer;
|
|
91
|
+
}
|