@slip-stream-kit/eslint-plugin 0.10.0 → 0.11.10

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.js CHANGED
@@ -1001,6 +1001,320 @@ var maxJsxReturnSize = {
1001
1001
  }
1002
1002
  };
1003
1003
 
1004
+ // src/utils/package-root.ts
1005
+ import fs from "node:fs";
1006
+ import path from "node:path";
1007
+ var CONFIG_FILENAME = "infra-kit.config.ts";
1008
+ var PACKAGE_MARKERS = [CONFIG_FILENAME, "package.json"];
1009
+ var REPO_MARKERS = ["pnpm-workspace.yaml"];
1010
+ var hasAnyMarker = (dir, markers) => {
1011
+ return markers.some((marker) => {
1012
+ return fs.existsSync(path.posix.join(dir, marker));
1013
+ });
1014
+ };
1015
+ var rootCache = /* @__PURE__ */ new Map();
1016
+ var findUp = (startDir, markers) => {
1017
+ let current = startDir;
1018
+ while (!hasAnyMarker(current, markers)) {
1019
+ const parent = path.posix.dirname(current);
1020
+ if (parent === current) {
1021
+ return null;
1022
+ }
1023
+ current = parent;
1024
+ }
1025
+ return current;
1026
+ };
1027
+ var findPackageRoot = (filename) => {
1028
+ if (!path.posix.isAbsolute(filename)) {
1029
+ return null;
1030
+ }
1031
+ const dir = path.posix.dirname(filename);
1032
+ const cached = rootCache.get(dir);
1033
+ if (cached !== void 0) {
1034
+ return cached;
1035
+ }
1036
+ const root = findUp(dir, PACKAGE_MARKERS);
1037
+ rootCache.set(dir, root);
1038
+ return root;
1039
+ };
1040
+ var dirsUnderSrc = (packageRoot, filename) => {
1041
+ const srcPrefix = `${packageRoot}/src/`;
1042
+ if (!filename.startsWith(srcPrefix)) {
1043
+ return null;
1044
+ }
1045
+ return filename.slice(srcPrefix.length).split("/").slice(0, -1);
1046
+ };
1047
+ var findRepoRoot = (packageRoot) => {
1048
+ return findUp(packageRoot, REPO_MARKERS);
1049
+ };
1050
+
1051
+ // ../config/dist/package-type.js
1052
+ import path2 from "node:path";
1053
+ var PACKAGE_TYPES = ["frontend", "backend", "lib", "e2e", "mobile"];
1054
+ var APP_DIRECTORY_TYPES = {
1055
+ ui: "frontend",
1056
+ api: "backend",
1057
+ tests: "e2e"
1058
+ };
1059
+ var MOBILE_BASENAME = "mobile-app";
1060
+ var DEPENDENCY_SIGNALS = [
1061
+ { type: "e2e", packages: ["@playwright/test"] },
1062
+ { type: "mobile", packages: ["@capacitor/core", "@capacitor/cli"] },
1063
+ // `aws-lambda` / `@types/aws-lambda` are deliberately NOT signals: every consumer backend already
1064
+ // lives at `apps/<app>/api` (row 2), while the only packages those two deps would catch are
1065
+ // libraries (`packages/lib-core` in both consumers, the infra-kit CLI itself) — a lambda types
1066
+ // dependency alone never means backend, exactly as `react` alone never means frontend.
1067
+ { type: "backend", packages: ["serverless"] }
1068
+ ];
1069
+ var fromDirectory = (packageDir, repoRoot) => {
1070
+ const segments = path2.relative(repoRoot, packageDir).split(path2.sep).filter((segment) => {
1071
+ return segment !== "" && segment !== ".";
1072
+ });
1073
+ const [first, , third] = segments;
1074
+ if (segments.length === 3 && first === "apps" && third !== void 0) {
1075
+ const appType = APP_DIRECTORY_TYPES[third];
1076
+ if (appType) return appType;
1077
+ }
1078
+ return segments.at(-1) === MOBILE_BASENAME ? "mobile" : null;
1079
+ };
1080
+ var fromDependencies = (pkgJson) => {
1081
+ const deps = /* @__PURE__ */ new Set([
1082
+ ...Object.keys(pkgJson.dependencies ?? {}),
1083
+ ...Object.keys(pkgJson.devDependencies ?? {}),
1084
+ ...Object.keys(pkgJson.peerDependencies ?? {})
1085
+ ]);
1086
+ for (const signal of DEPENDENCY_SIGNALS) {
1087
+ if (signal.packages.some((name) => {
1088
+ return deps.has(name);
1089
+ }))
1090
+ return signal.type;
1091
+ }
1092
+ const isViteReactApp = deps.has("vite") && deps.has("react") && Boolean(pkgJson.scripts?.dev);
1093
+ return isViteReactApp ? "frontend" : null;
1094
+ };
1095
+ var detectPackageType = ({
1096
+ packageDir,
1097
+ repoRoot,
1098
+ pkgJson,
1099
+ declaredType
1100
+ }) => {
1101
+ return declaredType ?? fromDirectory(packageDir, repoRoot) ?? fromDependencies(pkgJson) ?? "lib";
1102
+ };
1103
+
1104
+ // src/utils/package-type-reader.ts
1105
+ import fs2 from "node:fs";
1106
+ import path3 from "node:path";
1107
+ var BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
1108
+ var FULL_LINE_COMMENT = /^[ \t]*\/\/.*$/gm;
1109
+ var DECLARED_TYPE = new RegExp(`(?:^|[\\s{,])type:\\s*['"](${PACKAGE_TYPES.join("|")})['"]`, "m");
1110
+ var readDeclaredType = (text) => {
1111
+ const stripped = text.replace(BLOCK_COMMENT, "").replace(FULL_LINE_COMMENT, "");
1112
+ const literal = DECLARED_TYPE.exec(stripped)?.[1];
1113
+ return PACKAGE_TYPES.find((candidate) => {
1114
+ return candidate === literal;
1115
+ });
1116
+ };
1117
+ var typeCache = /* @__PURE__ */ new Map();
1118
+ var configMtimeMs = (configPath) => {
1119
+ try {
1120
+ return fs2.statSync(configPath).mtimeMs;
1121
+ } catch {
1122
+ return -1;
1123
+ }
1124
+ };
1125
+ var readManifest = (manifestPath) => {
1126
+ try {
1127
+ return JSON.parse(fs2.readFileSync(manifestPath, "utf8"));
1128
+ } catch {
1129
+ return {};
1130
+ }
1131
+ };
1132
+ var resolvePackageType = (packageRoot) => {
1133
+ const configPath = path3.posix.join(packageRoot, CONFIG_FILENAME);
1134
+ const mtimeMs = configMtimeMs(configPath);
1135
+ const hit = typeCache.get(packageRoot);
1136
+ if (hit !== void 0 && hit.mtimeMs === mtimeMs) {
1137
+ return hit.type;
1138
+ }
1139
+ const declaredType = mtimeMs >= 0 ? readDeclaredType(fs2.readFileSync(configPath, "utf8")) : void 0;
1140
+ const type = detectPackageType({
1141
+ packageDir: packageRoot,
1142
+ repoRoot: findRepoRoot(packageRoot) ?? packageRoot,
1143
+ pkgJson: readManifest(path3.posix.join(packageRoot, "package.json")),
1144
+ declaredType
1145
+ });
1146
+ typeCache.set(packageRoot, { mtimeMs, type });
1147
+ return type;
1148
+ };
1149
+
1150
+ // src/rules/package-structure/package-structure.ts
1151
+ var DEFAULT_ENTRIES = {
1152
+ frontend: {
1153
+ layers: ["app", "features", "lib", "components", "pages", "routes"],
1154
+ segments: {
1155
+ "features/*": ["containers", "components", "services", "__stories__", "__tests__"]
1156
+ },
1157
+ skill: "/infra-kit:fe-architect"
1158
+ },
1159
+ backend: {
1160
+ layers: ["controllers", "services", "lib", "config"],
1161
+ segments: {
1162
+ "services/*": ["__tests__"]
1163
+ },
1164
+ // The skill does not exist yet; the user adds `plugins/infra-kit/skills/be-architect/SKILL.md`
1165
+ // and accepted the dangling pointer until then.
1166
+ skill: "/infra-kit:be-architect"
1167
+ },
1168
+ e2e: {
1169
+ layers: ["tests", "pages", "config", "lib", "components", "mocks", "fixtures"],
1170
+ segments: {
1171
+ "tests/*": ["fixtures", "data", "mocks"]
1172
+ },
1173
+ skill: "/infra-kit:e2e-architect"
1174
+ }
1175
+ };
1176
+ var resolveEntry = (type, options) => {
1177
+ return options[type] ?? DEFAULT_ENTRIES[type];
1178
+ };
1179
+ var UNIT_KEY_SUFFIX = "/*";
1180
+ var assertSegmentKeysNameLayers = (type, entry) => {
1181
+ for (const key of Object.keys(entry.segments ?? {})) {
1182
+ const layer = key.endsWith(UNIT_KEY_SUFFIX) ? key.slice(0, -UNIT_KEY_SUFFIX.length) : key;
1183
+ if (!entry.layers.includes(layer)) {
1184
+ throw new Error(
1185
+ `@wl/package-structure: \`${type}.segments\` key "${key}" names a layer that is not in \`${type}.layers\` (${entry.layers.join(", ")}).`
1186
+ );
1187
+ }
1188
+ }
1189
+ };
1190
+ var LAYER_ENTRY_SCHEMA = {
1191
+ type: "object",
1192
+ properties: {
1193
+ layers: {
1194
+ type: "array",
1195
+ items: { type: "string", minLength: 1 },
1196
+ uniqueItems: true,
1197
+ description: "First-level directories allowed under `src/`."
1198
+ },
1199
+ segments: {
1200
+ type: "object",
1201
+ propertyNames: { pattern: "^[^/]+(/\\*)?$" },
1202
+ additionalProperties: {
1203
+ type: "array",
1204
+ items: { type: "string", minLength: 1 },
1205
+ uniqueItems: true
1206
+ },
1207
+ description: "Allowed directories one level down, keyed by `<layer>` (directly inside it) or `<layer>/*` (inside each of its folders)."
1208
+ },
1209
+ skill: {
1210
+ type: "string",
1211
+ description: 'Skill or doc that owns this layout, appended to the message; `""` drops the sentence.'
1212
+ }
1213
+ },
1214
+ required: ["layers"],
1215
+ additionalProperties: false
1216
+ };
1217
+ var findSegmentViolation = (dirs, entry) => {
1218
+ const [layer, second, third] = dirs;
1219
+ const segments = entry.segments ?? {};
1220
+ const perUnit = segments[`${layer}${UNIT_KEY_SUFFIX}`];
1221
+ if (perUnit !== void 0) {
1222
+ return second !== void 0 && third !== void 0 && !perUnit.includes(third) ? { messageId: "unknownSegment", name: third, scope: `${layer}/${second}`, allowed: perUnit } : null;
1223
+ }
1224
+ const direct = segments[layer];
1225
+ if (direct !== void 0 && second !== void 0 && !direct.includes(second)) {
1226
+ return { messageId: "unknownSegment", name: second, scope: layer, allowed: direct };
1227
+ }
1228
+ return null;
1229
+ };
1230
+ var findViolation = (dirs, entry) => {
1231
+ const layer = dirs[0];
1232
+ if (!entry.layers.includes(layer)) {
1233
+ return { messageId: "unknownLayer", name: layer, scope: "src", allowed: entry.layers };
1234
+ }
1235
+ return findSegmentViolation(dirs, entry);
1236
+ };
1237
+ var inspect = (filename, options) => {
1238
+ const packageRoot = findPackageRoot(filename);
1239
+ if (packageRoot === null) {
1240
+ return null;
1241
+ }
1242
+ const dirs = dirsUnderSrc(packageRoot, filename);
1243
+ if (dirs === null || dirs.length === 0) {
1244
+ return null;
1245
+ }
1246
+ const type = resolvePackageType(packageRoot);
1247
+ const entry = resolveEntry(type, options);
1248
+ if (entry === void 0) {
1249
+ return null;
1250
+ }
1251
+ assertSegmentKeysNameLayers(type, entry);
1252
+ const violation = findViolation(dirs, entry);
1253
+ return violation === null ? null : { violation, type, entry };
1254
+ };
1255
+ var packageStructure = {
1256
+ meta: {
1257
+ type: "problem",
1258
+ docs: {
1259
+ description: "Allow only the `src/` layers, and the segments inside them, that belong to the package type.",
1260
+ recommended: true,
1261
+ url: "https://github.com/ArthurSaenz/infra-kit/tree/main/apps/infra-kit/eslint-plugin#package-structure"
1262
+ },
1263
+ schema: [
1264
+ {
1265
+ type: "object",
1266
+ properties: {
1267
+ frontend: LAYER_ENTRY_SCHEMA,
1268
+ backend: LAYER_ENTRY_SCHEMA,
1269
+ lib: LAYER_ENTRY_SCHEMA,
1270
+ e2e: LAYER_ENTRY_SCHEMA,
1271
+ mobile: LAYER_ENTRY_SCHEMA,
1272
+ ignore: {
1273
+ type: "array",
1274
+ items: { type: "string" },
1275
+ description: "Optional globs; the rule is skipped for matching files."
1276
+ }
1277
+ },
1278
+ additionalProperties: false
1279
+ }
1280
+ ],
1281
+ messages: {
1282
+ unknownLayer: "`{{name}}` is not an allowed `src/` layer for package type `{{type}}` (allowed: {{allowed}}).{{seeSkill}}",
1283
+ unknownSegment: "`{{name}}` is not an allowed segment of `{{scope}}` for package type `{{type}}` (allowed: {{allowed}}).{{seeSkill}}"
1284
+ }
1285
+ },
1286
+ create(context) {
1287
+ const options = context.options[0] ?? {};
1288
+ const ignore = options.ignore ?? [];
1289
+ if (ignore.length > 0 && matchesAnyGlob(context.filename, ignore)) {
1290
+ return {};
1291
+ }
1292
+ const found = inspect(context.filename, options);
1293
+ if (found === null) {
1294
+ return {};
1295
+ }
1296
+ const { violation, type, entry } = found;
1297
+ return {
1298
+ // Reported on Program so the IDE marks the file from line 1: every file under the offending
1299
+ // directory carries the same message, which is how a "red folder" surfaces in the explorer.
1300
+ Program(program) {
1301
+ context.report({
1302
+ node: program,
1303
+ messageId: violation.messageId,
1304
+ data: {
1305
+ name: violation.name,
1306
+ scope: violation.scope,
1307
+ type,
1308
+ allowed: violation.allowed.join(", "),
1309
+ // Message templates have no conditionals, so the optional sentence is assembled here.
1310
+ seeSkill: entry.skill ? ` See ${entry.skill} for the ${type} layout.` : ""
1311
+ }
1312
+ });
1313
+ }
1314
+ };
1315
+ }
1316
+ };
1317
+
1004
1318
  // src/rules/props-destructuring-blank-line/props-destructuring-blank-line.ts
