@scalar/cli 0.2.140 → 0.2.142
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/dist/commands/bundle/BundleCommand.js +38 -0
- package/dist/commands/format/FormatCommand.js +44 -0
- package/dist/commands/init/InitCommand.js +145 -0
- package/dist/commands/mock/MockCommand.js +124 -0
- package/dist/commands/serve/ServeCommand.js +89 -0
- package/dist/commands/share/ShareCommand.js +58 -0
- package/dist/commands/validate/ValidateCommand.js +47 -0
- package/dist/commands/void/VoidCommand.js +38 -0
- package/dist/index.js +11 -962
- package/dist/package.json.js +3 -0
- package/dist/src/commands/bundle/BundleCommand.d.ts +3 -0
- package/dist/src/commands/bundle/BundleCommand.d.ts.map +1 -0
- package/dist/src/commands/format/FormatCommand.d.ts +3 -0
- package/dist/src/commands/format/FormatCommand.d.ts.map +1 -0
- package/dist/src/commands/format/format.test.d.ts +2 -0
- package/dist/src/commands/format/format.test.d.ts.map +1 -0
- package/dist/src/commands/index.d.ts +9 -0
- package/dist/src/commands/index.d.ts.map +1 -0
- package/dist/src/commands/init/InitCommand.d.ts +33 -0
- package/dist/src/commands/init/InitCommand.d.ts.map +1 -0
- package/dist/src/commands/init/init.test.d.ts +2 -0
- package/dist/src/commands/init/init.test.d.ts.map +1 -0
- package/dist/src/commands/mock/MockCommand.d.ts +3 -0
- package/dist/src/commands/mock/MockCommand.d.ts.map +1 -0
- package/dist/src/commands/mock/mock.test.d.ts +2 -0
- package/dist/src/commands/mock/mock.test.d.ts.map +1 -0
- package/dist/src/commands/serve/ServeCommand.d.ts +3 -0
- package/dist/src/commands/serve/ServeCommand.d.ts.map +1 -0
- package/dist/src/commands/share/ShareCommand.d.ts +3 -0
- package/dist/src/commands/share/ShareCommand.d.ts.map +1 -0
- package/dist/src/commands/validate/ValidateCommand.d.ts +6 -0
- package/dist/src/commands/validate/ValidateCommand.d.ts.map +1 -0
- package/dist/src/commands/validate/validate.test.d.ts +2 -0
- package/dist/src/commands/validate/validate.test.d.ts.map +1 -0
- package/dist/src/commands/void/VoidCommand.d.ts +3 -0
- package/dist/src/commands/void/VoidCommand.d.ts.map +1 -0
- package/dist/src/commands/void/void.test.d.ts +2 -0
- package/dist/src/commands/void/void.test.d.ts.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/options/version/version.test.d.ts +2 -0
- package/dist/src/options/version/version.test.d.ts.map +1 -0
- package/dist/src/utils/getFileOrUrl.d.ts +5 -0
- package/dist/src/utils/getFileOrUrl.d.ts.map +1 -0
- package/dist/src/utils/getHtmlDocument.d.ts +3 -0
- package/dist/src/utils/getHtmlDocument.d.ts.map +1 -0
- package/dist/src/utils/getMethodColor.d.ts +3 -0
- package/dist/src/utils/getMethodColor.d.ts.map +1 -0
- package/dist/src/utils/getOperationByMethodAndPath.d.ts +3 -0
- package/dist/src/utils/getOperationByMethodAndPath.d.ts.map +1 -0
- package/dist/src/utils/getOperationByMethodAndPath.test.d.ts +2 -0
- package/dist/src/utils/getOperationByMethodAndPath.test.d.ts.map +1 -0
- package/dist/src/utils/index.d.ts +9 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/isUrl.d.ts +5 -0
- package/dist/src/utils/isUrl.d.ts.map +1 -0
- package/dist/src/utils/isYamlFileName.d.ts +5 -0
- package/dist/src/utils/isYamlFileName.d.ts.map +1 -0
- package/dist/src/utils/loadOpenApiFile.d.ts +11 -0
- package/dist/src/utils/loadOpenApiFile.d.ts.map +1 -0
- package/dist/src/utils/printSpecificationBanner.d.ts +6 -0
- package/dist/src/utils/printSpecificationBanner.d.ts.map +1 -0
- package/dist/src/utils/readFile.d.ts +2 -0
- package/dist/src/utils/readFile.d.ts.map +1 -0
- package/dist/src/utils/useGivenFileOrConfiguration.d.ts +3 -0
- package/dist/src/utils/useGivenFileOrConfiguration.d.ts.map +1 -0
- package/dist/src/utils/watchFile.d.ts +7 -0
- package/dist/src/utils/watchFile.d.ts.map +1 -0
- package/dist/tests/invoke-cli.d.ts +12 -0
- package/dist/tests/invoke-cli.d.ts.map +1 -0
- package/dist/tests/matcher.d.ts +10 -0
- package/dist/tests/matcher.d.ts.map +1 -0
- package/dist/utils/getFileOrUrl.js +23 -0
- package/dist/utils/getHtmlDocument.js +37 -0
- package/dist/utils/getMethodColor.js +12 -0
- package/dist/utils/isUrl.js +9 -0
- package/dist/utils/isYamlFileName.js +13 -0
- package/dist/utils/loadOpenApiFile.js +41 -0
- package/dist/utils/printSpecificationBanner.js +19 -0
- package/dist/utils/readFile.js +16 -0
- package/dist/utils/useGivenFileOrConfiguration.js +31 -0
- package/dist/utils/watchFile.js +33 -0
- package/package.json +17 -8
- package/dist/package.json +0 -64
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { useGivenFileOrConfiguration } from '../../utils/useGivenFileOrConfiguration.js';
|
|
5
|
+
import { loadOpenApiFile } from '../../utils/loadOpenApiFile.js';
|
|
6
|
+
|
|
7
|
+
function BundleCommand() {
|
|
8
|
+
const cmd = new Command('bundle');
|
|
9
|
+
cmd.description('Resolve all references in an OpenAPI file');
|
|
10
|
+
cmd.argument('[file]', 'file to bundle');
|
|
11
|
+
cmd.option('-o, --output <file>', 'output file');
|
|
12
|
+
cmd.action(async (fileArgument) => {
|
|
13
|
+
const { output } = cmd.opts();
|
|
14
|
+
const startTime = performance.now();
|
|
15
|
+
const file = useGivenFileOrConfiguration(fileArgument);
|
|
16
|
+
const { specification: newContent } = await loadOpenApiFile(file);
|
|
17
|
+
// Replace file content with newContent
|
|
18
|
+
const cache = [];
|
|
19
|
+
const json = JSON.stringify(newContent, (key, value) => {
|
|
20
|
+
if (typeof value === 'object' && value !== null) {
|
|
21
|
+
if (cache.indexOf(value) !== -1) {
|
|
22
|
+
// Circular reference found, discard key
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Store value in our collection
|
|
26
|
+
cache.push(value);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}, 2);
|
|
30
|
+
fs.writeFileSync(output ?? file, json, 'utf8');
|
|
31
|
+
const endTime = performance.now();
|
|
32
|
+
console.log(kleur.green('OpenAPI Schema bundled'), kleur.grey(`in ${kleur.white(`${kleur.bold(`${Math.round(endTime - startTime)}`)} ms`)}`));
|
|
33
|
+
console.log();
|
|
34
|
+
});
|
|
35
|
+
return cmd;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { BundleCommand };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { openapi } from '@scalar/openapi-parser';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import kleur from 'kleur';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { getFileOrUrl } from '../../utils/getFileOrUrl.js';
|
|
6
|
+
import { isUrl } from '../../utils/isUrl.js';
|
|
7
|
+
import { isYamlFileName } from '../../utils/isYamlFileName.js';
|
|
8
|
+
import { useGivenFileOrConfiguration } from '../../utils/useGivenFileOrConfiguration.js';
|
|
9
|
+
|
|
10
|
+
function FormatCommand() {
|
|
11
|
+
const cmd = new Command('format');
|
|
12
|
+
cmd.description('Format an OpenAPI file');
|
|
13
|
+
cmd.argument('[file|url]', 'File or URL to format');
|
|
14
|
+
cmd.option('-o, --output <file>', 'Output file');
|
|
15
|
+
cmd.action(async (inputArgument, { output }) => {
|
|
16
|
+
const startTime = performance.now();
|
|
17
|
+
const input = useGivenFileOrConfiguration(inputArgument);
|
|
18
|
+
const specification = await getFileOrUrl(input);
|
|
19
|
+
if (!specification) {
|
|
20
|
+
console.error(kleur.bold().red('[ERROR]'), kleur.red('Couldn’t read file.'));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const newContent = isYamlFileName(output || input)
|
|
24
|
+
? await openapi().load(specification).toYaml()
|
|
25
|
+
: await openapi().load(specification).toJson();
|
|
26
|
+
// Replace file content with newContent
|
|
27
|
+
if (output) {
|
|
28
|
+
fs.writeFileSync(output, newContent, 'utf8');
|
|
29
|
+
}
|
|
30
|
+
else if (!isUrl(input)) {
|
|
31
|
+
fs.writeFileSync(input, newContent, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.error(kleur.bold().red('[ERROR]'), kleur.red('Output file is required for URLs. Try passing --output file flag.'));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const endTime = performance.now();
|
|
38
|
+
console.log(kleur.green('File formatted'), kleur.grey(`in ${kleur.white(`${kleur.bold(`${Math.round(endTime - startTime)}`)} ms`)}`));
|
|
39
|
+
console.log();
|
|
40
|
+
});
|
|
41
|
+
return cmd;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { FormatCommand };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { confirm, isCancel, text, cancel } from '@clack/prompts';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import GithubSlugger from 'github-slugger';
|
|
4
|
+
import kleur from 'kleur';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { CONFIG_FILE } from '../../utils/useGivenFileOrConfiguration.js';
|
|
8
|
+
|
|
9
|
+
function InitCommand() {
|
|
10
|
+
const cmd = new Command('init');
|
|
11
|
+
cmd.description('Create a new `scalar.config.json` file to configure where your OpenAPI file is placed.');
|
|
12
|
+
cmd.option('-f, --file [file]', 'your OpenAPI file');
|
|
13
|
+
cmd.option('-s, --subdomain [url]', 'subdomain to publish on');
|
|
14
|
+
cmd.option('--force', 'override existing configuration');
|
|
15
|
+
cmd.action(async ({ file, subdomain, force }) => {
|
|
16
|
+
// Path to `scalar.config.json` file
|
|
17
|
+
const configFile = path.resolve(CONFIG_FILE);
|
|
18
|
+
let validInput;
|
|
19
|
+
let input = file;
|
|
20
|
+
const nextSteps = () => {
|
|
21
|
+
console.log('What to do next:');
|
|
22
|
+
console.log(` ${kleur.cyan('scalar format')} ${kleur.gray('[options] [file|url]')} to format your OpenAPI file`);
|
|
23
|
+
console.log(` ${kleur.cyan('scalar validate')} ${kleur.gray('[file|url]')} to validate your OpenAPI file`);
|
|
24
|
+
console.log(` ${kleur.cyan('scalar bundle')} ${kleur.gray('[options] [file]')} to bundle your OpenAPI file`);
|
|
25
|
+
console.log(` ${kleur.cyan('scalar serve')} ${kleur.gray('[options] [file|url]')} to serve your OpenAPI file`);
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(kleur.white(`Run ${kleur.magenta('scalar --help')} to see all available commands.`));
|
|
28
|
+
};
|
|
29
|
+
// Handle cancel from the user
|
|
30
|
+
const handleCancel = () => {
|
|
31
|
+
cancel('Operation cancelled.');
|
|
32
|
+
nextSteps();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
};
|
|
35
|
+
// Function to validate file extension
|
|
36
|
+
function isValidFile(filePath) {
|
|
37
|
+
const validExtensions = ['.json', '.yaml', '.yml'];
|
|
38
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
39
|
+
return validExtensions.includes(extension);
|
|
40
|
+
}
|
|
41
|
+
// Check if `scalar.config.json` already exists
|
|
42
|
+
if (fs.existsSync(configFile)) {
|
|
43
|
+
console.log(`${kleur.green('⚠')} Found existing configuration: ${kleur.reset().green(`${CONFIG_FILE}`)}`);
|
|
44
|
+
if (force) {
|
|
45
|
+
console.log(`${kleur.green('✔')} Overwriting existing file…`);
|
|
46
|
+
}
|
|
47
|
+
const shouldOverwriteExisting = force ??
|
|
48
|
+
(await confirm({
|
|
49
|
+
message: 'Do you want to override the file?',
|
|
50
|
+
initialValue: false,
|
|
51
|
+
}));
|
|
52
|
+
if (isCancel(shouldOverwriteExisting)) {
|
|
53
|
+
handleCancel();
|
|
54
|
+
}
|
|
55
|
+
if (!shouldOverwriteExisting) {
|
|
56
|
+
handleCancel();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// New configuration object
|
|
60
|
+
const configuration = {
|
|
61
|
+
subdomain: '',
|
|
62
|
+
references: [],
|
|
63
|
+
guides: [],
|
|
64
|
+
};
|
|
65
|
+
// Subdomain
|
|
66
|
+
validInput = !!subdomain;
|
|
67
|
+
while (!validInput) {
|
|
68
|
+
const response = await text({
|
|
69
|
+
message: `What’s the name of your project? We’ll use that to create a custom subdomain for you.`,
|
|
70
|
+
validate(value) {
|
|
71
|
+
if (value.trim().length === 0) {
|
|
72
|
+
return `You didn’t provide a project name. Please provide a name!`;
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
// TODO: Check if the subdomain is available
|
|
78
|
+
if (isCancel(response)) {
|
|
79
|
+
handleCancel();
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
validInput = true;
|
|
83
|
+
}
|
|
84
|
+
const slugger = new GithubSlugger();
|
|
85
|
+
const slug = slugger.slug(response.toString());
|
|
86
|
+
// eslint-disable-next-line no-param-reassign
|
|
87
|
+
subdomain = `${slug}.apidocumentation.com`;
|
|
88
|
+
console.log(`${kleur.green('✔')} Subdomain: ${kleur.green(subdomain)}`);
|
|
89
|
+
}
|
|
90
|
+
configuration.subdomain = subdomain.trim();
|
|
91
|
+
// Reference
|
|
92
|
+
// Check if the file option is provided and valid
|
|
93
|
+
if (input) {
|
|
94
|
+
const validExtensions = ['.json', '.yaml', '.yml'];
|
|
95
|
+
const extension = path.extname(input).toLowerCase();
|
|
96
|
+
if (!validExtensions.includes(extension)) {
|
|
97
|
+
console.log(kleur.red('✖'), `Please enter a valid file path ${validExtensions.join(', ')}.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Ask for the file path
|
|
101
|
+
validInput = input && isValidFile(input);
|
|
102
|
+
while (!validInput) {
|
|
103
|
+
const response = await text({
|
|
104
|
+
message: `Where is your OpenAPI file? ${kleur.reset().grey('(Add a path to the file)')}`,
|
|
105
|
+
validate(value) {
|
|
106
|
+
if (value.length === 0) {
|
|
107
|
+
return `Value is required!`;
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
input = response;
|
|
113
|
+
if (isCancel(response)) {
|
|
114
|
+
handleCancel();
|
|
115
|
+
}
|
|
116
|
+
if (isValidFile(input)) {
|
|
117
|
+
validInput = true;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.log(kleur.red('✖'), `Invalid file extension. Expected: ${['.json', '.yaml', '.yml'].join(', ')}.`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
configuration.references.push({
|
|
124
|
+
name: 'API Reference',
|
|
125
|
+
path: input,
|
|
126
|
+
});
|
|
127
|
+
const content = JSON.stringify(configuration, null, 2);
|
|
128
|
+
// Create `scalar.config.json` file
|
|
129
|
+
fs.writeFileSync(configFile, content);
|
|
130
|
+
console.log(`${kleur.green('✔')} Configuration stored.`);
|
|
131
|
+
console.log();
|
|
132
|
+
console.log(`${kleur.bold().green(`${CONFIG_FILE}`)}`);
|
|
133
|
+
console.log();
|
|
134
|
+
console.log(`${kleur.grey(content
|
|
135
|
+
.split('\n')
|
|
136
|
+
.map((line) => ` ${line}`)
|
|
137
|
+
.join('\n'))}`);
|
|
138
|
+
console.log();
|
|
139
|
+
nextSteps();
|
|
140
|
+
console.log();
|
|
141
|
+
});
|
|
142
|
+
return cmd;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { InitCommand };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { createMockServer } from '@scalar/mock-server';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import kleur from 'kleur';
|
|
5
|
+
import { printSpecificationBanner } from '../../utils/printSpecificationBanner.js';
|
|
6
|
+
import { watchFile } from '../../utils/watchFile.js';
|
|
7
|
+
import { getMethodColor } from '../../utils/getMethodColor.js';
|
|
8
|
+
import { useGivenFileOrConfiguration } from '../../utils/useGivenFileOrConfiguration.js';
|
|
9
|
+
import { loadOpenApiFile } from '../../utils/loadOpenApiFile.js';
|
|
10
|
+
|
|
11
|
+
function MockCommand() {
|
|
12
|
+
const cmd = new Command('mock');
|
|
13
|
+
cmd.description('Mock an API from an OpenAPI file');
|
|
14
|
+
cmd.argument('[file|url]', 'OpenAPI file or URL to mock the server for');
|
|
15
|
+
cmd.option('-w, --watch', 'watch the file for changes');
|
|
16
|
+
cmd.option('-o, --once', 'run the server only once and exit after that');
|
|
17
|
+
cmd.option('-p, --port <port>', 'set the HTTP port for the mock server');
|
|
18
|
+
cmd.action(async (fileArgument, { watch, once, port }) => {
|
|
19
|
+
// Server instance
|
|
20
|
+
let server;
|
|
21
|
+
// Configuration
|
|
22
|
+
const input = useGivenFileOrConfiguration(fileArgument);
|
|
23
|
+
// Load OpenAPI file
|
|
24
|
+
const result = await loadOpenApiFile(input);
|
|
25
|
+
if (!result.valid) {
|
|
26
|
+
console.warn(kleur.bold().red('[ERROR]'), kleur.red('Invalid OpenAPI specification'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
printSpecificationBanner({
|
|
30
|
+
version: result.version,
|
|
31
|
+
schema: result.schema,
|
|
32
|
+
});
|
|
33
|
+
let { specification } = result;
|
|
34
|
+
// Watch OpenAPI file for changes
|
|
35
|
+
if (watch) {
|
|
36
|
+
await watchFile(input, async () => {
|
|
37
|
+
const newResult = await loadOpenApiFile(input);
|
|
38
|
+
const specificationHasChanged = newResult?.specification &&
|
|
39
|
+
JSON.stringify(specification) !==
|
|
40
|
+
JSON.stringify(newResult.specification);
|
|
41
|
+
if (specificationHasChanged) {
|
|
42
|
+
console.log(kleur.bold().white('[INFO]'), kleur.grey('OpenAPI file modified'));
|
|
43
|
+
printSpecificationBanner({
|
|
44
|
+
version: newResult.version,
|
|
45
|
+
schema: newResult.schema,
|
|
46
|
+
});
|
|
47
|
+
specification = newResult.specification;
|
|
48
|
+
// Update mock server
|
|
49
|
+
if (specification) {
|
|
50
|
+
server.close();
|
|
51
|
+
server = await bootServer({
|
|
52
|
+
specification: specification,
|
|
53
|
+
port,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// Show all paths from the specification
|
|
60
|
+
if (!specification) {
|
|
61
|
+
console.error(kleur.bold().yellow('[WARN]'), kleur.grey('Couldn’t find any paths in the OpenAPI file.'));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
printAvailablePaths(specification);
|
|
65
|
+
// Listen for requests
|
|
66
|
+
server = await bootServer({
|
|
67
|
+
specification,
|
|
68
|
+
port,
|
|
69
|
+
});
|
|
70
|
+
// Exit after the first run
|
|
71
|
+
if (once) {
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
server.close();
|
|
74
|
+
}, 2000);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return cmd;
|
|
78
|
+
}
|
|
79
|
+
async function bootServer({ specification, port, }) {
|
|
80
|
+
const app = await createMockServer({
|
|
81
|
+
specification,
|
|
82
|
+
onRequest,
|
|
83
|
+
});
|
|
84
|
+
return serve({
|
|
85
|
+
fetch: app.fetch,
|
|
86
|
+
port: port ?? 3000,
|
|
87
|
+
}, (info) => {
|
|
88
|
+
console.log(`${kleur.bold().green('➜ Mock Server')} ${kleur.white('listening on')} ${kleur.cyan(`http://localhost:${info.port}`)}`);
|
|
89
|
+
console.log();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function printAvailablePaths(specification) {
|
|
93
|
+
console.log(kleur.bold().white('Available Paths'));
|
|
94
|
+
console.log();
|
|
95
|
+
if (specification?.paths === undefined ||
|
|
96
|
+
Object.keys(specification?.paths).length === 0) {
|
|
97
|
+
console.log(kleur.bold().yellow('[WARN]'), kleur.grey('Couldn’t find any paths in the OpenAPI file.'));
|
|
98
|
+
}
|
|
99
|
+
// loop through all paths
|
|
100
|
+
for (const path in specification?.paths ?? []) {
|
|
101
|
+
if (specification?.paths?.[path] === undefined) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// loop through all methods
|
|
105
|
+
for (const method in specification.paths[path]) {
|
|
106
|
+
// @ts-expect-error - we know that the path exists
|
|
107
|
+
if (specification.paths[path][method] === undefined) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
console.log(`${kleur
|
|
111
|
+
.bold()[getMethodColor(method)](method.toUpperCase().padEnd(6))} ${kleur.grey(`${path}`)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
|
116
|
+
function onRequest({ context, operation, }) {
|
|
117
|
+
const { method } = context.req;
|
|
118
|
+
console.log(`${kleur
|
|
119
|
+
.bold()[getMethodColor(method)](method.toUpperCase().padEnd(6))} ${kleur.grey(`${context.req.path}`)}`, `${kleur.grey('→')} ${operation?.operationId
|
|
120
|
+
? kleur.white(operation.operationId)
|
|
121
|
+
: kleur.red('[ERROR] 404 Not Found')}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { MockCommand };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { stream } from 'hono/streaming';
|
|
5
|
+
import kleur from 'kleur';
|
|
6
|
+
import { printSpecificationBanner } from '../../utils/printSpecificationBanner.js';
|
|
7
|
+
import { getHtmlDocument } from '../../utils/getHtmlDocument.js';
|
|
8
|
+
import { useGivenFileOrConfiguration } from '../../utils/useGivenFileOrConfiguration.js';
|
|
9
|
+
import { loadOpenApiFile } from '../../utils/loadOpenApiFile.js';
|
|
10
|
+
import { watchFile } from '../../utils/watchFile.js';
|
|
11
|
+
|
|
12
|
+
function ServeCommand() {
|
|
13
|
+
const cmd = new Command('serve');
|
|
14
|
+
// Old name for the command
|
|
15
|
+
cmd.alias('reference');
|
|
16
|
+
cmd.description('Serve an API Reference from an OpenAPI file');
|
|
17
|
+
cmd.argument('[file|url]', 'OpenAPI file or URL to show the reference for');
|
|
18
|
+
cmd.option('-w, --watch', 'watch the file for changes');
|
|
19
|
+
cmd.option('-o, --once', 'run the server only once and exit after that');
|
|
20
|
+
cmd.option('-p, --port <port>', 'set the HTTP port for the API reference server');
|
|
21
|
+
cmd.action(async (inputArgument, { watch, once, port }) => {
|
|
22
|
+
const input = useGivenFileOrConfiguration(inputArgument);
|
|
23
|
+
const result = await loadOpenApiFile(input);
|
|
24
|
+
if (!result.valid) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let { specification } = result;
|
|
28
|
+
printSpecificationBanner({
|
|
29
|
+
version: result.version,
|
|
30
|
+
schema: result.schema,
|
|
31
|
+
});
|
|
32
|
+
if (specification?.paths === undefined ||
|
|
33
|
+
Object.keys(specification?.paths).length === 0) {
|
|
34
|
+
console.log(kleur.bold().yellow('[WARN]'), kleur.grey('Couldn’t find any paths in the OpenAPI file.'));
|
|
35
|
+
}
|
|
36
|
+
const app = new Hono();
|
|
37
|
+
app.get('/', (c) => {
|
|
38
|
+
return c.html(getHtmlDocument(specification ?? {}, watch));
|
|
39
|
+
});
|
|
40
|
+
app.use('/__watcher', async (c, next) => {
|
|
41
|
+
c.header('Content-Type', 'text/event-stream');
|
|
42
|
+
c.header('Cache-Control', 'no-cache');
|
|
43
|
+
c.header('Connection', 'keep-alive');
|
|
44
|
+
await next();
|
|
45
|
+
});
|
|
46
|
+
app.get('/__watcher', (c) => {
|
|
47
|
+
return stream(c, async (s) => {
|
|
48
|
+
// watch file for changes
|
|
49
|
+
if (watch) {
|
|
50
|
+
watchFile(input, async () => {
|
|
51
|
+
const newResult = await loadOpenApiFile(input);
|
|
52
|
+
const specificationHasChanged = newResult?.specification &&
|
|
53
|
+
JSON.stringify(specification) !==
|
|
54
|
+
JSON.stringify(newResult.specification);
|
|
55
|
+
if (specificationHasChanged) {
|
|
56
|
+
console.log(kleur.bold().white('[INFO]'), kleur.grey('OpenAPI file modified'));
|
|
57
|
+
printSpecificationBanner({
|
|
58
|
+
version: newResult.version,
|
|
59
|
+
schema: newResult.schema,
|
|
60
|
+
});
|
|
61
|
+
specification = newResult.specification;
|
|
62
|
+
s.write('data: file modified\n\n');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// eslint-disable-next-line no-constant-condition
|
|
67
|
+
while (true) {
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
const server = serve({
|
|
73
|
+
fetch: app.fetch,
|
|
74
|
+
port: port ?? 3000,
|
|
75
|
+
}, (info) => {
|
|
76
|
+
console.log(`${kleur.bold().green('➜ API Reference Server')} ${kleur.white('listening on')} ${kleur.cyan(`http://localhost:${info.port}`)}`);
|
|
77
|
+
console.log();
|
|
78
|
+
});
|
|
79
|
+
// Exit after the first run
|
|
80
|
+
if (once) {
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
server.close();
|
|
83
|
+
}, 2000);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return cmd;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { ServeCommand };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import kleur from 'kleur';
|
|
3
|
+
import { getFileOrUrl } from '../../utils/getFileOrUrl.js';
|
|
4
|
+
import { useGivenFileOrConfiguration } from '../../utils/useGivenFileOrConfiguration.js';
|
|
5
|
+
|
|
6
|
+
function ShareCommand() {
|
|
7
|
+
const cmd = new Command('share');
|
|
8
|
+
cmd.description('Share an OpenAPI file');
|
|
9
|
+
cmd.argument('[file]', 'file to share');
|
|
10
|
+
cmd.option('-t, --token <token>', 'pass a token to update an existing sandbox');
|
|
11
|
+
cmd.action(async (fileArgument, { token }) => {
|
|
12
|
+
const file = useGivenFileOrConfiguration(fileArgument);
|
|
13
|
+
const url = 'https://sandbox.scalar.com/api/share' + (token ? `?token=${token}` : '');
|
|
14
|
+
fetch(url, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
content: await getFileOrUrl(file),
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
23
|
+
.then((response) => response.json())
|
|
24
|
+
.then((data) => {
|
|
25
|
+
const { id, token: newToken } = data;
|
|
26
|
+
console.log(kleur.bold().green('Your OpenAPI file is public.'));
|
|
27
|
+
console.log();
|
|
28
|
+
console.log(`${kleur.green('➜')} ${kleur
|
|
29
|
+
.bold()
|
|
30
|
+
.white('API Reference:'.padEnd(14))} ${kleur.cyan(`https://sandbox.scalar.com/p/${id}`)}`);
|
|
31
|
+
console.log(`${kleur.grey('➜')} ${kleur
|
|
32
|
+
.bold()
|
|
33
|
+
.grey('Editor:'.padEnd(14))} ${kleur.cyan(`https://sandbox.scalar.com/e/${id}`)}`);
|
|
34
|
+
console.log();
|
|
35
|
+
console.log(`${kleur.grey('➜')} ${kleur
|
|
36
|
+
.bold()
|
|
37
|
+
.grey('OpenAPI JSON:'.padEnd(14))} ${kleur.cyan(`https://sandbox.scalar.com/files/${id}/openapi.json`)}`);
|
|
38
|
+
console.log(`${kleur.grey('➜')} ${kleur
|
|
39
|
+
.bold()
|
|
40
|
+
.grey('OpenAPI YAML:'.padEnd(14))} ${kleur.cyan(`https://sandbox.scalar.com/files/${id}/openapi.yaml`)}`);
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(kleur.white('Use the token to update the existing sandbox:'));
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(`${kleur.grey('$')} ${kleur.bold().white(`scalar share --token=`)}${kleur.bold().cyan(`${newToken}`)} `);
|
|
45
|
+
console.log();
|
|
46
|
+
})
|
|
47
|
+
.catch((error) => {
|
|
48
|
+
console.error('Failed to share the file.');
|
|
49
|
+
console.log();
|
|
50
|
+
console.error('Error:', error);
|
|
51
|
+
console.log();
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return cmd;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { ShareCommand };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { openapi } from '@scalar/openapi-parser';
|
|
2
|
+
import { fetchUrls } from '@scalar/openapi-parser/plugins/fetch-urls';
|
|
3
|
+
import { readFiles } from '@scalar/openapi-parser/plugins/read-files';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import kleur from 'kleur';
|
|
6
|
+
import prettyjson from 'prettyjson';
|
|
7
|
+
import { useGivenFileOrConfiguration } from '../../utils/useGivenFileOrConfiguration.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate an OpenAPI file against the OpenAPI specifications
|
|
11
|
+
*/
|
|
12
|
+
function ValidateCommand() {
|
|
13
|
+
const cmd = new Command('validate');
|
|
14
|
+
cmd.description('Validate an OpenAPI file');
|
|
15
|
+
cmd.argument('[file|url]', 'File or URL to validate');
|
|
16
|
+
cmd.action(async (inputArgument) => {
|
|
17
|
+
const startTime = performance.now();
|
|
18
|
+
// Read file
|
|
19
|
+
const input = useGivenFileOrConfiguration(inputArgument);
|
|
20
|
+
// Validate
|
|
21
|
+
const result = await openapi()
|
|
22
|
+
.load(input, {
|
|
23
|
+
plugins: [fetchUrls(), readFiles()],
|
|
24
|
+
})
|
|
25
|
+
.validate()
|
|
26
|
+
.get();
|
|
27
|
+
if (result.valid && result.version) {
|
|
28
|
+
console.log(kleur.green(`Matches the OpenAPI specification${kleur.white(` (OpenAPI ${kleur.bold(result.version)})`)}`));
|
|
29
|
+
const endTime = performance.now();
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(kleur.green('File validated'), kleur.grey(`in ${kleur.white(`${kleur.bold(`${Math.round(endTime - startTime)}`)} ms`)}`));
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.log(prettyjson.render(result.errors));
|
|
36
|
+
console.log();
|
|
37
|
+
console.error(kleur.red('File doesn’t match the OpenAPI specification.'));
|
|
38
|
+
console.log();
|
|
39
|
+
console.error(kleur.red(`${kleur.bold(`${result.errors?.length} error${result.errors && result.errors.length > 1 ? 's' : ''}`)} found.`));
|
|
40
|
+
console.log();
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return cmd;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { ValidateCommand };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { createVoidServer } from '@scalar/void-server';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import kleur from 'kleur';
|
|
5
|
+
|
|
6
|
+
function VoidCommand() {
|
|
7
|
+
const cmd = new Command('void');
|
|
8
|
+
cmd.description('Boot a server to mirror HTTP requests');
|
|
9
|
+
cmd.option('-o, --once', 'run the server only once and exit after that');
|
|
10
|
+
cmd.option('-p, --port <port>', 'set the HTTP port for the mock server');
|
|
11
|
+
cmd.action(async ({ once, port, }) => {
|
|
12
|
+
// Server instance
|
|
13
|
+
let server = undefined;
|
|
14
|
+
// Listen for requests
|
|
15
|
+
server = await bootServer({
|
|
16
|
+
port,
|
|
17
|
+
});
|
|
18
|
+
// Exit after the first run
|
|
19
|
+
if (once) {
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
server.close();
|
|
22
|
+
}, 2000);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return cmd;
|
|
26
|
+
}
|
|
27
|
+
async function bootServer({ port }) {
|
|
28
|
+
const app = await createVoidServer();
|
|
29
|
+
return serve({
|
|
30
|
+
fetch: app.fetch,
|
|
31
|
+
port: port ?? 3000,
|
|
32
|
+
}, (info) => {
|
|
33
|
+
console.log(`${kleur.bold().green('➜ Void Server')} ${kleur.white('listening on')} ${kleur.cyan(`http://localhost:${info.port}`)}`);
|
|
34
|
+
console.log();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { VoidCommand };
|