@sarj/eslint-plugin 15.17.2 → 15.17.3

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
@@ -5890,7 +5890,7 @@ var NO_RAW_FETCH_OUTSIDE_CLIENTS_DOCUMENTATION = {
5890
5890
  rationale: "Scattered fetch calls bypass shared transport policy and are harder to stub and observe consistently.",
5891
5891
  remediation: "Move the request into a client module and call that abstraction from application code.",
5892
5892
  category: "architecture",
5893
- limitations: ["Tests, client-layer paths, constructed handoffs, and pre-signed URL transfers are excluded."],
5893
+ limitations: ["Tests, client-layer paths, constructed handoffs, and pre-signed URL transfers are excluded. Configure the same literal Next.js basePath here and on prefer-server-actions so one rule owns each internal mutation."],
5894
5894
  examples: [
5895
5895
  { id: "client-call", title: "Use a client abstraction", outcome: "no-match", files: [{ path: "src/routes/handler.ts", source: "const response = await billingClient.getInvoice(id);" }], focusPath: "src/routes/handler.ts", expectedCount: 0, public: true },
5896
5896
  { id: "raw-fetch", title: "Do not call global fetch here", outcome: "match", files: [{ path: "src/routes/handler.ts", source: "const response = await fetch('/api/invoices');" }], focusPath: "src/routes/handler.ts", expectedCount: 1, public: true }
@@ -5935,7 +5935,10 @@ var ANALYTICS_SEGMENTS2 = /* @__PURE__ */ new Set([
5935
5935
  ]);
5936
5936
  var SERVER_ACTION_SKIP_FILE_RE = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
5937
5937
  var NON_REACT_FRAMEWORK_RE = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
5938
- var NEXT_MODULE_PATH_RE = /(?:^|[/\\])(?:app|pages)[/\\]/u;
5938
+ var BASE_PATH_RE = /^\/(?!$)(?!.*[?#])(?:[^/]+\/)*[^/]+$/u;
5939
+ function isValidBasePath(basePath) {
5940
+ return BASE_PATH_RE.test(basePath) && !basePath.split("/").some((segment) => segment === "." || segment === "..");
5941
+ }
5939
5942
  function isGlobalFetchCall(node, resolvesToGlobal) {
5940
5943
  const callee = node.callee;
5941
5944
  if (callee.type === "Identifier") {
@@ -6031,6 +6034,10 @@ var no_raw_fetch_outside_clients_default = createRule({
6031
6034
  type: "array",
6032
6035
  items: { type: "string" },
6033
6036
  description: "Regular-expression sources matched against the filename. Replaces the defaults."
6037
+ },
6038
+ basePath: {
6039
+ type: "string",
6040
+ pattern: "^/(?!$)(?!.*[?#])(?!(?:.*/)?\\.\\.?(?:/|$))(?:[^/]+/)*[^/]+$"
6034
6041
  }
6035
6042
  },
6036
6043
  additionalProperties: false
@@ -6052,12 +6059,18 @@ var no_raw_fetch_outside_clients_default = createRule({
6052
6059
  (statement) => statement.type === import_utils33.AST_NODE_TYPES.ImportDeclaration && typeof statement.source.value === "string" && NON_REACT_FRAMEWORK_RE.test(statement.source.value)
6053
6060
  );
6054
6061
  const hasUseClientDirective = context.sourceCode.ast.body.some(
6055
- (statement) => statement.type === import_utils33.AST_NODE_TYPES.ExpressionStatement && statement.expression.type === import_utils33.AST_NODE_TYPES.Literal && statement.expression.value === "use client"
6062
+ (statement) => statement.type === import_utils33.AST_NODE_TYPES.ExpressionStatement && statement.directive === "use client"
6056
6063
  );
6057
- const hasNextImport = context.sourceCode.ast.body.some(
6058
- (statement) => statement.type === import_utils33.AST_NODE_TYPES.ImportDeclaration && typeof statement.source.value === "string" && (statement.source.value === "next" || statement.source.value.startsWith("next/"))
6064
+ const hasUseServerDirective = context.sourceCode.ast.body.some(
6065
+ (statement) => statement.type === import_utils33.AST_NODE_TYPES.ExpressionStatement && statement.directive === "use server"
6059
6066
  );
6060
- const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE.test(filename);
6067
+ const importsServerOnly = context.sourceCode.ast.body.some(
6068
+ (statement) => statement.type === import_utils33.AST_NODE_TYPES.ImportDeclaration && typeof statement.source.value === "string" && (statement.source.value === "server-only" || statement.source.value === "next/server")
6069
+ );
6070
+ const internalApiPrefixes = ["/api"];
6071
+ if (options?.basePath !== void 0 && isValidBasePath(options.basePath)) {
6072
+ internalApiPrefixes.push(`${options.basePath}/api`);
6073
+ }
6061
6074
  function resolvesToGlobal(identifier) {
6062
6075
  const variable = import_utils33.ASTUtils.findVariable(
6063
6076
  context.sourceCode.getScope(identifier),
@@ -6090,15 +6103,15 @@ var no_raw_fetch_outside_clients_default = createRule({
6090
6103
  function isInternalApiUrl(node) {
6091
6104
  const resolved = resolveNode2(node ?? void 0);
6092
6105
  if (resolved?.type === import_utils33.AST_NODE_TYPES.Literal) {
6093
- return typeof resolved.value === "string" && /^\/(?!\/)(?:[^/]+\/)*api(?:\/|$)/.test(resolved.value);
6106
+ return typeof resolved.value === "string" && internalApiPrefixes.some(
6107
+ (prefix) => resolved.value === prefix || resolved.value.startsWith(`${prefix}/`)
6108
+ );
6094
6109
  }
6095
6110
  if (resolved?.type === import_utils33.AST_NODE_TYPES.TemplateLiteral) {
6096
6111
  const prefix = resolved.quasis[0]?.value.cooked;
6097
- return typeof prefix === "string" && /^\/(?!\/)(?:[^/]+\/)*api(?:\/|$)/.test(prefix);
6098
- }
6099
- if (resolved?.type === import_utils33.AST_NODE_TYPES.CallExpression && resolved.callee.type === import_utils33.AST_NODE_TYPES.Identifier && resolved.callee.name === "withBase") {
6100
- const first = resolved.arguments[0];
6101
- return first !== void 0 && first.type !== import_utils33.AST_NODE_TYPES.SpreadElement ? isInternalApiUrl(first) : false;
6112
+ return typeof prefix === "string" && internalApiPrefixes.some(
6113
+ (apiPrefix) => prefix === apiPrefix || prefix.startsWith(`${apiPrefix}/`)
6114
+ );
6102
6115
  }
6103
6116
  return resolved?.type === import_utils33.AST_NODE_TYPES.BinaryExpression && resolved.operator === "+" && isInternalApiUrl(resolved.left);
6104
6117
  }
@@ -6118,7 +6131,7 @@ var no_raw_fetch_outside_clients_default = createRule({
6118
6131
  return resolved?.type === import_utils33.AST_NODE_TYPES.LogicalExpression && resolved.operator === "||" && (isMutationMethod2(resolved.left) || isMutationMethod2(resolved.right));
6119
6132
  }
6120
6133
  function serverActionOwns(node) {
6121
- if (node.callee.type !== import_utils33.AST_NODE_TYPES.Identifier || !hasNextEvidence || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
6134
+ if (node.callee.type !== import_utils33.AST_NODE_TYPES.Identifier || !hasUseClientDirective || hasUseServerDirective || importsServerOnly || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
6122
6135
  return false;
6123
6136
  }
6124
6137
  const url = node.arguments[0];
@@ -9775,25 +9788,25 @@ var no_unsafe_mock_casting_default = createRule({
9775
9788
  }
9776
9789
  const directBindings = /* @__PURE__ */ new Set();
9777
9790
  const namespaceBindings = /* @__PURE__ */ new Set();
9778
- function resolve(identifier) {
9791
+ function resolve2(identifier) {
9779
9792
  return import_utils55.ASTUtils.findVariable(
9780
9793
  context.sourceCode.getScope(identifier),
9781
9794
  identifier.name
9782
9795
  );
9783
9796
  }
9784
9797
  function record(identifier, destination) {
9785
- const binding = resolve(identifier);
9798
+ const binding = resolve2(identifier);
9786
9799
  if (binding !== null) destination.add(binding);
9787
9800
  }
9788
9801
  function isMockTypeReference(node) {
9789
9802
  if (node.type !== import_utils55.AST_NODE_TYPES.TSTypeReference) return false;
9790
9803
  const typeName = node.typeName;
9791
9804
  if (typeName.type === import_utils55.AST_NODE_TYPES.Identifier) {
9792
- const binding = resolve(typeName);
9805
+ const binding = resolve2(typeName);
9793
9806
  return binding !== null && directBindings.has(binding);
9794
9807
  }
9795
9808
  if (typeName.type === import_utils55.AST_NODE_TYPES.TSQualifiedName && typeName.left.type === import_utils55.AST_NODE_TYPES.Identifier && MOCK_TYPE_NAMES.has(typeName.right.name)) {
9796
- const binding = resolve(typeName.left);
9809
+ const binding = resolve2(typeName.left);
9797
9810
  return binding !== null && namespaceBindings.has(binding);
9798
9811
  }
9799
9812
  return false;
@@ -11197,6 +11210,10 @@ function unwrapTransparentExport(node) {
11197
11210
 
11198
11211
  // src/rules/prefer-shadcn-primitives.ts
11199
11212
  var import_utils66 = require("@typescript-eslint/utils");
11213
+ var import_typescript_estree = require("@typescript-eslint/typescript-estree");
11214
+ var import_node_fs = require("fs");
11215
+ var import_node_path = require("path");
11216
+ var import_jsonc_parser = require("jsonc-parser");
11200
11217
  var PREFER_SHADCN_PRIMITIVES_DOCUMENTATION = {
11201
11218
  summary: "Require visible raw JSX controls to use the corresponding shared shadcn primitive.",
11202
11219
  rationale: "Shared primitives centralize interaction, accessibility, and visual behavior across the product.",
@@ -11204,22 +11221,35 @@ var PREFER_SHADCN_PRIMITIVES_DOCUMENTATION = {
11204
11221
  category: "style",
11205
11222
  limitations: [
11206
11223
  "Hidden and file inputs, unassociated labels, and non-control semantic elements are excluded.",
11207
- "Tests and the shared components/ui primitive implementation tree are excluded."
11224
+ "Tests and the shared components/ui primitive implementation tree are excluded.",
11225
+ "Package-local project detection is opt-in and fails closed unless components.json, one unambiguous tsconfig/jsconfig alias, the exact primitive module, and its expected export all exist."
11208
11226
  ],
11209
11227
  examples: [
11210
11228
  { id: "shared-button", title: "Use a shared button", outcome: "no-match", files: [{ path: "src/form.tsx", source: "import { Button } from '@/components/ui/button'; const action = <Button>Save</Button>;" }], focusPath: "src/form.tsx", expectedCount: 0, public: true },
11211
11229
  { id: "raw-button", title: "Do not use a raw button", outcome: "match", files: [{ path: "src/form.tsx", source: "import { Card } from '@/components/ui/card'; const action = <button>Save</button>;" }], focusPath: "src/form.tsx", expectedCount: 1, public: true }
11212
11230
  ]
11213
11231
  };
11214
- var SHADCN_PRIMITIVES = {
11215
- button: "Button",
11216
- dialog: "Dialog or AlertDialog family",
11217
- input: "Input",
11218
- label: "Label",
11219
- progress: "Progress",
11220
- select: "Select family",
11221
- table: "Table family",
11222
- textarea: "Textarea"
11232
+ var RAW_PRIMITIVES = {
11233
+ button: { capability: "Button", replacement: "Button" },
11234
+ dialog: { capability: "Dialog", replacement: "Dialog or AlertDialog family" },
11235
+ input: { capability: "Input", replacement: "Input" },
11236
+ label: { capability: "Label", replacement: "Label" },
11237
+ progress: { capability: "Progress", replacement: "Progress" },
11238
+ select: { capability: "Select", replacement: "Select family" },
11239
+ table: { capability: "Table", replacement: "Table family" },
11240
+ textarea: { capability: "Textarea", replacement: "Textarea" }
11241
+ };
11242
+ var CAPABILITIES = {
11243
+ Button: "button",
11244
+ Checkbox: "checkbox",
11245
+ Dialog: "dialog",
11246
+ Input: "input",
11247
+ Label: "label",
11248
+ Progress: "progress",
11249
+ RadioGroup: "radio-group",
11250
+ Select: "select",
11251
+ Table: "table",
11252
+ Textarea: "textarea"
11223
11253
  };
11224
11254
  var LABELABLE_ELEMENTS = /* @__PURE__ */ new Set([
11225
11255
  "button",
@@ -11231,7 +11261,7 @@ var LABELABLE_ELEMENTS = /* @__PURE__ */ new Set([
11231
11261
  "textarea"
11232
11262
  ]);
11233
11263
  var SHARED_PRIMITIVE_IMPLEMENTATION_RE = /(?:^|\/)components\/ui(?:\/|$)/i;
11234
- var SHARED_PRIMITIVE_IMPORT_RE = /(?:^|\/)components\/ui\/[^/]+$/i;
11264
+ var SHARED_PRIMITIVE_IMPORT_RE = /(?:^|\/)components\/ui\/([^/]+)$/i;
11235
11265
  var AMBIGUOUS_INPUT_TYPES = /* @__PURE__ */ new Set([
11236
11266
  "button",
11237
11267
  "color",
@@ -11240,10 +11270,175 @@ var AMBIGUOUS_INPUT_TYPES = /* @__PURE__ */ new Set([
11240
11270
  "reset",
11241
11271
  "submit"
11242
11272
  ]);
11273
+ var MAX_PROJECT_FILE_BYTES = 1048576;
11274
+ var PROJECT_PRIMITIVES_CACHE = /* @__PURE__ */ new Map();
11275
+ function detectProjectPrimitives(filename, requested) {
11276
+ const packageRoot = findPackageRoot((0, import_node_path.dirname)(filename));
11277
+ if (packageRoot === null) return { available: /* @__PURE__ */ new Set(), uiRoot: null };
11278
+ const manifest = readJsonc((0, import_node_path.join)(packageRoot, "components.json"));
11279
+ const alias = stringProperty(manifest, "aliases", "ui");
11280
+ const unresolvedUiRoot = alias === null ? null : resolveAlias(packageRoot, alias);
11281
+ if (unresolvedUiRoot === null) return { available: /* @__PURE__ */ new Set(), uiRoot: null };
11282
+ const uiRoot = safeContainedDirectory(packageRoot, unresolvedUiRoot);
11283
+ if (uiRoot === null) {
11284
+ return { available: /* @__PURE__ */ new Set(), uiRoot: null };
11285
+ }
11286
+ const moduleCandidates = /* @__PURE__ */ new Map();
11287
+ for (const [capability, moduleName] of Object.entries(CAPABILITIES)) {
11288
+ if (!requested.has(capability)) continue;
11289
+ moduleCandidates.set(capability, ["tsx", "ts", "jsx", "js"].flatMap((extension) => [
11290
+ (0, import_node_path.join)(uiRoot, `${moduleName}.${extension}`),
11291
+ (0, import_node_path.join)(uiRoot, moduleName, `index.${extension}`)
11292
+ ]));
11293
+ }
11294
+ const fingerprint = [
11295
+ (0, import_node_path.join)(packageRoot, "components.json"),
11296
+ (0, import_node_path.join)(packageRoot, "tsconfig.json"),
11297
+ (0, import_node_path.join)(packageRoot, "jsconfig.json"),
11298
+ ...[...moduleCandidates.values()].flat()
11299
+ ].map(fileFingerprint).join("|");
11300
+ const cacheKey = `${packageRoot}:${[...requested].sort().join(",")}`;
11301
+ const cached = PROJECT_PRIMITIVES_CACHE.get(cacheKey);
11302
+ if (cached?.fingerprint === fingerprint) return cached.value;
11303
+ const available = /* @__PURE__ */ new Set();
11304
+ for (const [capability, candidates] of moduleCandidates) {
11305
+ if (candidates.some(
11306
+ (candidate2) => exportsPrimitive(candidate2, capability)
11307
+ )) {
11308
+ available.add(capability);
11309
+ }
11310
+ }
11311
+ const value = { available, uiRoot };
11312
+ PROJECT_PRIMITIVES_CACHE.set(cacheKey, { fingerprint, value });
11313
+ return value;
11314
+ }
11315
+ function fileFingerprint(path) {
11316
+ try {
11317
+ const stat = (0, import_node_fs.lstatSync)(path);
11318
+ return stat.isFile() ? `${path}:${stat.size}:${stat.mtimeMs}` : `${path}:excluded`;
11319
+ } catch {
11320
+ return `${path}:missing`;
11321
+ }
11322
+ }
11323
+ function isWithin2(root, candidate2) {
11324
+ const path = (0, import_node_path.relative)(root, candidate2);
11325
+ return path === "" || !path.startsWith(`..${import_node_path.sep}`) && path !== ".." && !(0, import_node_path.isAbsolute)(path);
11326
+ }
11327
+ function safeContainedDirectory(root, candidate2) {
11328
+ try {
11329
+ if (!(0, import_node_fs.lstatSync)(candidate2).isDirectory()) return null;
11330
+ const realRoot = (0, import_node_fs.realpathSync)(root);
11331
+ const realCandidate = (0, import_node_fs.realpathSync)(candidate2);
11332
+ return isWithin2(realRoot, realCandidate) ? realCandidate : null;
11333
+ } catch {
11334
+ return null;
11335
+ }
11336
+ }
11337
+ function findPackageRoot(startDir) {
11338
+ let dir = startDir;
11339
+ const filesystemRoot = (0, import_node_path.parse)(dir).root;
11340
+ for (; ; ) {
11341
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
11342
+ const parent = (0, import_node_path.dirname)(dir);
11343
+ if (dir === filesystemRoot || parent === dir) return null;
11344
+ dir = parent;
11345
+ }
11346
+ }
11347
+ function readJsonc(path) {
11348
+ try {
11349
+ const source = readSmallRegularFile(path);
11350
+ if (source === null) return null;
11351
+ const errors = [];
11352
+ const value = (0, import_jsonc_parser.parse)(source, errors, {
11353
+ allowTrailingComma: true,
11354
+ disallowComments: false
11355
+ });
11356
+ return errors.length === 0 ? value : null;
11357
+ } catch {
11358
+ return null;
11359
+ }
11360
+ }
11361
+ function readSmallRegularFile(path) {
11362
+ try {
11363
+ const stat = (0, import_node_fs.lstatSync)(path);
11364
+ if (!stat.isFile() || stat.size > MAX_PROJECT_FILE_BYTES) return null;
11365
+ return (0, import_node_fs.readFileSync)(path, "utf8");
11366
+ } catch {
11367
+ return null;
11368
+ }
11369
+ }
11370
+ function stringProperty(value, ...keys) {
11371
+ let current = value;
11372
+ for (const key of keys) {
11373
+ if (typeof current !== "object" || current === null || !(key in current)) return null;
11374
+ current = current[key];
11375
+ }
11376
+ return typeof current === "string" && current.trim() !== "" ? current : null;
11377
+ }
11378
+ function resolveAlias(packageRoot, alias) {
11379
+ if ((0, import_node_path.isAbsolute)(alias)) return null;
11380
+ if (alias.startsWith(".")) return (0, import_node_path.resolve)(packageRoot, alias);
11381
+ const configPath = ["tsconfig.json", "jsconfig.json"].map((name) => (0, import_node_path.join)(packageRoot, name)).find(import_node_fs.existsSync);
11382
+ if (configPath === void 0) return null;
11383
+ const config = readJsonc(configPath);
11384
+ if (typeof config !== "object" || config === null) return null;
11385
+ const compilerOptions = config["compilerOptions"];
11386
+ if (typeof compilerOptions !== "object" || compilerOptions === null) return null;
11387
+ const options = compilerOptions;
11388
+ const baseUrl = typeof options["baseUrl"] === "string" ? options["baseUrl"] : ".";
11389
+ const paths = options["paths"];
11390
+ if (typeof paths !== "object" || paths === null) return null;
11391
+ const matches = [];
11392
+ for (const [pattern, rawTargets] of Object.entries(paths)) {
11393
+ if (!Array.isArray(rawTargets) || rawTargets.length !== 1) {
11394
+ continue;
11395
+ }
11396
+ const [target] = rawTargets;
11397
+ if (typeof target !== "string") continue;
11398
+ const star = pattern.indexOf("*");
11399
+ if (star === -1) {
11400
+ if (pattern === alias) matches.push(target);
11401
+ continue;
11402
+ }
11403
+ const prefix = pattern.slice(0, star);
11404
+ const suffix = pattern.slice(star + 1);
11405
+ if (!alias.startsWith(prefix) || !alias.endsWith(suffix)) continue;
11406
+ const substitution = alias.slice(prefix.length, alias.length - suffix.length);
11407
+ matches.push(target.replace("*", substitution));
11408
+ }
11409
+ const [match] = matches;
11410
+ if (matches.length !== 1 || match === void 0) return null;
11411
+ return (0, import_node_path.resolve)(packageRoot, baseUrl, match);
11412
+ }
11413
+ function exportsPrimitive(path, exportName) {
11414
+ try {
11415
+ const source = readSmallRegularFile(path);
11416
+ if (source === null) return false;
11417
+ const program = (0, import_typescript_estree.parse)(source, { jsx: true, sourceType: "module" });
11418
+ return program.body.some((statement) => {
11419
+ if (statement.type !== import_utils66.AST_NODE_TYPES.ExportNamedDeclaration) return false;
11420
+ if (statement.exportKind === "type") return false;
11421
+ if (statement.specifiers.some(
11422
+ (specifier) => specifier.type === import_utils66.AST_NODE_TYPES.ExportSpecifier && specifier.exportKind !== "type" && specifier.exported.type === import_utils66.AST_NODE_TYPES.Identifier && specifier.exported.name === exportName
11423
+ )) {
11424
+ return true;
11425
+ }
11426
+ const declaration = statement.declaration;
11427
+ if (declaration?.type === import_utils66.AST_NODE_TYPES.VariableDeclaration) {
11428
+ return declaration.declarations.some(
11429
+ (item) => item.id.type === import_utils66.AST_NODE_TYPES.Identifier && item.id.name === exportName
11430
+ );
11431
+ }
11432
+ return (declaration?.type === import_utils66.AST_NODE_TYPES.FunctionDeclaration || declaration?.type === import_utils66.AST_NODE_TYPES.ClassDeclaration) && declaration.id?.name === exportName;
11433
+ });
11434
+ } catch {
11435
+ return false;
11436
+ }
11437
+ }
11243
11438
  function rawElementName(node) {
11244
11439
  if (node.name.type !== import_utils66.AST_NODE_TYPES.JSXIdentifier) return null;
11245
11440
  const name = node.name.name;
11246
- return Object.hasOwn(SHADCN_PRIMITIVES, name) ? name : null;
11441
+ return Object.hasOwn(RAW_PRIMITIVES, name) ? name : null;
11247
11442
  }
11248
11443
  function effectiveAttribute(node, attributeName) {
11249
11444
  for (const attribute of node.attributes.toReversed()) {
@@ -11312,15 +11507,19 @@ function isStaticallyAssociatedLabel(node) {
11312
11507
  return node.parent.type === import_utils66.AST_NODE_TYPES.JSXElement && containsLabelableElement(node.parent);
11313
11508
  }
11314
11509
  function replacementFor(node, element) {
11315
- if (element !== "input") return SHADCN_PRIMITIVES[element];
11510
+ if (element !== "input") return RAW_PRIMITIVES[element];
11316
11511
  const typeAttribute = effectiveAttribute(node, "type");
11317
11512
  if (typeAttribute.kind === "unknown") return null;
11318
11513
  const inputType = typeAttribute.kind === "known" ? typeAttribute.value.toLowerCase() : "text";
11319
11514
  if (inputType === "hidden" || inputType === "file") return null;
11320
- if (inputType === "checkbox") return "Checkbox";
11321
- if (inputType === "radio") return "RadioGroup family";
11515
+ if (inputType === "checkbox") {
11516
+ return { capability: "Checkbox", replacement: "Checkbox" };
11517
+ }
11518
+ if (inputType === "radio") {
11519
+ return { capability: "RadioGroup", replacement: "RadioGroup family" };
11520
+ }
11322
11521
  if (AMBIGUOUS_INPUT_TYPES.has(inputType)) return null;
11323
- return "Input";
11522
+ return RAW_PRIMITIVES.input;
11324
11523
  }
11325
11524
  var prefer_shadcn_primitives_default = createRule({
11326
11525
  name: "prefer-shadcn-primitives",
@@ -11334,7 +11533,8 @@ var prefer_shadcn_primitives_default = createRule({
11334
11533
  {
11335
11534
  type: "object",
11336
11535
  properties: {
11337
- assumeAvailable: { type: "boolean" }
11536
+ assumeAvailable: { type: "boolean" },
11537
+ detectProjectPrimitives: { type: "boolean" }
11338
11538
  },
11339
11539
  additionalProperties: false
11340
11540
  }
@@ -11346,15 +11546,21 @@ var prefer_shadcn_primitives_default = createRule({
11346
11546
  defaultOptions: [{}],
11347
11547
  create(context, [options]) {
11348
11548
  const filename = context.filename.replaceAll("\\", "/");
11349
- if (isTestFile(filename) || SHARED_PRIMITIVE_IMPLEMENTATION_RE.test(filename)) {
11549
+ if (isTestFile(filename) || isStoryFile(filename) || isGeneratedFile(filename, context.sourceCode.text) || SHARED_PRIMITIVE_IMPLEMENTATION_RE.test(filename)) {
11350
11550
  return {};
11351
11551
  }
11352
- let hasSharedPrimitiveImport = options?.assumeAvailable ?? false;
11552
+ const detectsProject = options?.detectProjectPrimitives === true;
11553
+ let hasSharedPrimitiveImport = false;
11554
+ const importedCapabilities = /* @__PURE__ */ new Set();
11353
11555
  const candidates = [];
11354
11556
  return {
11355
11557
  ImportDeclaration(node) {
11356
- if (typeof node.source.value === "string" && SHARED_PRIMITIVE_IMPORT_RE.test(node.source.value)) {
11357
- hasSharedPrimitiveImport = true;
11558
+ if (typeof node.source.value !== "string") return;
11559
+ const match = SHARED_PRIMITIVE_IMPORT_RE.exec(node.source.value);
11560
+ if (match?.[1] === void 0) return;
11561
+ hasSharedPrimitiveImport = true;
11562
+ for (const [capability, moduleName] of Object.entries(CAPABILITIES)) {
11563
+ if (match[1].toLowerCase() === moduleName) importedCapabilities.add(capability);
11358
11564
  }
11359
11565
  },
11360
11566
  JSXOpeningElement(node) {
@@ -11363,11 +11569,15 @@ var prefer_shadcn_primitives_default = createRule({
11363
11569
  if (element === "label" && !isStaticallyAssociatedLabel(node)) return;
11364
11570
  const replacement = replacementFor(node, element);
11365
11571
  if (replacement === null) return;
11366
- candidates.push({ element, node, replacement });
11572
+ candidates.push({ element, node, ...replacement });
11367
11573
  },
11368
11574
  "Program:exit"() {
11369
- if (!hasSharedPrimitiveImport) return;
11370
- for (const { element, node, replacement } of candidates) {
11575
+ const requested = new Set(candidates.map(({ capability }) => capability));
11576
+ const projectPrimitives = detectsProject && requested.size > 0 ? detectProjectPrimitives(context.filename, requested) : { available: /* @__PURE__ */ new Set(), uiRoot: null };
11577
+ if (projectPrimitives.uiRoot !== null && isWithin2(projectPrimitives.uiRoot, realpathOrOriginal(context.filename))) return;
11578
+ for (const { capability, element, node, replacement } of candidates) {
11579
+ const available = options?.assumeAvailable === true || (detectsProject ? projectPrimitives.available.has(capability) || importedCapabilities.has(capability) : hasSharedPrimitiveImport);
11580
+ if (!available) continue;
11371
11581
  context.report({
11372
11582
  node,
11373
11583
  messageId: "preferShadcnPrimitive",
@@ -11378,6 +11588,13 @@ var prefer_shadcn_primitives_default = createRule({
11378
11588
  };
11379
11589
  }
11380
11590
  });
11591
+ function realpathOrOriginal(path) {
11592
+ try {
11593
+ return (0, import_node_fs.realpathSync)(path);
11594
+ } catch {
11595
+ return path;
11596
+ }
11597
+ }
11381
11598
 
11382
11599
  // src/rules/prefer-module-level-constant.ts
11383
11600
  var import_utils67 = require("@typescript-eslint/utils");
@@ -12706,11 +12923,11 @@ var prefer_native_random_uuid_default = createRule({
12706
12923
  create(context) {
12707
12924
  const directBindings = /* @__PURE__ */ new Set();
12708
12925
  const namespaceBindings = /* @__PURE__ */ new Set();
12709
- function resolve(identifier) {
12926
+ function resolve2(identifier) {
12710
12927
  return import_utils73.ASTUtils.findVariable(context.sourceCode.getScope(identifier), identifier.name);
12711
12928
  }
12712
12929
  function record(identifier, destination) {
12713
- const variable = resolve(identifier);
12930
+ const variable = resolve2(identifier);
12714
12931
  if (variable !== null) destination.add(variable);
12715
12932
  }
12716
12933
  function report2(node) {
@@ -12738,7 +12955,7 @@ var prefer_native_random_uuid_default = createRule({
12738
12955
  },
12739
12956
  VariableDeclarator(node) {
12740
12957
  if (node.parent.kind !== "const" || !requireUuid(node.init)) return;
12741
- if (node.init?.type !== import_utils73.AST_NODE_TYPES.CallExpression || node.init.callee.type !== import_utils73.AST_NODE_TYPES.Identifier || (resolve(node.init.callee)?.defs.length ?? 0) > 0) {
12958
+ if (node.init?.type !== import_utils73.AST_NODE_TYPES.CallExpression || node.init.callee.type !== import_utils73.AST_NODE_TYPES.Identifier || (resolve2(node.init.callee)?.defs.length ?? 0) > 0) {
12742
12959
  return;
12743
12960
  }
12744
12961
  if (node.id.type === import_utils73.AST_NODE_TYPES.Identifier) {
@@ -12755,14 +12972,14 @@ var prefer_native_random_uuid_default = createRule({
12755
12972
  "CallExpression:exit"(node) {
12756
12973
  if (node.arguments.length !== 0) return;
12757
12974
  if (node.callee.type === import_utils73.AST_NODE_TYPES.Identifier) {
12758
- const variable2 = resolve(node.callee);
12975
+ const variable2 = resolve2(node.callee);
12759
12976
  if (variable2 !== null && directBindings.has(variable2)) report2(node);
12760
12977
  return;
12761
12978
  }
12762
12979
  if (node.callee.type !== import_utils73.AST_NODE_TYPES.MemberExpression || node.callee.computed || node.callee.object.type !== import_utils73.AST_NODE_TYPES.Identifier || node.callee.property.type !== import_utils73.AST_NODE_TYPES.Identifier || node.callee.property.name !== "v4") {
12763
12980
  return;
12764
12981
  }
12765
- const variable = resolve(node.callee.object);
12982
+ const variable = resolve2(node.callee.object);
12766
12983
  if (variable !== null && namespaceBindings.has(variable)) report2(node);
12767
12984
  }
12768
12985
  };
@@ -12792,22 +13009,22 @@ function memberName4(node) {
12792
13009
  }
12793
13010
  return null;
12794
13011
  }
12795
- function isCryptoLoader(node, resolve) {
13012
+ function isCryptoLoader(node, resolve2) {
12796
13013
  if (node.type !== import_utils74.AST_NODE_TYPES.CallExpression || node.arguments.length !== 1) return false;
12797
13014
  const [argument] = node.arguments;
12798
13015
  if (argument === void 0 || argument.type === import_utils74.AST_NODE_TYPES.SpreadElement || !isCryptoSpecifier(argument)) {
12799
13016
  return false;
12800
13017
  }
12801
13018
  if (node.callee.type === import_utils74.AST_NODE_TYPES.Identifier) {
12802
- return node.callee.name === "require" && isUnshadowedBuiltinIdentifier(node.callee, resolve);
13019
+ return node.callee.name === "require" && isUnshadowedBuiltinIdentifier(node.callee, resolve2);
12803
13020
  }
12804
- return node.callee.type === import_utils74.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils74.AST_NODE_TYPES.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve) && memberName4(node.callee) === "getBuiltinModule";
13021
+ return node.callee.type === import_utils74.AST_NODE_TYPES.MemberExpression && node.callee.object.type === import_utils74.AST_NODE_TYPES.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve2) && memberName4(node.callee) === "getBuiltinModule";
12805
13022
  }
12806
13023
  function isCryptoSpecifier(node) {
12807
13024
  return node.type === import_utils74.AST_NODE_TYPES.Literal && (node.value === "crypto" || node.value === "node:crypto");
12808
13025
  }
12809
- function isUnshadowedBuiltinIdentifier(identifier, resolve) {
12810
- const variable = resolve(identifier);
13026
+ function isUnshadowedBuiltinIdentifier(identifier, resolve2) {
13027
+ const variable = resolve2(identifier);
12811
13028
  return variable === null || variable.defs.length === 0;
12812
13029
  }
12813
13030
  function propertyName4(node) {
@@ -12825,14 +13042,14 @@ var prefer_node_crypto_hash_default = createRule({
12825
13042
  create(context) {
12826
13043
  const directBindings = /* @__PURE__ */ new Set();
12827
13044
  const namespaceBindings = /* @__PURE__ */ new Set();
12828
- function resolve(identifier) {
13045
+ function resolve2(identifier) {
12829
13046
  return import_utils74.ASTUtils.findVariable(
12830
13047
  context.sourceCode.getScope(identifier),
12831
13048
  identifier.name
12832
13049
  );
12833
13050
  }
12834
13051
  function record(identifier, destination) {
12835
- const variable = resolve(identifier);
13052
+ const variable = resolve2(identifier);
12836
13053
  if (variable !== null) destination.add(variable);
12837
13054
  }
12838
13055
  return {
@@ -12847,7 +13064,7 @@ var prefer_node_crypto_hash_default = createRule({
12847
13064
  }
12848
13065
  },
12849
13066
  VariableDeclarator(node) {
12850
- if (node.parent.kind !== "const" || node.init === null || !isCryptoLoader(node.init, resolve)) {
13067
+ if (node.parent.kind !== "const" || node.init === null || !isCryptoLoader(node.init, resolve2)) {
12851
13068
  return;
12852
13069
  }
12853
13070
  if (node.id.type === import_utils74.AST_NODE_TYPES.Identifier) {
@@ -12871,7 +13088,7 @@ var prefer_node_crypto_hash_default = createRule({
12871
13088
  create,
12872
13089
  directBindings,
12873
13090
  namespaceBindings,
12874
- resolve
13091
+ resolve2
12875
13092
  ))
12876
13093
  return;
12877
13094
  context.report({ node, messageId: "preferNodeCryptoHash" });
@@ -12885,17 +13102,17 @@ function importedName5(node) {
12885
13102
  function isMemberCall(node, name) {
12886
13103
  return node.callee.type === import_utils74.AST_NODE_TYPES.MemberExpression && memberName4(node.callee) === name;
12887
13104
  }
12888
- function isCreateHashCall(node, directBindings, namespaceBindings, resolve) {
13105
+ function isCreateHashCall(node, directBindings, namespaceBindings, resolve2) {
12889
13106
  if (node.callee.type === import_utils74.AST_NODE_TYPES.Identifier) {
12890
- const variable2 = resolve(node.callee);
13107
+ const variable2 = resolve2(node.callee);
12891
13108
  return variable2 !== null && directBindings.has(variable2);
12892
13109
  }
12893
13110
  if (node.callee.type !== import_utils74.AST_NODE_TYPES.MemberExpression || memberName4(node.callee) !== "createHash") {
12894
13111
  return false;
12895
13112
  }
12896
- if (isCryptoLoader(node.callee.object, resolve)) return true;
13113
+ if (isCryptoLoader(node.callee.object, resolve2)) return true;
12897
13114
  if (node.callee.object.type !== import_utils74.AST_NODE_TYPES.Identifier) return false;
12898
- const variable = resolve(node.callee.object);
13115
+ const variable = resolve2(node.callee.object);
12899
13116
  return variable !== null && namespaceBindings.has(variable);
12900
13117
  }
12901
13118
 
@@ -14217,7 +14434,30 @@ var import_fs = require("fs");
14217
14434
  var import_path = require("path");
14218
14435
 
14219
14436
  // src/rules/_tailwind.ts
14220
- var tailwindBase = (token) => token.replace(/^(?:[a-z0-9-]+:)+/i, "").replace(/^!/, "");
14437
+ var tailwindVariantPrefix = (token) => {
14438
+ let bracketDepth = 0;
14439
+ let parenthesisDepth = 0;
14440
+ let escaped = false;
14441
+ let end = 0;
14442
+ for (let index = 0; index < token.length; index += 1) {
14443
+ const character = token[index];
14444
+ if (escaped) {
14445
+ escaped = false;
14446
+ continue;
14447
+ }
14448
+ if (character === "\\") {
14449
+ escaped = true;
14450
+ continue;
14451
+ }
14452
+ if (character === "[") bracketDepth += 1;
14453
+ else if (character === "]") bracketDepth = Math.max(0, bracketDepth - 1);
14454
+ else if (character === "(") parenthesisDepth += 1;
14455
+ else if (character === ")") parenthesisDepth = Math.max(0, parenthesisDepth - 1);
14456
+ else if (character === ":" && bracketDepth === 0 && parenthesisDepth === 0) end = index + 1;
14457
+ }
14458
+ return token.slice(0, end);
14459
+ };
14460
+ var tailwindBase = (token) => token.slice(tailwindVariantPrefix(token).length).replace(/^!/, "");
14221
14461
  var classTokens = (value) => value.split(/\s+/).filter(Boolean);
14222
14462
 
14223
14463
  // src/rules/prefer-semantic-colors.ts
@@ -14226,7 +14466,10 @@ var PREFER_SEMANTIC_COLORS_DOCUMENTATION = {
14226
14466
  rationale: "Semantic tokens keep themes and product meaning consistent while raw colors couple components to a palette value.",
14227
14467
  remediation: "Replace raw palette and literal colors with the closest semantic design-system token or CSS variable.",
14228
14468
  category: "style",
14229
- limitations: ["Email, PDF, icon artwork, masks, gradients, stories, and explicitly configured non-token projects have targeted exclusions."],
14469
+ limitations: [
14470
+ "Email, PDF, video-rendering, print-only, icon artwork, masks, gradients, stories, and explicitly configured non-token projects have targeted exclusions.",
14471
+ "Opaque-foreground checks are opt-in and require both a same-variant semantic background class and its package-local declared foreground token."
14472
+ ],
14230
14473
  examples: [
14231
14474
  { id: "semantic-text-color", title: "Use a semantic color token", outcome: "no-match", files: [{ path: "src/notice.tsx", source: 'const notice = <div className="text-destructive" />;' }], focusPath: "src/notice.tsx", expectedCount: 0, public: true },
14232
14475
  { id: "raw-text-color", title: "Do not use a raw palette color", outcome: "match", files: [{ path: "src/notice.tsx", source: 'const notice = <div className="text-red-500" />;' }], focusPath: "src/notice.tsx", expectedCount: 1, public: true }
@@ -14354,7 +14597,13 @@ var SVG_SHAPE_PRIMITIVES = /* @__PURE__ */ new Set([
14354
14597
  "tspan",
14355
14598
  "use"
14356
14599
  ]);
14357
- var EMAIL_OR_PDF_MODULE_RE = /^@react-(?:email|pdf)\//;
14600
+ var EXTERNAL_RENDERER_MODULE_RE = /^(?:@react-(?:email|pdf)\/|remotion$|@remotion\/)/;
14601
+ var OPAQUE_FOREGROUND_RE = /^text-(?:white|black)(?:\/100)?$/;
14602
+ var SEMANTIC_BACKGROUND_RE = /^bg-([a-z][a-z0-9-]*)$/;
14603
+ var CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//gu;
14604
+ var DECLARED_FOREGROUND_RE = /--(?:color-)?([a-z][a-z0-9-]*)-foreground\s*:/giu;
14605
+ var MAX_TOKEN_STYLESHEET_BYTES = 1048576;
14606
+ var SEMANTIC_DECLARATIONS_CACHE = /* @__PURE__ */ new Map();
14358
14607
  function isSvgLikeElementName(name) {
14359
14608
  return name === "svg" || SVG_DEFS_CONTAINERS.has(name) || /svg$/i.test(name);
14360
14609
  }
@@ -14491,20 +14740,68 @@ var expandWorkspaceGlob = (root, glob) => {
14491
14740
  if (star === -1) return [(0, import_path.join)(root, glob)];
14492
14741
  const prefix = glob.slice(0, star).replace(/\/$/u, "");
14493
14742
  const parent = prefix === "" ? root : (0, import_path.join)(root, prefix);
14494
- if (!(0, import_fs.existsSync)(parent)) return [];
14495
- return (0, import_fs.readdirSync)(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => (0, import_path.join)(parent, entry.name));
14743
+ if (!(0, import_fs.existsSync)(parent) || !(0, import_fs.lstatSync)(parent).isDirectory()) return [];
14744
+ return (0, import_fs.readdirSync)(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => (0, import_path.join)(parent, entry.name));
14496
14745
  };
14497
14746
  var propName = (key) => {
14498
14747
  if (key.type === import_utils82.AST_NODE_TYPES.Identifier) return key.name;
14499
14748
  if (key.type === import_utils82.AST_NODE_TYPES.Literal && typeof key.value === "string") return key.value;
14500
14749
  return null;
14501
14750
  };
14502
- var staticallyImportsEmailOrPdfRenderer = (program) => program.body.some((statement) => {
14751
+ var staticallyImportsExternalRenderer = (program) => program.body.some((statement) => {
14503
14752
  if (statement.type !== import_utils82.AST_NODE_TYPES.ImportDeclaration && statement.type !== import_utils82.AST_NODE_TYPES.ExportNamedDeclaration && statement.type !== import_utils82.AST_NODE_TYPES.ExportAllDeclaration) {
14504
14753
  return false;
14505
14754
  }
14506
- return statement.source !== null && typeof statement.source.value === "string" && EMAIL_OR_PDF_MODULE_RE.test(statement.source.value);
14755
+ return statement.source !== null && typeof statement.source.value === "string" && EXTERNAL_RENDERER_MODULE_RE.test(statement.source.value);
14507
14756
  });
14757
+ var semanticForegroundRoles = (filename) => {
14758
+ const packageRoot = nearestPackageRoot((0, import_path.dirname)(filename));
14759
+ if (packageRoot === null) return /* @__PURE__ */ new Set();
14760
+ const candidates = CSS_DETECTION_FILES.map((relative2) => (0, import_path.join)(packageRoot, relative2));
14761
+ const fingerprint = candidates.map(fileFingerprint2).join("|");
14762
+ const cached = SEMANTIC_DECLARATIONS_CACHE.get(packageRoot);
14763
+ if (cached?.fingerprint === fingerprint) return cached.value;
14764
+ const foregroundRoles = /* @__PURE__ */ new Set();
14765
+ for (const candidate2 of candidates) {
14766
+ const css = readTokenStylesheet(candidate2);
14767
+ if (css === null) continue;
14768
+ const declarations = css.replace(CSS_COMMENT_RE, "");
14769
+ for (const match of declarations.matchAll(DECLARED_FOREGROUND_RE)) {
14770
+ if (match[1] !== void 0) foregroundRoles.add(match[1].toLowerCase());
14771
+ }
14772
+ }
14773
+ SEMANTIC_DECLARATIONS_CACHE.set(packageRoot, { fingerprint, value: foregroundRoles });
14774
+ return foregroundRoles;
14775
+ };
14776
+ var nearestPackageRoot = (startDir) => {
14777
+ let dir = startDir;
14778
+ const root = (0, import_path.parse)(dir).root;
14779
+ for (; ; ) {
14780
+ if ((0, import_fs.existsSync)((0, import_path.join)(dir, "package.json"))) {
14781
+ return dir;
14782
+ }
14783
+ const parent = (0, import_path.dirname)(dir);
14784
+ if (dir === root || parent === dir) return null;
14785
+ dir = parent;
14786
+ }
14787
+ };
14788
+ var fileFingerprint2 = (path) => {
14789
+ try {
14790
+ const stat = (0, import_fs.lstatSync)(path);
14791
+ return stat.isFile() ? `${path}:${stat.size}:${stat.mtimeMs}` : `${path}:excluded`;
14792
+ } catch {
14793
+ return `${path}:missing`;
14794
+ }
14795
+ };
14796
+ var readTokenStylesheet = (path) => {
14797
+ try {
14798
+ const stat = (0, import_fs.lstatSync)(path);
14799
+ if (!stat.isFile() || stat.size > MAX_TOKEN_STYLESHEET_BYTES) return null;
14800
+ return (0, import_fs.readFileSync)(path, "utf8");
14801
+ } catch {
14802
+ return null;
14803
+ }
14804
+ };
14508
14805
  var prefer_semantic_colors_default = createRule({
14509
14806
  name: "prefer-semantic-colors",
14510
14807
  documentation: PREFER_SEMANTIC_COLORS_DOCUMENTATION,
@@ -14518,30 +14815,34 @@ var prefer_semantic_colors_default = createRule({
14518
14815
  type: "object",
14519
14816
  additionalProperties: false,
14520
14817
  properties: {
14521
- requireSemanticTokens: { type: "boolean" }
14818
+ requireSemanticTokens: { type: "boolean" },
14819
+ opaqueForegroundPairs: { type: "boolean" }
14522
14820
  }
14523
14821
  }
14524
14822
  ],
14525
14823
  messages: {
14526
14824
  rawPalette: "Raw palette class '{{class}}' \u2014 use a semantic token (e.g. text-foreground, bg-primary, text-destructive, bg-muted).",
14527
14825
  arbitraryColor: "Hardcoded color '{{class}}' \u2014 use a semantic token, or var(--\u2026). For charts/brand add an eslint-disable with a reason.",
14528
- inlineColor: "Hardcoded color '{{value}}' \u2014 use a semantic token / CSS variable. For charts/standalone pages add an eslint-disable with a reason."
14826
+ inlineColor: "Hardcoded color '{{value}}' \u2014 use a semantic token / CSS variable. For charts/standalone pages add an eslint-disable with a reason.",
14827
+ opaqueForegroundPair: "'{{class}}' bypasses the declared '{{replacement}}' token paired with '{{background}}'."
14529
14828
  }
14530
14829
  },
14531
14830
  defaultOptions: [{}],
14532
14831
  create(context, [options]) {
14533
14832
  if (STORIES_FILE_RE.test(context.filename)) return {};
14534
- if (staticallyImportsEmailOrPdfRenderer(context.sourceCode.ast)) return {};
14535
- if (options?.requireSemanticTokens === true && !hasSemanticTokenSystem(context.filename)) {
14536
- return {};
14537
- }
14538
- let importsEmailOrPdfRenderer = false;
14539
- const pendingReports = [];
14833
+ if (staticallyImportsExternalRenderer(context.sourceCode.ast)) return {};
14834
+ if (options?.requireSemanticTokens === true && !hasSemanticTokenSystem(context.filename)) return {};
14835
+ const foregroundRoles = options?.opaqueForegroundPairs === true ? semanticForegroundRoles(context.filename) : /* @__PURE__ */ new Set();
14836
+ const checkOpaqueForegroundPairs = foregroundRoles.size > 0;
14837
+ let importsExternalRenderer = false;
14838
+ const pendingReports = /* @__PURE__ */ new Map();
14540
14839
  const report2 = (node, messageId, data) => {
14541
- pendingReports.push({ node, messageId, data });
14840
+ const key = `${node.range[0]}:${node.range[1]}:${messageId}:${JSON.stringify(data)}`;
14841
+ pendingReports.set(key, { node, messageId, data });
14542
14842
  };
14543
14843
  const reportClasses = (value, node) => {
14544
- for (const token of classTokens(value)) {
14844
+ const tokens = classTokens(value);
14845
+ for (const token of tokens) {
14545
14846
  const base = tailwindBase(token);
14546
14847
  if (RAW_PALETTE_RE.test(base)) {
14547
14848
  report2(node, "rawPalette", { class: token });
@@ -14549,6 +14850,26 @@ var prefer_semantic_colors_default = createRule({
14549
14850
  report2(node, "arbitraryColor", { class: token });
14550
14851
  }
14551
14852
  }
14853
+ if (!checkOpaqueForegroundPairs || isInsideSvg(node)) return;
14854
+ for (const token of tokens) {
14855
+ const prefix = tailwindVariantPrefix(token);
14856
+ if (prefix.split(":").includes("print")) continue;
14857
+ const base = tailwindBase(token);
14858
+ if (!OPAQUE_FOREGROUND_RE.test(base)) continue;
14859
+ const semanticBackground = tokens.find((candidate2) => {
14860
+ if (tailwindVariantPrefix(candidate2) !== prefix) return false;
14861
+ const match = SEMANTIC_BACKGROUND_RE.exec(tailwindBase(candidate2));
14862
+ return match?.[1] !== void 0 && foregroundRoles.has(match[1]);
14863
+ });
14864
+ if (semanticBackground === void 0) continue;
14865
+ const role = SEMANTIC_BACKGROUND_RE.exec(tailwindBase(semanticBackground))?.[1];
14866
+ if (role === void 0) continue;
14867
+ report2(node, "opaqueForegroundPair", {
14868
+ background: semanticBackground,
14869
+ class: token,
14870
+ replacement: `${prefix}text-${role}-foreground`
14871
+ });
14872
+ }
14552
14873
  };
14553
14874
  const checkClassNode = (node) => {
14554
14875
  if (node === null) return;
@@ -14596,8 +14917,8 @@ var prefer_semantic_colors_default = createRule({
14596
14917
  }
14597
14918
  },
14598
14919
  CallExpression(node) {
14599
- if (node.callee.type === import_utils82.AST_NODE_TYPES.Identifier && node.callee.name === "require" && node.arguments[0]?.type === import_utils82.AST_NODE_TYPES.Literal && typeof node.arguments[0].value === "string" && EMAIL_OR_PDF_MODULE_RE.test(node.arguments[0].value)) {
14600
- importsEmailOrPdfRenderer = true;
14920
+ if (node.callee.type === import_utils82.AST_NODE_TYPES.Identifier && node.callee.name === "require" && node.arguments[0]?.type === import_utils82.AST_NODE_TYPES.Literal && typeof node.arguments[0].value === "string" && EXTERNAL_RENDERER_MODULE_RE.test(node.arguments[0].value)) {
14921
+ importsExternalRenderer = true;
14601
14922
  }
14602
14923
  if (node.callee.type === import_utils82.AST_NODE_TYPES.Identifier && CLASS_FNS.has(node.callee.name)) {
14603
14924
  for (const arg of node.arguments) {
@@ -14632,13 +14953,13 @@ var prefer_semantic_colors_default = createRule({
14632
14953
  if (name !== null && STYLE_COLOR_PROPS.has(name)) checkColorValueNode(node.value);
14633
14954
  },
14634
14955
  ImportExpression(node) {
14635
- if (node.source.type === import_utils82.AST_NODE_TYPES.Literal && typeof node.source.value === "string" && EMAIL_OR_PDF_MODULE_RE.test(node.source.value)) {
14636
- importsEmailOrPdfRenderer = true;
14956
+ if (node.source.type === import_utils82.AST_NODE_TYPES.Literal && typeof node.source.value === "string" && EXTERNAL_RENDERER_MODULE_RE.test(node.source.value)) {
14957
+ importsExternalRenderer = true;
14637
14958
  }
14638
14959
  },
14639
14960
  "Program:exit"() {
14640
- if (importsEmailOrPdfRenderer) return;
14641
- for (const descriptor of pendingReports) context.report(descriptor);
14961
+ if (importsExternalRenderer) return;
14962
+ for (const descriptor of pendingReports.values()) context.report(descriptor);
14642
14963
  }
14643
14964
  };
14644
14965
  }
@@ -14647,11 +14968,11 @@ var prefer_semantic_colors_default = createRule({
14647
14968
  // src/rules/prefer-server-actions.ts
14648
14969
  var import_utils83 = require("@typescript-eslint/utils");
14649
14970
  var PREFER_SERVER_ACTIONS_DOCUMENTATION = {
14650
- summary: "Prefer Next.js Server Actions over /api/* mutations.",
14971
+ summary: "Prefer Next.js Server Actions over same-origin API mutations.",
14651
14972
  rationale: "Server Actions preserve typed application calls and avoid an internal JSON request-response boundary.",
14652
14973
  remediation: "Move the mutation into a Server Action and invoke that action from the React client.",
14653
14974
  category: "architecture",
14654
- limitations: ["Only statically recognizable /api/ mutations in modules with positive Next.js evidence are reported: an explicit next import, or an app/pages path with a top-level use-client directive."],
14975
+ limitations: ["Only statically recognizable /api/ mutations, including one explicitly configured literal deployment base path, in use-client modules are reported; server boundaries and route handlers are excluded."],
14655
14976
  examples: [
14656
14977
  { id: "server-action-call", title: "Call a Server Action", outcome: "no-match", files: [{ path: "app/tasks/page.tsx", source: "import { createTask } from './actions'; await createTask(input);" }], focusPath: "app/tasks/page.tsx", expectedCount: 0, public: true },
14657
14978
  { id: "api-mutation", title: "Do not mutate through an API route", outcome: "match", files: [{ path: "app/tasks/page.tsx", source: "'use client'; await fetch('/api/tasks', { method: 'POST', body });" }], focusPath: "app/tasks/page.tsx", expectedCount: 1, public: true }
@@ -14659,12 +14980,21 @@ var PREFER_SERVER_ACTIONS_DOCUMENTATION = {
14659
14980
  };
14660
14981
  var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
14661
14982
  var AXIOS_MUTATION_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
14662
- var SKIP_FILE_REGEX = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
14983
+ var SKIP_FILE_REGEX = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|(?:^|\/)app(?:\/.*)?\/route\.[jt]sx?$|(?:^|\/)middleware\.[jt]sx?$|\/pages\/api\/)/;
14663
14984
  var NON_REACT_FRAMEWORK_RE2 = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
14664
- var NEXT_MODULE_PATH_RE2 = /(?:^|[/\\])(?:app|pages)[/\\]/u;
14985
+ var BASE_PATH_RE2 = /^\/(?!$)(?!.*[?#])(?:[^/]+\/)*[^/]+$/u;
14665
14986
  function getScope(context, node) {
14666
14987
  return context.sourceCode.getScope(node);
14667
14988
  }
14989
+ function resolvesToGlobalFetch(context, identifier) {
14990
+ let scope = getScope(context, identifier);
14991
+ while (scope) {
14992
+ const variable = scope.set.get(identifier.name);
14993
+ if (variable !== void 0) return variable.defs.length === 0;
14994
+ scope = scope.upper;
14995
+ }
14996
+ return true;
14997
+ }
14668
14998
  function resolveNode(node, context) {
14669
14999
  if (!node) return null;
14670
15000
  if (node.type !== "Identifier") return node;
@@ -14684,22 +15014,29 @@ function resolveNode(node, context) {
14684
15014
  }
14685
15015
  return node;
14686
15016
  }
14687
- function isApiUrl(node, context) {
15017
+ function isApiUrl(node, context, apiPrefixes) {
14688
15018
  const resolved = resolveNode(node, context);
14689
15019
  if (!resolved) return false;
14690
15020
  if (resolved.type === "Literal" && typeof resolved.value === "string") {
14691
- return resolved.value.startsWith("/api/");
15021
+ return apiPrefixes.some(
15022
+ (prefix) => resolved.value === prefix.slice(0, -1) || resolved.value.startsWith(prefix)
15023
+ );
14692
15024
  }
14693
15025
  if (resolved.type === "TemplateLiteral") {
14694
15026
  const firstQuasi = resolved.quasis[0];
14695
15027
  const cooked = firstQuasi?.value.cooked;
14696
- return typeof cooked === "string" && cooked.startsWith("/api/");
15028
+ return typeof cooked === "string" && apiPrefixes.some(
15029
+ (prefix) => cooked === prefix.slice(0, -1) || cooked.startsWith(prefix)
15030
+ );
14697
15031
  }
14698
15032
  if (resolved.type === "BinaryExpression" && resolved.operator === "+") {
14699
- return isApiUrl(resolved.left, context);
15033
+ return isApiUrl(resolved.left, context, apiPrefixes);
14700
15034
  }
14701
15035
  return false;
14702
15036
  }
15037
+ function isValidBasePath2(basePath) {
15038
+ return BASE_PATH_RE2.test(basePath) && !basePath.split("/").some((segment) => segment === "." || segment === "..");
15039
+ }
14703
15040
  function isMutationMethod(node, context) {
14704
15041
  const resolved = resolveNode(node, context);
14705
15042
  if (!resolved) return false;
@@ -14759,16 +15096,27 @@ var prefer_server_actions_default = createRule({
14759
15096
  meta: {
14760
15097
  type: "suggestion",
14761
15098
  docs: {
14762
- description: "Prefer Next.js Server Actions over /api/* mutations."
15099
+ description: "Prefer Next.js Server Actions over same-origin API mutations."
14763
15100
  },
14764
- schema: [],
15101
+ schema: [
15102
+ {
15103
+ type: "object",
15104
+ additionalProperties: false,
15105
+ properties: {
15106
+ basePath: {
15107
+ type: "string",
15108
+ pattern: "^/(?!$)(?!.*[?#])(?!(?:.*/)?\\.\\.?(?:/|$))(?:[^/]+/)*[^/]+$"
15109
+ }
15110
+ }
15111
+ }
15112
+ ],
14765
15113
  messages: {
14766
- preferServerAction: "Mutation against /api/* \u2014 prefer a Next.js Server Action for type-safety and to avoid the JSON round-trip."
15114
+ preferServerAction: "Mutation against a same-origin API route \u2014 prefer a Next.js Server Action for type-safety and to avoid the JSON round-trip."
14767
15115
  }
14768
15116
  },
14769
- defaultOptions: [],
14770
- create(context) {
14771
- const filename = context.filename;
15117
+ defaultOptions: [{}],
15118
+ create(context, [options]) {
15119
+ const filename = context.filename.replaceAll("\\", "/");
14772
15120
  if (SKIP_FILE_REGEX.test(filename)) {
14773
15121
  return {};
14774
15122
  }
@@ -14776,22 +15124,28 @@ var prefer_server_actions_default = createRule({
14776
15124
  (node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && NON_REACT_FRAMEWORK_RE2.test(node.source.value)
14777
15125
  );
14778
15126
  const hasUseClientDirective = context.sourceCode.ast.body.some(
14779
- (node) => node.type === "ExpressionStatement" && node.expression.type === "Literal" && node.expression.value === "use client"
15127
+ (node) => node.type === "ExpressionStatement" && node.directive === "use client"
15128
+ );
15129
+ const hasUseServerDirective = context.sourceCode.ast.body.some(
15130
+ (node) => node.type === "ExpressionStatement" && node.directive === "use server"
14780
15131
  );
14781
- const hasNextImport = context.sourceCode.ast.body.some(
14782
- (node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && (node.source.value === "next" || node.source.value.startsWith("next/"))
15132
+ const importsServerOnly = context.sourceCode.ast.body.some(
15133
+ (node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && (node.source.value === "server-only" || node.source.value === "next/server")
14783
15134
  );
14784
- const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE2.test(filename);
14785
- if (!hasNextEvidence) {
15135
+ if (!hasUseClientDirective || hasUseServerDirective || importsServerOnly) {
14786
15136
  return {};
14787
15137
  }
15138
+ const apiPrefixes = ["/api/"];
15139
+ if (options?.basePath !== void 0 && isValidBasePath2(options.basePath)) {
15140
+ apiPrefixes.push(`${options.basePath}/api/`);
15141
+ }
14788
15142
  return {
14789
15143
  CallExpression(node) {
14790
15144
  if (isNonReactFramework) return;
14791
15145
  let isMutation = false;
14792
- if (node.callee.type === "Identifier" && node.callee.name === "fetch") {
15146
+ if (node.callee.type === "Identifier" && node.callee.name === "fetch" && resolvesToGlobalFetch(context, node.callee)) {
14793
15147
  const urlArg = node.arguments[0];
14794
- if (urlArg && urlArg.type !== "SpreadElement" && isApiUrl(urlArg, context)) {
15148
+ if (urlArg && urlArg.type !== "SpreadElement" && isApiUrl(urlArg, context, apiPrefixes)) {
14795
15149
  const initArg = node.arguments[1];
14796
15150
  if (initArg && initArg.type !== "SpreadElement") {
14797
15151
  const resolvedInit = resolveNode(initArg, context);
@@ -14808,7 +15162,7 @@ var prefer_server_actions_default = createRule({
14808
15162
  const hasHandlerArg = node.arguments.some(
14809
15163
  (arg) => arg.type !== "SpreadElement" && isFunctionArgument(arg, context)
14810
15164
  );
14811
- if (urlArg && urlArg.type !== "SpreadElement" && !hasHandlerArg && isApiUrl(urlArg, context)) {
15165
+ if (urlArg && urlArg.type !== "SpreadElement" && !hasHandlerArg && isApiUrl(urlArg, context, apiPrefixes)) {
14812
15166
  isMutation = true;
14813
15167
  }
14814
15168
  }
@@ -14819,7 +15173,7 @@ var prefer_server_actions_default = createRule({
14819
15173
  if (configArg && configArg.type === "ObjectExpression") {
14820
15174
  const urlNode = getPropertyNode(configArg, "url");
14821
15175
  const methodNode = getPropertyNode(configArg, "method");
14822
- if (urlNode && isApiUrl(urlNode, context) && methodNode && isMutationMethod(methodNode, context)) {
15176
+ if (urlNode && isApiUrl(urlNode, context, apiPrefixes) && methodNode && isMutationMethod(methodNode, context)) {
14823
15177
  isMutation = true;
14824
15178
  }
14825
15179
  }
@@ -19167,7 +19521,7 @@ var RULES = {
19167
19521
  };
19168
19522
  var meta = {
19169
19523
  name: "@sarj/eslint-plugin",
19170
- version: "15.17.2"
19524
+ version: "15.17.3"
19171
19525
  };
19172
19526
  var APPLICATION_ONLY_RULES = [
19173
19527
  "no-restricted-library-load",