@@ -25,14 +25,24 @@ const jscs = jscodeshiftDefault || jscodeshiftNamespace;
25
25
26
26
// These are types not in the TS sense, but in the instance-of-a-Type-class sense
27
27
const {
28
+ ArrayPattern,
29
+ ClassDeclaration,
30
+ ExportAllDeclaration,
31
+ ExportDefaultDeclaration,
32
+ ExportDefaultSpecifier,
33
+ ExportNamedDeclaration,
28
34
ExportSpecifier,
35
+ FunctionDeclaration,
29
36
Identifier,
30
37
ImportSpecifier,
38
+ JSXIdentifier,
31
39
MemberExpression,
32
40
Node,
33
41
ObjectExpression,
34
42
ObjectPattern,
35
43
Property,
44
+ RestElement,
45
+ TSTypeParameter,
36
46
VariableDeclaration,
37
47
VariableDeclarator,
38
48
} = jscs ;
@@ -291,8 +301,6 @@ function maybeRenameNode(ast: AST, identifierPath: ASTPath<IdentifierNode>, alia
291
301
// it means we potentially need to rename something *not* already named `getServerSideProps`, `getStaticProps`, or
292
302
// `getStaticPaths`, meaning we need to rename nodes outside of the collection upon which we're currently acting.
293
303
if ( ExportSpecifier . check ( parent ) ) {
294
- // console.log(node);
295
- // debugger;
296
304
if ( parent . exported . name !== parent . local ?. name && node === parent . exported ) {
297
305
const currentLocalName = parent . local ?. name || '' ;
298
306
renameIdentifiers ( ast , currentLocalName , alias ) ;
@@ -320,3 +328,202 @@ export function removeComments(ast: AST): void {
320
328
const nodesWithComments = ast . find ( Node ) . filter ( nodePath => ! ! nodePath . node . comments ) ;
321
329
nodesWithComments . forEach ( nodePath => ( nodePath . node . comments = null ) ) ;
322
330
}
331
+
332
+ /**
333
+ * Determines from a given AST of a file whether the file has a default export or not.
334
+ */
335
+ export function hasDefaultExport ( ast : AST ) : boolean {
336
+ const defaultExports = ast . find ( Node , value => {
337
+ return (
338
+ ExportDefaultDeclaration . check ( value ) ||
339
+ ExportDefaultSpecifier . check ( value ) ||
340
+ ( ExportSpecifier . check ( value ) && value . exported . name === 'default' )
341
+ ) ;
342
+ } ) ;
343
+
344
+ // In theory there should only ever be 0 or 1, but who knows what people do
345
+ return defaultExports . length > 0 ;
346
+ }
347
+
348
+ /**
349
+ * Extracts all identifier names (`'constName'`) from an destructuringassignment'sArrayPattern (the `[constName]` in`const [constName] = [1]`).
350
+ *
351
+ * This function recursively calls itself and `getExportIdentifiersFromObjectPattern` since destructuring assignments
352
+ * can be deeply nested with objects and arrays.
353
+ *
354
+ * Example - take the following program:
355
+ *
356
+ * ```js
357
+ * export const [{ foo: name1 }, [{ bar: [name2]}, name3]] = [{ foo: 1 }, [{ bar: [2] }, 3]];
358
+ * ```
359
+ *
360
+ * The `ArrayPattern` node in question for this program is the left hand side of the assignment:
361
+ * `[{ foo: name1 }, [{ bar: [name2]}, name3]]`
362
+ *
363
+ * Applying this function to this `ArrayPattern` will return the following: `["name1", "name2", "name3"]`
364
+ *
365
+ * DISCLAIMER: This function only correcly extracts identifiers of `ArrayPatterns` in the context of export statements.
366
+ * Using this for `ArrayPattern` outside of exports would require us to handle more edgecases. Hence the "Export" in
367
+ * this function's name.
368
+ */
369
+ function getExportIdentifiersFromArrayPattern ( arrayPattern : jscsTypes . ArrayPattern ) : string [ ] {
370
+ const identifiers : string [ ] = [ ] ;
371
+
372
+ arrayPattern . elements . forEach ( element => {
373
+ if ( Identifier . check ( element ) ) {
374
+ identifiers . push ( element . name ) ;
375
+ } else if ( ObjectPattern . check ( element ) ) {
376
+ identifiers . push ( ...getExportIdentifiersFromObjectPattern ( element ) ) ;
377
+ } else if ( ArrayPattern . check ( element ) ) {
378
+ identifiers . push ( ...getExportIdentifiersFromArrayPattern ( element ) ) ;
379
+ } else if ( RestElement . check ( element ) && Identifier . check ( element . argument ) ) {
380
+ // `RestElements` are spread operators
381
+ identifiers . push ( element . argument . name ) ;
382
+ }
383
+ } ) ;
384
+
385
+ return identifiers ;
386
+ }
387
+
388
+ /**
389
+ * Grabs all identifiers from an ObjectPattern within a destructured named export declaration
390
+ * statement (`name` in "export const { val: name } = { val: 1 }").
391
+ *
392
+ * This function recursively calls itself and `getExportIdentifiersFromArrayPattern` since destructuring assignments
393
+ * can be deeply nested with objects and arrays.
394
+ *
395
+ * Example - take the following program:
396
+ *
397
+ * ```js
398
+ * export const { foo: [{ bar: name1 }], name2, ...name3 } = { foo: [{}] };
399
+ * ```
400
+ *
401
+ * The `ObjectPattern` node in question for this program is the left hand side of the assignment:
402
+ * `{ foo: [{ bar: name1 }], name2, ...name3 } = { foo: [{}] }`
403
+ *
404
+ * Applying this function to this `ObjectPattern` will return the following: `["name1", "name2", "name3"]`
405
+ *
406
+ * DISCLAIMER: This function only correcly extracts identifiers of `ObjectPatterns` in the context of export statements.
407
+ * Using this for `ObjectPatterns` outside of exports would require us to handle more edgecases. Hence the "Export" in
408
+ * this function's name.
409
+ */
410
+ function getExportIdentifiersFromObjectPattern ( objectPatternNode : jscsTypes . ObjectPattern ) : string [ ] {
411
+ const identifiers : string [ ] = [ ] ;
412
+
413
+ objectPatternNode . properties . forEach ( property => {
414
+ // An `ObjectPattern`'s properties can be either `Property`s or `RestElement`s.
415
+ if ( Property . check ( property ) ) {
416
+ if ( Identifier . check ( property . value ) ) {
417
+ identifiers . push ( property . value . name ) ;
418
+ } else if ( ObjectPattern . check ( property . value ) ) {
419
+ identifiers . push ( ...getExportIdentifiersFromObjectPattern ( property . value ) ) ;
420
+ } else if ( ArrayPattern . check ( property . value ) ) {
421
+ identifiers . push ( ...getExportIdentifiersFromArrayPattern ( property . value ) ) ;
422
+ } else if ( RestElement . check ( property . value ) && Identifier . check ( property . value . argument ) ) {
423
+ // `RestElements` are spread operators
424
+ identifiers . push ( property . value . argument . name ) ;
425
+ }
426
+ // @ts -ignore AST types are wrong here
427
+ } else if ( RestElement . check ( property ) && Identifier . check ( property . argument ) ) {
428
+ // `RestElements` are spread operators
429
+ // @ts -ignore AST types are wrong here
430
+ identifiers . push ( property . argument . name as string ) ;
431
+ }
432
+ } ) ;
433
+
434
+ return identifiers ;
435
+ }
436
+
437
+ /**
438
+ * Given the AST of a file, this function extracts all named exports from the file.
439
+ *
440
+ * @returns a list of deduplicated identifiers.
441
+ */
442
+ export function getExportIdentifierNames ( ast : AST ) : string [ ] {
443
+ // We'll use a set to dedupe at the end, but for now we use an array as our accumulator because you can add multiple elements to it at once.
444
+ const identifiers : string [ ] = [ ] ;
445
+
446
+ // The following variable collects all export statements that double as named declaration, e.g.:
447
+ // - export function myFunc() {}
448
+ // - export var myVar = 1337
449
+ // - export const myConst = 1337
450
+ // - export const { a, ..rest } = { a: 1, b: 2, c: 3 }
451
+ // We will narrow those situations down in subsequent code blocks.
452
+ const namedExportDeclarationNodeDeclarations = ast
453
+ . find ( ExportNamedDeclaration )
454
+ . nodes ( )
455
+ . map ( namedExportDeclarationNode => namedExportDeclarationNode . declaration ) ;
456
+
457
+ namedExportDeclarationNodeDeclarations
458
+ . filter ( ( declarationNode ) : declarationNode is jscsTypes . VariableDeclaration =>
459
+ // Narrow down to varible declarations, e.g.:
460
+ // export const a = ...;
461
+ // export var b = ...;
462
+ // export let c = ...;
463
+ // export let c, d = 1;
464
+ VariableDeclaration . check ( declarationNode ) ,
465
+ )
466
+ . map (
467
+ variableDeclarationNode =>
468
+ // Grab all declarations in a single export statement.
469
+ // There can be multiple in the case of for example in `export let a, b;`.
470
+ variableDeclarationNode . declarations ,
471
+ )
472
+ . reduce ( ( prev , curr ) => [ ...prev , ...curr ] , [ ] ) // flatten - now we have all declaration nodes in one flat array
473
+ . forEach ( declarationNode => {
474
+ if (
475
+ Identifier . check ( declarationNode ) || // should never happen
476
+ JSXIdentifier . check ( declarationNode ) || // JSX like `<name></name>` - we don't care about these
477
+ TSTypeParameter . check ( declarationNode ) // type definitions - we don't care about those
478
+ ) {
479
+ // We should never have to enter this branch, it is just for type narrowing.
480
+ } else if ( Identifier . check ( declarationNode . id ) ) {
481
+ // If it's a simple declaration with an identifier we collect it. (e.g. `const myIdentifier = 1;` -> "myIdentifier")
482
+ identifiers . push ( declarationNode . id . name ) ;
483
+ } else if ( ObjectPattern . check ( declarationNode . id ) ) {
484
+ // If we encounter a destructuring export like `export const { foo: name1, bar: name2 } = { foo: 1, bar: 2 };`,
485
+ // we try collecting the identifiers from the pattern `{ foo: name1, bar: name2 }`.
486
+ identifiers . push ( ...getExportIdentifiersFromObjectPattern ( declarationNode . id ) ) ;
487
+ } else if ( ArrayPattern . check ( declarationNode . id ) ) {
488
+ // If we encounter a destructuring export like `export const [name1, name2] = [1, 2];`,
489
+ // we try collecting the identifiers from the pattern `[name1, name2]`.
490
+ identifiers . push ( ...getExportIdentifiersFromArrayPattern ( declarationNode . id ) ) ;
491
+ }
492
+ } ) ;
493
+
494
+ namedExportDeclarationNodeDeclarations
495
+ . filter (
496
+ // Narrow down to class and function declarations, e.g.:
497
+ // export class Foo {};
498
+ // export function bar() {};
499
+ ( declarationNode ) : declarationNode is jscsTypes . ClassDeclaration | jscsTypes . FunctionDeclaration =>
500
+ ClassDeclaration . check ( declarationNode ) || FunctionDeclaration . check ( declarationNode ) ,
501
+ )
502
+ . map ( node => node . id ) // Grab the identifier of the function/class - Note: it might be `null` when it's anonymous
503
+ . filter ( ( id ) : id is jscsTypes . Identifier => Identifier . check ( id ) ) // Elaborate way of null-checking
504
+ . forEach ( id => identifiers . push ( id . name ) ) ; // Collect the name of the identifier
505
+
506
+ ast
507
+ . find ( ExportSpecifier ) // Find stuff like `export {<id [as name]>} [from ...];`
508
+ . nodes ( )
509
+ . forEach ( specifier => {
510
+ // Taking the example above `specifier.exported.name` always contains `id` unless `name` is specified, then it's `name`;
511
+ if ( specifier . exported . name !== 'default' ) {
512
+ // You can do default exports "export { something as default };" but we do not want to collect "default" in this
513
+ // function since it only wants to collect named exports.
514
+ identifiers . push ( specifier . exported . name ) ;
515
+ }
516
+ } ) ;
517
+
518
+ ast
519
+ . find ( ExportAllDeclaration ) // Find stuff like `export * from ..." and "export * as someVariable from ...`
520
+ . nodes ( )
521
+ . forEach ( declaration => {
522
+ // Narrow it down to only find `export * as someVariable from ...` (emphasis on "as someVariable")
523
+ if ( declaration . exported ) {
524
+ identifiers . push ( declaration . exported . name ) ; // `declaration.exported.name` contains "someVariable"
525
+ }
526
+ } ) ;
527
+
528
+ return [ ...new Set ( identifiers ) ] ; // dedupe
529
+ }
0 commit comments