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.
Files changed (3) hide show
  1. package/README.md +65 -0
  2. package/index.js +361 -0
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-code-style",
3
- "version": "1.0.14",
3
+ "version": "1.0.17",
4
4
  "description": "A custom ESLint plugin for enforcing consistent code formatting and style rules in React/JSX projects",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",