@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.js CHANGED
@@ -5849,7 +5849,7 @@ var NO_RAW_FETCH_OUTSIDE_CLIENTS_DOCUMENTATION = {
5849
5849
  rationale: "Scattered fetch calls bypass shared transport policy and are harder to stub and observe consistently.",
5850
5850
  remediation: "Move the request into a client module and call that abstraction from application code.",
5851
5851
  category: "architecture",
5852
- limitations: ["Tests, client-layer paths, constructed handoffs, and pre-signed URL transfers are excluded."],
5852
+ 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."],
5853
5853
  examples: [
5854
5854
  { 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 },
5855
5855
  { 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 }
@@ -5894,7 +5894,10 @@ var ANALYTICS_SEGMENTS2 = /* @__PURE__ */ new Set([
5894
5894
  ]);
5895
5895
  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\/)/;
5896
5896
  var NON_REACT_FRAMEWORK_RE = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
5897
- var NEXT_MODULE_PATH_RE = /(?:^|[/\\])(?:app|pages)[/\\]/u;
5897
+ var BASE_PATH_RE = /^\/(?!$)(?!.*[?#])(?:[^/]+\/)*[^/]+$/u;
5898
+ function isValidBasePath(basePath) {
5899
+ return BASE_PATH_RE.test(basePath) && !basePath.split("/").some((segment) => segment === "." || segment === "..");
5900
+ }
5898
5901
  function isGlobalFetchCall(node, resolvesToGlobal) {
5899
5902
  const callee = node.callee;
5900
5903
  if (callee.type === "Identifier") {
@@ -5990,6 +5993,10 @@ var no_raw_fetch_outside_clients_default = createRule({
5990
5993
  type: "array",
5991
5994
  items: { type: "string" },
5992
5995
  description: "Regular-expression sources matched against the filename. Replaces the defaults."
5996
+ },
5997
+ basePath: {
5998
+ type: "string",
5999
+ pattern: "^/(?!$)(?!.*[?#])(?!(?:.*/)?\\.\\.?(?:/|$))(?:[^/]+/)*[^/]+$"
5993
6000
  }
5994
6001
  },
5995
6002
  additionalProperties: false
@@ -6011,12 +6018,18 @@ var no_raw_fetch_outside_clients_default = createRule({
6011
6018
  (statement) => statement.type === AST_NODE_TYPES24.ImportDeclaration && typeof statement.source.value === "string" && NON_REACT_FRAMEWORK_RE.test(statement.source.value)
6012
6019
  );
6013
6020
  const hasUseClientDirective = context.sourceCode.ast.body.some(
6014
- (statement) => statement.type === AST_NODE_TYPES24.ExpressionStatement && statement.expression.type === AST_NODE_TYPES24.Literal && statement.expression.value === "use client"
6021
+ (statement) => statement.type === AST_NODE_TYPES24.ExpressionStatement && statement.directive === "use client"
6015
6022
  );
6016
- const hasNextImport = context.sourceCode.ast.body.some(
6017
- (statement) => statement.type === AST_NODE_TYPES24.ImportDeclaration && typeof statement.source.value === "string" && (statement.source.value === "next" || statement.source.value.startsWith("next/"))
6023
+ const hasUseServerDirective = context.sourceCode.ast.body.some(
6024
+ (statement) => statement.type === AST_NODE_TYPES24.ExpressionStatement && statement.directive === "use server"
6018
6025
  );
6019
- const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE.test(filename);
6026
+ const importsServerOnly = context.sourceCode.ast.body.some(
6027
+ (statement) => statement.type === AST_NODE_TYPES24.ImportDeclaration && typeof statement.source.value === "string" && (statement.source.value === "server-only" || statement.source.value === "next/server")
6028
+ );
6029
+ const internalApiPrefixes = ["/api"];
6030
+ if (options?.basePath !== void 0 && isValidBasePath(options.basePath)) {
6031
+ internalApiPrefixes.push(`${options.basePath}/api`);
6032
+ }
6020
6033
  function resolvesToGlobal(identifier) {
6021
6034
  const variable = ASTUtils7.findVariable(
6022
6035
  context.sourceCode.getScope(identifier),
@@ -6049,15 +6062,15 @@ var no_raw_fetch_outside_clients_default = createRule({
6049
6062
  function isInternalApiUrl(node) {
6050
6063
  const resolved = resolveNode2(node ?? void 0);
6051
6064
  if (resolved?.type === AST_NODE_TYPES24.Literal) {
6052
- return typeof resolved.value === "string" && /^\/(?!\/)(?:[^/]+\/)*api(?:\/|$)/.test(resolved.value);
6065
+ return typeof resolved.value === "string" && internalApiPrefixes.some(
6066
+ (prefix) => resolved.value === prefix || resolved.value.startsWith(`${prefix}/`)
6067
+ );
6053
6068
  }
6054
6069
  if (resolved?.type === AST_NODE_TYPES24.TemplateLiteral) {
6055
6070
  const prefix = resolved.quasis[0]?.value.cooked;
6056
- return typeof prefix === "string" && /^\/(?!\/)(?:[^/]+\/)*api(?:\/|$)/.test(prefix);
6057
- }
6058
- if (resolved?.type === AST_NODE_TYPES24.CallExpression && resolved.callee.type === AST_NODE_TYPES24.Identifier && resolved.callee.name === "withBase") {
6059
- const first = resolved.arguments[0];
6060
- return first !== void 0 && first.type !== AST_NODE_TYPES24.SpreadElement ? isInternalApiUrl(first) : false;
6071
+ return typeof prefix === "string" && internalApiPrefixes.some(
6072
+ (apiPrefix) => prefix === apiPrefix || prefix.startsWith(`${apiPrefix}/`)
6073
+ );
6061
6074
  }
6062
6075
  return resolved?.type === AST_NODE_TYPES24.BinaryExpression && resolved.operator === "+" && isInternalApiUrl(resolved.left);
6063
6076
  }
@@ -6077,7 +6090,7 @@ var no_raw_fetch_outside_clients_default = createRule({
6077
6090
  return resolved?.type === AST_NODE_TYPES24.LogicalExpression && resolved.operator === "||" && (isMutationMethod2(resolved.left) || isMutationMethod2(resolved.right));
6078
6091
  }
6079
6092
  function serverActionOwns(node) {
6080
- if (node.callee.type !== AST_NODE_TYPES24.Identifier || !hasNextEvidence || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
6093
+ if (node.callee.type !== AST_NODE_TYPES24.Identifier || !hasUseClientDirective || hasUseServerDirective || importsServerOnly || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
6081
6094
  return false;
6082
6095
  }
6083
6096
  const url = node.arguments[0];
@@ -9737,25 +9750,25 @@ var no_unsafe_mock_casting_default = createRule({
9737
9750
  }
9738
9751
  const directBindings = /* @__PURE__ */ new Set();
9739
9752
  const namespaceBindings = /* @__PURE__ */ new Set();
9740
- function resolve(identifier) {
9753
+ function resolve2(identifier) {
9741
9754
  return ASTUtils10.findVariable(
9742
9755
  context.sourceCode.getScope(identifier),
9743
9756
  identifier.name
9744
9757
  );
9745
9758
  }
9746
9759
  function record(identifier, destination) {
9747
- const binding = resolve(identifier);
9760
+ const binding = resolve2(identifier);
9748
9761
  if (binding !== null) destination.add(binding);
9749
9762
  }
9750
9763
  function isMockTypeReference(node) {
9751
9764
  if (node.type !== AST_NODE_TYPES40.TSTypeReference) return false;
9752
9765
  const typeName = node.typeName;
9753
9766
  if (typeName.type === AST_NODE_TYPES40.Identifier) {
9754
- const binding = resolve(typeName);
9767
+ const binding = resolve2(typeName);
9755
9768
  return binding !== null && directBindings.has(binding);
9756
9769
  }
9757
9770
  if (typeName.type === AST_NODE_TYPES40.TSQualifiedName && typeName.left.type === AST_NODE_TYPES40.Identifier && MOCK_TYPE_NAMES.has(typeName.right.name)) {
9758
- const binding = resolve(typeName.left);
9771
+ const binding = resolve2(typeName.left);
9759
9772
  return binding !== null && namespaceBindings.has(binding);
9760
9773
  }
9761
9774
  return false;
@@ -11169,6 +11182,10 @@ function unwrapTransparentExport(node) {
11169
11182
 
11170
11183
  // src/rules/prefer-shadcn-primitives.ts
11171
11184
  import { AST_NODE_TYPES as AST_NODE_TYPES50 } from "@typescript-eslint/utils";
11185
+ import { parse as parseTypeScript } from "@typescript-eslint/typescript-estree";
11186
+ import { existsSync, lstatSync, readFileSync, realpathSync } from "fs";
11187
+ import { dirname, isAbsolute, join, parse as parsePath, relative, resolve, sep } from "path";
11188
+ import { parse as parseJsonc } from "jsonc-parser";
11172
11189
  var PREFER_SHADCN_PRIMITIVES_DOCUMENTATION = {
11173
11190
  summary: "Require visible raw JSX controls to use the corresponding shared shadcn primitive.",
11174
11191
  rationale: "Shared primitives centralize interaction, accessibility, and visual behavior across the product.",
@@ -11176,22 +11193,35 @@ var PREFER_SHADCN_PRIMITIVES_DOCUMENTATION = {
11176
11193
  category: "style",
11177
11194
  limitations: [
11178
11195
  "Hidden and file inputs, unassociated labels, and non-control semantic elements are excluded.",
11179
- "Tests and the shared components/ui primitive implementation tree are excluded."
11196
+ "Tests and the shared components/ui primitive implementation tree are excluded.",
11197
+ "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."
11180
11198
  ],
11181
11199
  examples: [
11182
11200
  { 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 },
11183
11201
  { 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 }
11184
11202
  ]
11185
11203
  };
11186
- var SHADCN_PRIMITIVES = {
11187
- button: "Button",
11188
- dialog: "Dialog or AlertDialog family",
11189
- input: "Input",
11190
- label: "Label",
11191
- progress: "Progress",
11192
- select: "Select family",
11193
- table: "Table family",
11194
- textarea: "Textarea"
11204
+ var RAW_PRIMITIVES = {
11205
+ button: { capability: "Button", replacement: "Button" },
11206
+ dialog: { capability: "Dialog", replacement: "Dialog or AlertDialog family" },
11207
+ input: { capability: "Input", replacement: "Input" },
11208
+ label: { capability: "Label", replacement: "Label" },
11209
+ progress: { capability: "Progress", replacement: "Progress" },
11210
+ select: { capability: "Select", replacement: "Select family" },
11211
+ table: { capability: "Table", replacement: "Table family" },
11212
+ textarea: { capability: "Textarea", replacement: "Textarea" }
11213
+ };
11214
+ var CAPABILITIES = {
11215
+ Button: "button",
11216
+ Checkbox: "checkbox",
11217
+ Dialog: "dialog",
11218
+ Input: "input",
11219
+ Label: "label",
11220
+ Progress: "progress",
11221
+ RadioGroup: "radio-group",
11222
+ Select: "select",
11223
+ Table: "table",
11224
+ Textarea: "textarea"
11195
11225
  };
11196
11226
  var LABELABLE_ELEMENTS = /* @__PURE__ */ new Set([
11197
11227
  "button",
@@ -11203,7 +11233,7 @@ var LABELABLE_ELEMENTS = /* @__PURE__ */ new Set([
11203
11233
  "textarea"
11204
11234
  ]);
11205
11235
  var SHARED_PRIMITIVE_IMPLEMENTATION_RE = /(?:^|\/)components\/ui(?:\/|$)/i;
11206
- var SHARED_PRIMITIVE_IMPORT_RE = /(?:^|\/)components\/ui\/[^/]+$/i;
11236
+ var SHARED_PRIMITIVE_IMPORT_RE = /(?:^|\/)components\/ui\/([^/]+)$/i;
11207
11237
  var AMBIGUOUS_INPUT_TYPES = /* @__PURE__ */ new Set([
11208
11238
  "button",
11209
11239
  "color",
@@ -11212,10 +11242,175 @@ var AMBIGUOUS_INPUT_TYPES = /* @__PURE__ */ new Set([
11212
11242
  "reset",
11213
11243
  "submit"
11214
11244
  ]);
11245
+ var MAX_PROJECT_FILE_BYTES = 1048576;
11246
+ var PROJECT_PRIMITIVES_CACHE = /* @__PURE__ */ new Map();
11247
+ function detectProjectPrimitives(filename, requested) {
11248
+ const packageRoot = findPackageRoot(dirname(filename));
11249
+ if (packageRoot === null) return { available: /* @__PURE__ */ new Set(), uiRoot: null };
11250
+ const manifest = readJsonc(join(packageRoot, "components.json"));
11251
+ const alias = stringProperty(manifest, "aliases", "ui");
11252
+ const unresolvedUiRoot = alias === null ? null : resolveAlias(packageRoot, alias);
11253
+ if (unresolvedUiRoot === null) return { available: /* @__PURE__ */ new Set(), uiRoot: null };
11254
+ const uiRoot = safeContainedDirectory(packageRoot, unresolvedUiRoot);
11255
+ if (uiRoot === null) {
11256
+ return { available: /* @__PURE__ */ new Set(), uiRoot: null };
11257
+ }
11258
+ const moduleCandidates = /* @__PURE__ */ new Map();
11259
+ for (const [capability, moduleName] of Object.entries(CAPABILITIES)) {
11260
+ if (!requested.has(capability)) continue;
11261
+ moduleCandidates.set(capability, ["tsx", "ts", "jsx", "js"].flatMap((extension) => [
11262
+ join(uiRoot, `${moduleName}.${extension}`),
11263
+ join(uiRoot, moduleName, `index.${extension}`)
11264
+ ]));
11265
+ }
11266
+ const fingerprint = [
11267
+ join(packageRoot, "components.json"),
11268
+ join(packageRoot, "tsconfig.json"),
11269
+ join(packageRoot, "jsconfig.json"),
11270
+ ...[...moduleCandidates.values()].flat()
11271
+ ].map(fileFingerprint).join("|");
11272
+ const cacheKey = `${packageRoot}:${[...requested].sort().join(",")}`;
11273
+ const cached = PROJECT_PRIMITIVES_CACHE.get(cacheKey);
11274
+ if (cached?.fingerprint === fingerprint) return cached.value;
11275
+ const available = /* @__PURE__ */ new Set();
11276
+ for (const [capability, candidates] of moduleCandidates) {
11277
+ if (candidates.some(
11278
+ (candidate2) => exportsPrimitive(candidate2, capability)
11279
+ )) {
11280
+ available.add(capability);
11281
+ }
11282
+ }
11283
+ const value = { available, uiRoot };
11284
+ PROJECT_PRIMITIVES_CACHE.set(cacheKey, { fingerprint, value });
11285
+ return value;
11286
+ }
11287
+ function fileFingerprint(path) {
11288
+ try {
11289
+ const stat = lstatSync(path);
11290
+ return stat.isFile() ? `${path}:${stat.size}:${stat.mtimeMs}` : `${path}:excluded`;
11291
+ } catch {
11292
+ return `${path}:missing`;
11293
+ }
11294
+ }
11295
+ function isWithin2(root, candidate2) {
11296
+ const path = relative(root, candidate2);
11297
+ return path === "" || !path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path);
11298
+ }
11299
+ function safeContainedDirectory(root, candidate2) {
11300
+ try {
11301
+ if (!lstatSync(candidate2).isDirectory()) return null;
11302
+ const realRoot = realpathSync(root);
11303
+ const realCandidate = realpathSync(candidate2);
11304
+ return isWithin2(realRoot, realCandidate) ? realCandidate : null;
11305
+ } catch {
11306
+ return null;
11307
+ }
11308
+ }
11309
+ function findPackageRoot(startDir) {
11310
+ let dir = startDir;
11311
+ const filesystemRoot = parsePath(dir).root;
11312
+ for (; ; ) {
11313
+ if (existsSync(join(dir, "package.json"))) return dir;
11314
+ const parent = dirname(dir);
11315
+ if (dir === filesystemRoot || parent === dir) return null;
11316
+ dir = parent;
11317
+ }
11318
+ }
11319
+ function readJsonc(path) {
11320
+ try {
11321
+ const source = readSmallRegularFile(path);
11322
+ if (source === null) return null;
11323
+ const errors = [];
11324
+ const value = parseJsonc(source, errors, {
11325
+ allowTrailingComma: true,
11326
+ disallowComments: false
11327
+ });
11328
+ return errors.length === 0 ? value : null;
11329
+ } catch {
11330
+ return null;
11331
+ }
11332
+ }
11333
+ function readSmallRegularFile(path) {
11334
+ try {
11335
+ const stat = lstatSync(path);
11336
+ if (!stat.isFile() || stat.size > MAX_PROJECT_FILE_BYTES) return null;
11337
+ return readFileSync(path, "utf8");
11338
+ } catch {
11339
+ return null;
11340
+ }
11341
+ }
11342
+ function stringProperty(value, ...keys) {
11343
+ let current = value;
11344
+ for (const key of keys) {
11345
+ if (typeof current !== "object" || current === null || !(key in current)) return null;
11346
+ current = current[key];
11347
+ }
11348
+ return typeof current === "string" && current.trim() !== "" ? current : null;
11349
+ }
11350
+ function resolveAlias(packageRoot, alias) {
11351
+ if (isAbsolute(alias)) return null;
11352
+ if (alias.startsWith(".")) return resolve(packageRoot, alias);
11353
+ const configPath = ["tsconfig.json", "jsconfig.json"].map((name) => join(packageRoot, name)).find(existsSync);
11354
+ if (configPath === void 0) return null;
11355
+ const config = readJsonc(configPath);
11356
+ if (typeof config !== "object" || config === null) return null;
11357
+ const compilerOptions = config["compilerOptions"];
11358
+ if (typeof compilerOptions !== "object" || compilerOptions === null) return null;
11359
+ const options = compilerOptions;
11360
+ const baseUrl = typeof options["baseUrl"] === "string" ? options["baseUrl"] : ".";
11361
+ const paths = options["paths"];
11362
+ if (typeof paths !== "object" || paths === null) return null;
11363
+ const matches = [];
11364
+ for (const [pattern, rawTargets] of Object.entries(paths)) {
11365
+ if (!Array.isArray(rawTargets) || rawTargets.length !== 1) {
11366
+ continue;
11367
+ }
11368
+ const [target] = rawTargets;
11369
+ if (typeof target !== "string") continue;
11370
+ const star = pattern.indexOf("*");
11371
+ if (star === -1) {
11372
+ if (pattern === alias) matches.push(target);
11373
+ continue;
11374
+ }
11375
+ const prefix = pattern.slice(0, star);
11376
+ const suffix = pattern.slice(star + 1);
11377
+ if (!alias.startsWith(prefix) || !alias.endsWith(suffix)) continue;
11378
+ const substitution = alias.slice(prefix.length, alias.length - suffix.length);
11379
+ matches.push(target.replace("*", substitution));
11380
+ }
11381
+ const [match] = matches;
11382
+ if (matches.length !== 1 || match === void 0) return null;
11383
+ return resolve(packageRoot, baseUrl, match);
11384
+ }
11385
+ function exportsPrimitive(path, exportName) {
11386
+ try {
11387
+ const source = readSmallRegularFile(path);
11388
+ if (source === null) return false;
11389
+ const program = parseTypeScript(source, { jsx: true, sourceType: "module" });
11390
+ return program.body.some((statement) => {
11391
+ if (statement.type !== AST_NODE_TYPES50.ExportNamedDeclaration) return false;
11392
+ if (statement.exportKind === "type") return false;
11393
+ if (statement.specifiers.some(
11394
+ (specifier) => specifier.type === AST_NODE_TYPES50.ExportSpecifier && specifier.exportKind !== "type" && specifier.exported.type === AST_NODE_TYPES50.Identifier && specifier.exported.name === exportName
11395
+ )) {
11396
+ return true;
11397
+ }
11398
+ const declaration = statement.declaration;
11399
+ if (declaration?.type === AST_NODE_TYPES50.VariableDeclaration) {
11400
+ return declaration.declarations.some(
11401
+ (item) => item.id.type === AST_NODE_TYPES50.Identifier && item.id.name === exportName
11402
+ );
11403
+ }
11404
+ return (declaration?.type === AST_NODE_TYPES50.FunctionDeclaration || declaration?.type === AST_NODE_TYPES50.ClassDeclaration) && declaration.id?.name === exportName;
11405
+ });
11406
+ } catch {
11407
+ return false;
11408
+ }
11409
+ }
11215
11410
  function rawElementName(node) {
11216
11411
  if (node.name.type !== AST_NODE_TYPES50.JSXIdentifier) return null;
11217
11412
  const name = node.name.name;
11218
- return Object.hasOwn(SHADCN_PRIMITIVES, name) ? name : null;
11413
+ return Object.hasOwn(RAW_PRIMITIVES, name) ? name : null;
11219
11414
  }
11220
11415
  function effectiveAttribute(node, attributeName) {
11221
11416
  for (const attribute of node.attributes.toReversed()) {
@@ -11284,15 +11479,19 @@ function isStaticallyAssociatedLabel(node) {
11284
11479
  return node.parent.type === AST_NODE_TYPES50.JSXElement && containsLabelableElement(node.parent);
11285
11480
  }
11286
11481
  function replacementFor(node, element) {
11287
- if (element !== "input") return SHADCN_PRIMITIVES[element];
11482
+ if (element !== "input") return RAW_PRIMITIVES[element];
11288
11483
  const typeAttribute = effectiveAttribute(node, "type");
11289
11484
  if (typeAttribute.kind === "unknown") return null;
11290
11485
  const inputType = typeAttribute.kind === "known" ? typeAttribute.value.toLowerCase() : "text";
11291
11486
  if (inputType === "hidden" || inputType === "file") return null;
11292
- if (inputType === "checkbox") return "Checkbox";
11293
- if (inputType === "radio") return "RadioGroup family";
11487
+ if (inputType === "checkbox") {
11488
+ return { capability: "Checkbox", replacement: "Checkbox" };
11489
+ }
11490
+ if (inputType === "radio") {
11491
+ return { capability: "RadioGroup", replacement: "RadioGroup family" };
11492
+ }
11294
11493
  if (AMBIGUOUS_INPUT_TYPES.has(inputType)) return null;
11295
- return "Input";
11494
+ return RAW_PRIMITIVES.input;
11296
11495
  }
11297
11496
  var prefer_shadcn_primitives_default = createRule({
11298
11497
  name: "prefer-shadcn-primitives",
@@ -11306,7 +11505,8 @@ var prefer_shadcn_primitives_default = createRule({
11306
11505
  {
11307
11506
  type: "object",
11308
11507
  properties: {
11309
- assumeAvailable: { type: "boolean" }
11508
+ assumeAvailable: { type: "boolean" },
11509
+ detectProjectPrimitives: { type: "boolean" }
11310
11510
  },
11311
11511
  additionalProperties: false
11312
11512
  }
@@ -11318,15 +11518,21 @@ var prefer_shadcn_primitives_default = createRule({
11318
11518
  defaultOptions: [{}],
11319
11519
  create(context, [options]) {
11320
11520
  const filename = context.filename.replaceAll("\\", "/");
11321
- if (isTestFile(filename) || SHARED_PRIMITIVE_IMPLEMENTATION_RE.test(filename)) {
11521
+ if (isTestFile(filename) || isStoryFile(filename) || isGeneratedFile(filename, context.sourceCode.text) || SHARED_PRIMITIVE_IMPLEMENTATION_RE.test(filename)) {
11322
11522
  return {};
11323
11523
  }
11324
- let hasSharedPrimitiveImport = options?.assumeAvailable ?? false;
11524
+ const detectsProject = options?.detectProjectPrimitives === true;
11525
+ let hasSharedPrimitiveImport = false;
11526
+ const importedCapabilities = /* @__PURE__ */ new Set();
11325
11527
  const candidates = [];
11326
11528
  return {
11327
11529
  ImportDeclaration(node) {
11328
- if (typeof node.source.value === "string" && SHARED_PRIMITIVE_IMPORT_RE.test(node.source.value)) {
11329
- hasSharedPrimitiveImport = true;
11530
+ if (typeof node.source.value !== "string") return;
11531
+ const match = SHARED_PRIMITIVE_IMPORT_RE.exec(node.source.value);
11532
+ if (match?.[1] === void 0) return;
11533
+ hasSharedPrimitiveImport = true;
11534
+ for (const [capability, moduleName] of Object.entries(CAPABILITIES)) {
11535
+ if (match[1].toLowerCase() === moduleName) importedCapabilities.add(capability);
11330
11536
  }
11331
11537
  },
11332
11538
  JSXOpeningElement(node) {
@@ -11335,11 +11541,15 @@ var prefer_shadcn_primitives_default = createRule({
11335
11541
  if (element === "label" && !isStaticallyAssociatedLabel(node)) return;
11336
11542
  const replacement = replacementFor(node, element);
11337
11543
  if (replacement === null) return;
11338
- candidates.push({ element, node, replacement });
11544
+ candidates.push({ element, node, ...replacement });
11339
11545
  },
11340
11546
  "Program:exit"() {
11341
- if (!hasSharedPrimitiveImport) return;
11342
- for (const { element, node, replacement } of candidates) {
11547
+ const requested = new Set(candidates.map(({ capability }) => capability));
11548
+ const projectPrimitives = detectsProject && requested.size > 0 ? detectProjectPrimitives(context.filename, requested) : { available: /* @__PURE__ */ new Set(), uiRoot: null };
11549
+ if (projectPrimitives.uiRoot !== null && isWithin2(projectPrimitives.uiRoot, realpathOrOriginal(context.filename))) return;
11550
+ for (const { capability, element, node, replacement } of candidates) {
11551
+ const available = options?.assumeAvailable === true || (detectsProject ? projectPrimitives.available.has(capability) || importedCapabilities.has(capability) : hasSharedPrimitiveImport);
11552
+ if (!available) continue;
11343
11553
  context.report({
11344
11554
  node,
11345
11555
  messageId: "preferShadcnPrimitive",
@@ -11350,6 +11560,13 @@ var prefer_shadcn_primitives_default = createRule({
11350
11560
  };
11351
11561
  }
11352
11562
  });
11563
+ function realpathOrOriginal(path) {
11564
+ try {
11565
+ return realpathSync(path);
11566
+ } catch {
11567
+ return path;
11568
+ }
11569
+ }
11353
11570
 
11354
11571
  // src/rules/prefer-module-level-constant.ts
11355
11572
  import { AST_NODE_TYPES as AST_NODE_TYPES51 } from "@typescript-eslint/utils";
@@ -12684,11 +12901,11 @@ var prefer_native_random_uuid_default = createRule({
12684
12901
  create(context) {
12685
12902
  const directBindings = /* @__PURE__ */ new Set();
12686
12903
  const namespaceBindings = /* @__PURE__ */ new Set();
12687
- function resolve(identifier) {
12904
+ function resolve2(identifier) {
12688
12905
  return ASTUtils17.findVariable(context.sourceCode.getScope(identifier), identifier.name);
12689
12906
  }
12690
12907
  function record(identifier, destination) {
12691
- const variable = resolve(identifier);
12908
+ const variable = resolve2(identifier);
12692
12909
  if (variable !== null) destination.add(variable);
12693
12910
  }
12694
12911
  function report2(node) {
@@ -12716,7 +12933,7 @@ var prefer_native_random_uuid_default = createRule({
12716
12933
  },
12717
12934
  VariableDeclarator(node) {
12718
12935
  if (node.parent.kind !== "const" || !requireUuid(node.init)) return;
12719
- if (node.init?.type !== AST_NODE_TYPES57.CallExpression || node.init.callee.type !== AST_NODE_TYPES57.Identifier || (resolve(node.init.callee)?.defs.length ?? 0) > 0) {
12936
+ if (node.init?.type !== AST_NODE_TYPES57.CallExpression || node.init.callee.type !== AST_NODE_TYPES57.Identifier || (resolve2(node.init.callee)?.defs.length ?? 0) > 0) {
12720
12937
  return;
12721
12938
  }
12722
12939
  if (node.id.type === AST_NODE_TYPES57.Identifier) {
@@ -12733,14 +12950,14 @@ var prefer_native_random_uuid_default = createRule({
12733
12950
  "CallExpression:exit"(node) {
12734
12951
  if (node.arguments.length !== 0) return;
12735
12952
  if (node.callee.type === AST_NODE_TYPES57.Identifier) {
12736
- const variable2 = resolve(node.callee);
12953
+ const variable2 = resolve2(node.callee);
12737
12954
  if (variable2 !== null && directBindings.has(variable2)) report2(node);
12738
12955
  return;
12739
12956
  }
12740
12957
  if (node.callee.type !== AST_NODE_TYPES57.MemberExpression || node.callee.computed || node.callee.object.type !== AST_NODE_TYPES57.Identifier || node.callee.property.type !== AST_NODE_TYPES57.Identifier || node.callee.property.name !== "v4") {
12741
12958
  return;
12742
12959
  }
12743
- const variable = resolve(node.callee.object);
12960
+ const variable = resolve2(node.callee.object);
12744
12961
  if (variable !== null && namespaceBindings.has(variable)) report2(node);
12745
12962
  }
12746
12963
  };
@@ -12770,22 +12987,22 @@ function memberName4(node) {
12770
12987
  }
12771
12988
  return null;
12772
12989
  }
12773
- function isCryptoLoader(node, resolve) {
12990
+ function isCryptoLoader(node, resolve2) {
12774
12991
  if (node.type !== AST_NODE_TYPES58.CallExpression || node.arguments.length !== 1) return false;
12775
12992
  const [argument] = node.arguments;
12776
12993
  if (argument === void 0 || argument.type === AST_NODE_TYPES58.SpreadElement || !isCryptoSpecifier(argument)) {
12777
12994
  return false;
12778
12995
  }
12779
12996
  if (node.callee.type === AST_NODE_TYPES58.Identifier) {
12780
- return node.callee.name === "require" && isUnshadowedBuiltinIdentifier(node.callee, resolve);
12997
+ return node.callee.name === "require" && isUnshadowedBuiltinIdentifier(node.callee, resolve2);
12781
12998
  }
12782
- return node.callee.type === AST_NODE_TYPES58.MemberExpression && node.callee.object.type === AST_NODE_TYPES58.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve) && memberName4(node.callee) === "getBuiltinModule";
12999
+ return node.callee.type === AST_NODE_TYPES58.MemberExpression && node.callee.object.type === AST_NODE_TYPES58.Identifier && node.callee.object.name === "process" && isUnshadowedBuiltinIdentifier(node.callee.object, resolve2) && memberName4(node.callee) === "getBuiltinModule";
12783
13000
  }
12784
13001
  function isCryptoSpecifier(node) {
12785
13002
  return node.type === AST_NODE_TYPES58.Literal && (node.value === "crypto" || node.value === "node:crypto");
12786
13003
  }
12787
- function isUnshadowedBuiltinIdentifier(identifier, resolve) {
12788
- const variable = resolve(identifier);
13004
+ function isUnshadowedBuiltinIdentifier(identifier, resolve2) {
13005
+ const variable = resolve2(identifier);
12789
13006
  return variable === null || variable.defs.length === 0;
12790
13007
  }
12791
13008
  function propertyName4(node) {
@@ -12803,14 +13020,14 @@ var prefer_node_crypto_hash_default = createRule({
12803
13020
  create(context) {
12804
13021
  const directBindings = /* @__PURE__ */ new Set();
12805
13022
  const namespaceBindings = /* @__PURE__ */ new Set();
12806
- function resolve(identifier) {
13023
+ function resolve2(identifier) {
12807
13024
  return ASTUtils18.findVariable(
12808
13025
  context.sourceCode.getScope(identifier),
12809
13026
  identifier.name
12810
13027
  );
12811
13028
  }
12812
13029
  function record(identifier, destination) {
12813
- const variable = resolve(identifier);
13030
+ const variable = resolve2(identifier);
12814
13031
  if (variable !== null) destination.add(variable);
12815
13032
  }
12816
13033
  return {
@@ -12825,7 +13042,7 @@ var prefer_node_crypto_hash_default = createRule({
12825
13042
  }
12826
13043
  },
12827
13044
  VariableDeclarator(node) {
12828
- if (node.parent.kind !== "const" || node.init === null || !isCryptoLoader(node.init, resolve)) {
13045
+ if (node.parent.kind !== "const" || node.init === null || !isCryptoLoader(node.init, resolve2)) {
12829
13046
  return;
12830
13047
  }
12831
13048
  if (node.id.type === AST_NODE_TYPES58.Identifier) {
@@ -12849,7 +13066,7 @@ var prefer_node_crypto_hash_default = createRule({
12849
13066
  create,
12850
13067
  directBindings,
12851
13068
  namespaceBindings,
12852
- resolve
13069
+ resolve2
12853
13070
  ))
12854
13071
  return;
12855
13072
  context.report({ node, messageId: "preferNodeCryptoHash" });
@@ -12863,17 +13080,17 @@ function importedName5(node) {
12863
13080
  function isMemberCall(node, name) {
12864
13081
  return node.callee.type === AST_NODE_TYPES58.MemberExpression && memberName4(node.callee) === name;
12865
13082
  }
12866
- function isCreateHashCall(node, directBindings, namespaceBindings, resolve) {
13083
+ function isCreateHashCall(node, directBindings, namespaceBindings, resolve2) {
12867
13084
  if (node.callee.type === AST_NODE_TYPES58.Identifier) {
12868
- const variable2 = resolve(node.callee);
13085
+ const variable2 = resolve2(node.callee);
12869
13086
  return variable2 !== null && directBindings.has(variable2);
12870
13087
  }
12871
13088
  if (node.callee.type !== AST_NODE_TYPES58.MemberExpression || memberName4(node.callee) !== "createHash") {
12872
13089
  return false;
12873
13090
  }
12874
- if (isCryptoLoader(node.callee.object, resolve)) return true;
13091
+ if (isCryptoLoader(node.callee.object, resolve2)) return true;
12875
13092
  if (node.callee.object.type !== AST_NODE_TYPES58.Identifier) return false;
12876
- const variable = resolve(node.callee.object);
13093
+ const variable = resolve2(node.callee.object);
12877
13094
  return variable !== null && namespaceBindings.has(variable);
12878
13095
  }
12879
13096
 
@@ -14199,11 +14416,34 @@ var prefer_switch_for_repeated_equality_default = createRule({
14199
14416
 
14200
14417
  // src/rules/prefer-semantic-colors.ts
14201
14418
  import { AST_NODE_TYPES as AST_NODE_TYPES66 } from "@typescript-eslint/utils";
14202
- import { existsSync, readdirSync, readFileSync } from "fs";
14203
- import { dirname, join, parse } from "path";
14419
+ import { existsSync as existsSync2, lstatSync as lstatSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
14420
+ import { dirname as dirname2, join as join2, parse } from "path";
14204
14421
 
14205
14422
  // src/rules/_tailwind.ts
14206
- var tailwindBase = (token) => token.replace(/^(?:[a-z0-9-]+:)+/i, "").replace(/^!/, "");
14423
+ var tailwindVariantPrefix = (token) => {
14424
+ let bracketDepth = 0;
14425
+ let parenthesisDepth = 0;
14426
+ let escaped = false;
14427
+ let end = 0;
14428
+ for (let index = 0; index < token.length; index += 1) {
14429
+ const character = token[index];
14430
+ if (escaped) {
14431
+ escaped = false;
14432
+ continue;
14433
+ }
14434
+ if (character === "\\") {
14435
+ escaped = true;
14436
+ continue;
14437
+ }
14438
+ if (character === "[") bracketDepth += 1;
14439
+ else if (character === "]") bracketDepth = Math.max(0, bracketDepth - 1);
14440
+ else if (character === "(") parenthesisDepth += 1;
14441
+ else if (character === ")") parenthesisDepth = Math.max(0, parenthesisDepth - 1);
14442
+ else if (character === ":" && bracketDepth === 0 && parenthesisDepth === 0) end = index + 1;
14443
+ }
14444
+ return token.slice(0, end);
14445
+ };
14446
+ var tailwindBase = (token) => token.slice(tailwindVariantPrefix(token).length).replace(/^!/, "");
14207
14447
  var classTokens = (value) => value.split(/\s+/).filter(Boolean);
14208
14448
 
14209
14449
  // src/rules/prefer-semantic-colors.ts
@@ -14212,7 +14452,10 @@ var PREFER_SEMANTIC_COLORS_DOCUMENTATION = {
14212
14452
  rationale: "Semantic tokens keep themes and product meaning consistent while raw colors couple components to a palette value.",
14213
14453
  remediation: "Replace raw palette and literal colors with the closest semantic design-system token or CSS variable.",
14214
14454
  category: "style",
14215
- limitations: ["Email, PDF, icon artwork, masks, gradients, stories, and explicitly configured non-token projects have targeted exclusions."],
14455
+ limitations: [
14456
+ "Email, PDF, video-rendering, print-only, icon artwork, masks, gradients, stories, and explicitly configured non-token projects have targeted exclusions.",
14457
+ "Opaque-foreground checks are opt-in and require both a same-variant semantic background class and its package-local declared foreground token."
14458
+ ],
14216
14459
  examples: [
14217
14460
  { 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 },
14218
14461
  { 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 }
@@ -14340,7 +14583,13 @@ var SVG_SHAPE_PRIMITIVES = /* @__PURE__ */ new Set([
14340
14583
  "tspan",
14341
14584
  "use"
14342
14585
  ]);
14343
- var EMAIL_OR_PDF_MODULE_RE = /^@react-(?:email|pdf)\//;
14586
+ var EXTERNAL_RENDERER_MODULE_RE = /^(?:@react-(?:email|pdf)\/|remotion$|@remotion\/)/;
14587
+ var OPAQUE_FOREGROUND_RE = /^text-(?:white|black)(?:\/100)?$/;
14588
+ var SEMANTIC_BACKGROUND_RE = /^bg-([a-z][a-z0-9-]*)$/;
14589
+ var CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//gu;
14590
+ var DECLARED_FOREGROUND_RE = /--(?:color-)?([a-z][a-z0-9-]*)-foreground\s*:/giu;
14591
+ var MAX_TOKEN_STYLESHEET_BYTES = 1048576;
14592
+ var SEMANTIC_DECLARATIONS_CACHE = /* @__PURE__ */ new Map();
14344
14593
  function isSvgLikeElementName(name) {
14345
14594
  return name === "svg" || SVG_DEFS_CONTAINERS.has(name) || /svg$/i.test(name);
14346
14595
  }
@@ -14355,12 +14604,12 @@ var isInsideIconFactoryPath = (node) => {
14355
14604
  return false;
14356
14605
  };
14357
14606
  var hasMarkerAt = (dir) => {
14358
- if (PRESENCE_MARKERS.some((rel) => existsSync(join(dir, rel)))) return true;
14607
+ if (PRESENCE_MARKERS.some((rel) => existsSync2(join2(dir, rel)))) return true;
14359
14608
  for (const rel of CSS_DETECTION_FILES) {
14360
- const candidate2 = join(dir, rel);
14361
- if (!existsSync(candidate2)) continue;
14609
+ const candidate2 = join2(dir, rel);
14610
+ if (!existsSync2(candidate2)) continue;
14362
14611
  try {
14363
- const css = readFileSync(candidate2, "utf8");
14612
+ const css = readFileSync2(candidate2, "utf8");
14364
14613
  if (SEMANTIC_TOKEN_RE.test(css) || THEME_BLOCK_RE.test(css)) return true;
14365
14614
  } catch {
14366
14615
  }
@@ -14369,10 +14618,10 @@ var hasMarkerAt = (dir) => {
14369
14618
  };
14370
14619
  var readWorkspaceGlobs = (dir) => {
14371
14620
  const globs = [];
14372
- const packageJson = join(dir, "package.json");
14373
- if (existsSync(packageJson)) {
14621
+ const packageJson = join2(dir, "package.json");
14622
+ if (existsSync2(packageJson)) {
14374
14623
  try {
14375
- const parsed = JSON.parse(readFileSync(packageJson, "utf8"));
14624
+ const parsed = JSON.parse(readFileSync2(packageJson, "utf8"));
14376
14625
  const declared = typeof parsed === "object" && parsed !== null && "workspaces" in parsed ? parsed.workspaces : void 0;
14377
14626
  const list = Array.isArray(declared) ? declared : typeof declared === "object" && declared !== null && Array.isArray(declared.packages) ? declared.packages : [];
14378
14627
  for (const entry of list) if (typeof entry === "string") globs.push(entry);
@@ -14380,10 +14629,10 @@ var readWorkspaceGlobs = (dir) => {
14380
14629
  }
14381
14630
  }
14382
14631
  for (const name of ["pnpm-workspace.yaml", "pnpm-workspace.yml"]) {
14383
- const yaml = join(dir, name);
14384
- if (!existsSync(yaml)) continue;
14632
+ const yaml = join2(dir, name);
14633
+ if (!existsSync2(yaml)) continue;
14385
14634
  try {
14386
- for (const line of readFileSync(yaml, "utf8").split("\n")) {
14635
+ for (const line of readFileSync2(yaml, "utf8").split("\n")) {
14387
14636
  const match = /^\s*-\s*["']?([^"'#\s]+)["']?\s*$/u.exec(line);
14388
14637
  if (match?.[1] !== void 0) globs.push(match[1]);
14389
14638
  }
@@ -14393,7 +14642,7 @@ var readWorkspaceGlobs = (dir) => {
14393
14642
  return globs;
14394
14643
  };
14395
14644
  var hasSemanticTokenSystem = (filename) => {
14396
- const dir = dirname(filename);
14645
+ const dir = dirname2(filename);
14397
14646
  if (hasMarkerAtOrAbove(dir)) return true;
14398
14647
  const root = findWorkspaceRoot(dir);
14399
14648
  return root !== null && workspaceHasMarker(root);
@@ -14414,7 +14663,7 @@ var hasMarkerAtOrAbove = (startDir) => {
14414
14663
  answer = true;
14415
14664
  break;
14416
14665
  }
14417
- const parent = dirname(dir);
14666
+ const parent = dirname2(dir);
14418
14667
  if (dir === root || parent === dir) {
14419
14668
  answer = false;
14420
14669
  break;
@@ -14436,11 +14685,11 @@ var findWorkspaceRoot = (startDir) => {
14436
14685
  break;
14437
14686
  }
14438
14687
  visited.push(dir);
14439
- if (WORKSPACE_ROOT_FILES.some((name) => existsSync(join(dir, name))) || readWorkspaceGlobs(dir).length > 0) {
14688
+ if (WORKSPACE_ROOT_FILES.some((name) => existsSync2(join2(dir, name))) || readWorkspaceGlobs(dir).length > 0) {
14440
14689
  answer = dir;
14441
14690
  break;
14442
14691
  }
14443
- const parent = dirname(dir);
14692
+ const parent = dirname2(dir);
14444
14693
  if (dir === root || parent === dir) {
14445
14694
  answer = null;
14446
14695
  break;
@@ -14474,23 +14723,71 @@ var workspaceHasMarker = (root) => {
14474
14723
  };
14475
14724
  var expandWorkspaceGlob = (root, glob) => {
14476
14725
  const star = glob.indexOf("*");
14477
- if (star === -1) return [join(root, glob)];
14726
+ if (star === -1) return [join2(root, glob)];
14478
14727
  const prefix = glob.slice(0, star).replace(/\/$/u, "");
14479
- const parent = prefix === "" ? root : join(root, prefix);
14480
- if (!existsSync(parent)) return [];
14481
- return readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => join(parent, entry.name));
14728
+ const parent = prefix === "" ? root : join2(root, prefix);
14729
+ if (!existsSync2(parent) || !lstatSync2(parent).isDirectory()) return [];
14730
+ return readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => join2(parent, entry.name));
14482
14731
  };
14483
14732
  var propName = (key) => {
14484
14733
  if (key.type === AST_NODE_TYPES66.Identifier) return key.name;
14485
14734
  if (key.type === AST_NODE_TYPES66.Literal && typeof key.value === "string") return key.value;
14486
14735
  return null;
14487
14736
  };
14488
- var staticallyImportsEmailOrPdfRenderer = (program) => program.body.some((statement) => {
14737
+ var staticallyImportsExternalRenderer = (program) => program.body.some((statement) => {
14489
14738
  if (statement.type !== AST_NODE_TYPES66.ImportDeclaration && statement.type !== AST_NODE_TYPES66.ExportNamedDeclaration && statement.type !== AST_NODE_TYPES66.ExportAllDeclaration) {
14490
14739
  return false;
14491
14740
  }
14492
- return statement.source !== null && typeof statement.source.value === "string" && EMAIL_OR_PDF_MODULE_RE.test(statement.source.value);
14741
+ return statement.source !== null && typeof statement.source.value === "string" && EXTERNAL_RENDERER_MODULE_RE.test(statement.source.value);
14493
14742
  });
14743
+ var semanticForegroundRoles = (filename) => {
14744
+ const packageRoot = nearestPackageRoot(dirname2(filename));
14745
+ if (packageRoot === null) return /* @__PURE__ */ new Set();
14746
+ const candidates = CSS_DETECTION_FILES.map((relative2) => join2(packageRoot, relative2));
14747
+ const fingerprint = candidates.map(fileFingerprint2).join("|");
14748
+ const cached = SEMANTIC_DECLARATIONS_CACHE.get(packageRoot);
14749
+ if (cached?.fingerprint === fingerprint) return cached.value;
14750
+ const foregroundRoles = /* @__PURE__ */ new Set();
14751
+ for (const candidate2 of candidates) {
14752
+ const css = readTokenStylesheet(candidate2);
14753
+ if (css === null) continue;
14754
+ const declarations = css.replace(CSS_COMMENT_RE, "");
14755
+ for (const match of declarations.matchAll(DECLARED_FOREGROUND_RE)) {
14756
+ if (match[1] !== void 0) foregroundRoles.add(match[1].toLowerCase());
14757
+ }
14758
+ }
14759
+ SEMANTIC_DECLARATIONS_CACHE.set(packageRoot, { fingerprint, value: foregroundRoles });
14760
+ return foregroundRoles;
14761
+ };
14762
+ var nearestPackageRoot = (startDir) => {
14763
+ let dir = startDir;
14764
+ const root = parse(dir).root;
14765
+ for (; ; ) {
14766
+ if (existsSync2(join2(dir, "package.json"))) {
14767
+ return dir;
14768
+ }
14769
+ const parent = dirname2(dir);
14770
+ if (dir === root || parent === dir) return null;
14771
+ dir = parent;
14772
+ }
14773
+ };
14774
+ var fileFingerprint2 = (path) => {
14775
+ try {
14776
+ const stat = lstatSync2(path);
14777
+ return stat.isFile() ? `${path}:${stat.size}:${stat.mtimeMs}` : `${path}:excluded`;
14778
+ } catch {
14779
+ return `${path}:missing`;
14780
+ }
14781
+ };
14782
+ var readTokenStylesheet = (path) => {
14783
+ try {
14784
+ const stat = lstatSync2(path);
14785
+ if (!stat.isFile() || stat.size > MAX_TOKEN_STYLESHEET_BYTES) return null;
14786
+ return readFileSync2(path, "utf8");
14787
+ } catch {
14788
+ return null;
14789
+ }
14790
+ };
14494
14791
  var prefer_semantic_colors_default = createRule({
14495
14792
  name: "prefer-semantic-colors",
14496
14793
  documentation: PREFER_SEMANTIC_COLORS_DOCUMENTATION,
@@ -14504,30 +14801,34 @@ var prefer_semantic_colors_default = createRule({
14504
14801
  type: "object",
14505
14802
  additionalProperties: false,
14506
14803
  properties: {
14507
- requireSemanticTokens: { type: "boolean" }
14804
+ requireSemanticTokens: { type: "boolean" },
14805
+ opaqueForegroundPairs: { type: "boolean" }
14508
14806
  }
14509
14807
  }
14510
14808
  ],
14511
14809
  messages: {
14512
14810
  rawPalette: "Raw palette class '{{class}}' \u2014 use a semantic token (e.g. text-foreground, bg-primary, text-destructive, bg-muted).",
14513
14811
  arbitraryColor: "Hardcoded color '{{class}}' \u2014 use a semantic token, or var(--\u2026). For charts/brand add an eslint-disable with a reason.",
14514
- inlineColor: "Hardcoded color '{{value}}' \u2014 use a semantic token / CSS variable. For charts/standalone pages add an eslint-disable with a reason."
14812
+ inlineColor: "Hardcoded color '{{value}}' \u2014 use a semantic token / CSS variable. For charts/standalone pages add an eslint-disable with a reason.",
14813
+ opaqueForegroundPair: "'{{class}}' bypasses the declared '{{replacement}}' token paired with '{{background}}'."
14515
14814
  }
14516
14815
  },
14517
14816
  defaultOptions: [{}],
14518
14817
  create(context, [options]) {
14519
14818
  if (STORIES_FILE_RE.test(context.filename)) return {};
14520
- if (staticallyImportsEmailOrPdfRenderer(context.sourceCode.ast)) return {};
14521
- if (options?.requireSemanticTokens === true && !hasSemanticTokenSystem(context.filename)) {
14522
- return {};
14523
- }
14524
- let importsEmailOrPdfRenderer = false;
14525
- const pendingReports = [];
14819
+ if (staticallyImportsExternalRenderer(context.sourceCode.ast)) return {};
14820
+ if (options?.requireSemanticTokens === true && !hasSemanticTokenSystem(context.filename)) return {};
14821
+ const foregroundRoles = options?.opaqueForegroundPairs === true ? semanticForegroundRoles(context.filename) : /* @__PURE__ */ new Set();
14822
+ const checkOpaqueForegroundPairs = foregroundRoles.size > 0;
14823
+ let importsExternalRenderer = false;
14824
+ const pendingReports = /* @__PURE__ */ new Map();
14526
14825
  const report2 = (node, messageId, data) => {
14527
- pendingReports.push({ node, messageId, data });
14826
+ const key = `${node.range[0]}:${node.range[1]}:${messageId}:${JSON.stringify(data)}`;
14827
+ pendingReports.set(key, { node, messageId, data });
14528
14828
  };
14529
14829
  const reportClasses = (value, node) => {
14530
- for (const token of classTokens(value)) {
14830
+ const tokens = classTokens(value);
14831
+ for (const token of tokens) {
14531
14832
  const base = tailwindBase(token);
14532
14833
  if (RAW_PALETTE_RE.test(base)) {
14533
14834
  report2(node, "rawPalette", { class: token });
@@ -14535,6 +14836,26 @@ var prefer_semantic_colors_default = createRule({
14535
14836
  report2(node, "arbitraryColor", { class: token });
14536
14837
  }
14537
14838
  }
14839
+ if (!checkOpaqueForegroundPairs || isInsideSvg(node)) return;
14840
+ for (const token of tokens) {
14841
+ const prefix = tailwindVariantPrefix(token);
14842
+ if (prefix.split(":").includes("print")) continue;
14843
+ const base = tailwindBase(token);
14844
+ if (!OPAQUE_FOREGROUND_RE.test(base)) continue;
14845
+ const semanticBackground = tokens.find((candidate2) => {
14846
+ if (tailwindVariantPrefix(candidate2) !== prefix) return false;
14847
+ const match = SEMANTIC_BACKGROUND_RE.exec(tailwindBase(candidate2));
14848
+ return match?.[1] !== void 0 && foregroundRoles.has(match[1]);
14849
+ });
14850
+ if (semanticBackground === void 0) continue;
14851
+ const role = SEMANTIC_BACKGROUND_RE.exec(tailwindBase(semanticBackground))?.[1];
14852
+ if (role === void 0) continue;
14853
+ report2(node, "opaqueForegroundPair", {
14854
+ background: semanticBackground,
14855
+ class: token,
14856
+ replacement: `${prefix}text-${role}-foreground`
14857
+ });
14858
+ }
14538
14859
  };
14539
14860
  const checkClassNode = (node) => {
14540
14861
  if (node === null) return;
@@ -14582,8 +14903,8 @@ var prefer_semantic_colors_default = createRule({
14582
14903
  }
14583
14904
  },
14584
14905
  CallExpression(node) {
14585
- if (node.callee.type === AST_NODE_TYPES66.Identifier && node.callee.name === "require" && node.arguments[0]?.type === AST_NODE_TYPES66.Literal && typeof node.arguments[0].value === "string" && EMAIL_OR_PDF_MODULE_RE.test(node.arguments[0].value)) {
14586
- importsEmailOrPdfRenderer = true;
14906
+ if (node.callee.type === AST_NODE_TYPES66.Identifier && node.callee.name === "require" && node.arguments[0]?.type === AST_NODE_TYPES66.Literal && typeof node.arguments[0].value === "string" && EXTERNAL_RENDERER_MODULE_RE.test(node.arguments[0].value)) {
14907
+ importsExternalRenderer = true;
14587
14908
  }
14588
14909
  if (node.callee.type === AST_NODE_TYPES66.Identifier && CLASS_FNS.has(node.callee.name)) {
14589
14910
  for (const arg of node.arguments) {
@@ -14618,13 +14939,13 @@ var prefer_semantic_colors_default = createRule({
14618
14939
  if (name !== null && STYLE_COLOR_PROPS.has(name)) checkColorValueNode(node.value);
14619
14940
  },
14620
14941
  ImportExpression(node) {
14621
- if (node.source.type === AST_NODE_TYPES66.Literal && typeof node.source.value === "string" && EMAIL_OR_PDF_MODULE_RE.test(node.source.value)) {
14622
- importsEmailOrPdfRenderer = true;
14942
+ if (node.source.type === AST_NODE_TYPES66.Literal && typeof node.source.value === "string" && EXTERNAL_RENDERER_MODULE_RE.test(node.source.value)) {
14943
+ importsExternalRenderer = true;
14623
14944
  }
14624
14945
  },
14625
14946
  "Program:exit"() {
14626
- if (importsEmailOrPdfRenderer) return;
14627
- for (const descriptor of pendingReports) context.report(descriptor);
14947
+ if (importsExternalRenderer) return;
14948
+ for (const descriptor of pendingReports.values()) context.report(descriptor);
14628
14949
  }
14629
14950
  };
14630
14951
  }
@@ -14633,11 +14954,11 @@ var prefer_semantic_colors_default = createRule({
14633
14954
  // src/rules/prefer-server-actions.ts
14634
14955
  import "@typescript-eslint/utils";
14635
14956
  var PREFER_SERVER_ACTIONS_DOCUMENTATION = {
14636
- summary: "Prefer Next.js Server Actions over /api/* mutations.",
14957
+ summary: "Prefer Next.js Server Actions over same-origin API mutations.",
14637
14958
  rationale: "Server Actions preserve typed application calls and avoid an internal JSON request-response boundary.",
14638
14959
  remediation: "Move the mutation into a Server Action and invoke that action from the React client.",
14639
14960
  category: "architecture",
14640
- 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."],
14961
+ 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."],
14641
14962
  examples: [
14642
14963
  { 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 },
14643
14964
  { 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 }
@@ -14645,12 +14966,21 @@ var PREFER_SERVER_ACTIONS_DOCUMENTATION = {
14645
14966
  };
14646
14967
  var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
14647
14968
  var AXIOS_MUTATION_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
14648
- 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\/)/;
14969
+ 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\/)/;
14649
14970
  var NON_REACT_FRAMEWORK_RE2 = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
14650
- var NEXT_MODULE_PATH_RE2 = /(?:^|[/\\])(?:app|pages)[/\\]/u;
14971
+ var BASE_PATH_RE2 = /^\/(?!$)(?!.*[?#])(?:[^/]+\/)*[^/]+$/u;
14651
14972
  function getScope(context, node) {
14652
14973
  return context.sourceCode.getScope(node);
14653
14974
  }
14975
+ function resolvesToGlobalFetch(context, identifier) {
14976
+ let scope = getScope(context, identifier);
14977
+ while (scope) {
14978
+ const variable = scope.set.get(identifier.name);
14979
+ if (variable !== void 0) return variable.defs.length === 0;
14980
+ scope = scope.upper;
14981
+ }
14982
+ return true;
14983
+ }
14654
14984
  function resolveNode(node, context) {
14655
14985
  if (!node) return null;
14656
14986
  if (node.type !== "Identifier") return node;
@@ -14670,22 +15000,29 @@ function resolveNode(node, context) {
14670
15000
  }
14671
15001
  return node;
14672
15002
  }
14673
- function isApiUrl(node, context) {
15003
+ function isApiUrl(node, context, apiPrefixes) {
14674
15004
  const resolved = resolveNode(node, context);
14675
15005
  if (!resolved) return false;
14676
15006
  if (resolved.type === "Literal" && typeof resolved.value === "string") {
14677
- return resolved.value.startsWith("/api/");
15007
+ return apiPrefixes.some(
15008
+ (prefix) => resolved.value === prefix.slice(0, -1) || resolved.value.startsWith(prefix)
15009
+ );
14678
15010
  }
14679
15011
  if (resolved.type === "TemplateLiteral") {
14680
15012
  const firstQuasi = resolved.quasis[0];
14681
15013
  const cooked = firstQuasi?.value.cooked;
14682
- return typeof cooked === "string" && cooked.startsWith("/api/");
15014
+ return typeof cooked === "string" && apiPrefixes.some(
15015
+ (prefix) => cooked === prefix.slice(0, -1) || cooked.startsWith(prefix)
15016
+ );
14683
15017
  }
14684
15018
  if (resolved.type === "BinaryExpression" && resolved.operator === "+") {
14685
- return isApiUrl(resolved.left, context);
15019
+ return isApiUrl(resolved.left, context, apiPrefixes);
14686
15020
  }
14687
15021
  return false;
14688
15022
  }
15023
+ function isValidBasePath2(basePath) {
15024
+ return BASE_PATH_RE2.test(basePath) && !basePath.split("/").some((segment) => segment === "." || segment === "..");
15025
+ }
14689
15026
  function isMutationMethod(node, context) {
14690
15027
  const resolved = resolveNode(node, context);
14691
15028
  if (!resolved) return false;
@@ -14745,16 +15082,27 @@ var prefer_server_actions_default = createRule({
14745
15082
  meta: {
14746
15083
  type: "suggestion",
14747
15084
  docs: {
14748
- description: "Prefer Next.js Server Actions over /api/* mutations."
15085
+ description: "Prefer Next.js Server Actions over same-origin API mutations."
14749
15086
  },
14750
- schema: [],
15087
+ schema: [
15088
+ {
15089
+ type: "object",
15090
+ additionalProperties: false,
15091
+ properties: {
15092
+ basePath: {
15093
+ type: "string",
15094
+ pattern: "^/(?!$)(?!.*[?#])(?!(?:.*/)?\\.\\.?(?:/|$))(?:[^/]+/)*[^/]+$"
15095
+ }
15096
+ }
15097
+ }
15098
+ ],
14751
15099
  messages: {
14752
- preferServerAction: "Mutation against /api/* \u2014 prefer a Next.js Server Action for type-safety and to avoid the JSON round-trip."
15100
+ 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."
14753
15101
  }
14754
15102
  },
14755
- defaultOptions: [],
14756
- create(context) {
14757
- const filename = context.filename;
15103
+ defaultOptions: [{}],
15104
+ create(context, [options]) {
15105
+ const filename = context.filename.replaceAll("\\", "/");
14758
15106
  if (SKIP_FILE_REGEX.test(filename)) {
14759
15107
  return {};
14760
15108
  }
@@ -14762,22 +15110,28 @@ var prefer_server_actions_default = createRule({
14762
15110
  (node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && NON_REACT_FRAMEWORK_RE2.test(node.source.value)
14763
15111
  );
14764
15112
  const hasUseClientDirective = context.sourceCode.ast.body.some(
14765
- (node) => node.type === "ExpressionStatement" && node.expression.type === "Literal" && node.expression.value === "use client"
15113
+ (node) => node.type === "ExpressionStatement" && node.directive === "use client"
15114
+ );
15115
+ const hasUseServerDirective = context.sourceCode.ast.body.some(
15116
+ (node) => node.type === "ExpressionStatement" && node.directive === "use server"
14766
15117
  );
14767
- const hasNextImport = context.sourceCode.ast.body.some(
14768
- (node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && (node.source.value === "next" || node.source.value.startsWith("next/"))
15118
+ const importsServerOnly = context.sourceCode.ast.body.some(
15119
+ (node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && (node.source.value === "server-only" || node.source.value === "next/server")
14769
15120
  );
14770
- const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE2.test(filename);
14771
- if (!hasNextEvidence) {
15121
+ if (!hasUseClientDirective || hasUseServerDirective || importsServerOnly) {
14772
15122
  return {};
14773
15123
  }
15124
+ const apiPrefixes = ["/api/"];
15125
+ if (options?.basePath !== void 0 && isValidBasePath2(options.basePath)) {
15126
+ apiPrefixes.push(`${options.basePath}/api/`);
15127
+ }
14774
15128
  return {
14775
15129
  CallExpression(node) {
14776
15130
  if (isNonReactFramework) return;
14777
15131
  let isMutation = false;
14778
- if (node.callee.type === "Identifier" && node.callee.name === "fetch") {
15132
+ if (node.callee.type === "Identifier" && node.callee.name === "fetch" && resolvesToGlobalFetch(context, node.callee)) {
14779
15133
  const urlArg = node.arguments[0];
14780
- if (urlArg && urlArg.type !== "SpreadElement" && isApiUrl(urlArg, context)) {
15134
+ if (urlArg && urlArg.type !== "SpreadElement" && isApiUrl(urlArg, context, apiPrefixes)) {
14781
15135
  const initArg = node.arguments[1];
14782
15136
  if (initArg && initArg.type !== "SpreadElement") {
14783
15137
  const resolvedInit = resolveNode(initArg, context);
@@ -14794,7 +15148,7 @@ var prefer_server_actions_default = createRule({
14794
15148
  const hasHandlerArg = node.arguments.some(
14795
15149
  (arg) => arg.type !== "SpreadElement" && isFunctionArgument(arg, context)
14796
15150
  );
14797
- if (urlArg && urlArg.type !== "SpreadElement" && !hasHandlerArg && isApiUrl(urlArg, context)) {
15151
+ if (urlArg && urlArg.type !== "SpreadElement" && !hasHandlerArg && isApiUrl(urlArg, context, apiPrefixes)) {
14798
15152
  isMutation = true;
14799
15153
  }
14800
15154
  }
@@ -14805,7 +15159,7 @@ var prefer_server_actions_default = createRule({
14805
15159
  if (configArg && configArg.type === "ObjectExpression") {
14806
15160
  const urlNode = getPropertyNode(configArg, "url");
14807
15161
  const methodNode = getPropertyNode(configArg, "method");
14808
- if (urlNode && isApiUrl(urlNode, context) && methodNode && isMutationMethod(methodNode, context)) {
15162
+ if (urlNode && isApiUrl(urlNode, context, apiPrefixes) && methodNode && isMutationMethod(methodNode, context)) {
14809
15163
  isMutation = true;
14810
15164
  }
14811
15165
  }
@@ -19162,7 +19516,7 @@ var RULES = {
19162
19516
  };
19163
19517
  var meta = {
19164
19518
  name: "@sarj/eslint-plugin",
19165
- version: "15.17.2"
19519
+ version: "15.17.3"
19166
19520
  };
19167
19521
  var APPLICATION_ONLY_RULES = [
19168
19522
  "no-restricted-library-load",