@conarti/eslint-plugin-feature-sliced 2.0.0-rc.5 → 2.0.0-rc.7

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/dist/index.cjs CHANGED
@@ -4,7 +4,8 @@ var RULE_NAMES = {
4
4
  LAYERS_SLICES: `${PLUGIN_NAME}/layers-slices`,
5
5
  ABSOLUTE_RELATIVE: `${PLUGIN_NAME}/absolute-relative`,
6
6
  PUBLIC_API: `${PLUGIN_NAME}/public-api`,
7
- IMPORT_ORDER: `${PLUGIN_NAME}/import-order`
7
+ IMPORT_ORDER: `${PLUGIN_NAME}/import-order`,
8
+ NO_CROSS_SEGMENT_REEXPORT: `${PLUGIN_NAME}/no-cross-segment-reexport`
8
9
  };
9
10
  var layers = [
10
11
  "shared",
@@ -79,7 +80,7 @@ function canLayerContainSlices(layer, config) {
79
80
  }
80
81
 
81
82
  // package.json
82
- var version = "2.0.0-rc.4";
83
+ var version = "2.0.0-rc.7";
83
84
 
84
85
  // src/rules/index.ts
85
86
  var _eslintpluginimportx = require('eslint-plugin-import-x'); var _eslintpluginimportx2 = _interopRequireDefault(_eslintpluginimportx);
@@ -840,6 +841,228 @@ var layers_slices_default = createEslintRule({
840
841
  }
841
842
  });
842
843
 
