Skip to content

Commit 2e165ea

Browse files
committed
refactor: extract singleQuote utility
1 parent c87fde0 commit 2e165ea

File tree

4 files changed

+37
-15
lines changed

4 files changed

+37
-15
lines changed

packages/create-cli/src/lib/setup/codegen.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import path from 'node:path';
22
import type { CategoryRef } from '@code-pushup/models';
3-
import { mergeCategoriesBySlug, toUnixPath } from '@code-pushup/utils';
3+
import {
4+
mergeCategoriesBySlug,
5+
singleQuote,
6+
toUnixPath,
7+
} from '@code-pushup/utils';
48
import type {
59
ConfigFileFormat,
610
ImportDeclarationStructure,
@@ -195,15 +199,12 @@ function addCategories(
195199
categories.forEach(({ slug, title, description, docsUrl, refs }) => {
196200
builder.addLine('{', depth + 1);
197201
builder.addLine(`slug: '${slug}',`, depth + 2);
198-
builder.addLine(`title: ${toJsStringLiteral(title)},`, depth + 2);
202+
builder.addLine(`title: ${singleQuote(title)},`, depth + 2);
199203
if (description) {
200-
builder.addLine(
201-
`description: ${toJsStringLiteral(description)},`,
202-
depth + 2,
203-
);
204+
builder.addLine(`description: ${singleQuote(description)},`, depth + 2);
204205
}
205206
if (docsUrl) {
206-
builder.addLine(`docsUrl: ${toJsStringLiteral(docsUrl)},`, depth + 2);
207+
builder.addLine(`docsUrl: ${singleQuote(docsUrl)},`, depth + 2);
207208
}
208209
builder.addLine('refs: [', depth + 2);
209210
builder.addLines(refs.map(formatCategoryRef), depth + 3);
@@ -216,11 +217,3 @@ function addCategories(
216217
function formatCategoryRef(ref: CategoryRef): string {
217218
return `{ type: '${ref.type}', plugin: '${ref.plugin}', slug: '${ref.slug}', weight: ${ref.weight} },`;
218219
}
219-
220-
/** Wraps a value in single-quoted JS string literal with special characters escaped. */
221-
function toJsStringLiteral(value: string): string {
222-
const inner = JSON.stringify(value)
223-
.slice(1, -1)
224-
.replace(/'/g, String.raw`\'`);
225-
return `'${inner}'`;
226-
}

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export {
6060
pluralizeToken,
6161
roundDecimals,
6262
serializeCommandWithArgs,
63+
singleQuote,
6364
slugify,
6465
transformLines,
6566
truncateDescription,

packages/utils/src/lib/formatting.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,12 @@ export function formatCoveragePercentage(stats: {
196196
const percentage = (covered / total) * 100;
197197
return `${percentage.toFixed(1)}%`;
198198
}
199+
200+
/** Escapes a string and wraps it in single quotes for use in JS code. */
201+
export function singleQuote(value: string): string {
202+
const inner = JSON.stringify(value)
203+
.slice(1, -1)
204+
.replace(/\\"/g, '"')
205+
.replace(/'/g, String.raw`\'`);
206+
return `'${inner}'`;
207+
}

packages/utils/src/lib/formatting.unit.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
pluralizeToken,
1111
roundDecimals,
1212
serializeCommandWithArgs,
13+
singleQuote,
1314
slugify,
1415
transformLines,
1516
truncateMultilineText,
@@ -320,3 +321,21 @@ describe('formatCoveragePercentage', () => {
320321
expect(formatCoveragePercentage({ covered: 0, total: 0 })).toBe('-');
321322
});
322323
});
324+
325+
describe('singleQuote', () => {
326+
it.each([
327+
['hello', "'hello'"],
328+
["it's", String.raw`'it\'s'`],
329+
[String.raw`back\slash`, String.raw`'back\\slash'`],
330+
['line\nbreak', String.raw`'line\nbreak'`],
331+
])(
332+
'should escape %j for use in a single-quoted JS literal',
333+
(input, expected) => {
334+
expect(singleQuote(input)).toBe(expected);
335+
},
336+
);
337+
338+
it('should leave double quotes unescaped', () => {
339+
expect(singleQuote('say "hi"')).toBe(`'say "hi"'`);
340+
});
341+
});

0 commit comments

Comments
 (0)