@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 +460 -106
- package/dist/index.d.cts +14 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +478 -124
- package/package.json +4 -2
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
|
|
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.
|
|
6021
|
+
(statement) => statement.type === AST_NODE_TYPES24.ExpressionStatement && statement.directive === "use client"
|
|
6015
6022
|
);
|
|
6016
|
-
const
|
|
6017
|
-
(statement) => statement.type === AST_NODE_TYPES24.
|
|
6023
|
+
const hasUseServerDirective = context.sourceCode.ast.body.some(
|
|
6024
|
+
(statement) => statement.type === AST_NODE_TYPES24.ExpressionStatement && statement.directive === "use server"
|
|
6018
6025
|
);
|
|
6019
|
-
const
|
|
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" &&
|
|
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" &&
|
|
6057
|
-
|
|
6058
|
-
|
|
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 || !
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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\/[^/]
|
|
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(
|
|
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
|
|
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")
|
|
11293
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
11329
|
-
|
|
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
|
-
|
|
11342
|
-
|
|
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
|
|
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 =
|
|
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 || (
|
|
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 =
|
|
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 =
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
12788
|
-
const variable =
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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,
|
|
13083
|
+
function isCreateHashCall(node, directBindings, namespaceBindings, resolve2) {
|
|
12867
13084
|
if (node.callee.type === AST_NODE_TYPES58.Identifier) {
|
|
12868
|
-
const variable2 =
|
|
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,
|
|
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 =
|
|
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
|
|
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: [
|
|
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
|
|
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) =>
|
|
14607
|
+
if (PRESENCE_MARKERS.some((rel) => existsSync2(join2(dir, rel)))) return true;
|
|
14359
14608
|
for (const rel of CSS_DETECTION_FILES) {
|
|
14360
|
-
const candidate2 =
|
|
14361
|
-
if (!
|
|
14609
|
+
const candidate2 = join2(dir, rel);
|
|
14610
|
+
if (!existsSync2(candidate2)) continue;
|
|
14362
14611
|
try {
|
|
14363
|
-
const css =
|
|
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 =
|
|
14373
|
-
if (
|
|
14621
|
+
const packageJson = join2(dir, "package.json");
|
|
14622
|
+
if (existsSync2(packageJson)) {
|
|
14374
14623
|
try {
|
|
14375
|
-
const parsed = JSON.parse(
|
|
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 =
|
|
14384
|
-
if (!
|
|
14632
|
+
const yaml = join2(dir, name);
|
|
14633
|
+
if (!existsSync2(yaml)) continue;
|
|
14385
14634
|
try {
|
|
14386
|
-
for (const line of
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
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 [
|
|
14726
|
+
if (star === -1) return [join2(root, glob)];
|
|
14478
14727
|
const prefix = glob.slice(0, star).replace(/\/$/u, "");
|
|
14479
|
-
const parent = prefix === "" ? root :
|
|
14480
|
-
if (!
|
|
14481
|
-
return readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) =>
|
|
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
|
|
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" &&
|
|
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 (
|
|
14521
|
-
if (options?.requireSemanticTokens === true && !hasSemanticTokenSystem(context.filename)) {
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
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" &&
|
|
14586
|
-
|
|
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" &&
|
|
14622
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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" &&
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
14768
|
-
(node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && (node.source.value === "
|
|
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
|
-
|
|
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.
|
|
19519
|
+
version: "15.17.3"
|
|
19166
19520
|
};
|
|
19167
19521
|
var APPLICATION_ONLY_RULES = [
|
|
19168
19522
|
"no-restricted-library-load",
|