Skip to content

Commit 831bf94

Browse files
committed
add docs codemod command for scripts directory
1 parent 5b4cd43 commit 831bf94

File tree

4 files changed

+292
-1
lines changed

4 files changed

+292
-1
lines changed

scripts/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"check-package": "jiti ./check-package.ts",
1111
"docs:prettier:check": "cd ../docs && prettier --check ./_snippets || echo 'Please run \"docs:prettier:write\" in the \"scripts\" directory to fix the issues'",
1212
"docs:prettier:write": "cd ../docs && prettier --write ./_snippets",
13+
"docs:codemod": "jiti ./snippets/codemod.ts",
1314
"generate-sandboxes": "jiti ./sandbox/generate.ts",
1415
"get-report-message": "jiti ./get-report-message.ts",
1516
"get-template": "jiti ./get-template.ts",
@@ -131,6 +132,7 @@
131132
"fs-extra": "^11.2.0",
132133
"github-release-from-changelog": "^2.1.1",
133134
"glob": "^10.4.5",
135+
"globby": "^14.0.1",
134136
"http-server": "^14.1.1",
135137
"husky": "^4.3.7",
136138
"jiti": "^1.21.6",

scripts/snippets/codemod.ts

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/* eslint-disable @typescript-eslint/default-param-last */
2+
import os from 'node:os';
3+
import { join } from 'node:path';
4+
5+
import { program } from 'commander';
6+
import { promises as fs } from 'fs';
7+
import pLimit from 'p-limit';
8+
import picocolors from 'picocolors';
9+
import slash from 'slash';
10+
11+
import { configToCsfFactory } from '../../code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory';
12+
import { storyToCsfFactory } from '../../code/lib/cli-storybook/src/codemod/helpers/story-to-csf-factory';
13+
import { SNIPPETS_DIRECTORY } from '../utils/constants';
14+
15+
const logger = console;
16+
17+
export const maxConcurrentTasks = Math.max(1, os.cpus().length - 1);
18+
19+
type SnippetInfo = {
20+
path: string;
21+
source: string;
22+
attributes: {
23+
filename?: string;
24+
language?: string;
25+
renderer?: string;
26+
tabTitle?: string;
27+
highlightSyntax?: string;
28+
[key: string]: string;
29+
};
30+
};
31+
32+
type Codemod = {
33+
check: (snippetInfo: SnippetInfo) => boolean;
34+
transform: (snippetInfo: SnippetInfo) => string | Promise<string>;
35+
};
36+
37+
export async function runSnippetCodemod({
38+
glob,
39+
check,
40+
transform,
41+
dryRun = false,
42+
}: {
43+
glob: string;
44+
check: Codemod['check'];
45+
transform: Codemod['transform'];
46+
dryRun?: boolean;
47+
}) {
48+
let modifiedCount = 0;
49+
let unmodifiedCount = 0;
50+
let errorCount = 0;
51+
let skippedCount = 0;
52+
53+
try {
54+
// Dynamically import these packages because they are pure ESM modules
55+
// eslint-disable-next-line depend/ban-dependencies
56+
const { globby } = await import('globby');
57+
58+
const files = await globby(slash(glob), {
59+
followSymbolicLinks: true,
60+
ignore: ['node_modules/**', 'dist/**', 'storybook-static/**', 'build/**'],
61+
});
62+
63+
if (!files.length) {
64+
logger.error(`No files found for pattern ${glob}`);
65+
return;
66+
}
67+
68+
const limit = pLimit(10);
69+
70+
await Promise.all(
71+
files.map((file) =>
72+
limit(async () => {
73+
try {
74+
const source = await fs.readFile(file, 'utf-8');
75+
const snippets = extractSnippets(source);
76+
if (snippets.length === 0) {
77+
unmodifiedCount++;
78+
return;
79+
}
80+
81+
const targetSnippet = snippets.find(check);
82+
if (!targetSnippet) {
83+
skippedCount++;
84+
logger.log('Skipping file', file);
85+
return;
86+
}
87+
88+
const counterpartSnippets = snippets.filter((snippet) => {
89+
return (
90+
snippet !== targetSnippet &&
91+
snippet.attributes.renderer === targetSnippet.attributes.renderer &&
92+
snippet.attributes.language !== targetSnippet.attributes.language
93+
);
94+
});
95+
96+
const getSource = (snippet: SnippetInfo) =>
97+
`\n\`\`\`${formatAttributes(snippet.attributes)}\n${snippet.source}\n\`\`\`\n`;
98+
99+
try {
100+
let appendedContent = '';
101+
if (counterpartSnippets.length > 0) {
102+
appendedContent +=
103+
'\n<!-- js & ts-4-9 (when applicable) still needed while providing both CSF 3 & 4 -->\n';
104+
}
105+
106+
for (const snippet of [targetSnippet, ...counterpartSnippets]) {
107+
const newSnippet = { ...snippet };
108+
newSnippet.attributes.tabTitle = 'CSF 4 (experimental)';
109+
appendedContent += getSource({
110+
...newSnippet,
111+
attributes: {
112+
...newSnippet.attributes,
113+
renderer: 'react',
114+
tabTitle: 'CSF 4 (experimental)',
115+
},
116+
source: await transform(newSnippet),
117+
});
118+
}
119+
120+
const updatedSource = source + appendedContent;
121+
122+
if (!dryRun) {
123+
await fs.writeFile(file, updatedSource, 'utf-8');
124+
} else {
125+
logger.log(
126+
`Dry run: would have modified ${picocolors.yellow(file)} with \n` +
127+
picocolors.green(appendedContent)
128+
);
129+
}
130+
131+
modifiedCount++;
132+
} catch (transformError) {
133+
logger.error(`Error transforming snippet in file ${file}:`, transformError);
134+
errorCount++;
135+
}
136+
} catch (fileError) {
137+
logger.error(`Error processing file ${file}:`, fileError);
138+
errorCount++;
139+
}
140+
})
141+
)
142+
);
143+
} catch (error) {
144+
logger.error('Error applying snippet transform:', error);
145+
errorCount++;
146+
}
147+
148+
logger.log(
149+
`Summary: ${picocolors.green(`${modifiedCount} files modified`)}, ${picocolors.yellow(`${unmodifiedCount} files unmodified`)}, ${picocolors.gray(`${skippedCount} skipped`)}, ${picocolors.red(`${errorCount} errors`)}`
150+
);
151+
}
152+
153+
export function extractSnippets(source: string): SnippetInfo[] {
154+
const snippetRegex =
155+
/```(?<highlightSyntax>[a-zA-Z0-9]+)?(?<attributes>[^\n]*)\n(?<content>[\s\S]*?)```/g;
156+
const snippets: SnippetInfo[] = [];
157+
let match;
158+
159+
while ((match = snippetRegex.exec(source)) !== null) {
160+
const { highlightSyntax, attributes, content } = match.groups || {};
161+
const snippetAttributes = parseAttributes(attributes || '');
162+
if (highlightSyntax) {
163+
snippetAttributes.highlightSyntax = highlightSyntax.trim();
164+
}
165+
166+
snippets.push({
167+
path: snippetAttributes.filename || '',
168+
source: content.trim(),
169+
attributes: snippetAttributes,
170+
});
171+
}
172+
173+
return snippets;
174+
}
175+
176+
export function parseAttributes(attributes: string): Record<string, string> {
177+
const attributeRegex = /([a-zA-Z0-9.-]+)="([^"]+)"/g;
178+
const result: Record<string, string> = {};
179+
let match;
180+
181+
while ((match = attributeRegex.exec(attributes)) !== null) {
182+
result[match[1]] = match[2];
183+
}
184+
185+
return result;
186+
}
187+
188+
function formatAttributes(attributes: Record<string, string>): string {
189+
const formatted = Object.entries(attributes)
190+
.filter(([key]) => key !== 'highlightSyntax')
191+
.map(([key, value]) => `${key}="${value}"`)
192+
.join(' ');
193+
return `${attributes.highlightSyntax || 'js'} ${formatted}`;
194+
}
195+
196+
const codemods: Record<string, Codemod> = {
197+
'csf-factory-story': {
198+
check: (snippetInfo: SnippetInfo) => {
199+
return (
200+
snippetInfo.path.includes('.stories') &&
201+
snippetInfo.attributes.tabTitle !== 'CSF 4 (experimental)' &&
202+
snippetInfo.attributes.language === 'ts' &&
203+
(snippetInfo.attributes.renderer === 'react' ||
204+
snippetInfo.attributes.renderer === 'common')
205+
);
206+
},
207+
transform: storyToCsfFactory,
208+
},
209+
'csf-factory-config': {
210+
check: (snippetInfo: SnippetInfo) => {
211+
return (
212+
snippetInfo.attributes.tabTitle !== 'CSF 4 (experimental)' &&
213+
(snippetInfo.path.includes('preview') || snippetInfo.path.includes('main'))
214+
);
215+
},
216+
transform: (snippetInfo: SnippetInfo) => {
217+
const configType = snippetInfo.path.includes('preview') ? 'preview' : 'main';
218+
return configToCsfFactory(snippetInfo, {
219+
configType,
220+
frameworkPackage: '@storybook/your-framework',
221+
});
222+
},
223+
},
224+
};
225+
226+
program
227+
.name('command')
228+
.description('A minimal CLI for demonstration')
229+
.argument('<id>', 'ID to process')
230+
.requiredOption('--glob <pattern>', 'Glob pattern to match')
231+
.option('--dry-run', 'Run without making actual changes', false)
232+
.action(async (id, { glob, dryRun }) => {
233+
const codemod = codemods[id as keyof typeof codemods];
234+
if (!codemod) {
235+
logger.error(`Unknown codemod "${id}"`);
236+
logger.log(
237+
`\n\nAvailable codemods: ${Object.keys(codemods)
238+
.map((c) => `\n- ${c}`)
239+
.join('')}`
240+
);
241+
process.exit(1);
242+
}
243+
244+
await runSnippetCodemod({
245+
glob: join(SNIPPETS_DIRECTORY, glob),
246+
dryRun,
247+
...codemod,
248+
});
249+
});
250+
251+
// Parse and validate arguments
252+
program.parse(process.argv);

