Skip to content

Commit a2267eb

Browse files
authored
fix(cli): sanitize filenames when saving components and datasources (#235)
- Add @sindresorhus/slugify dependency for filename sanitization - Implement sanitizeFilename utility function that converts strings to URL-friendly slugs - Update component and datasource pull actions to use sanitized filenames for separate file saves - Add comprehensive tests for the new sanitizeFilename function - Fixes issues with components/datasources containing special characters like "/" in their names fixes #225
1 parent 544dcd5 commit a2267eb

File tree

6 files changed

+49
-139
lines changed

6 files changed

+49
-139
lines changed

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"chalk": "^5.4.1",
4646
"commander": "^13.1.0",
4747
"dotenv": "^16.5.0",
48+
"filenamify": "^6.0.0",
4849
"json-schema-to-typescript": "^15.0.4",
4950
"minimatch": "^10.0.3",
5051
"octokit": "^5.0.3",

packages/cli/src/commands/components/pull/actions.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { handleAPIError, handleFileSystemError } from '../../../utils';
22
import type { SpaceComponent, SpaceComponentGroup, SpaceComponentInternalTag, SpaceComponentPreset, SpaceComponentsData } from '../constants';
33
import { join, resolve } from 'node:path';
4-
import { resolvePath, saveToFile } from '../../../utils/filesystem';
4+
import { resolvePath, sanitizeFilename, saveToFile } from '../../../utils/filesystem';
55
import type { SaveComponentsOptions } from './constants';
66
import { mapiClient } from '../../../api';
77
// Components
@@ -99,13 +99,14 @@ export const saveComponentsToFiles = async (
9999
if (separateFiles) {
100100
// Save in separate files without nested structure
101101
for (const component of components) {
102-
const componentFilePath = join(resolvedPath, suffix ? `${component.name}.${suffix}.json` : `${component.name}.json`);
102+
const sanitizedName = sanitizeFilename(component.name);
103+
const componentFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${suffix}.json` : `${sanitizedName}.json`);
103104
await saveToFile(componentFilePath, JSON.stringify(component, null, 2));
104105

105106
// Find and save associated presets
106107
const componentPresets = presets.filter(preset => preset.component_id === component.id);
107108
if (componentPresets.length > 0) {
108-
const presetsFilePath = join(resolvedPath, suffix ? `${component.name}.presets.${suffix}.json` : `${component.name}.presets.json`);
109+
const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.presets.${suffix}.json` : `${sanitizedName}.presets.json`);
109110
await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
110111
}
111112
// Always save groups in a consolidated file

packages/cli/src/commands/datasources/pull/actions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { handleAPIError, handleFileSystemError } from '../../../utils';
22
import { mapiClient } from '../../../api';
33
import { join, resolve } from 'node:path';
4-
import { resolvePath, saveToFile } from '../../../utils/filesystem';
4+
import { resolvePath, sanitizeFilename, saveToFile } from '../../../utils/filesystem';
55
import type { SpaceDatasource, SpaceDatasourceEntry } from '../constants';
66
import type { SaveDatasourcesOptions } from './constants';
77

@@ -83,7 +83,8 @@ export const saveDatasourcesToFiles = async (
8383
if (separateFiles) {
8484
// Save in separate files without nested structure
8585
for (const datasource of datasources) {
86-
const datasourceFilePath = join(resolvedPath, suffix ? `${datasource.name}.${suffix}.json` : `${datasource.name}.json`);
86+
const sanitizedName = sanitizeFilename(datasource.name);
87+
const datasourceFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${suffix}.json` : `${sanitizedName}.json`);
8788
await saveToFile(datasourceFilePath, JSON.stringify(datasource, null, 2));
8889
}
8990
return;

packages/cli/src/utils/filesystem.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
12
import { vol } from 'memfs';
2-
import { getComponentNameFromFilename, getStoryblokGlobalPath, resolvePath, saveToFile } from './filesystem';
3+
import { getComponentNameFromFilename, getStoryblokGlobalPath, resolvePath, sanitizeFilename, saveToFile } from './filesystem';
34
import { join, resolve } from 'node:path';
45

56
// tell vitest to use fs mock from __mocks__ folder
@@ -170,4 +171,13 @@ describe('filesystem utils', async () => {
170171
expect(getComponentNameFromFilename('/path/to/my_component.js')).toBe('/path/to/my_component');
171172
});
172173
});
174+
175+
describe('sanitizeFilename', () => {
176+
it('should convert strings to URL-friendly slugs', () => {
177+
expect(sanitizeFilename('Country / Currency')).toBe('Country _ Currency');
178+
expect(sanitizeFilename('path/to/file')).toBe('path_to_file');
179+
expect(sanitizeFilename('My Component Name')).toBe('My Component Name');
180+
expect(sanitizeFilename('Special@Characters!')).toBe('Special@Characters!');
181+
});
182+
});
173183
});

packages/cli/src/utils/filesystem.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { join, parse, resolve } from 'node:path';
22
import { mkdir, readFile as readFileImpl, writeFile } from 'node:fs/promises';
33
import { handleFileSystemError } from './error/filesystem-error';
44
import type { FileReaderResult } from '../types';
5+
import filenamify from 'filenamify';
56

67
export interface FileOptions {
78
mode?: number;
@@ -66,6 +67,18 @@ export const getComponentNameFromFilename = (filename: string): string => {
6667
return filename.replace(/\.js$/, '');
6768
};
6869

70+
/**
71+
* Sanitizes a string to be safe for use as a filename by removing/replacing problematic characters
72+
* https://github.com/parshap/node-sanitize-filename/blob/master/index.js
73+
* @param filename - The filename to sanitize
74+
* @returns A safe filename string
75+
*/
76+
export const sanitizeFilename = (filename: string): string => {
77+
return filenamify(filename, {
78+
replacement: '_',
79+
});
80+
};
81+
6982
export async function readJsonFile<T>(filePath: string): Promise<FileReaderResult<T>> {
7083
try {
7184
const content = (await readFile(filePath)).toString();

pnpm-lock.yaml

Lines changed: 17 additions & 133 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)