@qraft/plugin 1.0.0-beta.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -0
  3. package/dist/index.d.ts +5 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +3 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/GeneratorFile.d.ts +9 -0
  8. package/dist/lib/GeneratorFile.d.ts.map +1 -0
  9. package/dist/lib/GeneratorFile.js +2 -0
  10. package/dist/lib/GeneratorFile.js.map +1 -0
  11. package/dist/lib/OutputOptions.d.ts +6 -0
  12. package/dist/lib/OutputOptions.d.ts.map +1 -0
  13. package/dist/lib/OutputOptions.js +2 -0
  14. package/dist/lib/OutputOptions.js.map +1 -0
  15. package/dist/lib/QraftCommand.d.ts +47 -0
  16. package/dist/lib/QraftCommand.d.ts.map +1 -0
  17. package/dist/lib/QraftCommand.js +187 -0
  18. package/dist/lib/QraftCommand.js.map +1 -0
  19. package/dist/lib/QraftCommandPlugin.d.ts +6 -0
  20. package/dist/lib/QraftCommandPlugin.d.ts.map +1 -0
  21. package/dist/lib/QraftCommandPlugin.js +2 -0
  22. package/dist/lib/QraftCommandPlugin.js.map +1 -0
  23. package/dist/lib/RedoclyConfigCommand.d.ts +28 -0
  24. package/dist/lib/RedoclyConfigCommand.d.ts.map +1 -0
  25. package/dist/lib/RedoclyConfigCommand.js +158 -0
  26. package/dist/lib/RedoclyConfigCommand.js.map +1 -0
  27. package/dist/lib/formatFileHeader.d.ts +2 -0
  28. package/dist/lib/formatFileHeader.d.ts.map +1 -0
  29. package/dist/lib/formatFileHeader.js +6 -0
  30. package/dist/lib/formatFileHeader.js.map +1 -0
  31. package/dist/lib/getRedocAPIsToQraft.d.ts +14 -0
  32. package/dist/lib/getRedocAPIsToQraft.d.ts.map +1 -0
  33. package/dist/lib/getRedocAPIsToQraft.js +49 -0
  34. package/dist/lib/getRedocAPIsToQraft.js.map +1 -0
  35. package/dist/lib/handleSchemaInput.d.ts +10 -0
  36. package/dist/lib/handleSchemaInput.d.ts.map +1 -0
  37. package/dist/lib/handleSchemaInput.js +40 -0
  38. package/dist/lib/handleSchemaInput.js.map +1 -0
  39. package/dist/lib/loadRedoclyConfig.d.ts +12 -0
  40. package/dist/lib/loadRedoclyConfig.d.ts.map +1 -0
  41. package/dist/lib/loadRedoclyConfig.js +17 -0
  42. package/dist/lib/loadRedoclyConfig.js.map +1 -0
  43. package/dist/lib/maybeEscapeShellArg.d.ts +2 -0
  44. package/dist/lib/maybeEscapeShellArg.d.ts.map +1 -0
  45. package/dist/lib/maybeEscapeShellArg.js +9 -0
  46. package/dist/lib/maybeEscapeShellArg.js.map +1 -0
  47. package/dist/lib/parseConfigToArgs.d.ts +7 -0
  48. package/dist/lib/parseConfigToArgs.d.ts.map +1 -0
  49. package/dist/lib/parseConfigToArgs.js +130 -0
  50. package/dist/lib/parseConfigToArgs.js.map +1 -0
  51. package/dist/lib/shouldQuoteCommandLineArg.d.ts +11 -0
  52. package/dist/lib/shouldQuoteCommandLineArg.d.ts.map +1 -0
  53. package/dist/lib/shouldQuoteCommandLineArg.js +33 -0
  54. package/dist/lib/shouldQuoteCommandLineArg.js.map +1 -0
  55. package/dist/lib/writeGeneratorFiles.d.ts +7 -0
  56. package/dist/lib/writeGeneratorFiles.d.ts.map +1 -0
  57. package/dist/lib/writeGeneratorFiles.js +51 -0
  58. package/dist/lib/writeGeneratorFiles.js.map +1 -0
  59. package/dist/packageVersion.d.ts +2 -0
  60. package/dist/packageVersion.d.ts.map +1 -0
  61. package/dist/packageVersion.js +3 -0
  62. package/dist/packageVersion.js.map +1 -0
  63. package/package.json +68 -0
  64. package/src/index.ts +12 -0
  65. package/src/lib/GeneratorFile.ts +5 -0
  66. package/src/lib/OutputOptions.ts +6 -0
  67. package/src/lib/QraftCommand.ts +381 -0
  68. package/src/lib/QraftCommandPlugin.ts +9 -0
  69. package/src/lib/RedoclyConfigCommand.ts +290 -0
  70. package/src/lib/formatFileHeader.ts +4 -0
  71. package/src/lib/getRedocAPIsToQraft.ts +92 -0
  72. package/src/lib/handleSchemaInput.ts +58 -0
  73. package/src/lib/loadRedoclyConfig.ts +47 -0
  74. package/src/lib/maybeEscapeShellArg.ts +10 -0
  75. package/src/lib/parseConfigToArgs.ts +134 -0
  76. package/src/lib/shouldQuoteCommandLineArg.ts +32 -0
  77. package/src/lib/writeGeneratorFiles.ts +82 -0
  78. package/src/packageVersion.ts +2 -0