1005
1319
  var isPropsDestructuring = (statement) => {
1006
1320
  if (statement.type !== "VariableDeclaration") {
@@ -1315,10 +1629,10 @@ var propsTypeReference = {
1315
1629
 
1316
1630
  // src/rules/require-component-stories/require-component-stories.ts
1317
1631
  import { existsSync } from "node:fs";
1318
- import path2 from "node:path";
1632
+ import path5 from "node:path";
1319
1633
 
1320
1634
  // src/utils/story-path.ts
1321
- import path from "node:path";
1635
+ import path4 from "node:path";
1322
1636
  var DEFAULT_STORY_PATH_OPTIONS = {
1323
1637
  storiesDir: "__stories__",
1324
1638
  storySuffix: ".stories",
@@ -1337,10 +1651,10 @@ var parse = (filePath) => {
1337
1651
  const normalized = toPosix(filePath);
1338
1652
  const segments = normalized.split("/");
1339
1653
  const basename = segments[segments.length - 1] ?? "";
1340
- const ext = path.posix.extname(basename);
1654
+ const ext = path4.posix.extname(basename);
1341
1655
  const base = ext ? basename.slice(0, -ext.length) : basename;
1342
1656
  return {
1343
- dir: path.posix.dirname(normalized),
1657
+ dir: path4.posix.dirname(normalized),
1344
1658
  base,
1345
1659
  ext,
1346
1660
  parent: segments[segments.length - 2] ?? "",
@@ -1382,10 +1696,10 @@ var deriveExpectedStoryPaths = (filePath, opts) => {
1382
1696
  return [];
1383
1697
  }
1384
1698
  const parsed = parse(filePath);
1385
- const storyBaseDir = classification.mode === "feature-root" ? path.posix.dirname(parsed.dir) : parsed.dir;
1386
- const storyDir = path.posix.join(storyBaseDir, options.storiesDir);
1699
+ const storyBaseDir = classification.mode === "feature-root" ? path4.posix.dirname(parsed.dir) : parsed.dir;
1700
+ const storyDir = path4.posix.join(storyBaseDir, options.storiesDir);
1387
1701
  return options.storyExtensions.map((extension) => {
1388
- return path.posix.join(storyDir, `${parsed.base}${options.storySuffix}${extension}`);
1702
+ return path4.posix.join(storyDir, `${parsed.base}${options.storySuffix}${extension}`);
1389
1703
  });
1390
1704
  };
1391
1705
 
@@ -1505,7 +1819,7 @@ var requireComponentStories = {
1505
1819
  node: program,
1506
1820
  messageId: "missingStory",
1507
1821
  data: {
1508
- component: path2.posix.basename(context.filename.split("\\").join("/")),
1822
+ component: path5.posix.basename(context.filename.split("\\").join("/")),
1509
1823
  expected: candidates[0] ?? ""
1510
1824
  }
1511
1825
  });
@@ -1803,7 +2117,8 @@ var rules = {
1803
2117
  "max-jsdoc-lines": maxJsdocLines,
1804
2118
  "max-jsdoc-summary-lines": maxJsdocSummaryLines,
1805
2119
  "require-component-stories": requireComponentStories,
1806
- "require-jsdoc-example": requireJsdocExample
2120
+ "require-jsdoc-example": requireJsdocExample,
2121
+ "package-structure": packageStructure
1807
2122
  };
1808
2123
 
1809
2124
  // src/index.ts
@@ -1811,7 +2126,7 @@ var PLUGIN_NAME = "@wl";
1811
2126
  var plugin = {
1812
2127
  meta: {
1813
2128
  name: "@wl/eslint-plugin",
1814
- version: "0.10.0"
2129
+ version: "0.11.10"
1815
2130
  },
1816
2131
  rules,
1817
2132
  configs: {}
@@ -1887,6 +2202,19 @@ plugin.configs.recommended = [
1887
2202
  rules: {
1888
2203
  [`${PLUGIN_NAME}/require-jsdoc-example`]: "warn"
1889
2204
  }
2205
+ },
2206
+ // `package-structure` judges a file by its directory, not its contents, so it must reach every
2207
+ // source extension — the `**/*.tsx` block above would never see a `.ts` under `src/core/`. Its
2208
+ // own block (like `require-jsdoc-example`) with `plugins` re-declared keeps it self-contained.
2209
+ // `error` from day one: the per-type entries are overridable per package via `config({ rules })`.
2210
+ {
2211
+ files: ["**/*.{ts,tsx,js,jsx}"],
2212
+ plugins: {
2213
+ [PLUGIN_NAME]: plugin
2214
+ },
2215
+ rules: {
2216
+ [`${PLUGIN_NAME}/package-structure`]: "error"
2217
+ }
1890
2218
  }
1891
2219
  ];
1892
2220
  var meta = plugin.meta;