scripts/utils/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const BEFORE_DIR_NAME = 'before-storybook';
55

66
export const ROOT_DIRECTORY = join(__dirname, '..', '..');
77
export const CODE_DIRECTORY = join(ROOT_DIRECTORY, 'code');
8+
export const SNIPPETS_DIRECTORY = join(ROOT_DIRECTORY, 'docs', '_snippets');
89
export const PACKS_DIRECTORY = join(ROOT_DIRECTORY, 'packs');
910
export const REPROS_DIRECTORY = join(ROOT_DIRECTORY, 'repros');
1011
export const SANDBOX_DIRECTORY = join(ROOT_DIRECTORY, 'sandbox');

scripts/yarn.lock

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1538,6 +1538,13 @@ __metadata:
15381538
languageName: node
15391539
linkType: hard
15401540

1541+
"@sindresorhus/merge-streams@npm:^2.1.0":
1542+
version: 2.3.0
1543+
resolution: "@sindresorhus/merge-streams@npm:2.3.0"
1544+
checksum: 10c0/69ee906f3125fb2c6bb6ec5cdd84e8827d93b49b3892bce8b62267116cc7e197b5cccf20c160a1d32c26014ecd14470a72a5e3ee37a58f1d6dadc0db1ccf3894
1545+
languageName: node
1546+
linkType: hard
1547+
15411548
"@snyk/github-codeowners@npm:1.1.0":
15421549
version: 1.1.0
15431550
resolution: "@snyk/github-codeowners@npm:1.1.0"
@@ -1673,6 +1680,7 @@ __metadata:
16731680
fs-extra: "npm:^11.2.0"
16741681
github-release-from-changelog: "npm:^2.1.1"
16751682
glob: "npm:^10.4.5"
1683+
globby: "npm:^14.0.1"
16761684
http-server: "npm:^14.1.1"
16771685
husky: "npm:^4.3.7"
16781686
jiti: "npm:^1.21.6"
@@ -7121,6 +7129,20 @@ __metadata:
71217129
languageName: node
71227130
linkType: hard
71237131