@@ -0,0 +1,92 @@
1
+ import type { Config } from '@redocly/openapi-core';
2
+ import { isAbsolute } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { CommanderError } from 'commander';
5
+ import { Ora } from 'ora';
6
+
7
+ export const OPENAPI_QRAFT_REDOC_CONFIG_KEY = 'x-openapi-qraft';
8
+ export const ASYNCAPI_QRAFT_REDOC_CONFIG_KEY = 'x-asyncapi-qraft';
9
+
10
+ export function getRedocAPIsToQraft(
11
+ redoc: Config,
12
+ cwd: URL,
13
+ spinner: Ora,
14
+ configKey: string = OPENAPI_QRAFT_REDOC_CONFIG_KEY,
15
+ apiName?: string
16
+ ) {
17
+ const hasRedoclyApis = Object.keys(redoc?.apis ?? {}).length > 0;
18
+ if (!hasRedoclyApis) {
19
+ throw new CommanderError(
20
+ 1,
21
+ 'ERR_MISSING_APIS',
22
+ 'No "apis" found in Redocly config. Please specify the APIs to be generated.'
23
+ );
24
+ }
25
+ if (!redoc.configFile) {
26
+ throw new CommanderError(
27
+ 1,
28
+ 'ERR_MISSING_CONFIG_FILE',
29
+ 'No "configFile" found in Redocly config. Please specify the path to the Redocly config file.'
30
+ );
31
+ }
32
+
33
+ const configRoot = isAbsolute(redoc.configFile)
34
+ ? new URL(pathToFileURL(redoc.configFile))
35
+ : new URL(redoc.configFile, cwd);
36
+
37
+ const apisToQraftEntries = Object.entries(redoc.apis)
38
+ .map(([itemApiName, api]) => {
39
+ if (apiName && apiName !== itemApiName) return;
40
+
41
+ const qraftConfig =
42
+ configKey in api
43
+ ? (api as Record<string, unknown>)[configKey]
44
+ : undefined;
45
+
46
+ if (!qraftConfig) {
47
+ return void spinner.warn(
48
+ `API ${itemApiName} is missing an \`${configKey}\` key. Skipping...`
49
+ );
50
+ }
51
+
52
+ if (typeof qraftConfig !== 'object') {
53
+ throw new CommanderError(
54
+ 1,
55
+ 'ERR_MISSING_API_CONFIG',
56
+ `API ${itemApiName} is missing an \`${configKey}\` key. See https://openapi-qraft.github.io/openapi-qraft/docs/codegen/CLI/redocly-config.`
57
+ );
58
+ }
59
+
60
+ const {
61
+ o,
62
+ ['output-dir']: outputPath = o,
63
+ ...qraftConfigRest
64
+ } = qraftConfig as {
65
+ ['output-dir']?: string;
66
+ o?: string;
67
+ };
68
+
69
+ if (!outputPath) {
70
+ throw new CommanderError(
71
+ 1,
72
+ 'ERR_MISSING_API_OUTPUT',
73
+ `API ${itemApiName} is missing an "output-dir" key. See https://openapi-qraft.github.io/openapi-qraft/docs/codegen/CLI/redocly-config.`
74
+ );
75
+ }
76
+
77
+ return [
78
+ itemApiName,
79
+ {
80
+ ...api,
81
+ root: new URL(api.root, configRoot),
82
+ [configKey]: {
83
+ ...qraftConfigRest,
84
+ ['output-dir']: fileURLToPath(new URL(outputPath, configRoot)),
85
+ },
86
+ },
87
+ ] as const;
88
+ })
89
+ .filter((api) => !!api);
90
+
91
+ return Object.fromEntries(apisToQraftEntries);
92
+ }
@@ -0,0 +1,58 @@
1
+ import type { Ora } from 'ora';
2
+ import process from 'node:process';
3
+ import { URL } from 'node:url';
4
+
5
+ /**
6
+ * Validates Schema's `input` and return `Readable` stream or `URL`
7
+ * @throws {Error} if input is invalid
8
+ */
9
+ export function handleSchemaInput(
10
+ input: unknown,
11
+ cwd: URL,
12
+ spinner: Ora,
13
+ schemaName: string
14
+ ) {
15
+ if (!input) {
16
+ /**
17
+ * Handle `stdin`, if no input file is provided
18
+ * @example
19
+ * ```bash
20
+ * cat schema.json | script.js
21
+ * ```
22
+ */
23
+ if (process.stdin.isTTY) {
24
+ spinner.fail(
25
+ `Input file not found or stdin is empty. Please specify \`input\` argument or pipe ${schemaName} to stdin.`
26
+ );
27
+
28
+ throw new Error('Invalid input.');
29
+ }
30
+
31
+ return process.stdin;
32
+ }
33
+
34
+ /**
35
+ * Handle single file
36
+ * @example
37
+ * ```bash
38
+ * script.js schema.json
39
+ * ```
40
+ */
41
+
42
+ if (typeof input !== 'string') {
43
+ spinner.fail('Invalid input. Please specify a single file or URL.');
44
+
45
+ throw new Error('Invalid input.');
46
+ }
47
+
48
+ // throw error on glob
49
+ if (input.includes('*')) {
50
+ spinner.fail(
51
+ 'Globbing is not supported. Please specify a single file or URL.'
52
+ );
53
+
54
+ throw new Error('Globbing is not supported.');
55
+ }
56
+
57
+ return new URL(input, cwd);
58
+ }
@@ -0,0 +1,47 @@
1
+ import { fileURLToPath, URL } from 'node:url';
2
+ import {
3
+ Config,
4
+ CONFIG_FILE_NAMES,
5
+ findConfig,
6
+ loadConfig,
7
+ } from '@redocly/openapi-core';
8
+ import { CommanderError } from 'commander';
9
+
10
+ /**
11
+ * Loads Redocly config from the default config file:
12
+ * 'redocly.yaml', 'redocly.yml', '.redocly.yaml', '.redocly.yml'
13
+ */
14
+ export async function loadRedoclyConfig(
15
+ configPath: null | undefined | true,
16
+ cwd: URL
17
+ ): Promise<Config>;
18
+ /**
19
+ * Loads Redocly config from the specified path or from the default config file.
20
+ */
21
+ export async function loadRedoclyConfig(
22
+ configPath: string,
23
+ cwd: URL
24
+ ): Promise<Config>;
25
+ export async function loadRedoclyConfig(
26
+ configPath: string | null | undefined | true,
27
+ cwd: URL
28
+ ): Promise<Config> {
29
+ const normalizedConfigPath =
30
+ typeof configPath === 'string'
31
+ ? configPath
32
+ : findConfig(fileURLToPath(cwd));
33
+
34
+ if (!normalizedConfigPath) {
35
+ throw new CommanderError(
36
+ 1,
37
+ 'INVALID_REDOCLY_CONFIG_PATH',
38
+ typeof configPath === 'string'
39
+ ? `Could not find Redocly config file at ${configPath}`
40
+ : `Could not find Redocly config file in ${cwd}. Searched in: ${CONFIG_FILE_NAMES.join(', ')}`
41
+ );
42
+ }
43
+
44
+ return await loadConfig({
45
+ configPath: normalizedConfigPath,
46
+ });
47
+ }
@@ -0,0 +1,10 @@
1
+ import { shouldQuoteCommandLineArg } from './shouldQuoteCommandLineArg.js';
2
+
3
+ export function maybeEscapeShellArg(arg: string): string {
4
+ if (shouldQuoteCommandLineArg(arg)) {
5
+ // bash-safe single-quote escaping
6
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
7
+ }
8
+
9
+ return arg;
10
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Converts a parsed YAML configuration into command line arguments
3
+ * @param config The parsed YAML configuration object
4
+ * @returns Array of command line arguments
5
+ */
6
+ export function parseConfigToArgs(config: Record<string, unknown>): string[] {
7
+ const args: string[] = [];
8
+
9
+ const isPrimitive = (val: any): val is string | number | boolean =>
10
+ typeof val === 'string' ||
11
+ typeof val === 'number' ||
12
+ typeof val === 'boolean';
13
+
14
+ function flattenMapping(node: any, prefixKeys: string[]): string[] {
15
+ const results: string[] = [];
16
+
17
+ if (isPrimitive(node)) {
18
+ if (typeof node === 'boolean') {
19
+ if (node) {
20
+ results.push(prefixKeys.join(':'));
21
+ }
22
+ } else {
23
+ results.push(`${prefixKeys.join(':')}:${node.toString()}`);
24
+ }
25
+ return results;
26
+ }
27
+
28
+ if (Array.isArray(node)) {
29
+ if (node.every((el) => isPrimitive(el))) {
30
+ const primitives = node.filter(
31
+ (el) => el !== null && el !== undefined
32
+ ) as (string | number | boolean)[];
33
+ const joined = primitives.map((el) => el.toString()).join(',');
34
+ if (joined) {
35
+ results.push(`${prefixKeys.join(':')}:${joined}`);
36
+ }
37
+ return results;
38
+ }
39
+
40
+ for (const el of node) {
41
+ if (el == null) {
42
+ continue;
43
+ }
44
+ results.push(...flattenMapping(el, prefixKeys));
45
+ }
46
+ return results;
47
+ }
48
+
49
+ if (typeof node === 'object' && node !== null) {
50
+ for (const [key, value] of Object.entries(node)) {
51
+ results.push(...flattenMapping(value, [...prefixKeys, key]));
52
+ }
53
+ return results;
54
+ }
55
+
56
+ return results;
57
+ }
58
+
59
+ for (const [key, value] of Object.entries(config)) {
60
+ if (typeof value === 'string' || typeof value === 'number') {
61
+ args.push(`--${key}`, value.toString());
62
+ } else if (typeof value === 'boolean') {
63
+ if (value) {
64
+ args.push(`--${key}`);
65
+ }
66
+ } else if (value == null) {
67
+ continue;
68
+ } else if (Array.isArray(value)) {
69
+ if (value.every((el) => isPrimitive(el))) {
70
+ const primitives = value.filter(
71
+ (el) => el !== null && el !== undefined
72
+ ) as (string | number | boolean)[];
73
+ args.push(`--${key}`, ...primitives.map((el) => el.toString()));
74
+ } else {
75
+ for (const el of value) {
76
+ if (el == null) {
77
+ continue;
78
+ }
79
+ if (typeof el === 'object') {
80
+ const mappingPaths = flattenMapping(el, []);
81
+ if (mappingPaths.length) {
82
+ args.push(`--${key}`, ...mappingPaths);
83
+ }
84
+ } else if (isPrimitive(el)) {
85
+ args.push(`--${key}`, el.toString());
86
+ }
87
+ }
88
+ }
89
+ } else if (typeof value === 'object') {
90
+ for (const [childKey, childValue] of Object.entries(
91
+ value as Record<string, unknown>
92
+ )) {
93
+ if (typeof childValue === 'string' || typeof childValue === 'number') {
94
+ args.push(`--${key}`, childKey, childValue.toString());
95
+ } else if (typeof childValue === 'boolean') {
96
+ if (childValue) {
97
+ args.push(`--${key}`, childKey);
98
+ }
99
+ } else if (childValue == null) {
100
+ continue;
101
+ } else if (Array.isArray(childValue)) {
102
+ if (childValue.every((el) => isPrimitive(el))) {
103
+ const primitives = childValue.filter(
104
+ (el) => el !== null && el !== undefined
105
+ ) as (string | number | boolean)[];
106
+ const joined = primitives.map((el) => el.toString()).join(',');
107
+ args.push(`--${key}`, childKey, joined);
108
+ } else {
109
+ for (const el of childValue) {
110
+ if (el == null) {
111
+ continue;
112
+ }
113
+ if (typeof el === 'object') {
114
+ const mappingPaths = flattenMapping(el, [childKey]);
115
+ if (mappingPaths.length) {
116
+ args.push(`--${key}`, ...mappingPaths);
117
+ }
118
+ } else if (isPrimitive(el)) {
119
+ args.push(`--${key}`, childKey, el.toString());
120
+ }
121
+ }
122
+ }
123
+ } else if (typeof childValue === 'object') {
124
+ const mappingPaths = flattenMapping(childValue, []);
125
+ if (mappingPaths.length) {
126
+ args.push(`--${key}`, childKey, ...mappingPaths);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ return args;
134
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Checks if a command-line argument needs to be quoted when used in a command line.
3
+ *
4
+ * Arguments containing special characters like spaces, quotes, or shell metacharacters
5
+ * need to be wrapped in quotes to be properly interpreted by the shell.
6
+ *
7
+ * @param arg The command-line argument to check
8
+ * @returns True if the argument needs quotes, false otherwise
9
+ */
10
+ export function shouldQuoteCommandLineArg(arg: string): boolean {
11
+ if (!arg) return false;
12
+
13
+ // Check for spaces
14
+ if (arg.includes(' ')) return true;
15
+
16
+ // Check for quotes
17
+ if (arg.includes('"') || arg.includes("'")) return true;
18
+
19
+ // Check for shell metacharacters and special characters
20
+ const shellSpecialChars = /[&|<>(){}[\]$;!*?~`\n\r\t#]/;
21
+ if (shellSpecialChars.test(arg)) return true;
22
+
23
+ // Check for control characters
24
+ // eslint-disable-next-line no-control-regex
25
+ if (/[\x00-\x1F\x7F]/.test(arg)) return true;
26
+
27
+ // Check if argument starts with a dash (to avoid confusion with options)
28
+ if (arg.startsWith('-') && !arg.startsWith('--')) return true;
29
+
30
+ // Check for wildcards
31
+ return arg.includes('*') || arg.includes('?');
32
+ }
@@ -0,0 +1,82 @@
1
+ import fs from 'node:fs';
2
+ import c from 'ansi-colors';
3
+ import { Ora } from 'ora';
4
+ import { GeneratorFile } from './GeneratorFile.js';
5
+
6
+ export const writeGeneratorFiles = async ({
7
+ spinner,
8
+ fileItems,
9
+ }: {
10
+ spinner: Ora;
11
+ fileItems: GeneratorFile[];
12
+ }) => {
13
+ await setupDirectories(spinner, fileItems);
14
+ await writeFiles(spinner, fileItems);
15
+ };
16
+
17
+ const setupDirectories = async (spinner: Ora, fileItems: GeneratorFile[]) => {
18
+ for (const fileItem of fileItems) {
19
+ if (!('directory' in fileItem)) continue;
20
+
21
+ if (fileItem.clean) {
22
+ spinner.text = `Cleaning ${c.magenta(fileItem.directory.pathname)}`;
23
+
24
+ try {
25
+ await fs.promises.rm(fileItem.directory, {
26
+ recursive: true,
27
+ force: true,
28
+ });
29
+ } catch (error) {
30
+ spinner.fail(
31
+ c.redBright(
32
+ `Error occurred during ${c.magenta(
33
+ fileItem.directory.pathname
34
+ )} directory cleaning`
35
+ )
36
+ );
37
+
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ try {
43
+ spinner.text = `Creating directory ${c.magenta(fileItem.directory.pathname)}`;
44
+
45
+ await fs.promises.mkdir(fileItem.directory, { recursive: true });
46
+ } catch (error) {
47
+ spinner.fail(
48
+ c.redBright(
49
+ `Error occurred during ${c.magenta(
50
+ fileItem.directory.pathname
51
+ )} directory creation`
52
+ )
53
+ );
54
+
55
+ throw error;
56
+ }
57
+ }
58
+ };
59
+
60
+ const writeFiles = async (spinner: Ora, fileItems: GeneratorFile[]) => {
61
+ for (const fileItem of fileItems) {
62
+ if ('directory' in fileItem) continue;
63
+
64
+ const { file, code } = fileItem;
65
+
66
+ spinner.text = `Writing ${c.magenta(file.pathname)}`;
67
+
68
+ try {
69
+ await fs.promises.writeFile(file, code, {
70
+ encoding: 'utf-8',
71
+ });
72
+ } catch (error) {
73
+ spinner.fail(
74
+ c.redBright(
75
+ `Error occurred during ${c.magenta(file.pathname)} file writing`
76
+ )
77
+ );
78
+
79
+ throw error;
80
+ }
81
+ }
82
+ };
@@ -0,0 +1,2 @@
1
+ // This file was generated automatically
2
+ export const packageVersion = '1.0.0-beta.0';