@redocly/cli 1.0.0 → 1.0.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/CHANGELOG.md +8 -0
- package/lib/commands/build-docs/index.js +2 -4
- package/lib/commands/build-docs/utils.d.ts +1 -1
- package/lib/commands/build-docs/utils.js +3 -3
- package/package.json +2 -2
- package/src/__mocks__/@redocly/openapi-core.ts +80 -0
- package/src/__mocks__/documents.ts +63 -0
- package/src/__mocks__/fs.ts +6 -0
- package/src/__mocks__/perf_hooks.ts +3 -0
- package/src/__mocks__/redoc.ts +2 -0
- package/src/__mocks__/utils.ts +19 -0
- package/src/__tests__/commands/build-docs.test.ts +62 -0
- package/src/__tests__/commands/bundle.test.ts +150 -0
- package/src/__tests__/commands/join.test.ts +122 -0
- package/src/__tests__/commands/lint.test.ts +190 -0
- package/src/__tests__/commands/push-region.test.ts +58 -0
- package/src/__tests__/commands/push.test.ts +492 -0
- package/src/__tests__/fetch-with-timeout.test.ts +35 -0
- package/src/__tests__/fixtures/config.ts +21 -0
- package/src/__tests__/fixtures/openapi.json +0 -0
- package/src/__tests__/fixtures/openapi.yaml +0 -0
- package/src/__tests__/fixtures/redocly.yaml +0 -0
- package/src/__tests__/utils.test.ts +564 -0
- package/src/__tests__/wrapper.test.ts +57 -0
- package/src/assert-node-version.ts +8 -0
- package/src/commands/build-docs/index.ts +50 -0
- package/src/commands/build-docs/template.hbs +23 -0
- package/src/commands/build-docs/types.ts +24 -0
- package/src/commands/build-docs/utils.ts +110 -0
- package/src/commands/bundle.ts +177 -0
- package/src/commands/join.ts +811 -0
- package/src/commands/lint.ts +151 -0
- package/src/commands/login.ts +27 -0
- package/src/commands/preview-docs/index.ts +190 -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 +156 -0
- package/src/commands/preview-docs/preview-server/server.ts +91 -0
- package/src/commands/push.ts +441 -0
- package/src/commands/split/__tests__/fixtures/samples.json +61 -0
- package/src/commands/split/__tests__/fixtures/spec.json +70 -0
- package/src/commands/split/__tests__/fixtures/webhooks.json +85 -0
- package/src/commands/split/__tests__/index.test.ts +137 -0
- package/src/commands/split/index.ts +385 -0
- package/src/commands/split/types.ts +85 -0
- package/src/commands/stats.ts +119 -0
- package/src/custom.d.ts +1 -0
- package/src/fetch-with-timeout.ts +21 -0
- package/src/index.ts +484 -0
- package/src/js-utils.ts +17 -0
- package/src/types.ts +40 -0
- package/src/update-version-notifier.ts +106 -0
- package/src/utils.ts +590 -0
- package/src/wrapper.ts +42 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Config,
|
|
3
|
+
findConfig,
|
|
4
|
+
formatProblems,
|
|
5
|
+
getMergedConfig,
|
|
6
|
+
getTotals,
|
|
7
|
+
lint,
|
|
8
|
+
lintConfig,
|
|
9
|
+
makeDocumentFromString,
|
|
10
|
+
stringifyYaml,
|
|
11
|
+
} from '@redocly/openapi-core';
|
|
12
|
+
import {
|
|
13
|
+
checkIfRulesetExist,
|
|
14
|
+
exitWithError,
|
|
15
|
+
getExecutionTime,
|
|
16
|
+
getFallbackApisOrExit,
|
|
17
|
+
handleError,
|
|
18
|
+
pluralize,
|
|
19
|
+
printConfigLintTotals,
|
|
20
|
+
printLintTotals,
|
|
21
|
+
printUnusedWarnings,
|
|
22
|
+
} from '../utils';
|
|
23
|
+
import type { OutputFormat, ProblemSeverity, RawConfig, RuleSeverity } from '@redocly/openapi-core';
|
|
24
|
+
import type { CommandOptions, Skips, Totals } from '../types';
|
|
25
|
+
import { blue, gray } from 'colorette';
|
|
26
|
+
import { performance } from 'perf_hooks';
|
|
27
|
+
|
|
28
|
+
export type LintOptions = {
|
|
29
|
+
apis?: string[];
|
|
30
|
+
'max-problems': number;
|
|
31
|
+
extends?: string[];
|
|
32
|
+
config?: string;
|
|
33
|
+
format: OutputFormat;
|
|
34
|
+
'generate-ignore-file'?: boolean;
|
|
35
|
+
'lint-config'?: RuleSeverity;
|
|
36
|
+
} & Omit<Skips, 'skip-decorator'>;
|
|
37
|
+
|
|
38
|
+
export async function handleLint(argv: LintOptions, config: Config, version: string) {
|
|
39
|
+
const apis = await getFallbackApisOrExit(argv.apis, config);
|
|
40
|
+
|
|
41
|
+
if (!apis.length) {
|
|
42
|
+
exitWithError('No APIs were provided');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (argv['generate-ignore-file']) {
|
|
46
|
+
config.styleguide.ignore = {}; // clear ignore
|
|
47
|
+
}
|
|
48
|
+
const totals: Totals = { errors: 0, warnings: 0, ignored: 0 };
|
|
49
|
+
let totalIgnored = 0;
|
|
50
|
+
|
|
51
|
+
// TODO: use shared externalRef resolver, blocked by preprocessors now as they can mutate documents
|
|
52
|
+
for (const { path, alias } of apis) {
|
|
53
|
+
try {
|
|
54
|
+
const startedAt = performance.now();
|
|
55
|
+
const resolvedConfig = getMergedConfig(config, alias);
|
|
56
|
+
const { styleguide } = resolvedConfig;
|
|
57
|
+
|
|
58
|
+
checkIfRulesetExist(styleguide.rules);
|
|
59
|
+
|
|
60
|
+
styleguide.skipRules(argv['skip-rule']);
|
|
61
|
+
styleguide.skipPreprocessors(argv['skip-preprocessor']);
|
|
62
|
+
|
|
63
|
+
if (styleguide.recommendedFallback) {
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
`No configurations were provided -- using built in ${blue(
|
|
66
|
+
'recommended'
|
|
67
|
+
)} configuration by default.\n\n`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
process.stderr.write(gray(`validating ${path.replace(process.cwd(), '')}...\n`));
|
|
71
|
+
const results = await lint({
|
|
72
|
+
ref: path,
|
|
73
|
+
config: resolvedConfig,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const fileTotals = getTotals(results);
|
|
77
|
+
totals.errors += fileTotals.errors;
|
|
78
|
+
totals.warnings += fileTotals.warnings;
|
|
79
|
+
totals.ignored += fileTotals.ignored;
|
|
80
|
+
|
|
81
|
+
if (argv['generate-ignore-file']) {
|
|
82
|
+
for (const m of results) {
|
|
83
|
+
config.styleguide.addIgnore(m);
|
|
84
|
+
totalIgnored++;
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
formatProblems(results, {
|
|
88
|
+
format: argv.format,
|
|
89
|
+
maxProblems: argv['max-problems'],
|
|
90
|
+
totals: fileTotals,
|
|
91
|
+
version,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const elapsed = getExecutionTime(startedAt);
|
|
96
|
+
process.stderr.write(gray(`${path.replace(process.cwd(), '')}: validated in ${elapsed}\n\n`));
|
|
97
|
+
} catch (e) {
|
|
98
|
+
handleError(e, path);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (argv['generate-ignore-file']) {
|
|
103
|
+
config.styleguide.saveIgnore();
|
|
104
|
+
process.stderr.write(
|
|
105
|
+
`Generated ignore file with ${totalIgnored} ${pluralize('problem', totalIgnored)}.\n\n`
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
printLintTotals(totals, apis.length);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
printUnusedWarnings(config.styleguide);
|
|
112
|
+
|
|
113
|
+
if (!(totals.errors === 0 || argv['generate-ignore-file'])) {
|
|
114
|
+
throw new Error('Lint failed.');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function lintConfigCallback(
|
|
119
|
+
argv: CommandOptions & Record<string, undefined>,
|
|
120
|
+
version: string
|
|
121
|
+
) {
|
|
122
|
+
if (argv['lint-config'] === 'off') {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (argv.format === 'json') {
|
|
127
|
+
// we can't print config lint results as it will break json output
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return async (config: RawConfig) => {
|
|
132
|
+
const configPath = findConfig(argv.config) || '';
|
|
133
|
+
const stringYaml = stringifyYaml(config);
|
|
134
|
+
const configContent = makeDocumentFromString(stringYaml, configPath);
|
|
135
|
+
const problems = await lintConfig({
|
|
136
|
+
document: configContent,
|
|
137
|
+
severity: argv['lint-config'] as ProblemSeverity,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const fileTotals = getTotals(problems);
|
|
141
|
+
|
|
142
|
+
formatProblems(problems, {
|
|
143
|
+
format: argv.format,
|
|
144
|
+
maxProblems: argv['max-problems'],
|
|
145
|
+
totals: fileTotals,
|
|
146
|
+
version,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
printConfigLintTotals(fileTotals);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Region, RedoclyClient, Config } from '@redocly/openapi-core';
|
|
2
|
+
import { blue, green, gray } from 'colorette';
|
|
3
|
+
import { promptUser } from '../utils';
|
|
4
|
+
|
|
5
|
+
export function promptClientToken(domain: string) {
|
|
6
|
+
return promptUser(
|
|
7
|
+
green(
|
|
8
|
+
`\n 🔑 Copy your API key from ${blue(`https://app.${domain}/profile`)} and paste it below`
|
|
9
|
+
),
|
|
10
|
+
true
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LoginOptions = {
|
|
15
|
+
verbose?: boolean;
|
|
16
|
+
region?: Region;
|
|
17
|
+
config?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function handleLogin(argv: LoginOptions, config: Config) {
|
|
21
|
+
const region = argv.region || config.region;
|
|
22
|
+
const client = new RedoclyClient(region);
|
|
23
|
+
const clientToken = await promptClientToken(client.domain);
|
|
24
|
+
process.stdout.write(gray('\n Logging in...\n'));
|
|
25
|
+
await client.login(clientToken, argv.verbose);
|
|
26
|
+
process.stdout.write(green(' Authorization confirmed. ✅\n\n'));
|
|
27
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as colorette from 'colorette';
|
|
2
|
+
import * as chockidar from 'chokidar';
|
|
3
|
+
import {
|
|
4
|
+
bundle,
|
|
5
|
+
ResolveError,
|
|
6
|
+
YamlParseError,
|
|
7
|
+
RedoclyClient,
|
|
8
|
+
getTotals,
|
|
9
|
+
getMergedConfig,
|
|
10
|
+
Config,
|
|
11
|
+
} from '@redocly/openapi-core';
|
|
12
|
+
import { getFallbackApisOrExit, loadConfigAndHandleErrors } from '../../utils';
|
|
13
|
+
import startPreviewServer from './preview-server/preview-server';
|
|
14
|
+
import type { Skips } from '../../types';
|
|
15
|
+
|
|
16
|
+
export type PreviewDocsOptions = {
|
|
17
|
+
port: number;
|
|
18
|
+
host: string;
|
|
19
|
+
'use-community-edition'?: boolean;
|
|
20
|
+
config?: string;
|
|
21
|
+
api?: string;
|
|
22
|
+
force?: boolean;
|
|
23
|
+
} & Omit<Skips, 'skip-rule'>;
|
|
24
|
+
|
|
25
|
+
export async function previewDocs(argv: PreviewDocsOptions, configFromFile: Config) {
|
|
26
|
+
let isAuthorizedWithRedocly = false;
|
|
27
|
+
let redocOptions: any = {};
|
|
28
|
+
let config = await reloadConfig(configFromFile);
|
|
29
|
+
|
|
30
|
+
const apis = await getFallbackApisOrExit(argv.api ? [argv.api] : [], config);
|
|
31
|
+
const api = apis[0];
|
|
32
|
+
|
|
33
|
+
let cachedBundle: any;
|
|
34
|
+
const deps = new Set<string>();
|
|
35
|
+
|
|
36
|
+
async function getBundle() {
|
|
37
|
+
return cachedBundle;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function updateBundle() {
|
|
41
|
+
process.stdout.write('\nBundling...\n\n');
|
|
42
|
+
try {
|
|
43
|
+
const {
|
|
44
|
+
bundle: openapiBundle,
|
|
45
|
+
problems,
|
|
46
|
+
fileDependencies,
|
|
47
|
+
} = await bundle({
|
|
48
|
+
ref: api.path,
|
|
49
|
+
config,
|
|
50
|
+
});
|
|
51
|
+
const removed = [...deps].filter((x) => !fileDependencies.has(x));
|
|
52
|
+
watcher.unwatch(removed);
|
|
53
|
+
watcher.add([...fileDependencies]);
|
|
54
|
+
deps.clear();
|
|
55
|
+
fileDependencies.forEach(deps.add, deps);
|
|
56
|
+
|
|
57
|
+
const fileTotals = getTotals(problems);
|
|
58
|
+
|
|
59
|
+
if (fileTotals.errors === 0) {
|
|
60
|
+
process.stdout.write(
|
|
61
|
+
fileTotals.errors === 0
|
|
62
|
+
? `Created a bundle for ${api.alias || api.path} ${
|
|
63
|
+
fileTotals.warnings > 0 ? 'with warnings' : 'successfully'
|
|
64
|
+
}\n`
|
|
65
|
+
: colorette.yellow(
|
|
66
|
+
`Created a bundle for ${
|
|
67
|
+
api.alias || api.path
|
|
68
|
+
} with errors. Docs may be broken or not accurate\n`
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return openapiBundle.parsed;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
handleError(e, api.path);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
setImmediate(() => {
|
|
80
|
+
cachedBundle = updateBundle();
|
|
81
|
+
}); // initial cache
|
|
82
|
+
|
|
83
|
+
const isAuthorized = isAuthorizedWithRedocly || redocOptions.licenseKey;
|
|
84
|
+
if (!isAuthorized) {
|
|
85
|
+
process.stderr.write(
|
|
86
|
+
`Using Redoc community edition.\nLogin with redocly ${colorette.blue(
|
|
87
|
+
'login'
|
|
88
|
+
)} or use an enterprise license key to preview with the premium docs.\n\n`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const hotClients = await startPreviewServer(argv.port, argv.host, {
|
|
93
|
+
getBundle,
|
|
94
|
+
getOptions: () => redocOptions,
|
|
95
|
+
useRedocPro: isAuthorized && !redocOptions.useCommunityEdition,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const watchPaths = [api.path, config.configFile!].filter((e) => !!e);
|
|
99
|
+
const watcher = chockidar.watch(watchPaths, {
|
|
100
|
+
disableGlobbing: true,
|
|
101
|
+
ignoreInitial: true,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const debouncedUpdatedBundle = debounce(async () => {
|
|
105
|
+
cachedBundle = updateBundle();
|
|
106
|
+
await cachedBundle;
|
|
107
|
+
hotClients.broadcast('{"type": "reload", "bundle": true}');
|
|
108
|
+
}, 2000);
|
|
109
|
+
|
|
110
|
+
const changeHandler = async (type: string, file: string) => {
|
|
111
|
+
process.stdout.write(`${colorette.green('watch')} ${type} ${colorette.blue(file)}\n`);
|
|
112
|
+
if (file === config.configFile) {
|
|
113
|
+
config = await reloadConfig();
|
|
114
|
+
hotClients.broadcast(JSON.stringify({ type: 'reload' }));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
debouncedUpdatedBundle();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
watcher.on('change', changeHandler.bind(undefined, 'changed'));
|
|
122
|
+
watcher.on('add', changeHandler.bind(undefined, 'added'));
|
|
123
|
+
watcher.on('unlink', changeHandler.bind(undefined, 'removed'));
|
|
124
|
+
|
|
125
|
+
watcher.on('ready', () => {
|
|
126
|
+
process.stdout.write(
|
|
127
|
+
`\n 👀 Watching ${colorette.blue(api.path)} and all related resources for changes\n\n`
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
async function reloadConfig(config?: Config) {
|
|
132
|
+
if (!config) {
|
|
133
|
+
try {
|
|
134
|
+
config = (await loadConfigAndHandleErrors({ configPath: argv.config })) as Config;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
config = new Config({ apis: {}, styleguide: {} });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const redoclyClient = new RedoclyClient();
|
|
140
|
+
isAuthorizedWithRedocly = await redoclyClient.isAuthorizedWithRedocly();
|
|
141
|
+
const resolvedConfig = getMergedConfig(config, argv.api);
|
|
142
|
+
const { styleguide } = resolvedConfig;
|
|
143
|
+
|
|
144
|
+
styleguide.skipPreprocessors(argv['skip-preprocessor']);
|
|
145
|
+
styleguide.skipDecorators(argv['skip-decorator']);
|
|
146
|
+
|
|
147
|
+
const referenceDocs = resolvedConfig.theme?.openapi;
|
|
148
|
+
redocOptions = {
|
|
149
|
+
...referenceDocs,
|
|
150
|
+
useCommunityEdition: argv['use-community-edition'] || referenceDocs?.useCommunityEdition,
|
|
151
|
+
licenseKey: process.env.REDOCLY_LICENSE_KEY || referenceDocs?.licenseKey,
|
|
152
|
+
whiteLabel: true,
|
|
153
|
+
};
|
|
154
|
+
return resolvedConfig;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function debounce(func: Function, wait: number, immediate?: boolean) {
|
|
159
|
+
let timeout: NodeJS.Timeout | null;
|
|
160
|
+
|
|
161
|
+
return function executedFunction(...args: any[]) {
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
163
|
+
// @ts-ignore
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
165
|
+
const context = this;
|
|
166
|
+
|
|
167
|
+
const later = () => {
|
|
168
|
+
timeout = null;
|
|
169
|
+
if (!immediate) func.apply(context, args);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const callNow = immediate && !timeout;
|
|
173
|
+
|
|
174
|
+
if (timeout) clearTimeout(timeout);
|
|
175
|
+
|
|
176
|
+
timeout = setTimeout(later, wait);
|
|
177
|
+
|
|
178
|
+
if (callNow) func.apply(context, args);
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleError(e: Error, ref: string) {
|
|
183
|
+
if (e instanceof ResolveError) {
|
|
184
|
+
process.stderr.write(`Failed to resolve api definition at ${ref}:\n\n - ${e.message}.\n\n`);
|
|
185
|
+
} else if (e instanceof YamlParseError) {
|
|
186
|
+
process.stderr.write(`Failed to parse api definition at ${ref}:\n\n - ${e.message}.\n\n`);
|
|
187
|
+
} else {
|
|
188
|
+
process.stderr.write(`Something went wrong when processing ${ref}:\n\n - ${e.message}.\n\n`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -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.redoc.ly/reference-docs/latest/oauth2-redirect.js"></script>
|
|
19
|
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,156 @@
|
|
|
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.redoc.ly/reference-docs/latest/redocly-reference-docs.min.js'
|
|
37
|
+
: 'https://cdn.redoc.ly/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.redoc.ly/reference-docs/latest/');"
|
|
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
|
+
let filePath =
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
103
|
+
// @ts-ignore
|
|
104
|
+
{
|
|
105
|
+
'/hot.js': path.join(__dirname, 'hot.js'),
|
|
106
|
+
'/oauth2-redirect.html': path.join(__dirname, 'oauth2-redirect.html'),
|
|
107
|
+
'/simplewebsocket.min.js': require.resolve('simple-websocket/simplewebsocket.min.js'),
|
|
108
|
+
}[request.url || ''];
|
|
109
|
+
|
|
110
|
+
if (!filePath) {
|
|
111
|
+
const basePath = htmlTemplate ? path.dirname(htmlTemplate) : process.cwd();
|
|
112
|
+
|
|
113
|
+
filePath = path.resolve(basePath, `.${request.url}`);
|
|
114
|
+
|
|
115
|
+
if (!isSubdir(basePath, filePath)) {
|
|
116
|
+
respondWithGzip('404 Not Found', request, response, { 'Content-Type': 'text/html' }, 404);
|
|
117
|
+
console.timeEnd(colorette.dim(`GET ${request.url}`));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const extname = String(path.extname(filePath)).toLowerCase() as keyof typeof mimeTypes;
|
|
123
|
+
|
|
124
|
+
const contentType = mimeTypes[extname] || 'application/octet-stream';
|
|
125
|
+
try {
|
|
126
|
+
respondWithGzip(await fsPromises.readFile(filePath), request, response, {
|
|
127
|
+
'Content-Type': contentType,
|
|
128
|
+
});
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (e.code === 'ENOENT') {
|
|
131
|
+
respondWithGzip('404 Not Found', request, response, { 'Content-Type': 'text/html' }, 404);
|
|
132
|
+
} else {
|
|
133
|
+
respondWithGzip(
|
|
134
|
+
`Something went wrong: ${e.code || e.message}...\n`,
|
|
135
|
+
request,
|
|
136
|
+
response,
|
|
137
|
+
{},
|
|
138
|
+
500
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
console.timeEnd(colorette.dim(`GET ${request.url}`));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const wsPort = await portfinder.getPortPromise({ port: 32201 });
|
|
147
|
+
|
|
148
|
+
const server = startHttpServer(port, host, handler);
|
|
149
|
+
server.on('listening', () => {
|
|
150
|
+
process.stdout.write(
|
|
151
|
+
`\n 🔎 Preview server running at ${colorette.blue(`http://${host}:${port}\n`)}`
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return startWsServer(wsPort);
|
|
156
|
+
}
|
|
@@ -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
|
+
}
|