7132+
"globby@npm:^14.0.1":
7133+
version: 14.0.2
7134+
resolution: "globby@npm:14.0.2"
7135+
dependencies:
7136+
"@sindresorhus/merge-streams": "npm:^2.1.0"
7137+
fast-glob: "npm:^3.3.2"
7138+
ignore: "npm:^5.2.4"
7139+
path-type: "npm:^5.0.0"
7140+
slash: "npm:^5.1.0"
7141+
unicorn-magic: "npm:^0.1.0"
7142+
checksum: 10c0/3f771cd683b8794db1e7ebc8b6b888d43496d93a82aad4e9d974620f578581210b6c5a6e75ea29573ed16a1345222fab6e9b877a8d1ed56eeb147e09f69c6f78
7143+
languageName: node
7144+
linkType: hard
7145+
71247146
"globby@npm:^7.1.1":
71257147
version: 7.1.1
71267148
resolution: "globby@npm:7.1.1"
@@ -11055,6 +11077,13 @@ __metadata:
1105511077
languageName: node
1105611078
linkType: hard
1105711079

11080+
"path-type@npm:^5.0.0":
11081+
version: 5.0.0
11082+
resolution: "path-type@npm:5.0.0"
11083+
checksum: 10c0/e8f4b15111bf483900c75609e5e74e3fcb79f2ddb73e41470028fcd3e4b5162ec65da9907be077ee5012c18801ff7fffb35f9f37a077f3f81d85a0b7d6578efd
11084+
languageName: node
11085+
linkType: hard
11086+
1105811087
"pathe@npm:^2.0.0":
1105911088
version: 2.0.1
1106011089
resolution: "pathe@npm:2.0.1"
@@ -13262,7 +13291,7 @@ __metadata:
1326213291
languageName: node
1326313292
linkType: hard
1326413293

13265-
"slash@npm:^5.0.0":
13294+
"slash@npm:^5.0.0, slash@npm:^5.1.0":
1326613295
version: 5.1.0
1326713296
resolution: "slash@npm:5.1.0"
1326813297
checksum: 10c0/eb48b815caf0bdc390d0519d41b9e0556a14380f6799c72ba35caf03544d501d18befdeeef074bc9c052acf69654bc9e0d79d7f1de0866284137a40805299eb3
@@ -14504,6 +14533,13 @@ __metadata:
1450414533
languageName: node
1450514534
linkType: hard
1450614535

14536+
"unicorn-magic@npm:^0.1.0":
14537+
version: 0.1.0
14538+
resolution: "unicorn-magic@npm:0.1.0"
14539+
checksum: 10c0/e4ed0de05b0a05e735c7d8a2930881e5efcfc3ec897204d5d33e7e6247f4c31eac92e383a15d9a6bccb7319b4271ee4bea946e211bf14951fec6ff2cbbb66a92
14540+
languageName: node
14541+
linkType: hard
14542+
1450714543
"unified-args@npm:^11.0.0":
1450814544
version: 11.0.1
1450914545
resolution: "unified-args@npm:11.0.1"

0 commit comments

Comments
 (0)