eslint-plugin-code-style 1.0.14 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/index.js +361 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,6 +195,7 @@ rules: {
|
|
|
195
195
|
"code-style/export-format": "error",
|
|
196
196
|
"code-style/import-format": "error",
|
|
197
197
|
"code-style/import-source-spacing": "error",
|
|
198
|
+
"code-style/index-export-style": "error",
|
|
198
199
|
"code-style/module-index-exports": "error",
|
|
199
200
|
"code-style/jsx-children-on-new-line": "error",
|
|
200
201
|
"code-style/jsx-closing-bracket-spacing": "error",
|
|
@@ -257,6 +258,7 @@ rules: {
|
|
|
257
258
|
| `export-format` | Format exports: collapse 1-3 specifiers, multiline for 4+ |
|
|
258
259
|
| `import-format` | Format imports: collapse 1-3 specifiers, multiline for 4+ |
|
|
259
260
|
| `import-source-spacing` | Enforce no extra spaces inside import path quotes |
|
|
261
|
+
| `index-export-style` | Enforce consistent export style in index files (shorthand or import-then-export) ⚙️ |
|
|
260
262
|
| `module-index-exports` | Enforce proper exports in index files ⚙️ |
|
|
261
263
|
| `jsx-children-on-new-line` | Enforce JSX children on separate lines from parent tags |
|
|
262
264
|
| `jsx-closing-bracket-spacing` | No space before `>` or `/>` in JSX tags |
|
|
@@ -785,6 +787,69 @@ import { Button } from " @mui/material ";
|
|
|
785
787
|
|
|
786
788
|
---
|
|
787
789
|
|
|
790
|
+
### `index-export-style`
|
|
791
|
+
|
|
792
|
+
Enforce consistent export style in index files. Choose between shorthand re-exports or import-then-export pattern. Also enforces no empty lines between exports/imports.
|
|
793
|
+
|
|
794
|
+
**Style: "shorthand" (default)**
|
|
795
|
+
```javascript
|
|
796
|
+
// Good - shorthand re-exports (no empty lines between them)
|
|
797
|
+
export { Button } from "./button";
|
|
798
|
+
export { Input, Select } from "./form";
|
|
799
|
+
export { StyledCard, StyledCardWithActions } from "./card";
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
**Style: "import-export"**
|
|
803
|
+
```javascript
|
|
804
|
+
// Good - imports grouped, single export statement at bottom
|
|
805
|
+
import { Button } from "./button";
|
|
806
|
+
import { Input, Select } from "./form";
|
|
807
|
+
import { StyledCard, StyledCardWithActions } from "./card";
|
|
808
|
+
|
|
809
|
+
export {
|
|
810
|
+
Button,
|
|
811
|
+
Input,
|
|
812
|
+
Select,
|
|
813
|
+
StyledCard,
|
|
814
|
+
StyledCardWithActions,
|
|
815
|
+
};
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
**Bad Examples**
|
|
819
|
+
```javascript
|
|
820
|
+
// Bad - mixing styles
|
|
821
|
+
export { Button } from "./button";
|
|
822
|
+
import { Input } from "./input";
|
|
823
|
+
export { Input };
|
|
824
|
+
|
|
825
|
+
// Bad - empty lines between shorthand exports
|
|
826
|
+
export { Button } from "./button";
|
|
827
|
+
|
|
828
|
+
export { Input } from "./input";
|
|
829
|
+
|
|
830
|
+
// Bad - multiple standalone exports (should be one)
|
|
831
|
+
import { Button } from "./button";
|
|
832
|
+
import { Input } from "./input";
|
|
833
|
+
export { Button };
|
|
834
|
+
export { Input };
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
**Customization Options:**
|
|
838
|
+
|
|
839
|
+
| Option | Type | Default | Description |
|
|
840
|
+
|--------|------|---------|-------------|
|
|
841
|
+
| `style` | `"shorthand"` \| `"import-export"` | `"shorthand"` | The export style to enforce |
|
|
842
|
+
|
|
843
|
+
```javascript
|
|
844
|
+
// Example: Use shorthand style (default)
|
|
845
|
+
"code-style/index-export-style": "error"
|
|
846
|
+
|
|
847
|
+
// Example: Use import-then-export style
|
|
848
|
+
"code-style/index-export-style": ["error", { style: "import-export" }]
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
---
|
|
852
|
+
|
|
788
853
|
### `module-index-exports`
|
|
789
854
|
|
|
790
855
|
Ensure module folders have index files that export all contents. Each module folder must have an index file that exports all subfolders and files in the module.
|
package/index.js
CHANGED
|
@@ -3387,6 +3387,366 @@ const moduleIndexExports = {
|
|
|
3387
3387
|
},
|
|
3388
3388
|
};
|
|
3389
3389
|
|
|
3390
|
+
/**
|
|
3391
|
+
* ───────────────────────────────────────────────────────────────
|
|
3392
|
+
* Rule: Index Export Style
|
|
3393
|
+
* ───────────────────────────────────────────────────────────────
|
|
3394
|
+
*
|
|
3395
|
+
* Description:
|
|
3396
|
+
* Enforce consistent export style in index files. Choose between
|
|
3397
|
+
* shorthand re-exports or import-then-export pattern.
|
|
3398
|
+
*
|
|
3399
|
+
* Options:
|
|
3400
|
+
* - style: "shorthand" (default) | "import-export"
|
|
3401
|
+
* - "shorthand": export { a } from "./file"; (no empty lines between exports)
|
|
3402
|
+
* - "import-export": import { a } from "./file"; export { a }; (single export statement)
|
|
3403
|
+
*
|
|
3404
|
+
* ✓ Good (style: "shorthand" - default):
|
|
3405
|
+
* export { Button } from "./button";
|
|
3406
|
+
* export { Input, Select } from "./form";
|
|
3407
|
+
* export { Modal } from "./modal";
|
|
3408
|
+
*
|
|
3409
|
+
* ✓ Good (style: "import-export"):
|
|
3410
|
+
* import { Button } from "./button";
|
|
3411
|
+
* import { Input, Select } from "./form";
|
|
3412
|
+
* import { Modal } from "./modal";
|
|
3413
|
+
*
|
|
3414
|
+
* export {
|
|
3415
|
+
* Button,
|
|
3416
|
+
* Input,
|
|
3417
|
+
* Modal,
|
|
3418
|
+
* Select,
|
|
3419
|
+
* };
|
|
3420
|
+
*
|
|
3421
|
+
* ✗ Bad (mixing styles):
|
|
3422
|
+
* export { Button } from "./button";
|
|
3423
|
+
* import { Input } from "./input";
|
|
3424
|
+
* export { Input };
|
|
3425
|
+
*
|
|
3426
|
+
* ✗ Bad (empty lines between shorthand exports):
|
|
3427
|
+
* export { Button } from "./button";
|
|
3428
|
+
*
|
|
3429
|
+
* export { Input } from "./input";
|
|
3430
|
+
*
|
|
3431
|
+
* ✗ Bad (multiple standalone exports):
|
|
3432
|
+
* import { Button } from "./button";
|
|
3433
|
+
* import { Input } from "./input";
|
|
3434
|
+
* export { Button };
|
|
3435
|
+
* export { Input };
|
|
3436
|
+
*
|
|
3437
|
+
* Configuration Example:
|
|
3438
|
+
* "code-style/index-export-style": ["error", { style: "shorthand" }]
|
|
3439
|
+
* "code-style/index-export-style": ["error", { style: "import-export" }]
|
|
3440
|
+
*/
|
|
3441
|
+
const indexExportStyle = {
|
|
3442
|
+
create(context) {
|
|
3443
|
+
const options = context.options[0] || {};
|
|
3444
|
+
const preferredStyle = options.style || "shorthand";
|
|
3445
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
3446
|
+
const filename = context.filename || context.getFilename();
|
|
3447
|
+
const normalizedFilename = filename.replace(/\\/g, "/");
|
|
3448
|
+
|
|
3449
|
+
// Only apply to index files
|
|
3450
|
+
const isIndexFile = /\/index\.(js|jsx|ts|tsx)$/.test(normalizedFilename);
|
|
3451
|
+
|
|
3452
|
+
if (!isIndexFile) return {};
|
|
3453
|
+
|
|
3454
|
+
return {
|
|
3455
|
+
Program(node) {
|
|
3456
|
+
const imports = [];
|
|
3457
|
+
const shorthandExports = [];
|
|
3458
|
+
const standaloneExports = [];
|
|
3459
|
+
const importSourceMap = new Map();
|
|
3460
|
+
|
|
3461
|
+
// Collect all imports and exports
|
|
3462
|
+
node.body.forEach((statement) => {
|
|
3463
|
+
if (statement.type === "ImportDeclaration" && statement.source) {
|
|
3464
|
+
imports.push(statement);
|
|
3465
|
+
|
|
3466
|
+
// Map imported names to their source
|
|
3467
|
+
statement.specifiers.forEach((spec) => {
|
|
3468
|
+
if (spec.local && spec.local.name) {
|
|
3469
|
+
importSourceMap.set(spec.local.name, {
|
|
3470
|
+
source: statement.source.value,
|
|
3471
|
+
statement,
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
});
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
if (statement.type === "ExportNamedDeclaration") {
|
|
3478
|
+
if (statement.source) {
|
|
3479
|
+
// Shorthand: export { a } from "./file"
|
|
3480
|
+
shorthandExports.push(statement);
|
|
3481
|
+
} else if (statement.specifiers && statement.specifiers.length > 0 && !statement.declaration) {
|
|
3482
|
+
// Standalone: export { a }
|
|
3483
|
+
standaloneExports.push(statement);
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
});
|
|
3487
|
+
|
|
3488
|
+
// Skip if no exports to check
|
|
3489
|
+
if (shorthandExports.length === 0 && standaloneExports.length === 0) return;
|
|
3490
|
+
|
|
3491
|
+
// Check for mixed styles
|
|
3492
|
+
const hasShorthand = shorthandExports.length > 0;
|
|
3493
|
+
const hasImportExport = standaloneExports.length > 0 && imports.length > 0;
|
|
3494
|
+
|
|
3495
|
+
if (hasShorthand && hasImportExport) {
|
|
3496
|
+
context.report({
|
|
3497
|
+
message: `Mixed export styles detected. Use consistent "${preferredStyle}" style throughout the index file.`,
|
|
3498
|
+
node,
|
|
3499
|
+
});
|
|
3500
|
+
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
if (preferredStyle === "shorthand") {
|
|
3505
|
+
// Check if using import-then-export pattern when shorthand is preferred
|
|
3506
|
+
if (standaloneExports.length > 0 && imports.length > 0) {
|
|
3507
|
+
// Collect all specifiers to convert
|
|
3508
|
+
const allSpecifiersToConvert = [];
|
|
3509
|
+
|
|
3510
|
+
standaloneExports.forEach((exportStmt) => {
|
|
3511
|
+
exportStmt.specifiers.forEach((spec) => {
|
|
3512
|
+
const exportedName = spec.local ? spec.local.name : spec.exported.name;
|
|
3513
|
+
const importInfo = importSourceMap.get(exportedName);
|
|
3514
|
+
|
|
3515
|
+
if (importInfo) {
|
|
3516
|
+
allSpecifiersToConvert.push({
|
|
3517
|
+
exportStmt,
|
|
3518
|
+
exportedName,
|
|
3519
|
+
importInfo,
|
|
3520
|
+
localName: spec.local ? spec.local.name : exportedName,
|
|
3521
|
+
spec,
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
});
|
|
3525
|
+
});
|
|
3526
|
+
|
|
3527
|
+
if (allSpecifiersToConvert.length > 0) {
|
|
3528
|
+
context.report({
|
|
3529
|
+
fix(fixer) {
|
|
3530
|
+
const fixes = [];
|
|
3531
|
+
|
|
3532
|
+
// Group specifiers by source
|
|
3533
|
+
const bySource = new Map();
|
|
3534
|
+
|
|
3535
|
+
allSpecifiersToConvert.forEach(({ exportedName, importInfo, localName }) => {
|
|
3536
|
+
const source = importInfo.source;
|
|
3537
|
+
|
|
3538
|
+
if (!bySource.has(source)) {
|
|
3539
|
+
bySource.set(source, []);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
if (exportedName === localName) {
|
|
3543
|
+
bySource.get(source).push(exportedName);
|
|
3544
|
+
} else {
|
|
3545
|
+
bySource.get(source).push(`${localName} as ${exportedName}`);
|
|
3546
|
+
}
|
|
3547
|
+
});
|
|
3548
|
+
|
|
3549
|
+
// Create shorthand exports (no empty lines between them)
|
|
3550
|
+
const newExports = [];
|
|
3551
|
+
|
|
3552
|
+
bySource.forEach((specifiers, source) => {
|
|
3553
|
+
newExports.push(`export { ${specifiers.join(", ")} } from "${source}";`);
|
|
3554
|
+
});
|
|
3555
|
+
|
|
3556
|
+
// Remove all standalone exports
|
|
3557
|
+
standaloneExports.forEach((exportStmt) => {
|
|
3558
|
+
fixes.push(fixer.remove(exportStmt));
|
|
3559
|
+
});
|
|
3560
|
+
|
|
3561
|
+
// Remove all imports
|
|
3562
|
+
imports.forEach((importStmt) => {
|
|
3563
|
+
fixes.push(fixer.remove(importStmt));
|
|
3564
|
+
});
|
|
3565
|
+
|
|
3566
|
+
// Insert new shorthand exports at the beginning
|
|
3567
|
+
const firstStatement = node.body[0];
|
|
3568
|
+
|
|
3569
|
+
if (firstStatement) {
|
|
3570
|
+
fixes.push(fixer.insertTextBefore(firstStatement, newExports.join("\n") + "\n"));
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
return fixes;
|
|
3574
|
+
},
|
|
3575
|
+
message: `Use shorthand export style: export { ... } from "source" instead of import then export.`,
|
|
3576
|
+
node,
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
// Check for empty lines between shorthand exports
|
|
3582
|
+
for (let i = 0; i < shorthandExports.length - 1; i += 1) {
|
|
3583
|
+
const currentExport = shorthandExports[i];
|
|
3584
|
+
const nextExport = shorthandExports[i + 1];
|
|
3585
|
+
const currentEndLine = currentExport.loc.end.line;
|
|
3586
|
+
const nextStartLine = nextExport.loc.start.line;
|
|
3587
|
+
|
|
3588
|
+
if (nextStartLine - currentEndLine > 1) {
|
|
3589
|
+
context.report({
|
|
3590
|
+
fix(fixer) {
|
|
3591
|
+
const textBetween = sourceCode.getText().slice(
|
|
3592
|
+
currentExport.range[1],
|
|
3593
|
+
nextExport.range[0],
|
|
3594
|
+
);
|
|
3595
|
+
|
|
3596
|
+
// Replace multiple newlines with single newline
|
|
3597
|
+
return fixer.replaceTextRange(
|
|
3598
|
+
[currentExport.range[1], nextExport.range[0]],
|
|
3599
|
+
"\n",
|
|
3600
|
+
);
|
|
3601
|
+
},
|
|
3602
|
+
message: "No empty lines between shorthand exports in index files.",
|
|
3603
|
+
node: nextExport,
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
} else if (preferredStyle === "import-export") {
|
|
3608
|
+
// Check if using shorthand when import-export is preferred
|
|
3609
|
+
if (shorthandExports.length > 0) {
|
|
3610
|
+
// Convert all shorthand exports to import-then-export with single export statement
|
|
3611
|
+
context.report({
|
|
3612
|
+
fix(fixer) {
|
|
3613
|
+
const fixes = [];
|
|
3614
|
+
const allImports = [];
|
|
3615
|
+
const allExportNames = [];
|
|
3616
|
+
|
|
3617
|
+
shorthandExports.forEach((exportStmt) => {
|
|
3618
|
+
const source = exportStmt.source.value;
|
|
3619
|
+
const importSpecifiers = [];
|
|
3620
|
+
|
|
3621
|
+
exportStmt.specifiers.forEach((spec) => {
|
|
3622
|
+
const imported = spec.local ? spec.local.name : spec.exported.name;
|
|
3623
|
+
const exported = spec.exported.name;
|
|
3624
|
+
|
|
3625
|
+
importSpecifiers.push(imported);
|
|
3626
|
+
allExportNames.push(exported);
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
allImports.push(`import { ${importSpecifiers.join(", ")} } from "${source}";`);
|
|
3630
|
+
|
|
3631
|
+
// Remove the shorthand export
|
|
3632
|
+
fixes.push(fixer.remove(exportStmt));
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3635
|
+
// Sort export names alphabetically
|
|
3636
|
+
allExportNames.sort((a, b) => a.localeCompare(b));
|
|
3637
|
+
|
|
3638
|
+
// Create single export statement with proper formatting
|
|
3639
|
+
let exportStatement;
|
|
3640
|
+
|
|
3641
|
+
if (allExportNames.length <= 3) {
|
|
3642
|
+
exportStatement = `export { ${allExportNames.join(", ")} };`;
|
|
3643
|
+
} else {
|
|
3644
|
+
exportStatement = `export {\n ${allExportNames.join(",\n ")},\n};`;
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
// Insert imports and export at the beginning
|
|
3648
|
+
const firstStatement = node.body[0];
|
|
3649
|
+
|
|
3650
|
+
if (firstStatement) {
|
|
3651
|
+
const newContent = allImports.join("\n") + "\n\n" + exportStatement + "\n";
|
|
3652
|
+
|
|
3653
|
+
fixes.push(fixer.insertTextBefore(firstStatement, newContent));
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
return fixes;
|
|
3657
|
+
},
|
|
3658
|
+
message: `Use import-then-export style with a single export statement.`,
|
|
3659
|
+
node,
|
|
3660
|
+
});
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
// Check for multiple standalone exports - should be combined into one
|
|
3664
|
+
if (standaloneExports.length > 1) {
|
|
3665
|
+
context.report({
|
|
3666
|
+
fix(fixer) {
|
|
3667
|
+
const fixes = [];
|
|
3668
|
+
const allExportNames = [];
|
|
3669
|
+
|
|
3670
|
+
standaloneExports.forEach((exportStmt) => {
|
|
3671
|
+
exportStmt.specifiers.forEach((spec) => {
|
|
3672
|
+
const exported = spec.exported.name;
|
|
3673
|
+
|
|
3674
|
+
allExportNames.push(exported);
|
|
3675
|
+
});
|
|
3676
|
+
|
|
3677
|
+
// Remove all but the last export
|
|
3678
|
+
fixes.push(fixer.remove(exportStmt));
|
|
3679
|
+
});
|
|
3680
|
+
|
|
3681
|
+
// Sort export names alphabetically
|
|
3682
|
+
allExportNames.sort((a, b) => a.localeCompare(b));
|
|
3683
|
+
|
|
3684
|
+
// Create single export statement
|
|
3685
|
+
let exportStatement;
|
|
3686
|
+
|
|
3687
|
+
if (allExportNames.length <= 3) {
|
|
3688
|
+
exportStatement = `export { ${allExportNames.join(", ")} };`;
|
|
3689
|
+
} else {
|
|
3690
|
+
exportStatement = `export {\n ${allExportNames.join(",\n ")},\n};`;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// Find last import to insert after
|
|
3694
|
+
const lastImport = imports[imports.length - 1];
|
|
3695
|
+
|
|
3696
|
+
if (lastImport) {
|
|
3697
|
+
fixes.push(fixer.insertTextAfter(lastImport, "\n\n" + exportStatement));
|
|
3698
|
+
}
|
|
3699
|
+
|
|
3700
|
+
return fixes;
|
|
3701
|
+
},
|
|
3702
|
+
message: `Combine multiple export statements into a single export statement.`,
|
|
3703
|
+
node,
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
// Check for empty lines between imports
|
|
3708
|
+
for (let i = 0; i < imports.length - 1; i += 1) {
|
|
3709
|
+
const currentImport = imports[i];
|
|
3710
|
+
const nextImport = imports[i + 1];
|
|
3711
|
+
const currentEndLine = currentImport.loc.end.line;
|
|
3712
|
+
const nextStartLine = nextImport.loc.start.line;
|
|
3713
|
+
|
|
3714
|
+
if (nextStartLine - currentEndLine > 1) {
|
|
3715
|
+
context.report({
|
|
3716
|
+
fix(fixer) {
|
|
3717
|
+
return fixer.replaceTextRange(
|
|
3718
|
+
[currentImport.range[1], nextImport.range[0]],
|
|
3719
|
+
"\n",
|
|
3720
|
+
);
|
|
3721
|
+
},
|
|
3722
|
+
message: "No empty lines between imports in index files.",
|
|
3723
|
+
node: nextImport,
|
|
3724
|
+
});
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
},
|
|
3729
|
+
};
|
|
3730
|
+
},
|
|
3731
|
+
meta: {
|
|
3732
|
+
docs: { description: "Enforce consistent export style in index files (shorthand or import-then-export)" },
|
|
3733
|
+
fixable: "code",
|
|
3734
|
+
schema: [
|
|
3735
|
+
{
|
|
3736
|
+
additionalProperties: false,
|
|
3737
|
+
properties: {
|
|
3738
|
+
style: {
|
|
3739
|
+
enum: ["shorthand", "import-export"],
|
|
3740
|
+
type: "string",
|
|
3741
|
+
},
|
|
3742
|
+
},
|
|
3743
|
+
type: "object",
|
|
3744
|
+
},
|
|
3745
|
+
],
|
|
3746
|
+
type: "layout",
|
|
3747
|
+
},
|
|
3748
|
+
};
|
|
3749
|
+
|
|
3390
3750
|
/**
|
|
3391
3751
|
* ───────────────────────────────────────────────────────────────
|
|
3392
3752
|
* Rule: JSX Children On New Line
|
|
@@ -7821,6 +8181,7 @@ export default {
|
|
|
7821
8181
|
"export-format": exportFormat,
|
|
7822
8182
|
"import-format": importFormat,
|
|
7823
8183
|
"import-source-spacing": importSourceSpacing,
|
|
8184
|
+
"index-export-style": indexExportStyle,
|
|
7824
8185
|
"module-index-exports": moduleIndexExports,
|
|
7825
8186
|
|
|
7826
8187
|
// JSX rules
|
package/package.json
CHANGED