@redocly/cli 1.25.15 → 1.26.0
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 +14 -0
- package/lib/__tests__/commands/join.test.js +24 -1
- package/lib/__tests__/utils.test.js +62 -1
- package/lib/commands/eject.js +15 -5
- package/lib/commands/join.js +6 -5
- package/lib/commands/preview-project/index.js +8 -2
- package/lib/commands/translations.js +12 -2
- package/lib/utils/miscellaneous.js +1 -1
- package/lib/utils/platform.d.ts +16 -0
- package/lib/utils/platform.js +34 -0
- package/package.json +2 -2
- package/src/__tests__/commands/join.test.ts +35 -2
- package/src/__tests__/utils.test.ts +72 -1
- package/src/commands/eject.ts +18 -5
- package/src/commands/join.ts +8 -7
- package/src/commands/preview-project/index.ts +9 -3
- package/src/commands/translations.ts +17 -4
- package/src/utils/miscellaneous.ts +1 -1
- package/src/utils/platform.ts +31 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @redocly/cli
|
|
2
2
|
|
|
3
|
+
## 1.26.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Introduced the `struct` rule and deprecated the `spec` rule.
|
|
8
|
+
Added the `spec` ruleset, which enforces compliance with the specifications.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Fixed an issue where the CLI would fail to run on Windows due to a breaking change in the Node.js API.
|
|
13
|
+
- Fixed an issue where `join` would throw an error when a glob pattern was provided.
|
|
14
|
+
- Updated `sourceDescriptions` to enforce a valid type field, ensuring compliance with the Arazzo specification.
|
|
15
|
+
- Updated @redocly/openapi-core to v1.26.0.
|
|
16
|
+
|
|
3
17
|
## 1.25.15
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -13,7 +13,29 @@ describe('handleJoin', () => {
|
|
|
13
13
|
colloreteYellowMock.mockImplementation((string) => string);
|
|
14
14
|
it('should call exitWithError because only one entrypoint', async () => {
|
|
15
15
|
await (0, join_1.handleJoin)({ argv: { apis: ['first.yaml'] }, config: {}, version: 'cli-version' });
|
|
16
|
-
expect(miscellaneous_1.exitWithError).toHaveBeenCalledWith(`At least 2
|
|
16
|
+
expect(miscellaneous_1.exitWithError).toHaveBeenCalledWith(`At least 2 APIs should be provided.`);
|
|
17
|
+
});
|
|
18
|
+
it('should call exitWithError if glob expands to less than 2 APIs', async () => {
|
|
19
|
+
miscellaneous_1.getFallbackApisOrExit.mockResolvedValueOnce([{ path: 'first.yaml' }]);
|
|
20
|
+
await (0, join_1.handleJoin)({
|
|
21
|
+
argv: { apis: ['*.yaml'] },
|
|
22
|
+
config: {},
|
|
23
|
+
version: 'cli-version',
|
|
24
|
+
});
|
|
25
|
+
expect(miscellaneous_1.exitWithError).toHaveBeenCalledWith(`At least 2 APIs should be provided.`);
|
|
26
|
+
});
|
|
27
|
+
it('should proceed if glob expands to 2 or more APIs', async () => {
|
|
28
|
+
openapi_core_1.detectSpec.mockReturnValue('oas3_1');
|
|
29
|
+
miscellaneous_1.getFallbackApisOrExit.mockResolvedValueOnce([
|
|
30
|
+
{ path: 'first.yaml' },
|
|
31
|
+
{ path: 'second.yaml' },
|
|
32
|
+
]);
|
|
33
|
+
await (0, join_1.handleJoin)({
|
|
34
|
+
argv: { apis: ['*.yaml'] },
|
|
35
|
+
config: config_1.ConfigFixture,
|
|
36
|
+
version: 'cli-version',
|
|
37
|
+
});
|
|
38
|
+
expect(miscellaneous_1.exitWithError).not.toHaveBeenCalled();
|
|
17
39
|
});
|
|
18
40
|
it('should call exitWithError because passed all 3 options for tags', async () => {
|
|
19
41
|
await (0, join_1.handleJoin)({
|
|
@@ -41,6 +63,7 @@ describe('handleJoin', () => {
|
|
|
41
63
|
expect(miscellaneous_1.exitWithError).toHaveBeenCalledWith(`You use prefix-tags-with-filename, without-x-tag-groups together.\nPlease choose only one!`);
|
|
42
64
|
});
|
|
43
65
|
it('should call exitWithError because Only OpenAPI 3.0 and OpenAPI 3.1 are supported', async () => {
|
|
66
|
+
openapi_core_1.detectSpec.mockReturnValueOnce('oas2_0');
|
|
44
67
|
await (0, join_1.handleJoin)({
|
|
45
68
|
argv: {
|
|
46
69
|
apis: ['first.yaml', 'second.yaml'],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const miscellaneous_1 = require("../utils/miscellaneous");
|
|
4
|
+
const platform_1 = require("../utils/platform");
|
|
4
5
|
const openapi_core_1 = require("@redocly/openapi-core");
|
|
5
6
|
const colorette_1 = require("colorette");
|
|
6
7
|
const fs_1 = require("fs");
|
|
@@ -407,7 +408,7 @@ describe('checkIfRulesetExist', () => {
|
|
|
407
408
|
oas3_1: {},
|
|
408
409
|
async2: {},
|
|
409
410
|
async3: {},
|
|
410
|
-
|
|
411
|
+
arazzo1: {},
|
|
411
412
|
};
|
|
412
413
|
expect(() => (0, miscellaneous_1.checkIfRulesetExist)(rules)).toThrowError('⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/');
|
|
413
414
|
});
|
|
@@ -530,3 +531,63 @@ describe('writeToFileByExtension', () => {
|
|
|
530
531
|
expect(process.stderr.write).toHaveBeenCalledWith(`test data`);
|
|
531
532
|
});
|
|
532
533
|
});
|
|
534
|
+
describe('runtime platform', () => {
|
|
535
|
+
describe('sanitizePath', () => {
|
|
536
|
+
test.each([
|
|
537
|
+
['C:\\Program Files\\App', 'C:\\Program Files\\App'],
|
|
538
|
+
['/usr/local/bin/app', '/usr/local/bin/app'],
|
|
539
|
+
['invalid|path?name*', 'invalidpathname'],
|
|
540
|
+
['', ''],
|
|
541
|
+
['<>:"|?*', ':'],
|
|
542
|
+
['C:/Program Files\\App', 'C:/Program Files\\App'],
|
|
543
|
+
['path\nname\r', 'pathname'],
|
|
544
|
+
['/usr/local; rm -rf /', '/usr/local rm -rf /'],
|
|
545
|
+
['C:\\data&& dir', 'C:\\data dir'],
|
|
546
|
+
])('should sanitize path %s to %s', (input, expected) => {
|
|
547
|
+
expect((0, platform_1.sanitizePath)(input)).toBe(expected);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
describe('sanitizeLocale', () => {
|
|
551
|
+
test.each([
|
|
552
|
+
['en-US', 'en-US'],
|
|
553
|
+
['fr_FR', 'fr_FR'],
|
|
554
|
+
['en<>US', 'enUS'],
|
|
555
|
+
['fr@FR', 'fr@FR'],
|
|
556
|
+
['en_US@#$%', 'en_US@'],
|
|
557
|
+
[' en-US ', 'en-US'],
|
|
558
|
+
['', ''],
|
|
559
|
+
])('should sanitize locale %s to %s', (input, expected) => {
|
|
560
|
+
expect((0, platform_1.sanitizeLocale)(input)).toBe(expected);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
describe('getPlatformSpawnArgs', () => {
|
|
564
|
+
const originalPlatform = process.platform;
|
|
565
|
+
afterEach(() => {
|
|
566
|
+
Object.defineProperty(process, 'platform', {
|
|
567
|
+
value: originalPlatform,
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
it('should return args for Windows platform', () => {
|
|
571
|
+
Object.defineProperty(process, 'platform', {
|
|
572
|
+
value: 'win32',
|
|
573
|
+
});
|
|
574
|
+
const result = (0, platform_1.getPlatformSpawnArgs)();
|
|
575
|
+
expect(result).toEqual({
|
|
576
|
+
npxExecutableName: 'npx.cmd',
|
|
577
|
+
sanitize: expect.any(Function),
|
|
578
|
+
shell: true,
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
it('should return args for non-Windows platform', () => {
|
|
582
|
+
Object.defineProperty(process, 'platform', {
|
|
583
|
+
value: 'linux',
|
|
584
|
+
});
|
|
585
|
+
const result = (0, platform_1.getPlatformSpawnArgs)();
|
|
586
|
+
expect(result).toEqual({
|
|
587
|
+
npxExecutableName: 'npx',
|
|
588
|
+
sanitize: expect.any(Function),
|
|
589
|
+
shell: false,
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
});
|
package/lib/commands/eject.js
CHANGED
|
@@ -2,17 +2,27 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleEject = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
|
+
const platform_1 = require("../utils/platform");
|
|
5
6
|
const handleEject = async ({ argv }) => {
|
|
6
7
|
process.stdout.write(`\nLaunching eject using NPX.\n\n`);
|
|
7
|
-
const npxExecutableName
|
|
8
|
-
(
|
|
8
|
+
const { npxExecutableName, sanitize, shell } = (0, platform_1.getPlatformSpawnArgs)();
|
|
9
|
+
const path = sanitize(argv.path, platform_1.sanitizePath);
|
|
10
|
+
const projectDir = sanitize(argv['project-dir'], platform_1.sanitizePath);
|
|
11
|
+
const child = (0, child_process_1.spawn)(npxExecutableName, [
|
|
9
12
|
'-y',
|
|
10
13
|
'@redocly/realm',
|
|
11
14
|
'eject',
|
|
12
15
|
`${argv.type}`,
|
|
13
|
-
|
|
14
|
-
`-d=${
|
|
16
|
+
path,
|
|
17
|
+
`-d=${projectDir}`,
|
|
15
18
|
argv.force ? `--force=${argv.force}` : '',
|
|
16
|
-
], {
|
|
19
|
+
], {
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
shell,
|
|
22
|
+
});
|
|
23
|
+
child.on('error', (error) => {
|
|
24
|
+
process.stderr.write(`Eject launch failed: ${error.message}`);
|
|
25
|
+
throw new Error('Eject launch failed.');
|
|
26
|
+
});
|
|
17
27
|
};
|
|
18
28
|
exports.handleEject = handleEject;
|
package/lib/commands/join.js
CHANGED
|
@@ -15,11 +15,7 @@ const xTagGroups = 'x-tagGroups';
|
|
|
15
15
|
let potentialConflictsTotal = 0;
|
|
16
16
|
async function handleJoin({ argv, config, version: packageVersion, }) {
|
|
17
17
|
const startedAt = perf_hooks_1.performance.now();
|
|
18
|
-
|
|
19
|
-
return (0, miscellaneous_1.exitWithError)(`At least 2 apis should be provided.`);
|
|
20
|
-
}
|
|
21
|
-
const fileExtension = (0, miscellaneous_1.getAndValidateFileExtension)(argv.output || argv.apis[0]);
|
|
22
|
-
const { 'prefix-components-with-info-prop': prefixComponentsWithInfoProp, 'prefix-tags-with-filename': prefixTagsWithFilename, 'prefix-tags-with-info-prop': prefixTagsWithInfoProp, 'without-x-tag-groups': withoutXTagGroups, output: specFilename = `openapi.${fileExtension}`, } = argv;
|
|
18
|
+
const { 'prefix-components-with-info-prop': prefixComponentsWithInfoProp, 'prefix-tags-with-filename': prefixTagsWithFilename, 'prefix-tags-with-info-prop': prefixTagsWithInfoProp, 'without-x-tag-groups': withoutXTagGroups, output, } = argv;
|
|
23
19
|
const usedTagsOptions = [
|
|
24
20
|
prefixTagsWithFilename && 'prefix-tags-with-filename',
|
|
25
21
|
prefixTagsWithInfoProp && 'prefix-tags-with-info-prop',
|
|
@@ -29,6 +25,11 @@ async function handleJoin({ argv, config, version: packageVersion, }) {
|
|
|
29
25
|
return (0, miscellaneous_1.exitWithError)(`You use ${(0, colorette_1.yellow)(usedTagsOptions.join(', '))} together.\nPlease choose only one!`);
|
|
30
26
|
}
|
|
31
27
|
const apis = await (0, miscellaneous_1.getFallbackApisOrExit)(argv.apis, config);
|
|
28
|
+
if (apis.length < 2) {
|
|
29
|
+
return (0, miscellaneous_1.exitWithError)(`At least 2 APIs should be provided.`);
|
|
30
|
+
}
|
|
31
|
+
const fileExtension = (0, miscellaneous_1.getAndValidateFileExtension)(output || apis[0].path);
|
|
32
|
+
const specFilename = output || `openapi.${fileExtension}`;
|
|
32
33
|
const externalRefResolver = new openapi_core_1.BaseResolver(config.resolve);
|
|
33
34
|
const documents = await Promise.all(apis.map(({ path }) => externalRefResolver.resolveDocument(null, path, true)));
|
|
34
35
|
const decorators = new Set([
|
|
@@ -5,6 +5,7 @@ const path = require("path");
|
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const child_process_1 = require("child_process");
|
|
7
7
|
const constants_1 = require("./constants");
|
|
8
|
+
const platform_1 = require("../../utils/platform");
|
|
8
9
|
const previewProject = async ({ argv }) => {
|
|
9
10
|
const { plan, port } = argv;
|
|
10
11
|
const projectDir = argv['project-dir'];
|
|
@@ -16,10 +17,15 @@ const previewProject = async ({ argv }) => {
|
|
|
16
17
|
const productName = constants_1.PRODUCT_NAMES[product];
|
|
17
18
|
const packageName = constants_1.PRODUCT_PACKAGES[product];
|
|
18
19
|
process.stdout.write(`\nLaunching preview of ${productName} ${plan} using NPX.\n\n`);
|
|
19
|
-
const npxExecutableName
|
|
20
|
-
(0, child_process_1.spawn)(npxExecutableName, ['-y', packageName, 'preview', `--plan=${plan}`, `--port=${port || 4000}`], {
|
|
20
|
+
const { npxExecutableName, shell } = (0, platform_1.getPlatformSpawnArgs)();
|
|
21
|
+
const child = (0, child_process_1.spawn)(npxExecutableName, ['-y', packageName, 'preview', `--plan=${plan}`, `--port=${port || 4000}`], {
|
|
21
22
|
stdio: 'inherit',
|
|
22
23
|
cwd: projectDir,
|
|
24
|
+
shell,
|
|
25
|
+
});
|
|
26
|
+
child.on('error', (error) => {
|
|
27
|
+
process.stderr.write(`Project preview launch failed: ${error.message}`);
|
|
28
|
+
throw new Error(`Project preview launch failed.`);
|
|
23
29
|
});
|
|
24
30
|
};
|
|
25
31
|
exports.previewProject = previewProject;
|
|
@@ -2,9 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleTranslations = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
|
+
const platform_1 = require("../utils/platform");
|
|
5
6
|
const handleTranslations = async ({ argv }) => {
|
|
6
7
|
process.stdout.write(`\nLaunching translate using NPX.\n\n`);
|
|
7
|
-
const npxExecutableName
|
|
8
|
-
|
|
8
|
+
const { npxExecutableName, sanitize, shell } = (0, platform_1.getPlatformSpawnArgs)();
|
|
9
|
+
const projectDir = sanitize(argv['project-dir'], platform_1.sanitizePath);
|
|
10
|
+
const locale = sanitize(argv.locale, platform_1.sanitizeLocale);
|
|
11
|
+
const child = (0, child_process_1.spawn)(npxExecutableName, ['-y', '@redocly/realm', 'translate', locale, `-d=${projectDir}`], {
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
shell,
|
|
14
|
+
});
|
|
15
|
+
child.on('error', (error) => {
|
|
16
|
+
process.stderr.write(`Translate launch failed: ${error.message}`);
|
|
17
|
+
throw new Error(`Translate launch failed.`);
|
|
18
|
+
});
|
|
9
19
|
};
|
|
10
20
|
exports.handleTranslations = handleTranslations;
|
|
@@ -427,7 +427,7 @@ function checkIfRulesetExist(rules) {
|
|
|
427
427
|
...rules.oas3_1,
|
|
428
428
|
...rules.async2,
|
|
429
429
|
...rules.async3,
|
|
430
|
-
...rules.
|
|
430
|
+
...rules.arazzo1,
|
|
431
431
|
};
|
|
432
432
|
if ((0, utils_1.isEmptyObject)(ruleset)) {
|
|
433
433
|
exitWithError('⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizes the input path by removing invalid characters.
|
|
3
|
+
*/
|
|
4
|
+
export declare const sanitizePath: (input: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* Sanitizes the input locale (ex. en-US) by removing invalid characters.
|
|
7
|
+
*/
|
|
8
|
+
export declare const sanitizeLocale: (input: string) => string;
|
|
9
|
+
/**
|
|
10
|
+
* Retrieves platform-specific arguments and utilities.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getPlatformSpawnArgs(): {
|
|
13
|
+
npxExecutableName: string;
|
|
14
|
+
sanitize: (input: string | undefined, sanitizer: (input: string) => string) => string;
|
|
15
|
+
shell: boolean;
|
|
16
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sanitizeLocale = exports.sanitizePath = void 0;
|
|
4
|
+
exports.getPlatformSpawnArgs = getPlatformSpawnArgs;
|
|
5
|
+
/**
|
|
6
|
+
* Sanitizes the input path by removing invalid characters.
|
|
7
|
+
*/
|
|
8
|
+
const sanitizePath = (input) => {
|
|
9
|
+
return input.replace(/[^a-zA-Z0-9 ._\-:\\/@]/g, '');
|
|
10
|
+
};
|
|
11
|
+
exports.sanitizePath = sanitizePath;
|
|
12
|
+
/**
|
|
13
|
+
* Sanitizes the input locale (ex. en-US) by removing invalid characters.
|
|
14
|
+
*/
|
|
15
|
+
const sanitizeLocale = (input) => {
|
|
16
|
+
return input.replace(/[^a-zA-Z0-9@._-]/g, '');
|
|
17
|
+
};
|
|
18
|
+
exports.sanitizeLocale = sanitizeLocale;
|
|
19
|
+
/**
|
|
20
|
+
* Retrieves platform-specific arguments and utilities.
|
|
21
|
+
*/
|
|
22
|
+
function getPlatformSpawnArgs() {
|
|
23
|
+
const isWindowsPlatform = process.platform === 'win32';
|
|
24
|
+
const npxExecutableName = isWindowsPlatform ? 'npx.cmd' : 'npx';
|
|
25
|
+
const sanitizeIfWindows = (input, sanitizer) => {
|
|
26
|
+
if (isWindowsPlatform && input) {
|
|
27
|
+
return sanitizer(input);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return input || '';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return { npxExecutableName, sanitize: sanitizeIfWindows, shell: isWindowsPlatform };
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redocly/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"Roman Hotsiy <roman@redocly.com> (https://redocly.com/)"
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@redocly/openapi-core": "1.
|
|
39
|
+
"@redocly/openapi-core": "1.26.0",
|
|
40
40
|
"abort-controller": "^3.0.0",
|
|
41
41
|
"chokidar": "^3.5.1",
|
|
42
42
|
"colorette": "^1.2.0",
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { yellow } from 'colorette';
|
|
2
2
|
import { detectSpec } from '@redocly/openapi-core';
|
|
3
3
|
import { handleJoin } from '../../commands/join';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
exitWithError,
|
|
6
|
+
getFallbackApisOrExit,
|
|
7
|
+
writeToFileByExtension,
|
|
8
|
+
} from '../../utils/miscellaneous';
|
|
5
9
|
import { loadConfig } from '../../__mocks__/@redocly/openapi-core';
|
|
6
10
|
import { ConfigFixture } from '../fixtures/config';
|
|
7
11
|
|
|
@@ -15,7 +19,35 @@ describe('handleJoin', () => {
|
|
|
15
19
|
|
|
16
20
|
it('should call exitWithError because only one entrypoint', async () => {
|
|
17
21
|
await handleJoin({ argv: { apis: ['first.yaml'] }, config: {} as any, version: 'cli-version' });
|
|
18
|
-
expect(exitWithError).toHaveBeenCalledWith(`At least 2
|
|
22
|
+
expect(exitWithError).toHaveBeenCalledWith(`At least 2 APIs should be provided.`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should call exitWithError if glob expands to less than 2 APIs', async () => {
|
|
26
|
+
(getFallbackApisOrExit as jest.Mock).mockResolvedValueOnce([{ path: 'first.yaml' }]);
|
|
27
|
+
|
|
28
|
+
await handleJoin({
|
|
29
|
+
argv: { apis: ['*.yaml'] },
|
|
30
|
+
config: {} as any,
|
|
31
|
+
version: 'cli-version',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
expect(exitWithError).toHaveBeenCalledWith(`At least 2 APIs should be provided.`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should proceed if glob expands to 2 or more APIs', async () => {
|
|
38
|
+
(detectSpec as jest.Mock).mockReturnValue('oas3_1');
|
|
39
|
+
(getFallbackApisOrExit as jest.Mock).mockResolvedValueOnce([
|
|
40
|
+
{ path: 'first.yaml' },
|
|
41
|
+
{ path: 'second.yaml' },
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
await handleJoin({
|
|
45
|
+
argv: { apis: ['*.yaml'] },
|
|
46
|
+
config: ConfigFixture as any,
|
|
47
|
+
version: 'cli-version',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(exitWithError).not.toHaveBeenCalled();
|
|
19
51
|
});
|
|
20
52
|
|
|
21
53
|
it('should call exitWithError because passed all 3 options for tags', async () => {
|
|
@@ -52,6 +84,7 @@ describe('handleJoin', () => {
|
|
|
52
84
|
});
|
|
53
85
|
|
|
54
86
|
it('should call exitWithError because Only OpenAPI 3.0 and OpenAPI 3.1 are supported', async () => {
|
|
87
|
+
(detectSpec as jest.Mock).mockReturnValueOnce('oas2_0');
|
|
55
88
|
await handleJoin({
|
|
56
89
|
argv: {
|
|
57
90
|
apis: ['first.yaml', 'second.yaml'],
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getAndValidateFileExtension,
|
|
16
16
|
writeToFileByExtension,
|
|
17
17
|
} from '../utils/miscellaneous';
|
|
18
|
+
import { sanitizeLocale, sanitizePath, getPlatformSpawnArgs } from '../utils/platform';
|
|
18
19
|
import {
|
|
19
20
|
ResolvedApi,
|
|
20
21
|
Totals,
|
|
@@ -500,7 +501,7 @@ describe('checkIfRulesetExist', () => {
|
|
|
500
501
|
oas3_1: {},
|
|
501
502
|
async2: {},
|
|
502
503
|
async3: {},
|
|
503
|
-
|
|
504
|
+
arazzo1: {},
|
|
504
505
|
};
|
|
505
506
|
expect(() => checkIfRulesetExist(rules)).toThrowError(
|
|
506
507
|
'⚠️ No rules were configured. Learn how to configure rules: https://redocly.com/docs/cli/rules/'
|
|
@@ -641,3 +642,73 @@ describe('writeToFileByExtension', () => {
|
|
|
641
642
|
expect(process.stderr.write).toHaveBeenCalledWith(`test data`);
|
|
642
643
|
});
|
|
643
644
|
});
|
|
645
|
+
|
|
646
|
+
describe('runtime platform', () => {
|
|
647
|
+
describe('sanitizePath', () => {
|
|
648
|
+
test.each([
|
|
649
|
+
['C:\\Program Files\\App', 'C:\\Program Files\\App'],
|
|
650
|
+
['/usr/local/bin/app', '/usr/local/bin/app'],
|
|
651
|
+
['invalid|path?name*', 'invalidpathname'],
|
|
652
|
+
['', ''],
|
|
653
|
+
['<>:"|?*', ':'],
|
|
654
|
+
['C:/Program Files\\App', 'C:/Program Files\\App'],
|
|
655
|
+
['path\nname\r', 'pathname'],
|
|
656
|
+
['/usr/local; rm -rf /', '/usr/local rm -rf /'],
|
|
657
|
+
['C:\\data&& dir', 'C:\\data dir'],
|
|
658
|
+
])('should sanitize path %s to %s', (input, expected) => {
|
|
659
|
+
expect(sanitizePath(input)).toBe(expected);
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
describe('sanitizeLocale', () => {
|
|
664
|
+
test.each([
|
|
665
|
+
['en-US', 'en-US'],
|
|
666
|
+
['fr_FR', 'fr_FR'],
|
|
667
|
+
['en<>US', 'enUS'],
|
|
668
|
+
['fr@FR', 'fr@FR'],
|
|
669
|
+
['en_US@#$%', 'en_US@'],
|
|
670
|
+
[' en-US ', 'en-US'],
|
|
671
|
+
['', ''],
|
|
672
|
+
])('should sanitize locale %s to %s', (input, expected) => {
|
|
673
|
+
expect(sanitizeLocale(input)).toBe(expected);
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
describe('getPlatformSpawnArgs', () => {
|
|
678
|
+
const originalPlatform = process.platform;
|
|
679
|
+
|
|
680
|
+
afterEach(() => {
|
|
681
|
+
Object.defineProperty(process, 'platform', {
|
|
682
|
+
value: originalPlatform,
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('should return args for Windows platform', () => {
|
|
687
|
+
Object.defineProperty(process, 'platform', {
|
|
688
|
+
value: 'win32',
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const result = getPlatformSpawnArgs();
|
|
692
|
+
|
|
693
|
+
expect(result).toEqual({
|
|
694
|
+
npxExecutableName: 'npx.cmd',
|
|
695
|
+
sanitize: expect.any(Function),
|
|
696
|
+
shell: true,
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it('should return args for non-Windows platform', () => {
|
|
701
|
+
Object.defineProperty(process, 'platform', {
|
|
702
|
+
value: 'linux',
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
const result = getPlatformSpawnArgs();
|
|
706
|
+
|
|
707
|
+
expect(result).toEqual({
|
|
708
|
+
npxExecutableName: 'npx',
|
|
709
|
+
sanitize: expect.any(Function),
|
|
710
|
+
shell: false,
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
});
|
package/src/commands/eject.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { getPlatformSpawnArgs, sanitizePath } from '../utils/platform';
|
|
2
3
|
|
|
3
4
|
import type { CommandArgs } from '../wrapper';
|
|
4
5
|
import type { VerifyConfigOptions } from '../types';
|
|
@@ -12,18 +13,30 @@ export type EjectOptions = {
|
|
|
12
13
|
|
|
13
14
|
export const handleEject = async ({ argv }: CommandArgs<EjectOptions>) => {
|
|
14
15
|
process.stdout.write(`\nLaunching eject using NPX.\n\n`);
|
|
15
|
-
const npxExecutableName
|
|
16
|
-
|
|
16
|
+
const { npxExecutableName, sanitize, shell } = getPlatformSpawnArgs();
|
|
17
|
+
|
|
18
|
+
const path = sanitize(argv.path, sanitizePath);
|
|
19
|
+
const projectDir = sanitize(argv['project-dir'], sanitizePath);
|
|
20
|
+
|
|
21
|
+
const child = spawn(
|
|
17
22
|
npxExecutableName,
|
|
18
23
|
[
|
|
19
24
|
'-y',
|
|
20
25
|
'@redocly/realm',
|
|
21
26
|
'eject',
|
|
22
27
|
`${argv.type}`,
|
|
23
|
-
|
|
24
|
-
`-d=${
|
|
28
|
+
path,
|
|
29
|
+
`-d=${projectDir}`,
|
|
25
30
|
argv.force ? `--force=${argv.force}` : '',
|
|
26
31
|
],
|
|
27
|
-
{
|
|
32
|
+
{
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
shell,
|
|
35
|
+
}
|
|
28
36
|
);
|
|
37
|
+
|
|
38
|
+
child.on('error', (error) => {
|
|
39
|
+
process.stderr.write(`Eject launch failed: ${error.message}`);
|
|
40
|
+
throw new Error('Eject launch failed.');
|
|
41
|
+
});
|
|
29
42
|
};
|
package/src/commands/join.ts
CHANGED
|
@@ -64,18 +64,12 @@ export async function handleJoin({
|
|
|
64
64
|
}: CommandArgs<JoinOptions>) {
|
|
65
65
|
const startedAt = performance.now();
|
|
66
66
|
|
|
67
|
-
if (argv.apis.length < 2) {
|
|
68
|
-
return exitWithError(`At least 2 apis should be provided.`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const fileExtension = getAndValidateFileExtension(argv.output || argv.apis[0]);
|
|
72
|
-
|
|
73
67
|
const {
|
|
74
68
|
'prefix-components-with-info-prop': prefixComponentsWithInfoProp,
|
|
75
69
|
'prefix-tags-with-filename': prefixTagsWithFilename,
|
|
76
70
|
'prefix-tags-with-info-prop': prefixTagsWithInfoProp,
|
|
77
71
|
'without-x-tag-groups': withoutXTagGroups,
|
|
78
|
-
output
|
|
72
|
+
output,
|
|
79
73
|
} = argv;
|
|
80
74
|
|
|
81
75
|
const usedTagsOptions = [
|
|
@@ -91,6 +85,13 @@ export async function handleJoin({
|
|
|
91
85
|
}
|
|
92
86
|
|
|
93
87
|
const apis = await getFallbackApisOrExit(argv.apis, config);
|
|
88
|
+
if (apis.length < 2) {
|
|
89
|
+
return exitWithError(`At least 2 APIs should be provided.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const fileExtension = getAndValidateFileExtension(output || apis[0].path);
|
|
93
|
+
const specFilename = output || `openapi.${fileExtension}`;
|
|
94
|
+
|
|
94
95
|
const externalRefResolver = new BaseResolver(config.resolve);
|
|
95
96
|
const documents = await Promise.all(
|
|
96
97
|
apis.map(
|
|
@@ -2,6 +2,7 @@ import path = require('path');
|
|
|
2
2
|
import { existsSync, readFileSync } from 'fs';
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { PRODUCT_NAMES, PRODUCT_PACKAGES } from './constants';
|
|
5
|
+
import { getPlatformSpawnArgs } from '../../utils/platform';
|
|
5
6
|
|
|
6
7
|
import type { PreviewProjectOptions, Product } from './types';
|
|
7
8
|
import type { CommandArgs } from '../../wrapper';
|
|
@@ -21,17 +22,22 @@ export const previewProject = async ({ argv }: CommandArgs<PreviewProjectOptions
|
|
|
21
22
|
const packageName = PRODUCT_PACKAGES[product];
|
|
22
23
|
|
|
23
24
|
process.stdout.write(`\nLaunching preview of ${productName} ${plan} using NPX.\n\n`);
|
|
25
|
+
const { npxExecutableName, shell } = getPlatformSpawnArgs();
|
|
24
26
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
spawn(
|
|
27
|
+
const child = spawn(
|
|
28
28
|
npxExecutableName,
|
|
29
29
|
['-y', packageName, 'preview', `--plan=${plan}`, `--port=${port || 4000}`],
|
|
30
30
|
{
|
|
31
31
|
stdio: 'inherit',
|
|
32
32
|
cwd: projectDir,
|
|
33
|
+
shell,
|
|
33
34
|
}
|
|
34
35
|
);
|
|
36
|
+
|
|
37
|
+
child.on('error', (error) => {
|
|
38
|
+
process.stderr.write(`Project preview launch failed: ${error.message}`);
|
|
39
|
+
throw new Error(`Project preview launch failed.`);
|
|
40
|
+
});
|
|
35
41
|
};
|
|
36
42
|
|
|
37
43
|
const isValidProduct = (product: string | undefined): product is Product => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { getPlatformSpawnArgs, sanitizeLocale, sanitizePath } from '../utils/platform';
|
|
2
3
|
|
|
3
4
|
import type { CommandArgs } from '../wrapper';
|
|
4
5
|
import type { VerifyConfigOptions } from '../types';
|
|
@@ -10,10 +11,22 @@ export type TranslationsOptions = {
|
|
|
10
11
|
|
|
11
12
|
export const handleTranslations = async ({ argv }: CommandArgs<TranslationsOptions>) => {
|
|
12
13
|
process.stdout.write(`\nLaunching translate using NPX.\n\n`);
|
|
13
|
-
const npxExecutableName
|
|
14
|
-
|
|
14
|
+
const { npxExecutableName, sanitize, shell } = getPlatformSpawnArgs();
|
|
15
|
+
|
|
16
|
+
const projectDir = sanitize(argv['project-dir'], sanitizePath);
|
|
17
|
+
const locale = sanitize(argv.locale, sanitizeLocale);
|
|
18
|
+
|
|
19
|
+
const child = spawn(
|
|
15
20
|
npxExecutableName,
|
|
16
|
-
['-y', '@redocly/realm', 'translate',
|
|
17
|
-
{
|
|
21
|
+
['-y', '@redocly/realm', 'translate', locale, `-d=${projectDir}`],
|
|
22
|
+
{
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
shell,
|
|
25
|
+
}
|
|
18
26
|
);
|
|
27
|
+
|
|
28
|
+
child.on('error', (error) => {
|
|
29
|
+
process.stderr.write(`Translate launch failed: ${error.message}`);
|
|
30
|
+
throw new Error(`Translate launch failed.`);
|
|
31
|
+
});
|
|
19
32
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitizes the input path by removing invalid characters.
|
|
3
|
+
*/
|
|
4
|
+
export const sanitizePath = (input: string): string => {
|
|
5
|
+
return input.replace(/[^a-zA-Z0-9 ._\-:\\/@]/g, '');
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sanitizes the input locale (ex. en-US) by removing invalid characters.
|
|
10
|
+
*/
|
|
11
|
+
export const sanitizeLocale = (input: string): string => {
|
|
12
|
+
return input.replace(/[^a-zA-Z0-9@._-]/g, '');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Retrieves platform-specific arguments and utilities.
|
|
17
|
+
*/
|
|
18
|
+
export function getPlatformSpawnArgs() {
|
|
19
|
+
const isWindowsPlatform = process.platform === 'win32';
|
|
20
|
+
const npxExecutableName = isWindowsPlatform ? 'npx.cmd' : 'npx';
|
|
21
|
+
|
|
22
|
+
const sanitizeIfWindows = (input: string | undefined, sanitizer: (input: string) => string) => {
|
|
23
|
+
if (isWindowsPlatform && input) {
|
|
24
|
+
return sanitizer(input);
|
|
25
|
+
} else {
|
|
26
|
+
return input || '';
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return { npxExecutableName, sanitize: sanitizeIfWindows, shell: isWindowsPlatform };
|
|
31
|
+
}
|