@storybook/codemod 7.0.0-beta.51 → 7.0.0-beta.53

Sign up to get free protection for your applications and to get access to all the features.
package/jest.config.js CHANGED
@@ -5,4 +5,5 @@ const baseConfig = require('../../jest.config.node');
5
5
  module.exports = {
6
6
  ...baseConfig,
7
7
  displayName: __dirname.split(path.sep).slice(-2).join(path.posix.sep),
8
+ resetMocks: true,
8
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook/codemod",
3
- "version": "7.0.0-beta.51",
3
+ "version": "7.0.0-beta.53",
4
4
  "description": "A collection of codemod scripts written with JSCodeshift",
5
5
  "keywords": [
6
6
  "storybook"
@@ -24,13 +24,13 @@
24
24
  ".": {
25
25
  "node": "./dist/index.js",
26
26
  "require": "./dist/index.js",
27
- "import": "./dist/index.mjs",
28
27
  "types": "./dist/index.d.ts"
29
28
  },
30
29
  "./dist/transforms/add-component-parameters.js": "./dist/transforms/add-component-parameters.js",
31
30
  "./dist/transforms/csf-2-to-3.js": "./dist/transforms/csf-2-to-3.js",
32
31
  "./dist/transforms/csf-hoist-story-annotations.js": "./dist/transforms/csf-hoist-story-annotations.js",
33
32
  "./dist/transforms/move-builtin-addons.js": "./dist/transforms/move-builtin-addons.js",
33
+ "./dist/transforms/mdx-to-csf.js": "./dist/transforms/mdx-to-csf.js",
34
34
  "./dist/transforms/storiesof-to-csf.js": "./dist/transforms/storiesof-to-csf.js",
35
35
  "./dist/transforms/update-addon-info.js": "./dist/transforms/update-addon-info.js",
36
36
  "./dist/transforms/update-organisation-name.js": "./dist/transforms/update-organisation-name.js",
@@ -39,7 +39,6 @@
39
39
  "./package.json": "./package.json"
40
40
  },
41
41
  "main": "dist/index.js",
42
- "module": "dist/index.mjs",
43
42
  "types": "dist/index.d.ts",
44
43
  "scripts": {
45
44
  "check": "../../../scripts/node_modules/.bin/tsc --noEmit",
@@ -50,9 +49,9 @@
50
49
  "@babel/preset-env": "^7.20.2",
51
50
  "@babel/types": "^7.20.7",
52
51
  "@storybook/csf": "next",
53
- "@storybook/csf-tools": "7.0.0-beta.51",
54
- "@storybook/node-logger": "7.0.0-beta.51",
55
- "@storybook/types": "7.0.0-beta.51",
52
+ "@storybook/csf-tools": "7.0.0-beta.53",
53
+ "@storybook/node-logger": "7.0.0-beta.53",
54
+ "@storybook/types": "7.0.0-beta.53",
56
55
  "cross-spawn": "^7.0.3",
57
56
  "globby": "^11.0.2",
58
57
  "jscodeshift": "^0.14.0",
@@ -66,25 +65,37 @@
66
65
  "ansi-regex": "^5.0.1",
67
66
  "jest": "^29.3.1",
68
67
  "jest-specific-snapshot": "^7.0.0",
69
- "typescript": "~4.9.3"
68
+ "mdast-util-mdx-jsx": "^2.1.2",
69
+ "mdast-util-mdxjs-esm": "^1.3.1",
70
+ "remark": "^14.0.2",
71
+ "remark-mdx": "^2.2.1",
72
+ "ts-dedent": "^2.2.0",
73
+ "typescript": "~4.9.3",
74
+ "unist-util-is": "^5.2.0",
75
+ "unist-util-select": "^4.0.3",
76
+ "unist-util-visit": "^4.1.2",
77
+ "vfile": "^5.3.7"
70
78
  },
71
79
  "publishConfig": {
72
80
  "access": "public"
73
81
  },
74
82
  "bundler": {
75
- "platform": "node",
76
83
  "entries": [
77
84
  "./src/index.js",
78
85
  "./src/transforms/add-component-parameters.js",
79
86
  "./src/transforms/csf-2-to-3.ts",
80
87
  "./src/transforms/csf-hoist-story-annotations.js",
88
+ "./src/transforms/mdx-to-csf.ts",
81
89
  "./src/transforms/move-builtin-addons.js",
82
90
  "./src/transforms/storiesof-to-csf.js",
83
91
  "./src/transforms/update-addon-info.js",
84
92
  "./src/transforms/update-organisation-name.js",
85
93
  "./src/transforms/upgrade-deprecated-types.ts",
86
94
  "./src/transforms/upgrade-hierarchy-separators.js"
95
+ ],
96
+ "formats": [
97
+ "cjs"
87
98
  ]
88
99
  },
89
- "gitHead": "92ae17be35e8e19be160d4d8acb9b56e40064be2"
100
+ "gitHead": "b1da06450dc3e4124a935785a2041b18204533ae"
90
101
  }
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-param-reassign */
1
2
  /* eslint import/prefer-default-export: "off" */