844
+ // src/rules/no-cross-segment-reexport/config.ts
845
+ var ERROR_MESSAGE_ID3 = {
846
+ NO_CROSS_SEGMENT_REEXPORT: "no-cross-segment-reexport",
847
+ MOVE_TO_SLICE_PUBLIC_API_SUGGESTION: "move-to-slice-public-api-suggestion"
848
+ };
849
+
850
+ // src/rules/no-cross-segment-reexport/model/errors.ts
851
+ function buildSlicePublicApiPath(sourcePath, targetSegment) {
852
+ const parts = sourcePath.split("/");
853
+ const segmentIndex = parts.findIndex((part) => part === targetSegment);
854
+ if (segmentIndex !== -1) {
855
+ return parts.slice(0, segmentIndex).join("/");
856
+ }
857
+ return parts.slice(0, -1).join("/") || "..";
858
+ }
859
+ function reportCrossSegmentReexport(context, node, currentSegment, targetSegment) {
860
+ const sourcePath = node.source.value;
861
+ const suggestedPath = buildSlicePublicApiPath(sourcePath, targetSegment);
862
+ context.report({
863
+ node: node.source,
864
+ messageId: ERROR_MESSAGE_ID3.NO_CROSS_SEGMENT_REEXPORT,
865
+ data: {
866
+ currentSegment,
867
+ targetSegment
868
+ },
869
+ suggest: [
870
+ {
871
+ messageId: ERROR_MESSAGE_ID3.MOVE_TO_SLICE_PUBLIC_API_SUGGESTION,
872
+ data: {
873
+ suggestedPath
874
+ },
875
+ fix: (fixer) => fixer.replaceTextRange(
876
+ getSourceRangeWithoutQuotes(node.source.range),
877
+ suggestedPath
878
+ )
879
+ }
880
+ ]
881
+ });
882
+ }
883
+
884
+ // src/rules/no-cross-segment-reexport/model/is-cross-segment-reexport.ts
885
+ var NOT_CROSS_SEGMENT = {
886
+ isCrossSegmentReexport: false,
887
+ currentSegment: null,
888
+ targetSegment: null
889
+ };
890
+ var FILE_EXT_REGEXP = /\..+$/;
891
+ var KNOWN_SEGMENTS = segments.map((s) => s.toLowerCase());
892
+ function splitPathParts(path3) {
893
+ return path3.split("/").filter(Boolean);
894
+ }
895
+ function normalizeToDirParts(parts) {
896
+ const INDEX_FILE_REGEXP = /^index\..+$/;
897
+ return parts.reduce((acc, part) => {
898
+ if (INDEX_FILE_REGEXP.test(part))
899
+ return acc;
900
+ if (FILE_EXT_REGEXP.test(part)) {
901
+ acc.push(part.replace(FILE_EXT_REGEXP, ""));
902
+ return acc;
903
+ }
904
+ acc.push(part);
905
+ return acc;
906
+ }, []);
907
+ }
908
+ function findLayerIndex(parts, layersWithSlices2) {
909
+ return parts.findIndex(
910
+ (part) => layersWithSlices2.includes(part.toLowerCase())
911
+ );
912
+ }
913
+ function extractSegmentAndSlice(pathParts) {
914
+ if (pathParts.length < 2)
915
+ return null;
916
+ const knownSegmentIndex = pathParts.findIndex(
917
+ (part) => KNOWN_SEGMENTS.includes(part.toLowerCase())
918
+ );
919
+ let segmentIndex;
920
+ if (knownSegmentIndex > 0) {
921
+ segmentIndex = knownSegmentIndex;
922
+ } else if (knownSegmentIndex === 0) {
923
+ return null;
924
+ } else {
925
+ segmentIndex = pathParts.length - 1;
926
+ if (segmentIndex < 1)
927
+ return null;
928
+ }
929
+ const segment = pathParts[segmentIndex];
930
+ const sliceParts = pathParts.slice(0, segmentIndex);
931
+ if (sliceParts.length === 0)
932
+ return null;
933
+ return { segment, sliceParts };
934
+ }
935
+ function findTargetSegmentInSameSlice(targetPathParts, currentSliceParts, currentSegment) {
936
+ if (targetPathParts.length === 0)
937
+ return null;
938
+ if (currentSliceParts.length > targetPathParts.length)
939
+ return null;
940
+ const targetHasSameSlice = currentSliceParts.every(
941
+ (part, i) => part.toLowerCase() === _optionalChain([targetPathParts, 'access', _9 => _9[i], 'optionalAccess', _10 => _10.toLowerCase, 'call', _11 => _11()])
942
+ );
943
+ if (!targetHasSameSlice)
944
+ return null;
945
+ const targetSegmentCandidate = targetPathParts[currentSliceParts.length];
946
+ if (!targetSegmentCandidate)
947
+ return null;
948
+ if (targetSegmentCandidate.toLowerCase() !== currentSegment.toLowerCase()) {
949
+ return targetSegmentCandidate;
950
+ }
951
+ return null;
952
+ }
953
+ function isCrossSegmentReexport(normalizedCurrentFilePath, absoluteTargetPath, config) {
954
+ const layersConfig = _nullishCoalesce(config, () => ( normalizeLayersConfig()));
955
+ const layersWithSlices2 = getLayersWithSlices(layersConfig).map((l) => l.toLowerCase());
956
+ const currentParts = splitPathParts(normalizedCurrentFilePath);
957
+ const targetParts = splitPathParts(absoluteTargetPath);
958
+ const currentLayerIndex = findLayerIndex(currentParts, layersWithSlices2);
959
+ if (currentLayerIndex === -1)
960
+ return NOT_CROSS_SEGMENT;
961
+ const currentAfterLayer = currentParts.slice(currentLayerIndex + 1);
962
+ const currentPathParts = normalizeToDirParts(currentAfterLayer);
963
+ const currentInfo = extractSegmentAndSlice(currentPathParts);
964
+ if (!currentInfo)
965
+ return NOT_CROSS_SEGMENT;
966
+ const targetLayerIndex = findLayerIndex(targetParts, layersWithSlices2);
967
+ if (targetLayerIndex === -1)
968
+ return NOT_CROSS_SEGMENT;
969
+ if (currentParts[currentLayerIndex].toLowerCase() !== targetParts[targetLayerIndex].toLowerCase()) {
970
+ return NOT_CROSS_SEGMENT;
971
+ }
972
+ const targetAfterLayer = targetParts.slice(targetLayerIndex + 1);
973
+ const targetPathParts = normalizeToDirParts(targetAfterLayer);
974
+ const targetSegment = findTargetSegmentInSameSlice(
975
+ targetPathParts,
976
+ currentInfo.sliceParts,
977
+ currentInfo.segment
978
+ );
979
+ if (!targetSegment)
980
+ return NOT_CROSS_SEGMENT;
981
+ return {
982
+ isCrossSegmentReexport: true,
983
+ currentSegment: currentInfo.segment,
984
+ targetSegment
985
+ };
986
+ }
987
+
988
+ // src/rules/no-cross-segment-reexport/model/validate-and-report.ts
989
+ function validateAndReport3(node, context, optionsWithDefault, config) {
990
+ if (!hasPath(node))
991
+ return;
992
+ const isIgnored2 = isIgnoredTarget(node, optionsWithDefault) || isIgnoredCurrentFile(context, optionsWithDefault);
993
+ if (isIgnored2)
994
+ return;
995
+ const {
996
+ normalizedCurrentFilePath,
997
+ absoluteTargetPath
998
+ } = extractPaths(node, context);
999
+ const result = isCrossSegmentReexport(
1000
+ normalizedCurrentFilePath,
1001
+ absoluteTargetPath,
1002
+ config
1003
+ );
1004
+ if (!result.isCrossSegmentReexport)
1005
+ return;
1006
+ reportCrossSegmentReexport(
1007
+ context,
1008
+ node,
1009
+ result.currentSegment,
1010
+ result.targetSegment
1011
+ );
1012
+ }
1013
+
1014
+ // src/rules/no-cross-segment-reexport/index.ts
1015
+ var no_cross_segment_reexport_default = createEslintRule({
1016
+ name: "no-cross-segment-reexport",
1017
+ meta: {
1018
+ type: "problem",
1019
+ docs: {
1020
+ description: "Checks for cross-segment re-exports within the same slice"
1021
+ },
1022
+ hasSuggestions: true,
1023
+ messages: {
1024
+ [ERROR_MESSAGE_ID3.NO_CROSS_SEGMENT_REEXPORT]: 'Segment "{{ currentSegment }}" should not re-export from sibling segment "{{ targetSegment }}". Move the re-export to the slice public API.',
1025
+ [ERROR_MESSAGE_ID3.MOVE_TO_SLICE_PUBLIC_API_SUGGESTION]: 'Replace import path with slice public API ("{{ suggestedPath }}")'
1026
+ },
1027
+ schema: [
1028
+ {
1029
+ type: "object",
1030
+ properties: {
1031
+ ignoreImports: {
1032
+ type: "array",
1033
+ items: {
1034
+ type: "string"
1035
+ }
1036
+ },
1037
+ ignoreFiles: {
1038
+ type: "array",
1039
+ items: {
1040
+ type: "string"
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ ]
1046
+ },
1047
+ defaultOptions: [
1048
+ {
1049
+ ignoreImports: [],
1050
+ ignoreFiles: []
1051
+ }
1052
+ ],
1053
+ create(context, optionsWithDefault) {
1054
+ const layersConfig = extractLayersConfig(context);
1055
+ return {
1056
+ ExportAllDeclaration(node) {
1057
+ validateAndReport3(node, context, optionsWithDefault, layersConfig);
1058
+ },
1059
+ ExportNamedDeclaration(node) {
1060
+ validateAndReport3(node, context, optionsWithDefault, layersConfig);
1061
+ }
1062
+ };
1063
+ }
1064
+ });
1065
+
843
1066
  // src/rules/public-api/config.ts
844
1067
  var MESSAGE_ID = {
845
1068
  SHOULD_BE_FROM_PUBLIC_API: "should-be-from-public-api",
@@ -963,7 +1186,7 @@ function shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig)
963
1186
  }
964
1187
 
965
1188
  // src/rules/public-api/model/validate-and-report.ts
966
- function validateAndReport3(node, context, optionsWithDefault, layersConfig) {
1189
+ function validateAndReport4(node, context, optionsWithDefault, layersConfig) {
967
1190
  if (!hasPath(node)) {
968
1191
  return;
969
1192
  }
@@ -1053,16 +1276,16 @@ var public_api_default = createEslintRule({
1053
1276
  const layersConfig = extractLayersConfig(context);
1054
1277
  return {
1055
1278
  ImportDeclaration(node) {
1056
- validateAndReport3(node, context, optionsWithDefault, layersConfig);
1279
+ validateAndReport4(node, context, optionsWithDefault, layersConfig);
1057
1280
  },
1058
1281
  ImportExpression(node) {
1059
- validateAndReport3(node, context, optionsWithDefault, layersConfig);
1282
+ validateAndReport4(node, context, optionsWithDefault, layersConfig);
1060
1283
  },
1061
1284
  ExportAllDeclaration(node) {
1062
- validateAndReport3(node, context, optionsWithDefault, layersConfig);
1285
+ validateAndReport4(node, context, optionsWithDefault, layersConfig);
1063
1286
  },
1064
1287
  ExportNamedDeclaration(node) {
1065
- validateAndReport3(node, context, optionsWithDefault, layersConfig);
1288
+ validateAndReport4(node, context, optionsWithDefault, layersConfig);
1066
1289
  },
1067
1290
  Program(node) {
1068
1291
  validateAndReportProgram(node, context, optionsWithDefault);
@@ -1076,6 +1299,7 @@ var rules = {
1076
1299
  "absolute-relative": absolute_relative_default,
1077
1300
  "import-order": _eslintpluginimportx2.default.rules.order,
1078
1301
  "layers-slices": layers_slices_default,
1302
+ "no-cross-segment-reexport": no_cross_segment_reexport_default,
1079
1303
  "public-api": public_api_default
1080
1304
  };
1081
1305
  var rules_default = rules;
@@ -1154,14 +1378,16 @@ var importOrderRuleConfigs = createImportOrderRuleConfigs();
1154
1378
  // src/create-plugin.ts
1155
1379
  function createPlugin(options = {}) {
1156
1380
  const {
1381
+ severity = "error",
1157
1382
  layers: layers2,
1158
1383
  sortImports = "recommended",
1159
1384
  absoluteRelative,
1160
1385
  layersSlices,
1161
- publicApi
1386
+ publicApi,
1387
+ noCrossSegmentReexport
1162
1388
  } = options;
1163
1389
  const normalizedLayers = normalizeLayersConfig(layers2);
1164
- const rules2 = defineRules({ absoluteRelative, layersSlices, publicApi, sortImports }, normalizedLayers);
1390
+ const rules2 = defineRules({ severity, absoluteRelative, layersSlices, publicApi, noCrossSegmentReexport, sortImports }, normalizedLayers);
1165
1391
  return {
1166
1392
  name: PLUGIN_NAME,
1167
1393
  plugins: {
@@ -1177,16 +1403,25 @@ function createPlugin(options = {}) {
1177
1403
  }
1178
1404
  function defineRules(options, layersConfig) {
1179
1405
  const {
1406
+ severity: globalSeverity = "error",
1180
1407
  absoluteRelative = {},
1181
1408
  layersSlices = {},
1182
1409
  publicApi = {},
1410
+ noCrossSegmentReexport = {},
1183
1411
  sortImports = "recommended"
1184
1412
  } = options;
1185
- const createRuleEntry = (ruleOptions) => ruleOptions ? ["error", ruleOptions] : ["off"];
1413
+ const createRuleEntry = (ruleOptions) => {
1414
+ if (ruleOptions === false) {
1415
+ return "off";
1416
+ }
1417
+ const { severity = globalSeverity, ...restOptions } = ruleOptions;
1418
+ return [severity, restOptions];
1419
+ };
1186
1420
  const rules2 = {
1187
1421
  [RULE_NAMES.LAYERS_SLICES]: createRuleEntry(layersSlices),
1188
1422
  [RULE_NAMES.ABSOLUTE_RELATIVE]: createRuleEntry(absoluteRelative),
1189
- [RULE_NAMES.PUBLIC_API]: createRuleEntry(publicApi)
1423
+ [RULE_NAMES.PUBLIC_API]: createRuleEntry(publicApi),
1424
+ [RULE_NAMES.NO_CROSS_SEGMENT_REEXPORT]: createRuleEntry(noCrossSegmentReexport)
1190
1425
  };
1191
1426
  if (sortImports) {
1192
1427
  const importOrderConfigs = createImportOrderRuleConfigs(layersConfig);
package/dist/index.d.cts CHANGED
@@ -12,6 +12,7 @@ declare const RULE_NAMES: {
12
12
  readonly ABSOLUTE_RELATIVE: "@conarti/feature-sliced/absolute-relative";
13
13
  readonly PUBLIC_API: "@conarti/feature-sliced/public-api";
14
14
  readonly IMPORT_ORDER: "@conarti/feature-sliced/import-order";
15
+ readonly NO_CROSS_SEGMENT_REEXPORT: "@conarti/feature-sliced/no-cross-segment-reexport";
15
16
  };
16
17
  type Layers = ReadonlyArray<'shared' | 'entities' | 'features' | 'widgets' | 'pages' | 'processes' | 'app'>;
17
18
  type Layer = Layers[number];
@@ -62,9 +63,9 @@ declare const VALIDATION_LEVEL: {
62
63
  readonly SEGMENTS: "segments";
63
64
  readonly SLICES: "slices";
64
65
  };
65
- type MessageIds$2 = typeof MESSAGE_ID[keyof typeof MESSAGE_ID];
66
+ type MessageIds$3 = typeof MESSAGE_ID[keyof typeof MESSAGE_ID];
66
67
  type ValidationLevel = typeof VALIDATION_LEVEL[keyof typeof VALIDATION_LEVEL];
67
- type Options$2 = [
68
+ type Options$3 = [
68
69
  {
69
70
  level: ValidationLevel;
70
71
  ignoreImports: string[];
@@ -72,32 +73,48 @@ type Options$2 = [
72
73
  }
73
74
  ];
74
75
 
76
+ type Severity = 'error' | 'warn';
75
77
  interface AbsoluteRelativeOptions {
78
+ /**
79
+ * Severity level for this rule
80
+ * @default uses global severity or 'error'
81
+ */
82
+ severity?: Severity;
76
83
  /**
77
84
  * Ignore certain import paths (import foo from '<path-to-ignore>')
78
85
  */
79
- ignoreImports: string[];
86
+ ignoreImports?: string[];
80
87
  /**
81
88
  * Disable the rule in certain files
82
89
  */
83
- ignoreFiles: string[];
90
+ ignoreFiles?: string[];
84
91
  }
85
92
  interface LayersSlicesOptions {
93
+ /**
94
+ * Severity level for this rule
95
+ * @default uses global severity or 'error'
96
+ */
97
+ severity?: Severity;
86
98
  /**
87
99
  * Ignore cross-imports of types
88
100
  * @default true
89
101
  */
90
- allowTypeImports: boolean;
102
+ allowTypeImports?: boolean;
91
103
  /**
92
104
  * Ignore certain import paths (import foo from '<path-to-ignore>')
93
105
  */
94
- ignoreImports: string[];
106
+ ignoreImports?: string[];
95
107
  /**
96
108
  * Disable the rule in certain files
97
109
  */
98
- ignoreFiles: string[];
110
+ ignoreFiles?: string[];
99
111
  }
100
112
  interface PublicApiOptions {
113
+ /**
114
+ * Severity level for this rule
115
+ * @default uses global severity or 'error'
116
+ */
117
+ severity?: Severity;
101
118
  /**
102
119
  * Adjusts the depth.
103
120
  * 'slices' will check for presence 'index' file at the slice level only,
@@ -105,17 +122,38 @@ interface PublicApiOptions {
105
122
  * Default is 'slices', but 'segments' is recommended
106
123
  * @default 'slices'
107
124
  */
108
- level: ValidationLevel;
125
+ level?: ValidationLevel;
126
+ /**
127
+ * Ignore certain import paths (import foo from '<path-to-ignore>')
128
+ */
129
+ ignoreImports?: string[];
130
+ /**
131
+ * Disable the rule in certain files
132
+ */
133
+ ignoreFiles?: string[];
134
+ }
135
+ interface NoCrossSegmentReexportOptions {
136
+ /**
137
+ * Severity level for this rule
138
+ * @default uses global severity or 'error'
139
+ */
140
+ severity?: Severity;
109
141
  /**
110
142
  * Ignore certain import paths (import foo from '<path-to-ignore>')
111
143
  */
112
- ignoreImports: string[];
144
+ ignoreImports?: string[];
113
145
  /**
114
146
  * Disable the rule in certain files
115
147
  */
116
- ignoreFiles: string[];
148
+ ignoreFiles?: string[];
117
149
  }
118
150
  interface ESLintPluginFeatureSlicedOptions {
151
+ /**
152
+ * Global severity level for all rules.
153
+ * Can be overridden per-rule.
154
+ * @default 'error'
155
+ */
156
+ severity?: Severity;
119
157
  /**
120
158
  * Custom layers configuration.
121
159
  * Supports mixed syntax: strings for layers with slices, objects for customization.
@@ -130,13 +168,26 @@ interface ESLintPluginFeatureSlicedOptions {
130
168
  * ]
131
169
  */
132
170
  layers?: LayersConfig;
133
- absoluteRelative?: false | AbsoluteRelativeOptions;
134
- layersSlices?: false | LayersSlicesOptions;
135
- publicApi?: false | PublicApiOptions;
171
+ absoluteRelative?: false | Partial<AbsoluteRelativeOptions>;
172
+ layersSlices?: false | Partial<LayersSlicesOptions>;
173
+ publicApi?: false | Partial<PublicApiOptions>;
174
+ noCrossSegmentReexport?: false | Partial<NoCrossSegmentReexportOptions>;
136
175
  sortImports?: false | ImportOrderConfigName;
137
176
  }
138
177
  declare function createPlugin(options?: ESLintPluginFeatureSlicedOptions): TypedFlatConfigItem;
139
178
 
179
+ declare const ERROR_MESSAGE_ID$2: {
180
+ readonly NO_CROSS_SEGMENT_REEXPORT: "no-cross-segment-reexport";
181
+ readonly MOVE_TO_SLICE_PUBLIC_API_SUGGESTION: "move-to-slice-public-api-suggestion";
182
+ };
183
+ type MessageIds$2 = typeof ERROR_MESSAGE_ID$2[keyof typeof ERROR_MESSAGE_ID$2];
184
+ type Options$2 = [
185
+ {
186
+ ignoreImports: string[];
187
+ ignoreFiles: string[];
188
+ }
189
+ ];
190
+
140
191
  declare const ERROR_MESSAGE_ID$1: {
141
192
  readonly CAN_NOT_IMPORT: "can-not-import";
142
193
  readonly INVALID_CROSS_IMPORT: "invalid-cross-import";
@@ -171,10 +222,11 @@ declare const plugin: {
171
222
  'absolute-relative': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds, Options, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
172
223
  'import-order': _typescript_eslint_utils_ts_eslint.RuleModule<"error" | "order" | "noLineWithinGroup" | "noLineBetweenGroups" | "oneLineBetweenGroups" | "oneLineBetweenTheMultiLineImport" | "oneLineBetweenThisMultiLineImport" | "noLineBetweenSingleLineImport", [(eslint_plugin_import_x_rules_order.Options | undefined)?], eslint_plugin_import_x_utils.ImportXPluginDocs, _typescript_eslint_utils_ts_eslint.RuleListener>;
173
224
  'layers-slices': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds$1, Options$1, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
174
- 'public-api': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds$2, Options$2, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
225
+ 'no-cross-segment-reexport': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds$2, Options$2, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
226
+ 'public-api': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds$3, Options$3, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
175
227
  };
176
228
  };
177
229
 
178
230
  // @ts-ignore
179
231
  export = createPlugin;
180
- export { type ImportOrderConfigName, type Layer, PLUGIN_NAME, RULE_NAMES, type Segment, type TypedFlatConfigItem, createPlugin, layers, plugin, segments };
232
+ export { type ImportOrderConfigName, type Layer, PLUGIN_NAME, RULE_NAMES, type Segment, type Severity, type TypedFlatConfigItem, createPlugin, layers, plugin, segments };
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ declare const RULE_NAMES: {
12
12
  readonly ABSOLUTE_RELATIVE: "@conarti/feature-sliced/absolute-relative";
13
13
  readonly PUBLIC_API: "@conarti/feature-sliced/public-api";
14
14
  readonly IMPORT_ORDER: "@conarti/feature-sliced/import-order";
15
+ readonly NO_CROSS_SEGMENT_REEXPORT: "@conarti/feature-sliced/no-cross-segment-reexport";
15
16
  };
16
17
  type Layers = ReadonlyArray<'shared' | 'entities' | 'features' | 'widgets' | 'pages' | 'processes' | 'app'>;
17
18
  type Layer = Layers[number];
@@ -62,9 +63,9 @@ declare const VALIDATION_LEVEL: {
62
63
  readonly SEGMENTS: "segments";
63
64
  readonly SLICES: "slices";
64
65
  };
65
- type MessageIds$2 = typeof MESSAGE_ID[keyof typeof MESSAGE_ID];
66
+ type MessageIds$3 = typeof MESSAGE_ID[keyof typeof MESSAGE_ID];
66
67
  type ValidationLevel = typeof VALIDATION_LEVEL[keyof typeof VALIDATION_LEVEL];
67
- type Options$2 = [
68
+ type Options$3 = [
68
69
  {
69
70
  level: ValidationLevel;
70
71
  ignoreImports: string[];
@@ -72,32 +73,48 @@ type Options$2 = [
72
73
  }
73
74
  ];
74
75
 
76
+ type Severity = 'error' | 'warn';
75
77
  interface AbsoluteRelativeOptions {
78
+ /**
79
+ * Severity level for this rule
80
+ * @default uses global severity or 'error'
81
+ */
82
+ severity?: Severity;
76
83
  /**
77
84
  * Ignore certain import paths (import foo from '<path-to-ignore>')
78
85
  */
79
- ignoreImports: string[];
86
+ ignoreImports?: string[];
80
87
  /**
81
88
  * Disable the rule in certain files
82
89
  */
83
- ignoreFiles: string[];
90
+ ignoreFiles?: string[];
84
91
  }
85
92
  interface LayersSlicesOptions {
93
+ /**
94
+ * Severity level for this rule
95
+ * @default uses global severity or 'error'
96
+ */
97
+ severity?: Severity;
86
98
  /**
87
99
  * Ignore cross-imports of types
88
100
  * @default true
89
101
  */
90
- allowTypeImports: boolean;
102
+ allowTypeImports?: boolean;
91
103
  /**
92
104
  * Ignore certain import paths (import foo from '<path-to-ignore>')
93
105
  */
94
- ignoreImports: string[];
106
+ ignoreImports?: string[];
95
107
  /**
96
108
  * Disable the rule in certain files
97
109
  */
98
- ignoreFiles: string[];
110
+ ignoreFiles?: string[];
99
111
  }
100
112
  interface PublicApiOptions {
113
+ /**
114
+ * Severity level for this rule
115
+ * @default uses global severity or 'error'
116
+ */
117
+ severity?: Severity;
101
118
  /**
102
119
  * Adjusts the depth.
103
120
  * 'slices' will check for presence 'index' file at the slice level only,
@@ -105,17 +122,38 @@ interface PublicApiOptions {
105
122
  * Default is 'slices', but 'segments' is recommended
106
123
  * @default 'slices'
107
124
  */
108
- level: ValidationLevel;
125
+ level?: ValidationLevel;
126
+ /**
127
+ * Ignore certain import paths (import foo from '<path-to-ignore>')
128
+ */
129
+ ignoreImports?: string[];
130
+ /**
131
+ * Disable the rule in certain files
132
+ */
133
+ ignoreFiles?: string[];
134
+ }
135
+ interface NoCrossSegmentReexportOptions {
136
+ /**
137
+ * Severity level for this rule
138
+ * @default uses global severity or 'error'
139
+ */
140
+ severity?: Severity;
109
141
  /**
110
142
  * Ignore certain import paths (import foo from '<path-to-ignore>')
111
143
  */
112
- ignoreImports: string[];
144
+ ignoreImports?: string[];
113
145
  /**
114
146
  * Disable the rule in certain files
115
147
  */
116
- ignoreFiles: string[];
148
+ ignoreFiles?: string[];
117
149
  }
118
150
  interface ESLintPluginFeatureSlicedOptions {
151
+ /**
152
+ * Global severity level for all rules.
153
+ * Can be overridden per-rule.
154
+ * @default 'error'
155
+ */
156
+ severity?: Severity;
119
157
  /**
120
158
  * Custom layers configuration.
121
159
  * Supports mixed syntax: strings for layers with slices, objects for customization.
@@ -130,13 +168,26 @@ interface ESLintPluginFeatureSlicedOptions {
130
168
  * ]
131
169
  */
132
170
  layers?: LayersConfig;
133
- absoluteRelative?: false | AbsoluteRelativeOptions;
134
- layersSlices?: false | LayersSlicesOptions;
135
- publicApi?: false | PublicApiOptions;
171
+ absoluteRelative?: false | Partial<AbsoluteRelativeOptions>;
172
+ layersSlices?: false | Partial<LayersSlicesOptions>;
173
+ publicApi?: false | Partial<PublicApiOptions>;
174
+ noCrossSegmentReexport?: false | Partial<NoCrossSegmentReexportOptions>;
136
175
  sortImports?: false | ImportOrderConfigName;
137
176
  }
138
177
  declare function createPlugin(options?: ESLintPluginFeatureSlicedOptions): TypedFlatConfigItem;
139
178
 
179
+ declare const ERROR_MESSAGE_ID$2: {
180
+ readonly NO_CROSS_SEGMENT_REEXPORT: "no-cross-segment-reexport";
181
+ readonly MOVE_TO_SLICE_PUBLIC_API_SUGGESTION: "move-to-slice-public-api-suggestion";
182
+ };
183
+ type MessageIds$2 = typeof ERROR_MESSAGE_ID$2[keyof typeof ERROR_MESSAGE_ID$2];
184
+ type Options$2 = [
185
+ {
186
+ ignoreImports: string[];
187
+ ignoreFiles: string[];
188
+ }
189
+ ];
190
+
140
191
  declare const ERROR_MESSAGE_ID$1: {
141
192
  readonly CAN_NOT_IMPORT: "can-not-import";
142
193
  readonly INVALID_CROSS_IMPORT: "invalid-cross-import";
@@ -171,8 +222,9 @@ declare const plugin: {
171
222
  'absolute-relative': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds, Options, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
172
223
  'import-order': _typescript_eslint_utils_ts_eslint.RuleModule<"error" | "order" | "noLineWithinGroup" | "noLineBetweenGroups" | "oneLineBetweenGroups" | "oneLineBetweenTheMultiLineImport" | "oneLineBetweenThisMultiLineImport" | "noLineBetweenSingleLineImport", [(eslint_plugin_import_x_rules_order.Options | undefined)?], eslint_plugin_import_x_utils.ImportXPluginDocs, _typescript_eslint_utils_ts_eslint.RuleListener>;
173
224
  'layers-slices': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds$1, Options$1, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
174
- 'public-api': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds$2, Options$2, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
225
+ 'no-cross-segment-reexport': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds$2, Options$2, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
226
+ 'public-api': _typescript_eslint_utils_ts_eslint.RuleModule<MessageIds$3, Options$3, unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
175
227
  };
176
228
  };
177
229
 
178
- export { type ImportOrderConfigName, type Layer, PLUGIN_NAME, RULE_NAMES, type Segment, type TypedFlatConfigItem, createPlugin, createPlugin as default, layers, plugin, segments };
230
+ export { type ImportOrderConfigName, type Layer, PLUGIN_NAME, RULE_NAMES, type Segment, type Severity, type TypedFlatConfigItem, createPlugin, createPlugin as default, layers, plugin, segments };
package/dist/index.js CHANGED
@@ -4,7 +4,8 @@ var RULE_NAMES = {
4
4
  LAYERS_SLICES: `${PLUGIN_NAME}/layers-slices`,
5
5
  ABSOLUTE_RELATIVE: `${PLUGIN_NAME}/absolute-relative`,
6
6
  PUBLIC_API: `${PLUGIN_NAME}/public-api`,
7
- IMPORT_ORDER: `${PLUGIN_NAME}/import-order`
7
+ IMPORT_ORDER: `${PLUGIN_NAME}/import-order`,
8
+ NO_CROSS_SEGMENT_REEXPORT: `${PLUGIN_NAME}/no-cross-segment-reexport`
8
9
  };
9
10
  var layers = [
10
11
  "shared",
@@ -79,7 +80,7 @@ function canLayerContainSlices(layer, config) {
79
80
  }
80
81
 
81
82
  // package.json
82
- var version = "2.0.0-rc.4";
83
+ var version = "2.0.0-rc.7";
83
84
 
84
85
  // src/rules/index.ts
85
86
  import pluginImport from "eslint-plugin-import-x";
@@ -840,6 +841,228 @@ var layers_slices_default = createEslintRule({
840
841
  }
841
842
  });
842
843
 
844
+ // src/rules/no-cross-segment-reexport/config.ts
845
+ var ERROR_MESSAGE_ID3 = {
846
+ NO_CROSS_SEGMENT_REEXPORT: "no-cross-segment-reexport",
847
+ MOVE_TO_SLICE_PUBLIC_API_SUGGESTION: "move-to-slice-public-api-suggestion"
848
+ };
849
+
850
+ // src/rules/no-cross-segment-reexport/model/errors.ts
851
+ function buildSlicePublicApiPath(sourcePath, targetSegment) {
852
+ const parts = sourcePath.split("/");
853
+ const segmentIndex = parts.findIndex((part) => part === targetSegment);
854
+ if (segmentIndex !== -1) {
855
+ return parts.slice(0, segmentIndex).join("/");
856
+ }
857
+ return parts.slice(0, -1).join("/") || "..";
858
+ }
859
+ function reportCrossSegmentReexport(context, node, currentSegment, targetSegment) {
860
+ const sourcePath = node.source.value;
861
+ const suggestedPath = buildSlicePublicApiPath(sourcePath, targetSegment);
862
+ context.report({
863
+ node: node.source,
864
+ messageId: ERROR_MESSAGE_ID3.NO_CROSS_SEGMENT_REEXPORT,
865
+ data: {
866
+ currentSegment,
867
+ targetSegment
868
+ },
869
+ suggest: [
870
+ {
871
+ messageId: ERROR_MESSAGE_ID3.MOVE_TO_SLICE_PUBLIC_API_SUGGESTION,
872
+ data: {
873
+ suggestedPath
874
+ },
875
+ fix: (fixer) => fixer.replaceTextRange(
876
+ getSourceRangeWithoutQuotes(node.source.range),
877
+ suggestedPath
878
+ )
879
+ }
880
+ ]
881
+ });
882
+ }
883
+
884
+ // src/rules/no-cross-segment-reexport/model/is-cross-segment-reexport.ts
885
+ var NOT_CROSS_SEGMENT = {
886
+ isCrossSegmentReexport: false,
887
+ currentSegment: null,
888
+ targetSegment: null
889
+ };
890
+ var FILE_EXT_REGEXP = /\..+$/;
891
+ var KNOWN_SEGMENTS = segments.map((s) => s.toLowerCase());
892
+ function splitPathParts(path3) {
893
+ return path3.split("/").filter(Boolean);
894
+ }
895
+ function normalizeToDirParts(parts) {
896
+ const INDEX_FILE_REGEXP = /^index\..+$/;
897
+ return parts.reduce((acc, part) => {
898
+ if (INDEX_FILE_REGEXP.test(part))
899
+ return acc;
900
+ if (FILE_EXT_REGEXP.test(part)) {
901
+ acc.push(part.replace(FILE_EXT_REGEXP, ""));
902
+ return acc;
903
+ }
904
+ acc.push(part);
905
+ return acc;
906
+ }, []);
907
+ }
908
+ function findLayerIndex(parts, layersWithSlices2) {
909
+ return parts.findIndex(
910
+ (part) => layersWithSlices2.includes(part.toLowerCase())
911
+ );
912
+ }
913
+ function extractSegmentAndSlice(pathParts) {
914
+ if (pathParts.length < 2)
915
+ return null;
916
+ const knownSegmentIndex = pathParts.findIndex(
917
+ (part) => KNOWN_SEGMENTS.includes(part.toLowerCase())
918
+ );
919
+ let segmentIndex;
920
+ if (knownSegmentIndex > 0) {
921
+ segmentIndex = knownSegmentIndex;
922
+ } else if (knownSegmentIndex === 0) {
923
+ return null;
924
+ } else {
925
+ segmentIndex = pathParts.length - 1;
926
+ if (segmentIndex < 1)
927
+ return null;
928
+ }
929
+ const segment = pathParts[segmentIndex];
930
+ const sliceParts = pathParts.slice(0, segmentIndex);
931
+ if (sliceParts.length === 0)
932
+ return null;
933
+ return { segment, sliceParts };
934
+ }
935
+ function findTargetSegmentInSameSlice(targetPathParts, currentSliceParts, currentSegment) {
936
+ if (targetPathParts.length === 0)
937
+ return null;
938
+ if (currentSliceParts.length > targetPathParts.length)
939
+ return null;
940
+ const targetHasSameSlice = currentSliceParts.every(
941
+ (part, i) => part.toLowerCase() === targetPathParts[i]?.toLowerCase()
942
+ );
943
+ if (!targetHasSameSlice)
944
+ return null;
945
+ const targetSegmentCandidate = targetPathParts[currentSliceParts.length];
946
+ if (!targetSegmentCandidate)
947
+ return null;
948
+ if (targetSegmentCandidate.toLowerCase() !== currentSegment.toLowerCase()) {
949
+ return targetSegmentCandidate;
950
+ }
951
+ return null;
952
+ }
953
+ function isCrossSegmentReexport(normalizedCurrentFilePath, absoluteTargetPath, config) {
954
+ const layersConfig = config ?? normalizeLayersConfig();
955
+ const layersWithSlices2 = getLayersWithSlices(layersConfig).map((l) => l.toLowerCase());
956
+ const currentParts = splitPathParts(normalizedCurrentFilePath);
957
+ const targetParts = splitPathParts(absoluteTargetPath);
958
+ const currentLayerIndex = findLayerIndex(currentParts, layersWithSlices2);
959
+ if (currentLayerIndex === -1)
960
+ return NOT_CROSS_SEGMENT;
961
+ const currentAfterLayer = currentParts.slice(currentLayerIndex + 1);
962
+ const currentPathParts = normalizeToDirParts(currentAfterLayer);
963
+ const currentInfo = extractSegmentAndSlice(currentPathParts);
964
+ if (!currentInfo)
965
+ return NOT_CROSS_SEGMENT;
966
+ const targetLayerIndex = findLayerIndex(targetParts, layersWithSlices2);
967
+ if (targetLayerIndex === -1)
968
+ return NOT_CROSS_SEGMENT;
969
+ if (currentParts[currentLayerIndex].toLowerCase() !== targetParts[targetLayerIndex].toLowerCase()) {
970
+ return NOT_CROSS_SEGMENT;
971
+ }
972
+ const targetAfterLayer = targetParts.slice(targetLayerIndex + 1);
973
+ const targetPathParts = normalizeToDirParts(targetAfterLayer);
974
+ const targetSegment = findTargetSegmentInSameSlice(
975
+ targetPathParts,
976
+ currentInfo.sliceParts,
977
+ currentInfo.segment
978
+ );
979
+ if (!targetSegment)
980
+ return NOT_CROSS_SEGMENT;
981
+ return {
982
+ isCrossSegmentReexport: true,
983
+ currentSegment: currentInfo.segment,
984
+ targetSegment
985
+ };
986
+ }
987
+
988
+ // src/rules/no-cross-segment-reexport/model/validate-and-report.ts
989
+ function validateAndReport3(node, context, optionsWithDefault, config) {
990
+ if (!hasPath(node))
991
+ return;
992
+ const isIgnored2 = isIgnoredTarget(node, optionsWithDefault) || isIgnoredCurrentFile(context, optionsWithDefault);
993
+ if (isIgnored2)
994
+ return;
995
+ const {
996
+ normalizedCurrentFilePath,
997
+ absoluteTargetPath
998
+ } = extractPaths(node, context);
999
+ const result = isCrossSegmentReexport(
1000
+ normalizedCurrentFilePath,
1001
+ absoluteTargetPath,
1002
+ config
1003
+ );
1004
+ if (!result.isCrossSegmentReexport)
1005
+ return;
1006
+ reportCrossSegmentReexport(
1007
+ context,
1008
+ node,
1009
+ result.currentSegment,
1010
+ result.targetSegment
1011
+ );
1012
+ }
1013
+
1014
+ // src/rules/no-cross-segment-reexport/index.ts
1015
+ var no_cross_segment_reexport_default = createEslintRule({
1016
+ name: "no-cross-segment-reexport",
1017
+ meta: {
1018
+ type: "problem",
1019
+ docs: {
1020
+ description: "Checks for cross-segment re-exports within the same slice"
1021
+ },
1022
+ hasSuggestions: true,
1023
+ messages: {
1024
+ [ERROR_MESSAGE_ID3.NO_CROSS_SEGMENT_REEXPORT]: 'Segment "{{ currentSegment }}" should not re-export from sibling segment "{{ targetSegment }}". Move the re-export to the slice public API.',
1025
+ [ERROR_MESSAGE_ID3.MOVE_TO_SLICE_PUBLIC_API_SUGGESTION]: 'Replace import path with slice public API ("{{ suggestedPath }}")'
1026
+ },
1027
+ schema: [
1028
+ {
1029
+ type: "object",
1030
+ properties: {
1031
+ ignoreImports: {
1032
+ type: "array",
1033
+ items: {
1034
+ type: "string"
1035
+ }
1036
+ },
1037
+ ignoreFiles: {
1038
+ type: "array",
1039
+ items: {
1040
+ type: "string"
1041
+ }
1042
+ }
1043
+ }
1044
+ }
1045
+ ]
1046
+ },
1047
+ defaultOptions: [
1048
+ {
1049
+ ignoreImports: [],
1050
+ ignoreFiles: []
1051
+ }
1052
+ ],
1053
+ create(context, optionsWithDefault) {
1054
+ const layersConfig = extractLayersConfig(context);
1055
+ return {
1056
+ ExportAllDeclaration(node) {
1057
+ validateAndReport3(node, context, optionsWithDefault, layersConfig);
1058
+ },
1059
+ ExportNamedDeclaration(node) {
1060
+ validateAndReport3(node, context, optionsWithDefault, layersConfig);
1061
+ }
1062
+ };
1063
+ }
1064
+ });
1065
+
843
1066
  // src/rules/public-api/config.ts
844
1067
  var MESSAGE_ID = {
845
1068
  SHOULD_BE_FROM_PUBLIC_API: "should-be-from-public-api",
@@ -963,7 +1186,7 @@ function shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig)
963
1186
  }
964
1187
 
965
1188
  // src/rules/public-api/model/validate-and-report.ts
966
- function validateAndReport3(node, context, optionsWithDefault, layersConfig) {
1189
+ function validateAndReport4(node, context, optionsWithDefault, layersConfig) {
967
1190
  if (!hasPath(node)) {
968
1191
  return;
969
1192
  }
@@ -1053,16 +1276,16 @@ var public_api_default = createEslintRule({
1053
1276
  const layersConfig = extractLayersConfig(context);
1054
1277
  return {
1055
1278
  ImportDeclaration(node) {
1056
- validateAndReport3(node, context, optionsWithDefault, layersConfig);
1279
+ validateAndReport4(node, context, optionsWithDefault, layersConfig);
1057
1280
  },
1058
1281
  ImportExpression(node) {
1059
- validateAndReport3(node, context, optionsWithDefault, layersConfig);
1282
+ validateAndReport4(node, context, optionsWithDefault, layersConfig);
1060
1283
  },
1061
1284
  ExportAllDeclaration(node) {
1062
- validateAndReport3(node, context, optionsWithDefault, layersConfig);
1285
+ validateAndReport4(node, context, optionsWithDefault, layersConfig);
1063
1286
  },
1064
1287
  ExportNamedDeclaration(node) {
1065
- validateAndReport3(node, context, optionsWithDefault, layersConfig);
1288
+ validateAndReport4(node, context, optionsWithDefault, layersConfig);
1066
1289
  },
1067
1290
  Program(node) {
1068
1291
  validateAndReportProgram(node, context, optionsWithDefault);
@@ -1076,6 +1299,7 @@ var rules = {
1076
1299
  "absolute-relative": absolute_relative_default,
1077
1300
  "import-order": pluginImport.rules.order,
1078
1301
  "layers-slices": layers_slices_default,
1302
+ "no-cross-segment-reexport": no_cross_segment_reexport_default,
1079
1303
  "public-api": public_api_default
1080
1304
  };
1081
1305
  var rules_default = rules;
@@ -1154,14 +1378,16 @@ var importOrderRuleConfigs = createImportOrderRuleConfigs();
1154
1378
  // src/create-plugin.ts
1155
1379
  function createPlugin(options = {}) {
1156
1380
  const {
1381
+ severity = "error",
1157
1382
  layers: layers2,
1158
1383
  sortImports = "recommended",
1159
1384
  absoluteRelative,
1160
1385
  layersSlices,
1161
- publicApi
1386
+ publicApi,
1387
+ noCrossSegmentReexport
1162
1388
  } = options;
1163
1389
  const normalizedLayers = normalizeLayersConfig(layers2);
1164
- const rules2 = defineRules({ absoluteRelative, layersSlices, publicApi, sortImports }, normalizedLayers);
1390
+ const rules2 = defineRules({ severity, absoluteRelative, layersSlices, publicApi, noCrossSegmentReexport, sortImports }, normalizedLayers);
1165
1391
  return {
1166
1392
  name: PLUGIN_NAME,
1167
1393
  plugins: {
@@ -1177,16 +1403,25 @@ function createPlugin(options = {}) {
1177
1403
  }
1178
1404
  function defineRules(options, layersConfig) {
1179
1405
  const {
1406
+ severity: globalSeverity = "error",
1180
1407
  absoluteRelative = {},
1181
1408
  layersSlices = {},
1182
1409
  publicApi = {},
1410
+ noCrossSegmentReexport = {},
1183
1411
  sortImports = "recommended"
1184
1412
  } = options;
1185
- const createRuleEntry = (ruleOptions) => ruleOptions ? ["error", ruleOptions] : ["off"];
1413
+ const createRuleEntry = (ruleOptions) => {
1414
+ if (ruleOptions === false) {
1415
+ return "off";
1416
+ }
1417
+ const { severity = globalSeverity, ...restOptions } = ruleOptions;
1418
+ return [severity, restOptions];
1419
+ };
1186
1420
  const rules2 = {
1187
1421
  [RULE_NAMES.LAYERS_SLICES]: createRuleEntry(layersSlices),
1188
1422
  [RULE_NAMES.ABSOLUTE_RELATIVE]: createRuleEntry(absoluteRelative),
1189
- [RULE_NAMES.PUBLIC_API]: createRuleEntry(publicApi)
1423
+ [RULE_NAMES.PUBLIC_API]: createRuleEntry(publicApi),
1424
+ [RULE_NAMES.NO_CROSS_SEGMENT_REEXPORT]: createRuleEntry(noCrossSegmentReexport)
1190
1425
  };
1191
1426
  if (sortImports) {
1192
1427
  const importOrderConfigs = createImportOrderRuleConfigs(layersConfig);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@conarti/eslint-plugin-feature-sliced",
3
3
  "type": "module",
4
- "version": "2.0.0-rc.5",
4
+ "version": "2.0.0-rc.7",
5
5
  "description": "Feature-sliced design methodology plugin",
6
6
  "author": "Aleksandr Belous <abelous2009@gmail.com>",
7
7
  "license": "ISC",