diff --git a/.changeset/silent-facts-wonder.md b/.changeset/silent-facts-wonder.md new file mode 100644 index 0000000000..37b894b704 --- /dev/null +++ b/.changeset/silent-facts-wonder.md @@ -0,0 +1,7 @@ +--- +"@redocly/cli": patch +--- + +Fixed the `split` command to properly handle root-level paths. +Previously, the root path `/` was converted to an empty string as a filename, leading to incorrect file structure and broken links. +Now, it correctly maps to the specified path separator. diff --git a/docs/@v2/commands/split.md b/docs/@v2/commands/split.md index 0e34cf47c4..442dba3634 100644 --- a/docs/@v2/commands/split.md +++ b/docs/@v2/commands/split.md @@ -24,15 +24,15 @@ redocly split --version ## Options -| Option | Type | Description | -| ------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| api | string | **REQUIRED.** Path to the API description file that you want to split into a multi-file structure. | -| --config | string | Specify path to the [configuration file](../configuration/index.md). | -| --help | boolean | Show help. | -| --lint-config | string | Specify the severity level for the configuration file.
**Possible values:** `warn`, `error`, `off`. Default value is `warn`. | -| --outDir | string | **REQUIRED.** Path to the directory where you want to save the split files. If the specified directory doesn't exist, it is created automatically. | -| --separator | string | File path separator used while splitting. The default value is `_`. This controls the file names generated in the `paths` folder (e.g. `/users/create` path becomes `user_create.yaml`). | -| --version | boolean | Show version number. | +| Option | Type | Description | +| ------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| api | string | **REQUIRED.** Path to the API description file that you want to split into a multi-file structure. | +| --config | string | Specify path to the [configuration file](../configuration/index.md). | +| --help | boolean | Show help. | +| --lint-config | string | Specify the severity level for the configuration file.
**Possible values:** `warn`, `error`, `off`. Default value is `warn`. | +| --outDir | string | **REQUIRED.** Path to the directory where you want to save the split files. If the specified directory doesn't exist, it is created automatically. | +| --separator | string | File path separator used while splitting. The default value is `_`. This controls the file names generated in the `paths` folder (e.g. `/users/create` path becomes `user_create.yaml`, root level path `/` becomes `_.yaml`, and so on). | +| --version | boolean | Show version number. | ## Examples diff --git a/package-lock.json b/package-lock.json index e523b321bc..30d046c633 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12593,24 +12593,6 @@ } } }, - "node_modules/vite-node/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/vitest": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", @@ -12811,24 +12793,6 @@ } } }, - "node_modules/vitest/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", diff --git a/packages/cli/src/utils/miscellaneous.ts b/packages/cli/src/utils/miscellaneous.ts index 25bee21aec..6f1ff1abc0 100644 --- a/packages/cli/src/utils/miscellaneous.ts +++ b/packages/cli/src/utils/miscellaneous.ts @@ -121,11 +121,15 @@ export function printExecutionTime(commandName: string, startedAt: number, api: } export function pathToFilename(path: string, pathSeparator: string) { + if (path === '/') { + return pathSeparator; + } + return path - .replace(/~1/g, '/') - .replace(/~0/g, '~') + .replaceAll('~1', '/') + .replaceAll('~0', '~') .replace(/^\//, '') - .replace(/\//g, pathSeparator); + .replaceAll('/', pathSeparator); } export function escapeLanguageName(lang: string) { diff --git a/tests/e2e/commands.test.ts b/tests/e2e/commands.test.ts index bba15e30f4..b0d76367f7 100644 --- a/tests/e2e/commands.test.ts +++ b/tests/e2e/commands.test.ts @@ -375,6 +375,45 @@ describe('E2E', () => { const result = getCommandOutput(args, { testPath }); await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot.txt')); }); + + test('root endpoint correctly split', async () => { + const testPath = join(__dirname, `split/root-endpoint`); + + const args = getParams(indexEntryPoint, ['split', 'openapi.yaml', '--outDir=output']); + const result = getCommandOutput(args, { testPath }); + await expect(cleanupOutput(result)).toMatchFileSnapshot(join(testPath, 'snapshot.txt')); + }); + + test('root endpoint split and bundle again to the content', async () => { + const testPath = join(__dirname, `split/root-endpoint`); + + let args = getParams(indexEntryPoint, ['split', 'openapi.yaml', '--outDir=output']); + spawnSync('node', args, { + cwd: testPath, + env: { + ...process.env, + NODE_ENV: 'production', + NO_COLOR: 'TRUE', + }, + }); + + args = getParams(indexEntryPoint, [ + 'bundle', + 'output/openapi.yaml', + '-o=output/bundled.yaml', + ]); + getCommandOutput(args, { testPath }); + + const expected = readFileSync(join(testPath, 'openapi.yaml'), 'utf8'); + const actual = readFileSync(join(testPath, 'output/bundled.yaml'), 'utf8'); + + // Clean up output folder after splitting so the produced files do not interfere with other tests + spawnSync('rm', ['-rf', 'output'], { + cwd: testPath, + }); + + expect(actual).toEqual(expected); + }); }); describe('join', () => { diff --git a/tests/e2e/split/root-endpoint/openapi.yaml b/tests/e2e/split/root-endpoint/openapi.yaml new file mode 100644 index 0000000000..54ca42cd4c --- /dev/null +++ b/tests/e2e/split/root-endpoint/openapi.yaml @@ -0,0 +1,111 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Test + license: + name: MIT +servers: + - url: http://localhost/v1 +paths: + /: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pets' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' diff --git a/tests/e2e/split/root-endpoint/snapshot.txt b/tests/e2e/split/root-endpoint/snapshot.txt new file mode 100644 index 0000000000..3e6a8a0bc5 --- /dev/null +++ b/tests/e2e/split/root-endpoint/snapshot.txt @@ -0,0 +1,114 @@ + +type: object +required: + - code + - message +properties: + code: + type: integer + format: int32 + message: + type: string +type: object +required: + - id + - name +properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string +type: array +items: + $ref: ./Pet.yaml +get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pets' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +openapi: 3.0.0 +info: + version: 1.0.0 + title: Test + license: + name: MIT +servers: + - url: http://localhost/v1 +paths: + /: + $ref: paths/_.yaml + /pets/{petId}: + $ref: paths/pets_{petId}.yaml +🪓 Document: openapi.yaml is successfully split + and all related files are saved to the directory: output + +openapi.yaml: split processed in ms +