2
3
  import fs from 'fs';
3
4
  import path from 'path';
@@ -55,16 +56,22 @@ export async function runCodemod(codemod, { glob, logger, dryRun, rename, parser
55
56
 
56
57
  const files = await globby([glob, '!**/node_modules', '!**/dist']);
57
58
  logger.log(`=> Applying ${codemod}: ${files.length} files`);
58
- if (!dryRun) {
59
+ if (files.length === 0) {
60
+ logger.log(`=> No matching files for glob: ${glob}`);
61
+ return;
62
+ }
63
+
64
+ if (!dryRun && files.length > 0) {
59
65
  const parserArgs = inferredParser ? ['--parser', inferredParser] : [];
60
- spawnSync(
61
- 'npx',
66
+ const result = spawnSync(
67
+ 'node',
62
68
  [
63
- 'jscodeshift',
69
+ require.resolve('jscodeshift/bin/jscodeshift'),
64
70
  // this makes sure codeshift doesn't transform our own source code with babel
65
71
  // which is faster, and also makes sure the user won't see babel messages such as:
66
72
  // [BABEL] Note: The code generator has deoptimised the styling of repo/node_modules/prettier/index.js as it exceeds the max of 500KB.
67
73
  '--no-babel',
74
+ '--fail-on-error',
68
75
  '-t',
69
76
  `${TRANSFORM_DIR}/${codemod}.js`,
70
77
  ...parserArgs,
@@ -75,6 +82,15 @@ export async function runCodemod(codemod, { glob, logger, dryRun, rename, parser
75
82
  shell: true,
76
83
  }
77
84
  );
85
+ if (result.status === 1) {
86
+ logger.log('Skipped renaming because of errors.');
87
+ return;
88
+ }
89
+ }
90
+
91
+ if (!renameParts && codemod === 'mdx-to-csf') {
92
+ renameParts = ['.stories.mdx', '.mdx'];
93
+ rename = '.stories.mdx:.mdx;';
78
94
  }
79
95
 
80
96
  if (renameParts) {
@@ -0,0 +1,574 @@
1
+ import * as fs_ from 'node:fs';
2
+ import { expect, test } from '@jest/globals';
3
+ import dedent from 'ts-dedent';
4
+ import jscodeshift, { nameToValidExport } from '../mdx-to-csf';
5
+
6
+ expect.addSnapshotSerializer({
7
+ print: (val: any) => (typeof val === 'string' ? val : JSON.stringify(val, null, 2) ?? ''),
8
+ test: () => true,
9
+ });
10
+
11
+ jest.mock('node:fs');
12
+ const fs = fs_ as jest.Mocked<typeof import('node:fs')>;
13
+
14
+ beforeEach(() => {
15
+ fs.existsSync.mockImplementation(() => false);
16
+ });
17
+
18
+ test('drop invalid story nodes', () => {
19
+ const input = dedent`
20
+ import { Meta } from '@storybook/addon-docs';
21
+
22
+ <Meta title="Foobar" />
23
+
24
+ <Story>No name!</Story>
25
+
26
+ <Story name="Primary">Story</Story>
27
+
28
+ `;
29
+
30
+ const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
31
+
32
+ expect(mdx).toMatchInlineSnapshot(`
33
+ import { Meta } from '@storybook/addon-docs';
34
+
35
+ <Meta of={FoobarStories} />
36
+
37
+
38
+
39
+ <Story of={FoobarStories.Primary} />
40
+
41
+ `);
42
+ });
43
+
44
+ test('convert story re-definition', () => {
45
+ const input = dedent`
46
+ import { Meta, Story } from '@storybook/addon-docs';
47
+ import { Primary } from './Foobar.stories';
48
+
49
+ <Meta title="Foobar" />
50
+
51
+ <Story story={Primary} />
52
+ `;
53
+
54
+ fs.existsSync.mockImplementation((path) => path === 'Foobar.stories.js');
55
+
56
+ const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
57
+
58
+ expect(mdx).toMatchInlineSnapshot(`
59
+ import { Meta, Story } from '@storybook/blocks';
60
+ import { Primary } from './Foobar.stories';
61
+ import * as FoobarStories from './Foobar_.stories';
62
+
63
+ <Meta of={FoobarStories} />
64
+
65
+ <Story of={FoobarStories.Primary} />
66
+
67
+ `);
68
+ const [csfFileName, csf] = fs.writeFileSync.mock.calls[0];
69
+ expect(csfFileName).toMatchInlineSnapshot(`Foobar_.stories.js`);
70
+ expect(csf).toMatchInlineSnapshot(`
71
+ import { Primary } from './Foobar.stories';
72
+
73
+ export default {
74
+ title: 'Foobar',
75
+ };
76
+
77
+ export { Primary };
78
+
79
+ `);
80
+ });
81
+
82
+ test('Comment out story nodes with id', () => {
83
+ const input = dedent`
84
+ import { Meta, Story } from '@storybook/addon-docs';
85
+
86
+ <Meta title="Foobar" />
87
+
88
+ <Story id="button--primary" />
89
+ `;
90
+
91
+ const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
92
+
93
+ expect(mdx).toMatchInlineSnapshot(`
94
+ import { Meta, Story } from '@storybook/blocks';
95
+ import * as FoobarStories from './Foobar.stories';
96
+
97
+ <Meta of={FoobarStories} />
98
+
99
+ {/* <Story id="button--primary" /> is deprecated, please migrate it to <Story of={referenceToStory} /> */}
100
+
101
+ <Story id="button--primary" />
102
+
103
+ `);
104
+ });
105
+
106
+ test('convert correct story nodes', () => {
107
+ const input = dedent`
108
+ import { Meta, Story } from '@storybook/addon-docs';
109
+
110
+ <Meta title="Foobar" />
111
+
112
+ <Story name="Primary">Story</Story>
113
+ `;
114
+
115
+ const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
116
+
117
+ expect(mdx).toMatchInlineSnapshot(`
118
+ import { Meta, Story } from '@storybook/blocks';
119
+ import * as FoobarStories from './Foobar.stories';
120
+
121
+ <Meta of={FoobarStories} />
122
+
123
+ <Story of={FoobarStories.Primary} />
124
+
125
+ `);
126
+
127
+ const [, csf] = fs.writeFileSync.mock.calls[0];
128
+ expect(csf).toMatchInlineSnapshot(`
129
+ export default {
130
+ title: 'Foobar',
131
+ };
132
+
133
+ export const Primary = {
134
+ render: () => 'Story',
135
+ name: 'Primary',
136
+ };
137
+
138
+ `);
139
+ });
140
+
141
+ test('convert story nodes with spaces', () => {
142
+ const input = dedent`
143
+ import { Meta, Story } from '@storybook/addon-docs';
144
+
145
+ <Meta title="Foobar" />
146
+
147
+ <Story name="Primary Space">Story</Story>
148
+ `;
149
+
150
+ const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
151
+
152
+ expect(mdx).toMatchInlineSnapshot(`
153
+ import { Meta, Story } from '@storybook/blocks';
154
+ import * as FoobarStories from './Foobar.stories';
155
+
156
+ <Meta of={FoobarStories} />
157
+
158
+ <Story of={FoobarStories.PrimarySpace} />
159
+
160
+ `);
161
+
162
+ const [, csf] = fs.writeFileSync.mock.calls[0];
163
+ expect(csf).toMatchInlineSnapshot(`
164
+ export default {
165
+ title: 'Foobar',
166
+ };
167
+
168
+ export const PrimarySpace = {
169
+ render: () => 'Story',
170
+ name: 'Primary Space',
171
+ };
172
+
173
+ `);
174
+ });
175
+
176
+ test('extract esm into csf head code', () => {
177
+ const input = dedent`
178
+ import { Meta, Story } from '@storybook/addon-docs';
179
+ import { Button } from './Button';
180
+
181
+ # hello
182
+
183
+ export const args = { bla: 1 };
184
+ export const Template = (args) => <Button {...args} />;
185
+
186
+ <Meta title="foobar" />
187
+
188
+ world {2 + 1}
189
+
190
+ <Story name="foo">bar</Story>
191
+
192
+ <Story
193
+ name="Unchecked"
194
+ args={{
195
+ ...args,
196
+ label: 'Unchecked',
197
+ }}>
198
+ {Template.bind({})}
199
+ </Story>
200
+ `;
201
+
202
+ const mdx = jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
203
+
204
+ expect(mdx).toMatchInlineSnapshot(`
205
+ import { Meta, Story } from '@storybook/blocks';
206
+ import { Button } from './Button';
207
+ import * as FoobarStories from './Foobar.stories';
208
+
209
+ # hello
210
+
211
+ export const args = { bla: 1 };
212
+ export const Template = (args) => <Button {...args} />;
213
+
214
+ <Meta of={FoobarStories} />
215
+
216
+ world {2 + 1}
217
+
218
+ <Story of={FoobarStories.Foo} />
219
+
220
+ <Story of={FoobarStories.Unchecked} />
221
+
222
+ `);
223
+
224
+ const [csfFileName, csf] = fs.writeFileSync.mock.calls[0];
225
+ expect(csfFileName).toMatchInlineSnapshot(`Foobar.stories.js`);
226
+ expect(csf).toMatchInlineSnapshot(`
227
+ import { Button } from './Button';
228
+
229
+ const args = { bla: 1 };
230
+ const Template = (args) => <Button {...args} />;
231
+
232
+ export default {
233
+ title: 'foobar',
234
+ };
235
+
236
+ export const Foo = {
237
+ render: () => 'bar',
238
+ name: 'foo',
239
+ };
240
+
241
+ export const Unchecked = {
242
+ render: Template.bind({}),
243
+ name: 'Unchecked',
244
+
245
+ args: {
246
+ ...args,
247
+ label: 'Unchecked',
248
+ },
249
+ };
250
+
251
+ `);
252
+ });
253
+
254
+ test('extract all meta parameters', () => {
255
+ const input = dedent`
256
+ import { Meta, Story } from '@storybook/addon-docs';
257
+
258
+ export const args = { bla: 1 };
259
+
260
+ <Meta title="foobar" args={{...args}} parameters={{a: '1'}} />
261
+
262
+ <Story name="foo">bar</Story>
263
+ `;
264
+
265
+ jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
266
+
267
+ const [, csf] = fs.writeFileSync.mock.calls[0];
268
+
269
+ expect(csf).toMatchInlineSnapshot(`
270
+ const args = { bla: 1 };
271
+
272
+ export default {
273
+ title: 'foobar',
274
+
275
+ args: {
276
+ ...args,
277
+ },
278
+
279
+ parameters: {
280
+ a: '1',
281
+ },
282
+ };
283
+
284
+ export const Foo = {
285
+ render: () => 'bar',
286
+ name: 'foo',
287
+ };
288
+
289
+ `);
290
+ });
291
+
292
+ test('extract all story attributes', () => {
293
+ const input = dedent`
294
+ import { Meta, Story } from '@storybook/addon-docs';
295
+ import { Button } from './Button';
296
+
297
+ export const args = { bla: 1 };
298
+ export const Template = (args) => <Button {...args} />;
299
+
300
+ <Meta title="foobar" />
301
+
302
+ <Story name="foo">bar</Story>
303
+
304
+ <Story
305
+ name="Unchecked"
306
+ args={{
307
+ ...args,
308
+ label: 'Unchecked',
309
+ }}>
310
+ {Template.bind({})}
311
+ </Story>
312
+
313
+ <Story name="Second">{Template.bind({})}</Story>
314
+
315
+ `;
316
+
317
+ jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
318
+
319
+ const [, csf] = fs.writeFileSync.mock.calls[0];
320
+
321
+ expect(csf).toMatchInlineSnapshot(`
322
+ import { Button } from './Button';
323
+
324
+ const args = { bla: 1 };
325
+ const Template = (args) => <Button {...args} />;
326
+
327
+ export default {
328
+ title: 'foobar',
329
+ };
330
+
331
+ export const Foo = {
332
+ render: () => 'bar',
333
+ name: 'foo',
334
+ };
335
+
336
+ export const Unchecked = {
337
+ render: Template.bind({}),
338
+ name: 'Unchecked',
339
+
340
+ args: {
341
+ ...args,
342
+ label: 'Unchecked',
343
+ },
344
+ };
345
+
346
+ export const Second = {
347
+ render: Template.bind({}),
348
+ name: 'Second',
349
+ };
350
+
351
+ `);
352
+ });
353
+
354
+ test('duplicate story name', () => {
355
+ const input = dedent`
356
+ import { Meta, Story } from '@storybook/addon-docs';
357
+ import { Button } from './Button';
358
+
359
+ export const Default = (args) => <Button {...args} />;
360
+
361
+ <Meta title="Button" />
362
+
363
+ <Story name="Default">
364
+ {Default.bind({})}
365
+ </Story>
366
+
367
+ <Story name="Second">{Default.bind({})}</Story>
368
+
369
+ `;
370
+
371
+ jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
372
+
373
+ const [, csf] = fs.writeFileSync.mock.calls[0];
374
+
375
+ expect(csf).toMatchInlineSnapshot(`
376
+ import { Button } from './Button';
377
+
378
+ const Default = (args) => <Button {...args} />;
379
+
380
+ export default {
381
+ title: 'Button',
382
+ };
383
+
384
+ export const Default_ = {
385
+ render: Default.bind({}),
386
+ name: 'Default',
387
+ };
388
+
389
+ export const Second = {
390
+ render: Default.bind({}),
391
+ name: 'Second',
392
+ };
393
+
394
+ `);
395
+ });
396
+
397
+ test('kebab case file name', () => {
398
+ const input = dedent`
399
+ import { Meta, Story } from '@storybook/addon-docs';
400
+ import { Kebab } from './my-component/some-kebab-case';
401
+
402
+ export const Template = (args) => <Kebab {...args} />;
403
+
404
+ <Meta title="Kebab" />
405
+
406
+ <Story name="Much-Kebab">
407
+ {Template.bind({})}
408
+ </Story>
409
+
410
+ <Story name="Really-Much-Kebab">{Template.bind({})}</Story>
411
+
412
+ `;
413
+
414
+ const mdx = jscodeshift({ source: input, path: 'some-kebab-case.stories.mdx' });
415
+
416
+ expect(mdx).toMatchInlineSnapshot(`
417
+ import { Meta, Story } from '@storybook/blocks';
418
+ import { Kebab } from './my-component/some-kebab-case';
419
+ import * as SomeKebabCaseStories from './some-kebab-case.stories';
420
+
421
+ export const Template = (args) => <Kebab {...args} />;
422
+
423
+ <Meta of={SomeKebabCaseStories} />
424
+
425
+ <Story of={SomeKebabCaseStories.MuchKebab} />
426
+
427
+ <Story of={SomeKebabCaseStories.ReallyMuchKebab} />
428
+
429
+ `);
430
+
431
+ const [, csf] = fs.writeFileSync.mock.calls[0];
432
+
433
+ expect(csf).toMatchInlineSnapshot(`
434
+ import { Kebab } from './my-component/some-kebab-case';
435
+
436
+ const Template = (args) => <Kebab {...args} />;
437
+
438
+ export default {
439
+ title: 'Kebab',
440
+ };
441
+
442
+ export const MuchKebab = {
443
+ render: Template.bind({}),
444
+ name: 'Much-Kebab',
445
+ };
446
+
447
+ export const ReallyMuchKebab = {
448
+ render: Template.bind({}),
449
+ name: 'Really-Much-Kebab',
450
+ };
451
+
452
+ `);
453
+ });
454
+
455
+ test('story child is jsx', () => {
456
+ const input = dedent`
457
+ import { Canvas, Meta, Story } from '@storybook/addon-docs';
458
+ import { Button } from './button';
459
+
460
+ <Story name="Primary">
461
+ <Button>
462
+ <div>Hello!</div>
463
+ </Button>
464
+ </Story>
465
+ `;
466
+
467
+ jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
468
+
469
+ const [, csf] = fs.writeFileSync.mock.calls[0];
470
+
471
+ expect(csf).toMatchInlineSnapshot(`
472
+ import { Button } from './button';
473
+ export default {};
474
+
475
+ export const Primary = {
476
+ render: () => (
477
+ <Button>
478
+ <div>Hello!</div>
479
+ </Button>
480
+ ),
481
+
482
+ name: 'Primary',
483
+ };
484
+
485
+ `);
486
+ });
487
+
488
+ test('story child is CSF3', () => {
489
+ const input = dedent`
490
+ import { Story } from '@storybook/addon-docs';
491
+ import { Button } from './button';
492
+
493
+ <Story name="Primary" render={(args) => <Button {...args}></Button> } args={{label: 'Hello' }} />
494
+ `;
495
+
496
+ jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
497
+
498
+ const [, csf] = fs.writeFileSync.mock.calls[0];
499
+
500
+ expect(csf).toMatchInlineSnapshot(`
501
+ import { Button } from './button';
502
+ export default {};
503
+
504
+ export const Primary = {
505
+ name: 'Primary',
506
+ render: (args) => <Button {...args}></Button>,
507
+
508
+ args: {
509
+ label: 'Hello',
510
+ },
511
+ };
512
+
513
+ `);
514
+ });
515
+
516
+ test('story child is arrow function', () => {
517
+ const input = dedent`
518
+ import { Canvas, Meta, Story } from '@storybook/addon-docs';
519
+ import { Button } from './button';
520
+
521
+ <Story name="Primary">
522
+ {(args) => <Button />}
523
+ </Story>
524
+ `;
525
+
526
+ jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
527
+
528
+ const [, csf] = fs.writeFileSync.mock.calls[0];
529
+
530
+ expect(csf).toMatchInlineSnapshot(`
531
+ import { Button } from './button';
532
+ export default {};
533
+
534
+ export const Primary = {
535
+ render: (args) => <Button />,
536
+ name: 'Primary',
537
+ };
538
+
539
+ `);
540
+ });
541
+
542
+ test('story child is identifier', () => {
543
+ const input = dedent`
544
+ import { Canvas, Meta, Story } from '@storybook/addon-docs';
545
+ import { Button } from './button';
546
+
547
+ <Story name="Primary">
548
+ {Button}
549
+ </Story>
550
+ `;
551
+
552
+ jscodeshift({ source: input, path: 'Foobar.stories.mdx' });
553
+
554
+ const [, csf] = fs.writeFileSync.mock.calls[0];
555
+
556
+ expect(csf).toMatchInlineSnapshot(`
557
+ import { Button } from './button';
558
+ export default {};
559
+
560
+ export const Primary = {
561
+ render: Button,
562
+ name: 'Primary',
563
+ };
564
+
565
+ `);
566
+ });
567
+
568
+ test('nameToValidExport', () => {
569
+ expect(nameToValidExport('1 starts with digit')).toMatchInlineSnapshot(`$1StartsWithDigit`);
570
+ expect(nameToValidExport('name')).toMatchInlineSnapshot(`Name`);
571
+ expect(nameToValidExport('Multi words')).toMatchInlineSnapshot(`MultiWords`);
572
+ // Unicode is valid in JS variable names
573
+ expect(nameToValidExport('Keep unicode 😅')).toMatchInlineSnapshot(`KeepUnicode😅`);
574
+ });