mirror of
https://github.com/LukeHagar/redocly-cli.git
synced 2025-12-06 04:21:09 +00:00
fix: bundling multiple files specified as CLI arguments (#1717)
This commit is contained in:
5
.changeset/late-readers-design.md
Normal file
5
.changeset/late-readers-design.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@redocly/cli": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fixed a bug where bundling multiple API description files specified as CLI arguments, along with the `--output` option, stored the result in a single file instead of a folder.
|
||||||
@@ -22,23 +22,23 @@ redocly bundle --version
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Option | Type | Description |
|
| Option | Type | Description |
|
||||||
| -------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| apis | [string] | List of API description root filenames or names assigned in the `apis` section of your Redocly configuration file. Default values are all names defined in the `apis` section within your configuration file. |
|
| apis | [string] | List of API description root filenames or names assigned in the `apis` section of your Redocly configuration file. Default values are names defined in the `apis` section of your configuration file. |
|
||||||
| --config | string | Specify path to the [configuration file](#use-alternative-configuration-file). |
|
| --config | string | Specify the path to the [configuration file](#use-alternative-configuration-file). |
|
||||||
| --dereferenced, -d | boolean | Generate fully dereferenced bundle. |
|
| --dereferenced, -d | boolean | Generate fully dereferenced bundle. |
|
||||||
| --ext | string | Specify bundled file extension. Possible values are `json`, `yaml`, or `yml`. Default value is `yaml`. |
|
| --ext | string | Specify the bundled file's extension. The possible values are `json`, `yaml`, or `yml`. Default value is `yaml`. |
|
||||||
| --extends | [string] | Can be used in combination with `--lint` to [extend a specific configuration](./lint.md#extend-configuration). Default values are taken from the Redocly configuration file. |
|
| --extends | [string] | Can be used in combination with `--lint` to [extend a specific configuration](./lint.md#extend-configuration). The default values are taken from the Redocly configuration file. |
|
||||||
| --force, -f | boolean | Generate bundle output even when errors occur. |
|
| --force, -f | boolean | Generate a bundle output even when errors occur. |
|
||||||
| --help | boolean | Show help. |
|
| --help | boolean | Show help. |
|
||||||
| --keep-url-references, -k | boolean | Keep absolute url references. |
|
| --keep-url-references, -k | boolean | Preserve absolute URL references. |
|
||||||
| --lint-config | string | Specify the severity level for the configuration file. <br/> **Possible values:** `warn`, `error`, `off`. Default value is `warn`. |
|
| --lint-config | string | Specify the severity level for the configuration file. <br/> **Possible values:** `warn`, `error`, `off`. Default value is `warn`. |
|
||||||
| --metafile | string | Path for the bundle metadata file. |
|
| --metafile | string | Path for the bundle metadata file. |
|
||||||
| --output, -o | string | Name or folder for the bundle file. If you don't specify the file extension, `.yaml` is used by default. If the specified folder doesn't exist, it's created automatically. **If the file specified as the bundler's output already exists, it's overwritten.** Use this option when bundling a single API only; otherwise use the `output` option in per-API configuration. |
|
| --output, -o | string | Name or folder for the bundle file. If you don't specify the file extension, `.yaml` is used by default. If the specified folder doesn't exist, it's created automatically. **Overwrites existing bundler output file.** |
|
||||||
| --remove-unused-components | boolean | Remove unused components from the `bundle` output. |
|
| --remove-unused-components | boolean | Remove unused components from the `bundle` output. |
|
||||||
| --skip-decorator | [string] | Ignore certain decorators. See the [Skip preprocessor, rule, or decorator section](#skip-preprocessor-rule-or-decorator). |
|
| --skip-decorator | [string] | Ignore certain decorators. See the [Skip preprocessor, rule, or decorator section](#skip-preprocessor-rule-or-decorator). |
|
||||||
| --skip-preprocessor | [string] | Ignore certain preprocessors. See the [Skip preprocessor, rule, or decorator section](#skip-preprocessor-rule-or-decorator). |
|
| --skip-preprocessor | [string] | Ignore certain preprocessors. See the [Skip preprocessor, rule, or decorator section](#skip-preprocessor-rule-or-decorator). |
|
||||||
| --version | boolean | Show version number. |
|
| --version | boolean | Show version number. |
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
@@ -127,88 +127,88 @@ describe('bundle', () => {
|
|||||||
expect(handleError).toHaveBeenCalledTimes(0);
|
expect(handleError).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should store bundled API descriptions in the output files described in the apis section of config IF no positional apis provided AND output is specified for both apis', async () => {
|
|
||||||
const apis = {
|
|
||||||
foo: {
|
|
||||||
root: 'foo.yaml',
|
|
||||||
output: 'output/foo.yaml',
|
|
||||||
},
|
|
||||||
bar: {
|
|
||||||
root: 'bar.yaml',
|
|
||||||
output: 'output/bar.json',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const config = {
|
|
||||||
apis,
|
|
||||||
styleguide: {
|
|
||||||
skipPreprocessors: jest.fn(),
|
|
||||||
skipDecorators: jest.fn(),
|
|
||||||
},
|
|
||||||
} as unknown as Config;
|
|
||||||
// @ts-ignore
|
|
||||||
getFallbackApisOrExit = jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(
|
|
||||||
Object.entries(apis).map(([alias, { root, ...api }]) => ({ ...api, path: root, alias }))
|
|
||||||
);
|
|
||||||
(getTotals as jest.Mock).mockReturnValue({
|
|
||||||
errors: 0,
|
|
||||||
warnings: 0,
|
|
||||||
ignored: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleBundle({
|
|
||||||
argv: { apis: [] }, // positional
|
|
||||||
version: 'test',
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(saveBundle).toBeCalledTimes(2);
|
|
||||||
expect(saveBundle).toHaveBeenNthCalledWith(1, 'output/foo.yaml', expect.any(String));
|
|
||||||
expect(saveBundle).toHaveBeenNthCalledWith(2, 'output/bar.json', expect.any(String));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should store bundled API descriptions in the output files described in the apis section of config AND print the bundled api without the output specified to the terminal IF no positional apis provided AND output is specified for one api', async () => {
|
|
||||||
const apis = {
|
|
||||||
foo: {
|
|
||||||
root: 'foo.yaml',
|
|
||||||
output: 'output/foo.yaml',
|
|
||||||
},
|
|
||||||
bar: {
|
|
||||||
root: 'bar.yaml',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const config = {
|
|
||||||
apis,
|
|
||||||
styleguide: {
|
|
||||||
skipPreprocessors: jest.fn(),
|
|
||||||
skipDecorators: jest.fn(),
|
|
||||||
},
|
|
||||||
} as unknown as Config;
|
|
||||||
// @ts-ignore
|
|
||||||
getFallbackApisOrExit = jest
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(
|
|
||||||
Object.entries(apis).map(([alias, { root, ...api }]) => ({ ...api, path: root, alias }))
|
|
||||||
);
|
|
||||||
(getTotals as jest.Mock).mockReturnValue({
|
|
||||||
errors: 0,
|
|
||||||
warnings: 0,
|
|
||||||
ignored: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
await handleBundle({
|
|
||||||
argv: { apis: [] }, // positional
|
|
||||||
version: 'test',
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(saveBundle).toBeCalledTimes(1);
|
|
||||||
expect(saveBundle).toHaveBeenCalledWith('output/foo.yaml', expect.any(String));
|
|
||||||
expect(process.stdout.write).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('per api output', () => {
|
describe('per api output', () => {
|
||||||
|
it('should store bundled API descriptions in the output files described in the apis section of config IF no positional apis provided AND output is specified for both apis', async () => {
|
||||||
|
const apis = {
|
||||||
|
foo: {
|
||||||
|
root: 'foo.yaml',
|
||||||
|
output: 'output/foo.yaml',
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
root: 'bar.yaml',
|
||||||
|
output: 'output/bar.json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const config = {
|
||||||
|
apis,
|
||||||
|
styleguide: {
|
||||||
|
skipPreprocessors: jest.fn(),
|
||||||
|
skipDecorators: jest.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as Config;
|
||||||
|
// @ts-ignore
|
||||||
|
getFallbackApisOrExit = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
Object.entries(apis).map(([alias, { root, ...api }]) => ({ ...api, path: root, alias }))
|
||||||
|
);
|
||||||
|
(getTotals as jest.Mock).mockReturnValue({
|
||||||
|
errors: 0,
|
||||||
|
warnings: 0,
|
||||||
|
ignored: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleBundle({
|
||||||
|
argv: { apis: [] }, // positional
|
||||||
|
version: 'test',
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(saveBundle).toBeCalledTimes(2);
|
||||||
|
expect(saveBundle).toHaveBeenNthCalledWith(1, 'output/foo.yaml', expect.any(String));
|
||||||
|
expect(saveBundle).toHaveBeenNthCalledWith(2, 'output/bar.json', expect.any(String));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store bundled API descriptions in the output files described in the apis section of config AND print the bundled api without the output specified to the terminal IF no positional apis provided AND output is specified for one api', async () => {
|
||||||
|
const apis = {
|
||||||
|
foo: {
|
||||||
|
root: 'foo.yaml',
|
||||||
|
output: 'output/foo.yaml',
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
root: 'bar.yaml',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const config = {
|
||||||
|
apis,
|
||||||
|
styleguide: {
|
||||||
|
skipPreprocessors: jest.fn(),
|
||||||
|
skipDecorators: jest.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as Config;
|
||||||
|
// @ts-ignore
|
||||||
|
getFallbackApisOrExit = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
Object.entries(apis).map(([alias, { root, ...api }]) => ({ ...api, path: root, alias }))
|
||||||
|
);
|
||||||
|
(getTotals as jest.Mock).mockReturnValue({
|
||||||
|
errors: 0,
|
||||||
|
warnings: 0,
|
||||||
|
ignored: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleBundle({
|
||||||
|
argv: { apis: [] }, // positional
|
||||||
|
version: 'test',
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(saveBundle).toBeCalledTimes(1);
|
||||||
|
expect(saveBundle).toHaveBeenCalledWith('output/foo.yaml', expect.any(String));
|
||||||
|
expect(process.stdout.write).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('should NOT store bundled API descriptions in the output files described in the apis section of config IF no there is a positional api provided', async () => {
|
it('should NOT store bundled API descriptions in the output files described in the apis section of config IF no there is a positional api provided', async () => {
|
||||||
const apis = {
|
const apis = {
|
||||||
foo: {
|
foo: {
|
||||||
@@ -240,5 +240,46 @@ describe('bundle', () => {
|
|||||||
expect(saveBundle).toBeCalledTimes(0);
|
expect(saveBundle).toBeCalledTimes(0);
|
||||||
expect(process.stdout.write).toHaveBeenCalledTimes(1);
|
expect(process.stdout.write).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should store bundled API descriptions in the directory specified in argv IF multiple positional apis provided AND --output specified', async () => {
|
||||||
|
const apis = {
|
||||||
|
foo: {
|
||||||
|
root: 'foo.yaml',
|
||||||
|
output: 'output/foo.yaml',
|
||||||
|
},
|
||||||
|
bar: {
|
||||||
|
root: 'bar.yaml',
|
||||||
|
output: 'output/bar.json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const config = {
|
||||||
|
apis,
|
||||||
|
styleguide: {
|
||||||
|
skipPreprocessors: jest.fn(),
|
||||||
|
skipDecorators: jest.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as Config;
|
||||||
|
// @ts-ignore
|
||||||
|
getFallbackApisOrExit = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce(
|
||||||
|
Object.entries(apis).map(([alias, { root, ...api }]) => ({ ...api, path: root, alias }))
|
||||||
|
);
|
||||||
|
(getTotals as jest.Mock).mockReturnValue({
|
||||||
|
errors: 0,
|
||||||
|
warnings: 0,
|
||||||
|
ignored: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleBundle({
|
||||||
|
argv: { apis: ['foo.yaml', 'bar.yaml'], output: 'dist' }, // cli options
|
||||||
|
version: 'test',
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(saveBundle).toBeCalledTimes(2);
|
||||||
|
expect(saveBundle).toHaveBeenNthCalledWith(1, 'dist/foo.yaml', expect.any(String));
|
||||||
|
expect(saveBundle).toHaveBeenNthCalledWith(2, 'dist/bar.yaml', expect.any(String));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -70,7 +70,13 @@ export async function handleBundle({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fileTotals = getTotals(problems);
|
const fileTotals = getTotals(problems);
|
||||||
const { outputFile, ext } = getOutputFileName(path, output || argv.output, argv.ext);
|
const { outputFile, ext } = getOutputFileName({
|
||||||
|
entrypoint: path,
|
||||||
|
output,
|
||||||
|
argvOutput: argv.output,
|
||||||
|
ext: argv.ext,
|
||||||
|
entries: argv?.apis?.length || 0,
|
||||||
|
});
|
||||||
|
|
||||||
if (fileTotals.errors === 0 || argv.force) {
|
if (fileTotals.errors === 0 || argv.force) {
|
||||||
if (!outputFile) {
|
if (!outputFile) {
|
||||||
|
|||||||
@@ -363,20 +363,40 @@ export function printConfigLintTotals(totals: Totals, command?: string | number)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOutputFileName(entrypoint: string, output?: string, ext?: BundleOutputFormat) {
|
export function getOutputFileName({
|
||||||
let outputFile = output;
|
entrypoint,
|
||||||
|
output,
|
||||||
|
argvOutput,
|
||||||
|
ext,
|
||||||
|
entries,
|
||||||
|
}: {
|
||||||
|
entrypoint: string;
|
||||||
|
output?: string;
|
||||||
|
argvOutput?: string;
|
||||||
|
ext?: BundleOutputFormat;
|
||||||
|
entries: number;
|
||||||
|
}) {
|
||||||
|
let outputFile = output || argvOutput;
|
||||||
if (!outputFile) {
|
if (!outputFile) {
|
||||||
return { ext: ext || 'yaml' };
|
return { ext: ext || 'yaml' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputFile) {
|
if (entries > 1 && argvOutput) {
|
||||||
ext = ext || (extname(outputFile).substring(1) as BundleOutputFormat);
|
ext = ext || (extname(entrypoint).substring(1) as BundleOutputFormat);
|
||||||
|
if (!outputExtensions.includes(ext)) {
|
||||||
|
throw new Error(`Invalid file extension: ${ext}.`);
|
||||||
|
}
|
||||||
|
outputFile = join(argvOutput, basename(entrypoint, extname(entrypoint))) + '.' + ext;
|
||||||
|
} else {
|
||||||
|
ext =
|
||||||
|
ext ||
|
||||||
|
(extname(outputFile).substring(1) as BundleOutputFormat) ||
|
||||||
|
(extname(entrypoint).substring(1) as BundleOutputFormat);
|
||||||
|
if (!outputExtensions.includes(ext)) {
|
||||||
|
throw new Error(`Invalid file extension: ${ext}.`);
|
||||||
|
}
|
||||||
|
outputFile = join(dirname(outputFile), basename(outputFile, extname(outputFile))) + '.' + ext;
|
||||||
}
|
}
|
||||||
ext = ext || (extname(entrypoint).substring(1) as BundleOutputFormat);
|
|
||||||
if (!outputExtensions.includes(ext)) {
|
|
||||||
throw new Error(`Invalid file extension: ${ext}.`);
|
|
||||||
}
|
|
||||||
outputFile = join(dirname(outputFile), basename(outputFile, extname(outputFile))) + '.' + ext;
|
|
||||||
return { outputFile, ext };
|
return { outputFile, ext };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user