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

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
@@ -38,6 +38,14 @@ var segments = [
38
38
  "config",
39
39
  "assets"
40
40
  ];
41
+ var DEFAULT_SEGMENTS = [
42
+ "ui",
43
+ "model",
44
+ "lib",
45
+ "api",
46
+ "config",
47
+ "assets"
48
+ ];
41
49
  var pathSeparator = "/";
42
50
 
43
51
  // src/lib/feature-sliced/layers-config.ts
@@ -80,7 +88,7 @@ function canLayerContainSlices(layer, config) {
80
88
  }
81
89
 
82
90
  // package.json
83
- var version = "2.0.0-rc.7";
91
+ var version = "2.0.0-rc.9";
84
92
 
85
93
  // src/rules/index.ts
86
94
  var _eslintpluginimportx = require('eslint-plugin-import-x'); var _eslintpluginimportx2 = _interopRequireDefault(_eslintpluginimportx);
@@ -132,7 +140,7 @@ function convertToAbsolute(base, target) {
132
140
 
133
141
  // src/lib/rule/extract-current-file-path.ts
134
142
  function extractCurrentFilePath(context) {
135
- const currentFilePath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
143
+ const currentFilePath = _nullishCoalesce(_nullishCoalesce(context.physicalFilename, () => ( context.filename)), () => ( (context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename())));
136
144
  return normalizePath(currentFilePath);
137
145
  }
138
146
 
@@ -163,7 +171,7 @@ function isUndefined(target) {
163
171
 
164
172
  // src/lib/rule/extract-cwd.ts
165
173
  function extractCwd(context) {
166
- const cwd = _optionalChain([context, 'access', _2 => _2.getCwd, 'optionalCall', _3 => _3()]);
174
+ const cwd = _nullishCoalesce(context.cwd, () => ( _optionalChain([context, 'access', _2 => _2.getCwd, 'optionalCall', _3 => _3()])));
167
175
  if (isUndefined(cwd)) {
168
176
  return void 0;
169
177
  }
@@ -213,6 +221,46 @@ function extractRuleOptions(optionsWithDefault) {
213
221
  return optionsWithDefault[0];
214
222
  }
215
223
 
224
+ // src/lib/feature-sliced/segments-config.ts
225
+ function normalizeSegmentsConfig(config) {
226
+ if (!config) {
227
+ return [...DEFAULT_SEGMENTS];
228
+ }
229
+ if (Array.isArray(config)) {
230
+ const customSegments = config.map((s) => s.toLowerCase());
231
+ const combined = [...DEFAULT_SEGMENTS, ...customSegments];
232
+ return [...new Set(combined)];
233
+ }
234
+ const replacedSegments = config.replace.map((s) => s.toLowerCase());
235
+ return [...new Set(replacedSegments)];
236
+ }
237
+ function isKnownSegment(segment, segments2) {
238
+ if (typeof segment !== "string" || segment === "") {
239
+ return false;
240
+ }
241
+ return segments2.includes(segment.toLowerCase());
242
+ }
243
+
244
+ // src/lib/rule/extract-segments-config.ts
245
+ function isValidSegmentsConfig(value) {
246
+ if (Array.isArray(value)) {
247
+ return value.every((item) => typeof item === "string");
248
+ }
249
+ if (typeof value === "object" && value !== null && "replace" in value) {
250
+ const replaceValue = value.replace;
251
+ return Array.isArray(replaceValue) && replaceValue.every((item) => typeof item === "string");
252
+ }
253
+ return false;
254
+ }
255
+ function extractSegmentsConfig(context) {
256
+ const settings = context.settings;
257
+ const pluginSettings = _optionalChain([settings, 'optionalAccess', _6 => _6[PLUGIN_NAME]]);
258
+ if (_optionalChain([pluginSettings, 'optionalAccess', _7 => _7.segments]) && isValidSegmentsConfig(pluginSettings.segments)) {
259
+ return normalizeSegmentsConfig(pluginSettings.segments);
260
+ }
261
+ return normalizeSegmentsConfig();
262
+ }
263
+
216
264
  // src/lib/rule/get-source-range-without-quotes.ts
217
265
  function getSourceRangeWithoutQuotes([rangeStart, rangeEnd]) {
218
266
  return [rangeStart + 1, rangeEnd - 1];
@@ -286,7 +334,7 @@ function extractCrossImportInfo(path3) {
286
334
  }
287
335
  const crossImportRegex = /(?<sourceSlice>[\w-]+)\/@x\/(?<targetSlice>[\w-]+)(?:\.\w+)?$/;
288
336
  const match = normalizedPath.match(crossImportRegex);
289
- if (!_optionalChain([match, 'optionalAccess', _6 => _6.groups])) {
337
+ if (!_optionalChain([match, 'optionalAccess', _8 => _8.groups])) {
290
338
  return EMPTY_RESULT;
291
339
  }
292
340
  const { sourceSlice, targetSlice } = match.groups;
@@ -317,17 +365,18 @@ function extractLayer(targetPath, cwd, config) {
317
365
  }
318
366
 
319
367
  // src/lib/feature-sliced/extract-segment.ts
320
- function createFsdPartsRegExp(layersWithSlices2) {
368
+ function createFsdPartsRegExp(layersWithSlices2, segmentsList) {
321
369
  const layersUnion = layersWithSlices2.join("|");
322
- const segmentsUnion = segments.join("|");
370
+ const segmentsUnion = segmentsList.join("|");
323
371
  return new RegExp(
324
372
  `(?<=(?<layer>${layersUnion}))\\/(?<slice>([\\w-]*\\/)+?)(?<segment>(${segmentsUnion})(\\.\\w+)?)(\\/(?<segmentFiles>.*))?`
325
373
  );
326
374
  }
327
- function extractSegment(targetPath, config) {
328
- const layersConfig = _nullishCoalesce(config, () => ( normalizeLayersConfig()));
329
- const layersWithSlices2 = getLayersWithSlices(layersConfig);
330
- const fsdPartsRegExp = createFsdPartsRegExp(layersWithSlices2);
375
+ function extractSegment(targetPath, layersConfig, segmentsConfig) {
376
+ const normalizedLayersConfig = _nullishCoalesce(layersConfig, () => ( normalizeLayersConfig()));
377
+ const segmentsList = _nullishCoalesce(segmentsConfig, () => ( [...DEFAULT_SEGMENTS]));
378
+ const layersWithSlices2 = getLayersWithSlices(normalizedLayersConfig);
379
+ const fsdPartsRegExp = createFsdPartsRegExp(layersWithSlices2, segmentsList);
331
380
  const fsdParts = targetPath.match(fsdPartsRegExp);
332
381
  if (fsdParts === null) {
333
382
  return [null, null];
@@ -337,14 +386,15 @@ function extractSegment(targetPath, config) {
337
386
  segmentFiles = null
338
387
  } = fsdParts.groups || {};
339
388
  const fileExtensionRegExp = /\.[^/.]+$/;
340
- const segmentWithoutFileExtension = _optionalChain([segment, 'optionalAccess', _7 => _7.replace, 'call', _8 => _8(fileExtensionRegExp, "")]) || null;
389
+ const segmentWithoutFileExtension = _optionalChain([segment, 'optionalAccess', _9 => _9.replace, 'call', _10 => _10(fileExtensionRegExp, "")]) || null;
341
390
  return [segmentWithoutFileExtension, segmentFiles];
342
391
  }
343
392
 
344
393
  // src/lib/feature-sliced/extract-slice.ts
345
- function extractSlice(targetPath, config) {
346
- const layersConfig = _nullishCoalesce(config, () => ( normalizeLayersConfig()));
347
- const layersWithSlices2 = getLayersWithSlices(layersConfig);
394
+ function extractSlice(targetPath, layersConfig, segmentsConfig) {
395
+ const normalizedLayersConfig = _nullishCoalesce(layersConfig, () => ( normalizeLayersConfig()));
396
+ const segmentsList = _nullishCoalesce(segmentsConfig, () => ( [...DEFAULT_SEGMENTS]));
397
+ const layersWithSlices2 = getLayersWithSlices(normalizedLayersConfig);
348
398
  const pathWithoutFile = targetPath.replace(/\/[\w-]+\.\w+$/, "");
349
399
  const parts = pathWithoutFile.split("/").filter(Boolean);
350
400
  const layerIndex = parts.findIndex(
@@ -355,7 +405,7 @@ function extractSlice(targetPath, config) {
355
405
  }
356
406
  const partsAfterLayer = parts.slice(layerIndex + 1);
357
407
  const segmentIndex = partsAfterLayer.findIndex(
358
- (part) => segments.some((seg) => seg.toLowerCase() === part.toLowerCase())
408
+ (part) => segmentsList.some((seg) => seg.toLowerCase() === part.toLowerCase())
359
409
  );
360
410
  if (segmentIndex > 0) {
361
411
  return partsAfterLayer[segmentIndex - 1];
@@ -367,10 +417,11 @@ function extractSlice(targetPath, config) {
367
417
  }
368
418
 
369
419
  // src/lib/feature-sliced/extract-feature-sliced-parts.ts
370
- function extractFeatureSlicedParts(targetPath, cwd, config) {
371
- const layer = extractLayer(targetPath, cwd, config);
372
- const slice = extractSlice(targetPath, config);
373
- const [segment, segmentFiles] = extractSegment(targetPath, config);
420
+ function extractFeatureSlicedParts(targetPath, cwd, options) {
421
+ const { layersConfig, segmentsConfig } = _nullishCoalesce(options, () => ( {}));
422
+ const layer = extractLayer(targetPath, cwd, layersConfig);
423
+ const slice = extractSlice(targetPath, layersConfig, segmentsConfig);
424
+ const [segment, segmentFiles] = extractSegment(targetPath, layersConfig, segmentsConfig);
374
425
  return {
375
426
  layer,
376
427
  slice,
@@ -442,7 +493,8 @@ function compareFeatureSlicedParts(fsPartsToCompare) {
442
493
  isSameLayerWithoutSlices
443
494
  };
444
495
  }
445
- function extractPathsInfo(node, context, config) {
496
+ function extractPathsInfo(node, context, options) {
497
+ const { layersConfig, segmentsConfig } = _nullishCoalesce(options, () => ( {}));
446
498
  const {
447
499
  targetPath,
448
500
  normalizedTargetPath,
@@ -450,10 +502,10 @@ function extractPathsInfo(node, context, config) {
450
502
  absoluteTargetPath,
451
503
  normalizedCwd
452
504
  } = extractPaths(node, context);
453
- const fsdPartsOfTarget = extractFeatureSlicedParts(absoluteTargetPath, normalizedCwd, config);
454
- const fsdPartsOfCurrentFile = extractFeatureSlicedParts(normalizedCurrentFilePath, normalizedCwd, config);
455
- const validatedFeatureSlicedPartsOfTarget = validateExtractedFeatureSlicedParts(fsdPartsOfTarget, config);
456
- const validatedFeatureSlicedPartsOfCurrentFile = validateExtractedFeatureSlicedParts(fsdPartsOfCurrentFile, config);
505
+ const fsdPartsOfTarget = extractFeatureSlicedParts(absoluteTargetPath, normalizedCwd, { layersConfig, segmentsConfig });
506
+ const fsdPartsOfCurrentFile = extractFeatureSlicedParts(normalizedCurrentFilePath, normalizedCwd, { layersConfig, segmentsConfig });
507
+ const validatedFeatureSlicedPartsOfTarget = validateExtractedFeatureSlicedParts(fsdPartsOfTarget, layersConfig);
508
+ const validatedFeatureSlicedPartsOfCurrentFile = validateExtractedFeatureSlicedParts(fsdPartsOfCurrentFile, layersConfig);
457
509
  const {
458
510
  hasUnknownLayers,
459
511
  isSameLayer,
@@ -552,7 +604,7 @@ function validateAndReport(node, context, optionsWithDefault, options = { needCh
552
604
  if (isIgnoredForValidation) {
553
605
  return;
554
606
  }
555
- const pathsInfo = extractPathsInfo(node, context, layersConfig);
607
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig });
556
608
  if (shouldBeRelative(pathsInfo)) {
557
609
  reportShouldBeRelative(node, context);
558
610
  }
@@ -758,7 +810,7 @@ function validate(node, pathsInfo, allowTypeImports, config) {
758
810
  function reportValidationErrors(nodes, context, pathsInfo, config) {
759
811
  nodes.forEach((node) => reportCanNotImportLayer(context, node, pathsInfo, config));
760
812
  }
761
- function validateAndReport2(node, context, optionsWithDefault, config) {
813
+ function validateAndReport2(node, context, optionsWithDefault, config, segmentsConfig) {
762
814
  if (!hasPath(node)) {
763
815
  return;
764
816
  }
@@ -766,7 +818,7 @@ function validateAndReport2(node, context, optionsWithDefault, config) {
766
818
  if (isIgnoredForValidation) {
767
819
  return;
768
820
  }
769
- const pathsInfo = extractPathsInfo(node, context, config);
821
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig: config, segmentsConfig });
770
822
  const crossImportInfo = extractCrossImportInfo(pathsInfo.normalizedTargetPath);
771
823
  if (crossImportInfo.isCrossImport) {
772
824
  const currentFileSlice = pathsInfo.fsdPartsOfCurrentFile.slice;
@@ -830,12 +882,13 @@ var layers_slices_default = createEslintRule({
830
882
  ],
831
883
  create(context, optionsWithDefault) {
832
884
  const layersConfig = extractLayersConfig(context);
885
+ const segmentsConfig = extractSegmentsConfig(context);
833
886
  return {
834
887
  ImportDeclaration(node) {
835
- validateAndReport2(node, context, optionsWithDefault, layersConfig);
888
+ validateAndReport2(node, context, optionsWithDefault, layersConfig, segmentsConfig);
836
889
  },
837
890
  ImportExpression(node) {
838
- validateAndReport2(node, context, optionsWithDefault, layersConfig);
891
+ validateAndReport2(node, context, optionsWithDefault, layersConfig, segmentsConfig);
839
892
  }
840
893
  };
841
894
  }
@@ -938,7 +991,7 @@ function findTargetSegmentInSameSlice(targetPathParts, currentSliceParts, curren
938
991
  if (currentSliceParts.length > targetPathParts.length)
939
992
  return null;
940
993
  const targetHasSameSlice = currentSliceParts.every(
941
- (part, i) => part.toLowerCase() === _optionalChain([targetPathParts, 'access', _9 => _9[i], 'optionalAccess', _10 => _10.toLowerCase, 'call', _11 => _11()])
994
+ (part, i) => part.toLowerCase() === _optionalChain([targetPathParts, 'access', _11 => _11[i], 'optionalAccess', _12 => _12.toLowerCase, 'call', _13 => _13()])
942
995
  );
943
996
  if (!targetHasSameSlice)
944
997
  return null;
@@ -1067,7 +1120,8 @@ var no_cross_segment_reexport_default = createEslintRule({
1067
1120
  var MESSAGE_ID = {
1068
1121
  SHOULD_BE_FROM_PUBLIC_API: "should-be-from-public-api",
1069
1122
  REMOVE_SUGGESTION: "remove-suggestion",
1070
- LAYERS_PUBLIC_API_NOT_ALLOWED: "layers-public-api-not-allowed"
1123
+ LAYERS_PUBLIC_API_NOT_ALLOWED: "layers-public-api-not-allowed",
1124
+ UNKNOWN_SEGMENT: "unknown-segment"
1071
1125
  };
1072
1126
  var VALIDATION_LEVEL = {
1073
1127
  SEGMENTS: "segments",
@@ -1109,8 +1163,8 @@ function convertToPublicApi(pathsInfo) {
1109
1163
  }
1110
1164
 
1111
1165
  // src/rules/public-api/model/errors.ts
1112
- function reportShouldBeFromPublicApi(node, context, layersConfig) {
1113
- const pathsInfo = extractPathsInfo(node, context, layersConfig);
1166
+ function reportShouldBeFromPublicApi(node, context, layersConfig, segmentsConfig) {
1167
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig, segmentsConfig });
1114
1168
  const [fixedPath, valueToRemove] = convertToPublicApi(pathsInfo);
1115
1169
  context.report({
1116
1170
  node: node.source,
@@ -1129,6 +1183,15 @@ function reportShouldBeFromPublicApi(node, context, layersConfig) {
1129
1183
  ]
1130
1184
  });
1131
1185
  }
1186
+ function reportUnknownSegment(node, context, segment) {
1187
+ context.report({
1188
+ node: node.source,
1189
+ messageId: MESSAGE_ID.UNKNOWN_SEGMENT,
1190
+ data: {
1191
+ segment
1192
+ }
1193
+ });
1194
+ }
1132
1195
  function reportLayersPublicApiNotAllowed(node, context) {
1133
1196
  context.report({
1134
1197
  node,
@@ -1136,6 +1199,49 @@ function reportLayersPublicApiNotAllowed(node, context) {
1136
1199
  });
1137
1200
  }
1138
1201
 
1202
+ // src/rules/public-api/model/is-unknown-segment.ts
1203
+ function isGroupFolder(part) {
1204
+ return part.startsWith("(") && part.endsWith(")");
1205
+ }
1206
+ function extractPotentialSegment(targetPath, layersConfig) {
1207
+ const normalizedLayersConfig = _nullishCoalesce(layersConfig, () => ( normalizeLayersConfig()));
1208
+ const layersWithSlices2 = getLayersWithSlices(normalizedLayersConfig);
1209
+ const parts = targetPath.split("/").filter(Boolean);
1210
+ const layerIndex = parts.findIndex(
1211
+ (part) => layersWithSlices2.some((layer) => layer.toLowerCase() === part.toLowerCase())
1212
+ );
1213
+ if (layerIndex === -1 || layerIndex >= parts.length - 2) {
1214
+ return null;
1215
+ }
1216
+ const partsAfterLayer = parts.slice(layerIndex + 1);
1217
+ const nonGroupParts = partsAfterLayer.filter((part) => !isGroupFolder(part));
1218
+ if (nonGroupParts.length < 2) {
1219
+ return null;
1220
+ }
1221
+ const potentialSegment = nonGroupParts[1];
1222
+ const segmentWithoutExt = _optionalChain([potentialSegment, 'optionalAccess', _14 => _14.replace, 'call', _15 => _15(/\.\w+$/, "")]) || null;
1223
+ return segmentWithoutExt;
1224
+ }
1225
+ function isUnknownSegment(node, context, optionsWithDefault, layersConfig, segmentsConfig) {
1226
+ const ruleOptions = extractRuleOptions(optionsWithDefault);
1227
+ if (ruleOptions.level !== VALIDATION_LEVEL.SEGMENTS) {
1228
+ return null;
1229
+ }
1230
+ const effectiveSegmentsConfig = _nullishCoalesce(segmentsConfig, () => ( [...DEFAULT_SEGMENTS]));
1231
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig, segmentsConfig: effectiveSegmentsConfig });
1232
+ if (pathsInfo.fsdPartsOfTarget.segment) {
1233
+ return null;
1234
+ }
1235
+ const potentialSegment = extractPotentialSegment(pathsInfo.normalizedTargetPath, layersConfig);
1236
+ if (!potentialSegment) {
1237
+ return null;
1238
+ }
1239
+ if (!isKnownSegment(potentialSegment, effectiveSegmentsConfig)) {
1240
+ return potentialSegment;
1241
+ }
1242
+ return null;
1243
+ }
1244
+
1139
1245
  // src/rules/public-api/model/is-index-file.ts
1140
1246
  function isIndexFile(segmentFiles) {
1141
1247
  return /^index\.\w+/i.test(segmentFiles);
@@ -1176,8 +1282,8 @@ function shouldBeFromSegmentsPublicApi(pathsInfo, validateOptions) {
1176
1282
  const needValidateSegments = validateOptions.level === VALIDATION_LEVEL.SEGMENTS;
1177
1283
  return needValidateSegments && !isSegmentsPublicApi(pathsInfo);
1178
1284
  }
1179
- function shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig) {
1180
- const pathsInfo = extractPathsInfo(node, context, layersConfig);
1285
+ function shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig, segmentsConfig) {
1286
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig, segmentsConfig });
1181
1287
  const ruleOptions = extractRuleOptions(optionsWithDefault);
1182
1288
  if (hasNestedCrossImportPath(pathsInfo.normalizedTargetPath)) {
1183
1289
  return true;
@@ -1186,7 +1292,7 @@ function shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig)
1186
1292
  }
1187
1293
 
1188
1294
  // src/rules/public-api/model/validate-and-report.ts
1189
- function validateAndReport4(node, context, optionsWithDefault, layersConfig) {
1295
+ function validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig) {
1190
1296
  if (!hasPath(node)) {
1191
1297
  return;
1192
1298
  }
@@ -1194,8 +1300,13 @@ function validateAndReport4(node, context, optionsWithDefault, layersConfig) {
1194
1300
  if (isIgnoredForValidation) {
1195
1301
  return;
1196
1302
  }
1197
- if (shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig)) {
1198
- reportShouldBeFromPublicApi(node, context, layersConfig);
1303
+ const unknownSegment = isUnknownSegment(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1304
+ if (unknownSegment) {
1305
+ reportUnknownSegment(node, context, unknownSegment);
1306
+ return;
1307
+ }
1308
+ if (shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig, segmentsConfig)) {
1309
+ reportShouldBeFromPublicApi(node, context, layersConfig, segmentsConfig);
1199
1310
  }
1200
1311
  }
1201
1312
 
@@ -1236,7 +1347,8 @@ var public_api_default = createEslintRule({
1236
1347
  messages: {
1237
1348
  [MESSAGE_ID.SHOULD_BE_FROM_PUBLIC_API]: 'Absolute imports are only allowed from public api ("{{ fixedPath }}")',
1238
1349
  [MESSAGE_ID.REMOVE_SUGGESTION]: 'Remove the "{{ valueToRemove }}"',
1239
- [MESSAGE_ID.LAYERS_PUBLIC_API_NOT_ALLOWED]: "The layer public API is not allowed. It harms both architecturally and practically (code splitting)"
1350
+ [MESSAGE_ID.LAYERS_PUBLIC_API_NOT_ALLOWED]: "The layer public API is not allowed. It harms both architecturally and practically (code splitting)",
1351
+ [MESSAGE_ID.UNKNOWN_SEGMENT]: 'Unknown segment "{{ segment }}". Add it to segments configuration or use the public API.'
1240
1352
  },
1241
1353
  schema: [
1242
1354
  {
@@ -1274,18 +1386,19 @@ var public_api_default = createEslintRule({
1274
1386
  ],
1275
1387
  create(context, optionsWithDefault) {
1276
1388
  const layersConfig = extractLayersConfig(context);
1389
+ const segmentsConfig = extractSegmentsConfig(context);
1277
1390
  return {
1278
1391
  ImportDeclaration(node) {
1279
- validateAndReport4(node, context, optionsWithDefault, layersConfig);
1392
+ validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1280
1393
  },
1281
1394
  ImportExpression(node) {
1282
- validateAndReport4(node, context, optionsWithDefault, layersConfig);
1395
+ validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1283
1396
  },
1284
1397
  ExportAllDeclaration(node) {
1285
- validateAndReport4(node, context, optionsWithDefault, layersConfig);
1398
+ validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1286
1399
  },
1287
1400
  ExportNamedDeclaration(node) {
1288
- validateAndReport4(node, context, optionsWithDefault, layersConfig);
1401
+ validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1289
1402
  },
1290
1403
  Program(node) {
1291
1404
  validateAndReportProgram(node, context, optionsWithDefault);
package/dist/index.d.cts CHANGED
@@ -42,6 +42,7 @@ type Segments = ReadonlyArray<'ui' | 'model' | 'lib' | 'api' | 'config' | 'asset
42
42
  type Segment = Segments[number];
43
43
  /**
44
44
  * Slice segments regulated by feature-sliced methodologies
45
+ * @deprecated Use normalizeSegmentsConfig() from segments-config.ts for custom segments support
45
46
  */
46
47
  declare const segments: Segments;
47
48
  type TypedFlatConfigItem = Omit<Linter.Config<Linter.RulesRecord>, 'plugins'> & {
@@ -58,6 +59,7 @@ declare const MESSAGE_ID: {
58
59
  readonly SHOULD_BE_FROM_PUBLIC_API: "should-be-from-public-api";
59
60
  readonly REMOVE_SUGGESTION: "remove-suggestion";
60
61
  readonly LAYERS_PUBLIC_API_NOT_ALLOWED: "layers-public-api-not-allowed";
62
+ readonly UNKNOWN_SEGMENT: "unknown-segment";
61
63
  };
62
64
  declare const VALIDATION_LEVEL: {
63
65
  readonly SEGMENTS: "segments";
package/dist/index.d.ts CHANGED
@@ -42,6 +42,7 @@ type Segments = ReadonlyArray<'ui' | 'model' | 'lib' | 'api' | 'config' | 'asset
42
42
  type Segment = Segments[number];
43
43
  /**
44
44
  * Slice segments regulated by feature-sliced methodologies
45
+ * @deprecated Use normalizeSegmentsConfig() from segments-config.ts for custom segments support
45
46
  */
46
47
  declare const segments: Segments;
47
48
  type TypedFlatConfigItem = Omit<Linter.Config<Linter.RulesRecord>, 'plugins'> & {
@@ -58,6 +59,7 @@ declare const MESSAGE_ID: {
58
59
  readonly SHOULD_BE_FROM_PUBLIC_API: "should-be-from-public-api";
59
60
  readonly REMOVE_SUGGESTION: "remove-suggestion";
60
61
  readonly LAYERS_PUBLIC_API_NOT_ALLOWED: "layers-public-api-not-allowed";
62
+ readonly UNKNOWN_SEGMENT: "unknown-segment";
61
63
  };
62
64
  declare const VALIDATION_LEVEL: {
63
65
  readonly SEGMENTS: "segments";
package/dist/index.js CHANGED
@@ -38,6 +38,14 @@ var segments = [
38
38
  "config",
39
39
  "assets"
40
40
  ];
41
+ var DEFAULT_SEGMENTS = [
42
+ "ui",
43
+ "model",
44
+ "lib",
45
+ "api",
46
+ "config",
47
+ "assets"
48
+ ];
41
49
  var pathSeparator = "/";
42
50
 
43
51
  // src/lib/feature-sliced/layers-config.ts
@@ -80,7 +88,7 @@ function canLayerContainSlices(layer, config) {
80
88
  }
81
89
 
82
90
  // package.json
83
- var version = "2.0.0-rc.7";
91
+ var version = "2.0.0-rc.9";
84
92
 
85
93
  // src/rules/index.ts
86
94
  import pluginImport from "eslint-plugin-import-x";
@@ -132,7 +140,7 @@ function convertToAbsolute(base, target) {
132
140
 
133
141
  // src/lib/rule/extract-current-file-path.ts
134
142
  function extractCurrentFilePath(context) {
135
- const currentFilePath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
143
+ const currentFilePath = context.physicalFilename ?? context.filename ?? (context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename());
136
144
  return normalizePath(currentFilePath);
137
145
  }
138
146
 
@@ -163,7 +171,7 @@ function isUndefined(target) {
163
171
 
164
172
  // src/lib/rule/extract-cwd.ts
165
173
  function extractCwd(context) {
166
- const cwd = context.getCwd?.();
174
+ const cwd = context.cwd ?? context.getCwd?.();
167
175
  if (isUndefined(cwd)) {
168
176
  return void 0;
169
177
  }
@@ -213,6 +221,46 @@ function extractRuleOptions(optionsWithDefault) {
213
221
  return optionsWithDefault[0];
214
222
  }
215
223
 
224
+ // src/lib/feature-sliced/segments-config.ts
225
+ function normalizeSegmentsConfig(config) {
226
+ if (!config) {
227
+ return [...DEFAULT_SEGMENTS];
228
+ }
229
+ if (Array.isArray(config)) {
230
+ const customSegments = config.map((s) => s.toLowerCase());
231
+ const combined = [...DEFAULT_SEGMENTS, ...customSegments];
232
+ return [...new Set(combined)];
233
+ }
234
+ const replacedSegments = config.replace.map((s) => s.toLowerCase());
235
+ return [...new Set(replacedSegments)];
236
+ }
237
+ function isKnownSegment(segment, segments2) {
238
+ if (typeof segment !== "string" || segment === "") {
239
+ return false;
240
+ }
241
+ return segments2.includes(segment.toLowerCase());
242
+ }
243
+
244
+ // src/lib/rule/extract-segments-config.ts
245
+ function isValidSegmentsConfig(value) {
246
+ if (Array.isArray(value)) {
247
+ return value.every((item) => typeof item === "string");
248
+ }
249
+ if (typeof value === "object" && value !== null && "replace" in value) {
250
+ const replaceValue = value.replace;
251
+ return Array.isArray(replaceValue) && replaceValue.every((item) => typeof item === "string");
252
+ }
253
+ return false;
254
+ }
255
+ function extractSegmentsConfig(context) {
256
+ const settings = context.settings;
257
+ const pluginSettings = settings?.[PLUGIN_NAME];
258
+ if (pluginSettings?.segments && isValidSegmentsConfig(pluginSettings.segments)) {
259
+ return normalizeSegmentsConfig(pluginSettings.segments);
260
+ }
261
+ return normalizeSegmentsConfig();
262
+ }
263
+
216
264
  // src/lib/rule/get-source-range-without-quotes.ts
217
265
  function getSourceRangeWithoutQuotes([rangeStart, rangeEnd]) {
218
266
  return [rangeStart + 1, rangeEnd - 1];
@@ -317,17 +365,18 @@ function extractLayer(targetPath, cwd, config) {
317
365
  }
318
366
 
319
367
  // src/lib/feature-sliced/extract-segment.ts
320
- function createFsdPartsRegExp(layersWithSlices2) {
368
+ function createFsdPartsRegExp(layersWithSlices2, segmentsList) {
321
369
  const layersUnion = layersWithSlices2.join("|");
322
- const segmentsUnion = segments.join("|");
370
+ const segmentsUnion = segmentsList.join("|");
323
371
  return new RegExp(
324
372
  `(?<=(?<layer>${layersUnion}))\\/(?<slice>([\\w-]*\\/)+?)(?<segment>(${segmentsUnion})(\\.\\w+)?)(\\/(?<segmentFiles>.*))?`
325
373
  );
326
374
  }
327
- function extractSegment(targetPath, config) {
328
- const layersConfig = config ?? normalizeLayersConfig();
329
- const layersWithSlices2 = getLayersWithSlices(layersConfig);
330
- const fsdPartsRegExp = createFsdPartsRegExp(layersWithSlices2);
375
+ function extractSegment(targetPath, layersConfig, segmentsConfig) {
376
+ const normalizedLayersConfig = layersConfig ?? normalizeLayersConfig();
377
+ const segmentsList = segmentsConfig ?? [...DEFAULT_SEGMENTS];
378
+ const layersWithSlices2 = getLayersWithSlices(normalizedLayersConfig);
379
+ const fsdPartsRegExp = createFsdPartsRegExp(layersWithSlices2, segmentsList);
331
380
  const fsdParts = targetPath.match(fsdPartsRegExp);
332
381
  if (fsdParts === null) {
333
382
  return [null, null];
@@ -342,9 +391,10 @@ function extractSegment(targetPath, config) {
342
391
  }
343
392
 
344
393
  // src/lib/feature-sliced/extract-slice.ts
345
- function extractSlice(targetPath, config) {
346
- const layersConfig = config ?? normalizeLayersConfig();
347
- const layersWithSlices2 = getLayersWithSlices(layersConfig);
394
+ function extractSlice(targetPath, layersConfig, segmentsConfig) {
395
+ const normalizedLayersConfig = layersConfig ?? normalizeLayersConfig();
396
+ const segmentsList = segmentsConfig ?? [...DEFAULT_SEGMENTS];
397
+ const layersWithSlices2 = getLayersWithSlices(normalizedLayersConfig);
348
398
  const pathWithoutFile = targetPath.replace(/\/[\w-]+\.\w+$/, "");
349
399
  const parts = pathWithoutFile.split("/").filter(Boolean);
350
400
  const layerIndex = parts.findIndex(
@@ -355,7 +405,7 @@ function extractSlice(targetPath, config) {
355
405
  }
356
406
  const partsAfterLayer = parts.slice(layerIndex + 1);
357
407
  const segmentIndex = partsAfterLayer.findIndex(
358
- (part) => segments.some((seg) => seg.toLowerCase() === part.toLowerCase())
408
+ (part) => segmentsList.some((seg) => seg.toLowerCase() === part.toLowerCase())
359
409
  );
360
410
  if (segmentIndex > 0) {
361
411
  return partsAfterLayer[segmentIndex - 1];
@@ -367,10 +417,11 @@ function extractSlice(targetPath, config) {
367
417
  }
368
418
 
369
419
  // src/lib/feature-sliced/extract-feature-sliced-parts.ts
370
- function extractFeatureSlicedParts(targetPath, cwd, config) {
371
- const layer = extractLayer(targetPath, cwd, config);
372
- const slice = extractSlice(targetPath, config);
373
- const [segment, segmentFiles] = extractSegment(targetPath, config);
420
+ function extractFeatureSlicedParts(targetPath, cwd, options) {
421
+ const { layersConfig, segmentsConfig } = options ?? {};
422
+ const layer = extractLayer(targetPath, cwd, layersConfig);
423
+ const slice = extractSlice(targetPath, layersConfig, segmentsConfig);
424
+ const [segment, segmentFiles] = extractSegment(targetPath, layersConfig, segmentsConfig);
374
425
  return {
375
426
  layer,
376
427
  slice,
@@ -442,7 +493,8 @@ function compareFeatureSlicedParts(fsPartsToCompare) {
442
493
  isSameLayerWithoutSlices
443
494
  };
444
495
  }
445
- function extractPathsInfo(node, context, config) {
496
+ function extractPathsInfo(node, context, options) {
497
+ const { layersConfig, segmentsConfig } = options ?? {};
446
498
  const {
447
499
  targetPath,
448
500
  normalizedTargetPath,
@@ -450,10 +502,10 @@ function extractPathsInfo(node, context, config) {
450
502
  absoluteTargetPath,
451
503
  normalizedCwd
452
504
  } = extractPaths(node, context);
453
- const fsdPartsOfTarget = extractFeatureSlicedParts(absoluteTargetPath, normalizedCwd, config);
454
- const fsdPartsOfCurrentFile = extractFeatureSlicedParts(normalizedCurrentFilePath, normalizedCwd, config);
455
- const validatedFeatureSlicedPartsOfTarget = validateExtractedFeatureSlicedParts(fsdPartsOfTarget, config);
456
- const validatedFeatureSlicedPartsOfCurrentFile = validateExtractedFeatureSlicedParts(fsdPartsOfCurrentFile, config);
505
+ const fsdPartsOfTarget = extractFeatureSlicedParts(absoluteTargetPath, normalizedCwd, { layersConfig, segmentsConfig });
506
+ const fsdPartsOfCurrentFile = extractFeatureSlicedParts(normalizedCurrentFilePath, normalizedCwd, { layersConfig, segmentsConfig });
507
+ const validatedFeatureSlicedPartsOfTarget = validateExtractedFeatureSlicedParts(fsdPartsOfTarget, layersConfig);
508
+ const validatedFeatureSlicedPartsOfCurrentFile = validateExtractedFeatureSlicedParts(fsdPartsOfCurrentFile, layersConfig);
457
509
  const {
458
510
  hasUnknownLayers,
459
511
  isSameLayer,
@@ -552,7 +604,7 @@ function validateAndReport(node, context, optionsWithDefault, options = { needCh
552
604
  if (isIgnoredForValidation) {
553
605
  return;
554
606
  }
555
- const pathsInfo = extractPathsInfo(node, context, layersConfig);
607
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig });
556
608
  if (shouldBeRelative(pathsInfo)) {
557
609
  reportShouldBeRelative(node, context);
558
610
  }
@@ -758,7 +810,7 @@ function validate(node, pathsInfo, allowTypeImports, config) {
758
810
  function reportValidationErrors(nodes, context, pathsInfo, config) {
759
811
  nodes.forEach((node) => reportCanNotImportLayer(context, node, pathsInfo, config));
760
812
  }
761
- function validateAndReport2(node, context, optionsWithDefault, config) {
813
+ function validateAndReport2(node, context, optionsWithDefault, config, segmentsConfig) {
762
814
  if (!hasPath(node)) {
763
815
  return;
764
816
  }
@@ -766,7 +818,7 @@ function validateAndReport2(node, context, optionsWithDefault, config) {
766
818
  if (isIgnoredForValidation) {
767
819
  return;
768
820
  }
769
- const pathsInfo = extractPathsInfo(node, context, config);
821
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig: config, segmentsConfig });
770
822
  const crossImportInfo = extractCrossImportInfo(pathsInfo.normalizedTargetPath);
771
823
  if (crossImportInfo.isCrossImport) {
772
824
  const currentFileSlice = pathsInfo.fsdPartsOfCurrentFile.slice;
@@ -830,12 +882,13 @@ var layers_slices_default = createEslintRule({
830
882
  ],
831
883
  create(context, optionsWithDefault) {
832
884
  const layersConfig = extractLayersConfig(context);
885
+ const segmentsConfig = extractSegmentsConfig(context);
833
886
  return {
834
887
  ImportDeclaration(node) {
835
- validateAndReport2(node, context, optionsWithDefault, layersConfig);
888
+ validateAndReport2(node, context, optionsWithDefault, layersConfig, segmentsConfig);
836
889
  },
837
890
  ImportExpression(node) {
838
- validateAndReport2(node, context, optionsWithDefault, layersConfig);
891
+ validateAndReport2(node, context, optionsWithDefault, layersConfig, segmentsConfig);
839
892
  }
840
893
  };
841
894
  }
@@ -1067,7 +1120,8 @@ var no_cross_segment_reexport_default = createEslintRule({
1067
1120
  var MESSAGE_ID = {
1068
1121
  SHOULD_BE_FROM_PUBLIC_API: "should-be-from-public-api",
1069
1122
  REMOVE_SUGGESTION: "remove-suggestion",
1070
- LAYERS_PUBLIC_API_NOT_ALLOWED: "layers-public-api-not-allowed"
1123
+ LAYERS_PUBLIC_API_NOT_ALLOWED: "layers-public-api-not-allowed",
1124
+ UNKNOWN_SEGMENT: "unknown-segment"
1071
1125
  };
1072
1126
  var VALIDATION_LEVEL = {
1073
1127
  SEGMENTS: "segments",
@@ -1109,8 +1163,8 @@ function convertToPublicApi(pathsInfo) {
1109
1163
  }
1110
1164
 
1111
1165
  // src/rules/public-api/model/errors.ts
1112
- function reportShouldBeFromPublicApi(node, context, layersConfig) {
1113
- const pathsInfo = extractPathsInfo(node, context, layersConfig);
1166
+ function reportShouldBeFromPublicApi(node, context, layersConfig, segmentsConfig) {
1167
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig, segmentsConfig });
1114
1168
  const [fixedPath, valueToRemove] = convertToPublicApi(pathsInfo);
1115
1169
  context.report({
1116
1170
  node: node.source,
@@ -1129,6 +1183,15 @@ function reportShouldBeFromPublicApi(node, context, layersConfig) {
1129
1183
  ]
1130
1184
  });
1131
1185
  }
1186
+ function reportUnknownSegment(node, context, segment) {
1187
+ context.report({
1188
+ node: node.source,
1189
+ messageId: MESSAGE_ID.UNKNOWN_SEGMENT,
1190
+ data: {
1191
+ segment
1192
+ }
1193
+ });
1194
+ }
1132
1195
  function reportLayersPublicApiNotAllowed(node, context) {
1133
1196
  context.report({
1134
1197
  node,
@@ -1136,6 +1199,49 @@ function reportLayersPublicApiNotAllowed(node, context) {
1136
1199
  });
1137
1200
  }
1138
1201
 
1202
+ // src/rules/public-api/model/is-unknown-segment.ts
1203
+ function isGroupFolder(part) {
1204
+ return part.startsWith("(") && part.endsWith(")");
1205
+ }
1206
+ function extractPotentialSegment(targetPath, layersConfig) {
1207
+ const normalizedLayersConfig = layersConfig ?? normalizeLayersConfig();
1208
+ const layersWithSlices2 = getLayersWithSlices(normalizedLayersConfig);
1209
+ const parts = targetPath.split("/").filter(Boolean);
1210
+ const layerIndex = parts.findIndex(
1211
+ (part) => layersWithSlices2.some((layer) => layer.toLowerCase() === part.toLowerCase())
1212
+ );
1213
+ if (layerIndex === -1 || layerIndex >= parts.length - 2) {
1214
+ return null;
1215
+ }
1216
+ const partsAfterLayer = parts.slice(layerIndex + 1);
1217
+ const nonGroupParts = partsAfterLayer.filter((part) => !isGroupFolder(part));
1218
+ if (nonGroupParts.length < 2) {
1219
+ return null;
1220
+ }
1221
+ const potentialSegment = nonGroupParts[1];
1222
+ const segmentWithoutExt = potentialSegment?.replace(/\.\w+$/, "") || null;
1223
+ return segmentWithoutExt;
1224
+ }
1225
+ function isUnknownSegment(node, context, optionsWithDefault, layersConfig, segmentsConfig) {
1226
+ const ruleOptions = extractRuleOptions(optionsWithDefault);
1227
+ if (ruleOptions.level !== VALIDATION_LEVEL.SEGMENTS) {
1228
+ return null;
1229
+ }
1230
+ const effectiveSegmentsConfig = segmentsConfig ?? [...DEFAULT_SEGMENTS];
1231
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig, segmentsConfig: effectiveSegmentsConfig });
1232
+ if (pathsInfo.fsdPartsOfTarget.segment) {
1233
+ return null;
1234
+ }
1235
+ const potentialSegment = extractPotentialSegment(pathsInfo.normalizedTargetPath, layersConfig);
1236
+ if (!potentialSegment) {
1237
+ return null;
1238
+ }
1239
+ if (!isKnownSegment(potentialSegment, effectiveSegmentsConfig)) {
1240
+ return potentialSegment;
1241
+ }
1242
+ return null;
1243
+ }
1244
+
1139
1245
  // src/rules/public-api/model/is-index-file.ts
1140
1246
  function isIndexFile(segmentFiles) {
1141
1247
  return /^index\.\w+/i.test(segmentFiles);
@@ -1176,8 +1282,8 @@ function shouldBeFromSegmentsPublicApi(pathsInfo, validateOptions) {
1176
1282
  const needValidateSegments = validateOptions.level === VALIDATION_LEVEL.SEGMENTS;
1177
1283
  return needValidateSegments && !isSegmentsPublicApi(pathsInfo);
1178
1284
  }
1179
- function shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig) {
1180
- const pathsInfo = extractPathsInfo(node, context, layersConfig);
1285
+ function shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig, segmentsConfig) {
1286
+ const pathsInfo = extractPathsInfo(node, context, { layersConfig, segmentsConfig });
1181
1287
  const ruleOptions = extractRuleOptions(optionsWithDefault);
1182
1288
  if (hasNestedCrossImportPath(pathsInfo.normalizedTargetPath)) {
1183
1289
  return true;
@@ -1186,7 +1292,7 @@ function shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig)
1186
1292
  }
1187
1293
 
1188
1294
  // src/rules/public-api/model/validate-and-report.ts
1189
- function validateAndReport4(node, context, optionsWithDefault, layersConfig) {
1295
+ function validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig) {
1190
1296
  if (!hasPath(node)) {
1191
1297
  return;
1192
1298
  }
@@ -1194,8 +1300,13 @@ function validateAndReport4(node, context, optionsWithDefault, layersConfig) {
1194
1300
  if (isIgnoredForValidation) {
1195
1301
  return;
1196
1302
  }
1197
- if (shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig)) {
1198
- reportShouldBeFromPublicApi(node, context, layersConfig);
1303
+ const unknownSegment = isUnknownSegment(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1304
+ if (unknownSegment) {
1305
+ reportUnknownSegment(node, context, unknownSegment);
1306
+ return;
1307
+ }
1308
+ if (shouldBeFromPublicApi(node, context, optionsWithDefault, layersConfig, segmentsConfig)) {
1309
+ reportShouldBeFromPublicApi(node, context, layersConfig, segmentsConfig);
1199
1310
  }
1200
1311
  }
1201
1312
 
@@ -1236,7 +1347,8 @@ var public_api_default = createEslintRule({
1236
1347
  messages: {
1237
1348
  [MESSAGE_ID.SHOULD_BE_FROM_PUBLIC_API]: 'Absolute imports are only allowed from public api ("{{ fixedPath }}")',
1238
1349
  [MESSAGE_ID.REMOVE_SUGGESTION]: 'Remove the "{{ valueToRemove }}"',
1239
- [MESSAGE_ID.LAYERS_PUBLIC_API_NOT_ALLOWED]: "The layer public API is not allowed. It harms both architecturally and practically (code splitting)"
1350
+ [MESSAGE_ID.LAYERS_PUBLIC_API_NOT_ALLOWED]: "The layer public API is not allowed. It harms both architecturally and practically (code splitting)",
1351
+ [MESSAGE_ID.UNKNOWN_SEGMENT]: 'Unknown segment "{{ segment }}". Add it to segments configuration or use the public API.'
1240
1352
  },
1241
1353
  schema: [
1242
1354
  {
@@ -1274,18 +1386,19 @@ var public_api_default = createEslintRule({
1274
1386
  ],
1275
1387
  create(context, optionsWithDefault) {
1276
1388
  const layersConfig = extractLayersConfig(context);
1389
+ const segmentsConfig = extractSegmentsConfig(context);
1277
1390
  return {
1278
1391
  ImportDeclaration(node) {
1279
- validateAndReport4(node, context, optionsWithDefault, layersConfig);
1392
+ validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1280
1393
  },
1281
1394
  ImportExpression(node) {
1282
- validateAndReport4(node, context, optionsWithDefault, layersConfig);
1395
+ validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1283
1396
  },
1284
1397
  ExportAllDeclaration(node) {
1285
- validateAndReport4(node, context, optionsWithDefault, layersConfig);
1398
+ validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1286
1399
  },
1287
1400
  ExportNamedDeclaration(node) {
1288
- validateAndReport4(node, context, optionsWithDefault, layersConfig);
1401
+ validateAndReport4(node, context, optionsWithDefault, layersConfig, segmentsConfig);
1289
1402
  },
1290
1403
  Program(node) {
1291
1404
  validateAndReportProgram(node, context, optionsWithDefault);
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@conarti/eslint-plugin-feature-sliced",
3
3
  "type": "module",
4
- "version": "2.0.0-rc.7",
4
+ "version": "2.0.0-rc.9",
5
5
  "description": "Feature-sliced design methodology plugin",
6
6
  "author": "Aleksandr Belous <abelous2009@gmail.com>",
7
7
  "license": "ISC",
8
- "repository": "https://github.com/conarti/eslint-plugin-feature-sliced",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/conarti/eslint-plugin-feature-sliced.git"
11
+ },
9
12
  "keywords": [
10
13
  "eslint",
11
14
  "eslintplugin",