eslint-plugin-astro
Version:
ESLint plugin for Astro component
1,565 lines • 136 kB
JavaScript
import Module, { createRequire } from "node:module";
import globals from "globals";
import * as parser$1 from "astro-eslint-parser";
import { parseTemplate, traverseNodes } from "astro-eslint-parser";
import path from "node:path";
import { AST_NODE_TYPES } from "@typescript-eslint/types";
import { READ, ReferenceTracker, getPropertyName, getStaticValue, isClosingBraceToken, isClosingParenToken, isCommaToken, isOpeningParenToken, isParenthesized, isSemicolonToken } from "@eslint-community/eslint-utils";
import postcss from "postcss";
import parser from "postcss-selector-parser";
import { decode } from "@jridgewell/sourcemap-codec";
import * as espree from "espree";
//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
//#endregion
//#region src/utils/resolve-parser/require-user.ts
/** Require from user local */
function requireUserLocal(id) {
try {
const cwd = process.cwd();
const relativeTo = path.join(cwd, "__placeholder__.js");
return createRequire(relativeTo)(id);
} catch {
return null;
}
}
//#endregion
//#region src/configs/has-typescript-eslint-parser.ts
/** Load the TypeScript parser installed in the user's project. */
function loadTypescriptEslintParser() {
const parser = requireUserLocal("@typescript-eslint/parser");
if (parser) return parser;
const typescriptEslint = requireUserLocal("typescript-eslint");
return typescriptEslint?.parser ?? typescriptEslint?.default?.parser ?? null;
}
const tsESLintParser = loadTypescriptEslintParser();
const hasTypescriptEslintParser = Boolean(tsESLintParser);
//#endregion
//#region src/environments/index.ts
const environments$1 = { astro: { globals: {
Astro: false,
Fragment: false
} } };
//#endregion
//#region src/utils/index.ts
/**
* Define the rule.
* @param ruleName ruleName
* @param rule rule module
*/
function createRule(ruleName, rule) {
return {
meta: {
...rule.meta,
docs: {
available: () => true,
...rule.meta.docs,
url: `https://ota-meshi.github.io/eslint-plugin-astro/rules/${ruleName}/`,
ruleId: `astro/${ruleName}`,
ruleName
}
},
create: rule.create
};
}
//#endregion
//#region src/utils/ast-utils.ts
const anyFunctionPattern = /^(?:Function(?:Declaration|Expression)|ArrowFunctionExpression)$/u;
/**
* Get the attribute key name from given attribute node
*/
function getAttributeName(node) {
if (node.type === "JSXSpreadAttribute") return null;
const { name } = node;
return getName(name);
}
/**
* Get the element name from given node
*/
function getElementName(node) {
const nameNode = node.openingElement.name;
return getName(nameNode);
}
/**
* Find the attribute from the given element node
*/
function findAttribute(node, name) {
const openingElement = node.openingElement;
for (const attr of openingElement.attributes) {
if (attr.type === "JSXSpreadAttribute") continue;
if (getAttributeName(attr) === name) return attr;
}
return null;
}
/**
* Gets the spread attributes from the given element node
*/
function getSpreadAttributes(node) {
return node.openingElement.attributes.filter((attr) => attr.type === "JSXSpreadAttribute");
}
/**
* Get the static attribute string value from given attribute
*/
function getStaticAttributeStringValue(node, context) {
const value = getStaticAttributeValue(node, context);
if (!value) return null;
return value.value != null ? String(value.value) : value.value;
}
/**
* Get the static attribute value from given attribute
*/
function getStaticAttributeValue(node, context) {
if (node.value?.type === AST_NODE_TYPES.Literal) return { value: node.value.value };
if (context && node.value?.type === "JSXExpressionContainer" && node.value.expression.type !== "JSXEmptyExpression") {
const sourceCode = context.sourceCode;
const staticValue = getStaticValue(node.value.expression, sourceCode.scopeManager.globalScope);
if (staticValue != null) return staticValue;
}
return null;
}
/** Checks whether given node evaluate type is string */
function isStringCallExpression(node) {
if (node.type === AST_NODE_TYPES.CallExpression) return node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === "String";
return false;
}
/** Checks whether given node is StringLiteral */
function isStringLiteral(node) {
return node.type === AST_NODE_TYPES.Literal && typeof node.value === "string";
}
/** If it is concatenated with a plus, it gets its elements as an array. */
function extractConcatExpressions(node, sourceCode) {
if (node.operator !== "+") return null;
const leftResult = processLeft(node.left);
if (leftResult == null) return null;
return [...leftResult, node.right];
/** Process for left expression */
function processLeft(expr) {
if (expr.type === AST_NODE_TYPES.BinaryExpression) {
if (!isParenthesized(expr, sourceCode) && expr.operator !== "*" && expr.operator !== "/") return extractConcatExpressions(expr, sourceCode);
}
return [expr];
}
}
/**
* Get the value of a given node if it's a literal or a template literal.
*/
function getStringIfConstant(node) {
if (node.type === "Literal") {
if (typeof node.value === "string") return node.value;
} else if (node.type === "TemplateLiteral") {
let str = "";
const quasis = [...node.quasis];
const expressions = [...node.expressions];
let quasi, expr;
while (quasi = quasis.shift()) {
str += quasi.value.cooked;
expr = expressions.shift();
if (expr) {
const exprStr = getStringIfConstant(expr);
if (exprStr == null) return null;
str += exprStr;
}
}
return str;
} else if (node.type === "BinaryExpression") {
if (node.operator === "+") {
const left = getStringIfConstant(node.left);
if (left == null) return null;
const right = getStringIfConstant(node.right);
if (right == null) return null;
return left + right;
}
}
return null;
}
/**
* Check if it need parentheses.
*/
function needParentheses(node, kind) {
if (node.type === "ArrowFunctionExpression" || node.type === "AssignmentExpression" || node.type === "BinaryExpression" || node.type === "ConditionalExpression" || node.type === "LogicalExpression" || node.type === "SequenceExpression" || node.type === "UnaryExpression" || node.type === "UpdateExpression") return true;
if (kind === "logical") return node.type === "FunctionExpression";
return false;
}
/**
* Get parenthesized range from the given node
*/
function getParenthesizedTokens(node, sourceCode) {
let lastLeft = sourceCode.getFirstToken(node);
let lastRight = sourceCode.getLastToken(node);
let maybeLeftParen, maybeRightParen;
while ((maybeLeftParen = sourceCode.getTokenBefore(lastLeft)) && (maybeRightParen = sourceCode.getTokenAfter(lastRight)) && isOpeningParenToken(maybeLeftParen) && isClosingParenToken(maybeRightParen) && maybeLeftParen !== getParentSyntaxParen(node, sourceCode)) {
lastLeft = maybeLeftParen;
lastRight = maybeRightParen;
maybeLeftParen = sourceCode.getTokenBefore(lastLeft);
maybeRightParen = sourceCode.getTokenAfter(lastRight);
}
return {
left: lastLeft,
right: lastRight
};
}
/**
* Get parenthesized range from the given node
*/
function getParenthesizedRange(node, sourceCode) {
const { left, right } = getParenthesizedTokens(node, sourceCode);
return [left.range[0], right.range[1]];
}
/**
* Get the left parenthesis of the parent node syntax if it exists.
* E.g., `if (a) {}` then the `(`.
* @param {Node} node The AST node to check.
* @param {SourceCode} sourceCode The source code object to get tokens.
* @returns {Token|null} The left parenthesis of the parent node syntax
*/
function getParentSyntaxParen(node, sourceCode) {
const parent = node.parent;
switch (parent.type) {
case "CallExpression":
case "NewExpression":
if (parent.arguments.length === 1 && parent.arguments[0] === node) return sourceCode.getTokenAfter(parent.callee, {
includeComments: false,
filter: isOpeningParenToken
});
return null;
case "DoWhileStatement":
if (parent.test === node) return sourceCode.getTokenAfter(parent.body, {
includeComments: false,
filter: isOpeningParenToken
});
return null;
case "IfStatement":
case "WhileStatement":
if (parent.test === node) return sourceCode.getFirstToken(parent, {
includeComments: false,
skip: 1
});
return null;
case "ImportExpression":
if (parent.source === node) return sourceCode.getFirstToken(parent, {
includeComments: false,
skip: 1
});
return null;
case "SwitchStatement":
if (parent.discriminant === node) return sourceCode.getFirstToken(parent, {
includeComments: false,
skip: 1
});
return null;
case "WithStatement":
if (parent.object === node) return sourceCode.getFirstToken(parent, {
includeComments: false,
skip: 1
});
return null;
default: return null;
}
}
/**
* Get the name from given name node
*/
function getName(nameNode) {
if (nameNode.type === "JSXIdentifier") return nameNode.name;
if (nameNode.type === "JSXNamespacedName") return `${nameNode.namespace.name}:${nameNode.name.name}`;
if (nameNode.type === "JSXMemberExpression") return `${getName(nameNode.object)}.${nameNode.property.name}`;
return null;
}
/**
* Determines whether two adjacent tokens are on the same line.
* @param left The left token object.
* @param right The right token object.
* @returns Whether or not the tokens are on the same line.
* @public
*/
function isTokenOnSameLine(left, right) {
return left?.loc?.end.line === right?.loc?.start.line;
}
/**
* Gets next location when the result is not out of bound, otherwise returns null.
*
* Assumptions:
*
* - The given location represents a valid location in the given source code.
* - Columns are 0-based.
* - Lines are 1-based.
* - Column immediately after the last character in a line (not incl. linebreaks) is considered to be a valid location.
* - If the source code ends with a linebreak, `sourceCode.lines` array will have an extra element (empty string) at the end.
* The start (column 0) of that extra line is considered to be a valid location.
*
* Examples of successive locations (line, column):
*
* code: foo
* locations: (1, 0) -> (1, 1) -> (1, 2) -> (1, 3) -> null
*
* code: foo<LF>
* locations: (1, 0) -> (1, 1) -> (1, 2) -> (1, 3) -> (2, 0) -> null
*
* code: foo<CR><LF>
* locations: (1, 0) -> (1, 1) -> (1, 2) -> (1, 3) -> (2, 0) -> null
*
* code: a<LF>b
* locations: (1, 0) -> (1, 1) -> (2, 0) -> (2, 1) -> null
*
* code: a<LF>b<LF>
* locations: (1, 0) -> (1, 1) -> (2, 0) -> (2, 1) -> (3, 0) -> null
*
* code: a<CR><LF>b<CR><LF>
* locations: (1, 0) -> (1, 1) -> (2, 0) -> (2, 1) -> (3, 0) -> null
*
* code: a<LF><LF>
* locations: (1, 0) -> (1, 1) -> (2, 0) -> (3, 0) -> null
*
* code: <LF>
* locations: (1, 0) -> (2, 0) -> null
*
* code:
* locations: (1, 0) -> null
* @param sourceCode The sourceCode
* @param location The location
* @returns Next location
*/
function getNextLocation(sourceCode, { column, line }) {
if (column < sourceCode.lines[line - 1].length) return {
column: column + 1,
line
};
if (line < sourceCode.lines.length) return {
column: 0,
line: line + 1
};
return null;
}
/**
* Finds a function node from ancestors of a node.
* @param node A start node to find.
* @returns A found function node.
*/
function getUpperFunction(node) {
for (let currentNode = node; currentNode; currentNode = currentNode.parent) if (anyFunctionPattern.test(currentNode.type)) return currentNode;
return null;
}
//#endregion
//#region src/rules/missing-client-only-directive-value.ts
var missing_client_only_directive_value_default = createRule("missing-client-only-directive-value", {
meta: {
docs: {
description: "the client:only directive is missing the correct component's framework value",
category: "Possible Errors",
recommended: true
},
schema: [],
messages: { missingValue: "`client:only` directive is missing a value" },
type: "problem"
},
create(context) {
if (!context.sourceCode.parserServices?.isAstro) return {};
/** VerifyDirectiveValue */
function verifyDirectiveValue(attr) {
if (getAttributeName(attr) !== "client:only") return;
if (getStaticAttributeValue(attr, context) !== null) return;
context.report({
node: attr.name,
messageId: "missingValue"
});
}
return {
JSXAttribute: verifyDirectiveValue,
AstroTemplateLiteralAttribute: verifyDirectiveValue
};
}
});
//#endregion
//#region src/rules/no-conflict-set-directives.ts
var no_conflict_set_directives_default = createRule("no-conflict-set-directives", {
meta: {
docs: {
description: "disallow conflicting set directives and child contents",
category: "Possible Errors",
recommended: true
},
schema: [],
messages: { conflict: "{{name}} conflicts with {{conflictTargets}}." },
type: "problem"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isAstro) return {};
return { JSXElement(node) {
const reportData = [];
for (const attr of node.openingElement.attributes) {
const directiveName = getAttributeName(attr);
if (directiveName === "set:text" || directiveName === "set:html") reportData.push({
loc: attr.loc,
name: `'${directiveName}'`
});
}
if (reportData.length) {
const targetChildren = node.children.filter((child) => {
if (child.type === "AstroHTMLComment") return false;
if (child.type === "JSXText" || child.type === "AstroRawText") return Boolean(child.value.trim());
return true;
}).map((child) => {
if (child.type === "JSXText" || child.type === "AstroRawText") {
const leadingSpaces = /^\s*/.exec(child.value)[0];
const trailingSpaces = /\s*$/.exec(child.value)[0];
return { loc: {
start: sourceCode.getLocFromIndex(child.range[0] + leadingSpaces.length),
end: sourceCode.getLocFromIndex(child.range[1] - trailingSpaces.length)
} };
}
return child;
});
if (targetChildren.length) reportData.push({
loc: {
start: targetChildren[0].loc.start,
end: targetChildren[targetChildren.length - 1].loc.end
},
name: "child contents"
});
}
if (reportData.length >= 2) for (const data of reportData) {
const conflictTargets = reportData.filter((d) => d !== data).map((d) => d.name);
context.report({
loc: data.loc,
messageId: "conflict",
data: {
name: data.name,
conflictTargets: [conflictTargets.slice(0, -1).join(", "), conflictTargets.slice(-1)[0]].filter(Boolean).join(", and ")
}
});
}
} };
}
});
//#endregion
//#region src/rules/no-deprecated-astro-canonicalurl.ts
var no_deprecated_astro_canonicalurl_default = createRule("no-deprecated-astro-canonicalurl", {
meta: {
docs: {
description: "disallow using deprecated `Astro.canonicalURL`",
category: "Possible Errors",
recommended: true
},
schema: [],
messages: { deprecated: "'Astro.canonicalURL' is deprecated. Use 'Astro.url' helper instead." },
type: "problem"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isAstro) return {};
return { "Program:exit"(node) {
const tracker = new ReferenceTracker(sourceCode.getScope(node));
for (const { node, path } of tracker.iterateGlobalReferences({ Astro: { canonicalURL: { [READ]: true } } })) context.report({
node,
messageId: "deprecated",
data: { name: path.join(".") }
});
} };
}
});
//#endregion
//#region src/rules/no-deprecated-astro-fetchcontent.ts
var no_deprecated_astro_fetchcontent_default = createRule("no-deprecated-astro-fetchcontent", {
meta: {
docs: {
description: "disallow using deprecated `Astro.fetchContent()`",
category: "Possible Errors",
recommended: true
},
schema: [],
messages: { deprecated: "'Astro.fetchContent()' is deprecated. Use 'Astro.glob()' instead." },
type: "problem",
fixable: "code"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isAstro) return {};
return { "Program:exit"(node) {
const tracker = new ReferenceTracker(sourceCode.getScope(node));
for (const { node, path } of tracker.iterateGlobalReferences({ Astro: { fetchContent: { [READ]: true } } })) context.report({
node,
messageId: "deprecated",
data: { name: path.join(".") },
fix(fixer) {
if (node.type !== "MemberExpression" || node.computed) return null;
return fixer.replaceText(node.property, "glob");
}
});
} };
}
});
//#endregion
//#region src/rules/no-deprecated-astro-resolve.ts
var no_deprecated_astro_resolve_default = createRule("no-deprecated-astro-resolve", {
meta: {
docs: {
description: "disallow using deprecated `Astro.resolve()`",
category: "Possible Errors",
recommended: true
},
schema: [],
messages: { deprecated: "'Astro.resolve()' is deprecated." },
type: "problem"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isAstro) return {};
return { "Program:exit"(node) {
const tracker = new ReferenceTracker(sourceCode.getScope(node));
for (const { node, path } of tracker.iterateGlobalReferences({ Astro: { resolve: { [READ]: true } } })) context.report({
node,
messageId: "deprecated",
data: { name: path.join(".") }
});
} };
}
});
//#endregion
//#region src/rules/no-deprecated-getentrybyslug.ts
var no_deprecated_getentrybyslug_default = createRule("no-deprecated-getentrybyslug", {
meta: {
docs: {
description: "disallow using deprecated `getEntryBySlug()`",
category: "Possible Errors",
recommended: true
},
schema: [],
messages: { deprecated: "'getEntryBySlug()' is deprecated. Use 'getEntry()' instead." },
type: "problem"
},
create(context) {
if (!context.sourceCode.parserServices?.isAstro) return {};
return { ImportSpecifier(node) {
if (node.imported.type === "Identifier" && node.imported.name === "getEntryBySlug" && node.parent?.type === "ImportDeclaration" && node.parent.source.value === "astro:content") context.report({
node,
messageId: "deprecated"
});
} };
}
});
//#endregion
//#region src/rules/no-exports-from-components.ts
const ALLOWED_EXPORTS = /* @__PURE__ */ new Set([
"getStaticPaths",
"partial",
"prerender"
]);
var no_exports_from_components_default = createRule("no-exports-from-components", {
meta: {
docs: {
description: "disallow value export",
category: "Possible Errors",
recommended: true
},
schema: [],
messages: { disallowExport: "Exporting values from components is not allowed." },
type: "problem"
},
create(context) {
if (!context.sourceCode.parserServices?.isAstro) return {};
/**
* Verify for export declarations
*/
function verifyDeclaration(node) {
if (!node) return;
if (node.type.startsWith("TS") && !node.type.endsWith("Expression")) return;
if (node.type === "FunctionDeclaration" && node.id && ALLOWED_EXPORTS.has(node.id.name) || node.type === "VariableDeclaration" && node.declarations.every((decl) => decl.id.type === "Identifier" && ALLOWED_EXPORTS.has(decl.id.name))) return;
context.report({
node,
messageId: "disallowExport"
});
}
return {
ExportAllDeclaration(node) {
if (node.exportKind === "type") return;
context.report({
node,
messageId: "disallowExport"
});
},
ExportDefaultDeclaration(node) {
if (node.exportKind === "type") return;
verifyDeclaration(node.declaration);
},
ExportNamedDeclaration(node) {
if (node.exportKind === "type") return;
verifyDeclaration(node.declaration);
for (const spec of node.specifiers) {
if (spec.exportKind === "type" || spec.exported.type !== "Identifier") continue;
if (ALLOWED_EXPORTS.has(spec.exported.name)) continue;
context.report({
node: spec,
messageId: "disallowExport"
});
}
}
};
}
});
//#endregion
//#region src/rules/no-omitted-end-tags.ts
var no_omitted_end_tags_default = createRule("no-omitted-end-tags", {
meta: {
docs: {
description: "disallow omitted end tags",
category: "Stylistic Issues",
recommended: false
},
deprecated: {
message: "This rule is no longer useful because omitted end tags are now rejected before ESLint rules can run.",
url: "https://ota-meshi.github.io/eslint-plugin-astro/rules/no-omitted-end-tags/",
replacedBy: []
},
schema: [],
messages: {},
type: "layout"
},
create() {
return {};
}
});
//#endregion
//#region src/rules/no-prerender-export-outside-pages.ts
const PAGES_DIR_PATTERN = /(?:^|[/\\])pages[/\\]/;
const rule = createRule("no-prerender-export-outside-pages", {
meta: {
docs: {
description: "disallow `prerender` export outside of pages/ directory",
category: "Possible Errors",
recommended: true
},
schema: [],
messages: { disallowPrerenderOutsidePages: "'prerender' export is only valid inside a pages/ directory." },
type: "problem"
},
create(context) {
if (!context.sourceCode.parserServices?.isAstro) return {};
const filename = context.filename;
if (PAGES_DIR_PATTERN.test(filename)) return {};
/**
* Verify for export declarations
*/
function verifyDeclaration(node) {
if (!node) return;
if (node.type === "VariableDeclaration" && node.declarations.some((decl) => decl.id.type === "Identifier" && decl.id.name === "prerender")) context.report({
node,
messageId: "disallowPrerenderOutsidePages"
});
}
return { ExportNamedDeclaration(node) {
if (node.exportKind === "type") return;
verifyDeclaration(node.declaration);
for (const spec of node.specifiers) {
if (spec.exportKind === "type") continue;
if (spec.exported.type === "Identifier" && spec.exported.name === "prerender") context.report({
node: spec,
messageId: "disallowPrerenderOutsidePages"
});
}
} };
}
});
//#endregion
//#region src/rules/no-set-html-directive.ts
var no_set_html_directive_default = createRule("no-set-html-directive", {
meta: {
docs: {
description: "disallow use of `set:html` to prevent XSS attack",
category: "Security Vulnerability",
recommended: false
},
schema: [],
messages: { unexpected: "`set:html` can lead to XSS attack." },
type: "suggestion"
},
create(context) {
if (!context.sourceCode.parserServices?.isAstro) return {};
/** Verify */
function verifyName(attr) {
if (getAttributeName(attr) !== "set:html") return;
context.report({
node: attr.name,
messageId: "unexpected"
});
}
return {
JSXAttribute: verifyName,
AstroTemplateLiteralAttribute: verifyName
};
}
});
//#endregion
//#region src/rules/no-set-text-directive.ts
var no_set_text_directive_default = createRule("no-set-text-directive", {
meta: {
docs: {
description: "disallow use of `set:text`",
category: "Best Practices",
recommended: false
},
schema: [],
messages: { disallow: "Don't use `set:text`." },
type: "suggestion",
fixable: "code"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isAstro) return {};
/** Verify */
function verifyName(attr) {
if (getAttributeName(attr) !== "set:text") return;
context.report({
node: attr.name,
messageId: "disallow",
*fix(fixer) {
const element = attr.parent.parent;
if (!attr.value || !element || element.type !== "JSXElement") return;
if (element.children.some((child) => child.type !== "JSXText" || child.value.trim())) return;
const valueText = attr.type === "AstroTemplateLiteralAttribute" ? `{${sourceCode.getText(attr.value)}}` : sourceCode.getText(attr.value);
if (element.openingElement.selfClosing) {
if (sourceCode.text.slice(element.openingElement.range[1] - 2, element.openingElement.range[1]) !== "/>") return;
yield fixer.remove(attr);
yield fixer.removeRange([element.openingElement.range[1] - 2, element.openingElement.range[1] - 1]);
yield fixer.insertTextAfter(element.openingElement, `${valueText}</${sourceCode.getText(element.openingElement.name)}>`);
} else {
yield fixer.remove(attr);
yield* element.children.map((child) => fixer.remove(child));
yield fixer.insertTextAfter(element.openingElement, valueText);
}
}
});
}
return {
JSXAttribute: verifyName,
AstroTemplateLiteralAttribute: verifyName
};
}
});
//#endregion
//#region src/rules/no-unsafe-inline-scripts.ts
var no_unsafe_inline_scripts_default = createRule("no-unsafe-inline-scripts", {
meta: {
docs: {
description: "disallow inline `<script>` without `src` to encourage CSP-safe patterns",
category: "Security Vulnerability",
recommended: false,
default: "warn"
},
schema: [{
type: "object",
properties: {
allowDefineVars: { type: "boolean" },
allowModuleScripts: { type: "boolean" },
allowNonExecutingTypes: {
type: "array",
items: { type: "string" }
},
allowNonce: { type: "boolean" }
},
additionalProperties: false
}],
messages: { unexpected: "Unsafe inline <script> detected. Move code to an external file (use src) or a safer pattern." },
type: "suggestion"
},
create(context) {
if (!context.sourceCode.parserServices?.isAstro) return {};
const options = context.options[0] ?? {};
const allowDefineVars = options.allowDefineVars === true;
const allowModuleScripts = options.allowModuleScripts === true;
const allowNonExecutingTypes = new Set((options.allowNonExecutingTypes ?? ["application/ld+json", "application/json"]).map((type) => type.trim().toLowerCase().split(";")[0].trim()));
const allowNonce = options.allowNonce === true;
return { JSXElement(node) {
if (getElementName(node) !== "script") return;
if (!isInlineScript(node)) return;
const typeAttr = findAttribute(node, "type");
if (isAllowedByType(typeAttr, context, allowNonExecutingTypes)) return;
if (isModuleScript(typeAttr, context, allowModuleScripts)) return;
const attrs = node.openingElement.attributes;
if (isDefineVars(attrs, allowDefineVars)) return;
if (allowNonce && hasNonce(attrs)) return;
const reportTarget = node.openingElement.name;
context.report({
node: reportTarget,
messageId: "unexpected"
});
} };
}
});
/**
* Normalize a MIME type string by trimming, lowercasing, and dropping parameters.
*/
function normalizeMimeType(value) {
if (!value) return null;
return String(value).trim().toLowerCase().split(";")[0].trim();
}
/**
* Determine if the provided type attribute is listed in the allowed MIME type set.
*/
function isAllowedByType(attr, context, allowedTypes) {
if (!attr) return false;
const value = getStaticAttributeStringValue(attr, context);
if (!value) return false;
const normalizedType = normalizeMimeType(value);
return normalizedType != null && allowedTypes.has(normalizedType);
}
/**
* Check whether inline scripts with define:vars should be treated as allowed.
*/
function isDefineVars(attrs, allowDefineVars) {
if (!allowDefineVars) return false;
for (const attr of attrs) {
if (attr.type === "JSXSpreadAttribute") continue;
if (getAttributeName(attr) === "define:vars") return true;
}
return false;
}
/**
* Verify if the script's type attribute qualifies as an allowed module script.
*/
function isModuleScript(attr, context, allowModuleScripts) {
if (!allowModuleScripts) return false;
if (!attr) return false;
return normalizeMimeType(getStaticAttributeStringValue(attr, context)) === "module";
}
/**
* Detect the presence of a nonce attribute to satisfy CSP allowances.
*/
function hasNonce(attrs) {
for (const attr of attrs) {
if (attr.type === "JSXSpreadAttribute") continue;
if (getAttributeName(attr) === "nonce") return true;
}
return false;
}
/**
* Determine if a <script> element is inline by checking for the absence of src.
*/
function isInlineScript(node) {
if (findAttribute(node, "src")) return false;
return true;
}
//#endregion
//#region src/utils/transform/utils.ts
const cache$1 = /* @__PURE__ */ new WeakMap();
/**
* Load module
*/
function loadModule(context, name) {
const key = context.sourceCode.ast;
let modules = cache$1.get(key);
if (!modules) {
modules = {};
cache$1.set(key, modules);
}
const mod = modules[name];
if (mod) return mod;
try {
const cwd = context.cwd;
const relativeTo = path.join(cwd, "__placeholder__.js");
return modules[name] = Module.createRequire(relativeTo)(name);
} catch {
return null;
}
}
/** Get content range */
function getContentRange(node) {
if (node.closingElement) return [node.openingElement.range[1], node.closingElement.range[0]];
return [node.openingElement.range[1], node.range[1]];
}
//#endregion
//#region src/utils/transform/postcss.ts
/**
* Transform with postcss
*/
function transform$3(node, context) {
const postcssLoadConfig = loadPostcssLoadConfig(context);
if (!postcssLoadConfig) return null;
const inputRange = getContentRange(node);
const code = context.sourceCode.text.slice(...inputRange);
const filename = `${context.filename}.css`;
try {
const config = postcssLoadConfig.sync({
cwd: context.cwd ?? process.cwd(),
from: filename
});
const result = postcss(config.plugins).process(code, {
...config.options,
map: { inline: false }
});
return {
inputRange,
output: result.content,
mappings: result.map.toJSON().mappings
};
} catch {
return null;
}
}
/**
* Load postcss-load-config
*/
function loadPostcssLoadConfig(context) {
return loadModule(context, "postcss-load-config");
}
//#endregion
//#region src/utils/transform/sass.ts
/**
* Transpile with sass
*/
function transform$2(node, context, type) {
const sass = loadSass(context);
if (!sass) return null;
const inputRange = getContentRange(node);
const code = context.sourceCode.text.slice(...inputRange);
try {
const output = sass.compileString(code, {
sourceMap: true,
syntax: type === "sass" ? "indented" : void 0
});
if (!output) return null;
return {
inputRange,
output: output.css,
mappings: output.sourceMap.mappings
};
} catch {
return null;
}
}
/**
* Load sass
*/
function loadSass(context) {
return loadModule(context, "sass");
}
//#endregion
//#region src/utils/transform/less.ts
/**
* Transpile with less
*/
function transform$1(node, context) {
const less = loadLess(context);
if (!less) return null;
const inputRange = getContentRange(node);
const code = context.sourceCode.text.slice(...inputRange);
const filename = `${context.filename}.less`;
try {
let output;
less.render(code, {
sourceMap: {},
syncImport: true,
filename,
lint: false
}, (_error, result) => {
output = result;
});
if (!output) return null;
return {
inputRange,
output: output.css,
mappings: JSON.parse(output.map).mappings
};
} catch {
return null;
}
}
/**
* Load less
*/
function loadLess(context) {
return loadModule(context, "less");
}
//#endregion
//#region src/utils/transform/stylus.ts
/**
* Transpile with stylus
*/
function transform(node, context) {
const stylus = loadStylus(context);
if (!stylus) return null;
const inputRange = getContentRange(node);
const code = context.sourceCode.text.slice(...inputRange);
const filename = `${context.filename}.stylus`;
try {
let output;
const style = stylus(code, { filename }).set("sourcemap", {});
style.render((_error, outputCode) => {
output = outputCode;
});
if (output == null) return null;
return {
inputRange,
output,
mappings: style.sourcemap.mappings
};
} catch {
return null;
}
}
/**
* Load stylus
*/
function loadStylus(context) {
return loadModule(context, "stylus");
}
//#endregion
//#region src/utils/transform/lines-and-columns.ts
var LinesAndColumns = class {
lineStartIndices;
code;
constructor(code) {
const len = code.length;
const lineStartIndices = [0];
for (let index = 0; index < len; index++) {
const c = code[index];
if (c === "\r") {
if ((code[index + 1] || "") === "\n") index++;
lineStartIndices.push(index + 1);
} else if (c === "\n") lineStartIndices.push(index + 1);
}
this.code = code;
this.lineStartIndices = lineStartIndices;
}
getLocFromIndex(index) {
const lineNumber = sortedLastIndex$1(this.lineStartIndices, index);
return {
line: lineNumber,
column: index - this.lineStartIndices[lineNumber - 1]
};
}
getIndexFromLoc(loc) {
const lineIndex = loc.line - 1;
if (this.lineStartIndices.length > lineIndex) return this.lineStartIndices[lineIndex] + loc.column;
else if (this.lineStartIndices.length === lineIndex) return this.code.length + loc.column;
return this.code.length + loc.column;
}
};
/**
* Uses a binary search to determine the highest index at which value should be inserted into array in order to maintain its sort order.
*/
function sortedLastIndex$1(array, value) {
let lower = 0;
let upper = array.length;
while (lower < upper) {
const mid = Math.floor(lower + (upper - lower) / 2);
const target = array[mid];
if (target < value) lower = mid + 1;
else if (target > value) upper = mid;
else return mid + 1;
}
return upper;
}
//#endregion
//#region src/utils/transform/index.ts
const cache = /* @__PURE__ */ new WeakMap();
/** Get style content css */
function getStyleContentCSS(node, context) {
const cachedResult = cache.get(node);
if (cachedResult) return cachedResult;
const sourceCode = context.sourceCode;
const langNode = findAttribute(node, "lang");
const lang = langNode && getStaticAttributeStringValue(langNode);
if (!langNode || lang === "css") {
const inputRange = getContentRange(node);
return {
css: sourceCode.text.slice(...inputRange),
remap: (i) => inputRange[0] + i
};
}
let transform$4 = null;
if (lang === "postcss") transform$4 = transform$3(node, context);
else if (lang === "scss" || lang === "sass") transform$4 = transform$2(node, context, lang);
else if (lang === "less") transform$4 = transform$1(node, context);
else if (lang === "styl" || lang === "stylus") transform$4 = transform(node, context);
if (!transform$4) return null;
const result = transformToStyleContentCSS(transform$4, context);
cache.set(node, result);
return result;
}
/** TransformResult to style content css */
function transformToStyleContentCSS(transform, context) {
const sourceCode = context.sourceCode;
let outputLocs = null;
let inputLocs = null;
let decoded = null;
return {
css: transform.output,
remap: (index) => {
outputLocs = outputLocs ?? new LinesAndColumns(transform.output);
inputLocs = inputLocs ?? new LinesAndColumns(sourceCode.text.slice(...transform.inputRange));
const inputCodePos = remapPosition(outputLocs.getLocFromIndex(index));
return inputLocs.getIndexFromLoc(inputCodePos) + transform.inputRange[0];
}
};
/** Remapping source position */
function remapPosition(pos) {
decoded = decoded ?? decode(transform.mappings);
const lineMaps = decoded[pos.line - 1];
if (!lineMaps?.length) {
for (let line = pos.line - 1; line >= 0; line--) {
const prevLineMaps = decoded[line];
if (prevLineMaps?.length) {
const [, , sourceCodeLine, sourceCodeColumn] = prevLineMaps[prevLineMaps.length - 1];
return {
line: sourceCodeLine + 1,
column: sourceCodeColumn
};
}
}
return {
line: -1,
column: -1
};
}
for (let index = 0; index < lineMaps.length - 1; index++) {
const [generateCodeColumn, , sourceCodeLine, sourceCodeColumn] = lineMaps[index];
if (generateCodeColumn <= pos.column && pos.column < lineMaps[index + 1][0]) return {
line: sourceCodeLine + 1,
column: sourceCodeColumn + (pos.column - generateCodeColumn)
};
}
const [generateCodeColumn, , sourceCodeLine, sourceCodeColumn] = lineMaps[lineMaps.length - 1];
return {
line: sourceCodeLine + 1,
column: sourceCodeColumn + (pos.column - generateCodeColumn)
};
}
}
//#endregion
//#region src/rules/no-unused-css-selector.ts
var no_unused_css_selector_default = createRule("no-unused-css-selector", {
meta: {
docs: {
description: "disallow selectors defined in `style` tag that don't use in HTML",
category: "Best Practices",
recommended: false
},
schema: [],
messages: { unused: "Unused CSS selector `{{selector}}`" },
type: "problem"
},
create(context) {
const sourceCode = context.sourceCode;
if (!sourceCode.parserServices?.isAstro) return {};
const styles = [];
const rootTree = {
parent: null,
node: null,
childElements: []
};
const allTreeElements = [];
let currTree = rootTree;
/** Verify for CSS */
function verifyCSS(css) {
let root;
try {
root = postcss.parse(css.css);
} catch {
return;
}
const ignoreNodes = /* @__PURE__ */ new Set();
root.walk((psNode) => {
if (psNode.parent && ignoreNodes.has(psNode.parent)) {
ignoreNodes.add(psNode);
return;
}
if (psNode.type !== "rule") {
if (psNode.type === "atrule") {
if (psNode.name === "keyframes") ignoreNodes.add(psNode);
}
return;
}
const rule = psNode;
const raws = rule.raws;
const rawSelectorText = raws.selector ? raws.selector.raw : rule.selector;
for (const selector of parseSelector(rawSelectorText, context)) {
if (selector.error) continue;
if (allTreeElements.some((tree) => selector.test(tree))) continue;
reportSelector(rule.source.start.offset + selector.offset, selector.selector);
}
});
/** Report selector */
function reportSelector(start, selector) {
const remapStart = css.remap(start);
const remapEnd = css.remap(start + selector.length);
context.report({
loc: {
start: sourceCode.getLocFromIndex(remapStart),
end: sourceCode.getLocFromIndex(remapEnd)
},
messageId: "unused",
data: { selector }
});
}
}
return {
JSXElement(node) {
const name = getElementName(node);
if (name === "Fragment" || name === "slot") return;
if (name === "style" && !findAttribute(node, "is:global")) styles.push(node);
const tree = {
parent: currTree,
node,
childElements: []
};
allTreeElements.unshift(tree);
currTree.childElements.push(tree);
currTree = tree;
},
"JSXElement:exit"(node) {
if (currTree.node === node) {
if (currTree.node) {
const expressions = currTree.node.children.filter((e) => e.type === "JSXExpressionContainer");
if (expressions.length) for (const child of currTree.childElements) child.withinExpression = expressions.some((e) => e.range[0] <= child.node.range[0] && child.node.range[1] <= e.range[1]);
}
currTree = currTree.parent;
}
},
"Program:exit"() {
for (const style of styles) {
const css = getStyleContentCSS(style, context);
if (css) verifyCSS(css);
}
}
};
}
});
var SelectorError = class extends Error {};
/**
* Parses CSS selectors and returns an object with a function that tests JSXElement.
*/
function parseSelector(selector, context) {
let astSelector;
try {
astSelector = parser().astSync(selector);
} catch (error) {
return [{
error,
selector,
offset: 0,
test: () => false
}];
}
return astSelector.nodes.map((sel) => {
const nodes = removeGlobals(cleanSelectorChildren(sel));
try {
const test = selectorToJSXElementMatcher(nodes, context);
return {
selector: sel.toString().trim(),
offset: sel.sourceIndex ?? sel.nodes[0].sourceIndex,
test(element) {
return test(element, null);
}
};
} catch (error) {
if (error instanceof SelectorError) return {
error,
selector: sel.toString().trim(),
offset: sel.sourceIndex ?? sel.nodes[0].sourceIndex,
test: () => false
};
throw error;
}
});
/** Remove :global() on both sides */
function removeGlobals(nodes) {
let start = 0;
let end = nodes.length;
while (nodes[end - 1] && isGlobalPseudo(nodes[end - 1])) {
end--;
if (nodes[end - 1]?.type === "combinator") end--;
}
while (nodes[start] && isGlobalPseudo(nodes[start])) {
start++;
if (nodes[start]?.type === "combinator") start++;
}
if (nodes.some(isRootPseudo)) {
while (nodes[start] && !isRootPseudo(nodes[start])) start++;
start++;
while (nodes[start] && nodes[start].type !== "combinator") start++;
if (nodes[start]?.type === "combinator") start++;
}
return nodes.slice(start, end);
}
}
/**
* Convert nodes to JSXElementMatcher
* @param {parser.Selector[]} selectorNodes
* @returns {JSXElementMatcher}
*/
function selectorsToJSXElementMatcher(selectorNodes, context) {
const selectors = selectorNodes.map((n) => selectorToJSXElementMatcher(cleanSelectorChildren(n), context));
return (element, subject) => selectors.some((sel) => sel(element, subject));
}
/**
* @param {parser.Node|null} node
* @returns {node is parser.Combinator}
*/
function isDescendantCombinator(node) {
return Boolean(node && node.type === "combinator" && !node.value.trim());
}
/**
* Clean and get the selector child nodes.
* @param {parser.Selector} selector
* @returns {ChildNode[]}
*/
function cleanSelectorChildren(selector) {
const nodes = [];
let last = null;
for (const node of selector.nodes) {
if (node.type === "root") throw new SelectorError("Unexpected state type=root");
if (node.type === "comment") continue;
if ((last == null || last.type === "combinator") && isDescendantCombinator(node)) continue;
if (isDescendantCombinator(last) && node.type === "combinator") nodes.pop();
nodes.push(node);
last = node;
}
if (isDescendantCombinator(last)) nodes.pop();
return nodes;
}
/**
* Convert Selector child nodes to JSXElementMatcher
* @param {ChildNode[]} selectorChildren
* @returns {JSXElementMatcher}
*/
function selectorToJSXElementMatcher(selectorChildren, context) {
const nodes = [...selectorChildren];
let node = nodes.shift();
let result = null;
while (node) {
if (node.type === "combinator") {
const combinator = node.value;
node = nodes.shift();
if (!node) throw new SelectorError(`Expected selector after '${combinator}'.`);
if (node.type === "combinator") throw new SelectorError(`Unexpected combinator '${node.value}'.`);
const right = nodeToJSXElementMatcher(node, context);
result = combination(result || ((element, subject) => element === subject), combinator, right);
} else {
const sel = nodeToJSXElementMatcher(node, context);
result = result ? compound(result, sel) : sel;
}
node = nodes.shift();
}
if (!result) return () => true;
return result;
}
/**
* @param {JSXElementMatcher} left
* @param {string} combinator
* @param {JSXElementMatcher} right
* @returns {JSXElementMatcher}
*/
function combination(left, combinator, right) {
switch (combinator.trim()) {
case "": return (element, subject) => {
if (right(element, null)) {
let parent = element.parent;
while (parent.node) {
if (left(parent, subject)) return true;
parent = parent.parent;
}
}
return false;
};
case ">": return (element, subject) => {
if (right(element, null)) {
const parent = element.parent;
if (parent.node) return left(parent, subject);
}
return false;
};
case "+": return (element, subject) => {
if (right(element, null)) {
const before = getBeforeElement(element);
if (before) return left(before, subject);
}
return false;
};
case "~": return (element, subject) => {
if (right(element, null)) {
for (const before of getBeforeElements(element)) if (left(before, subject)) return true;
}
return false;
};
default: throw new SelectorError(`Unknown combinator: ${combinator}.`);
}
}
/**
* Convert node to JSXElementMatcher
* @param {Exclude<parser.Node, {type:'combinator'|'comment'|'root'|'selector'}>} selector
* @returns {JSXElementMatcher}
*/
function nodeToJSXElementMatcher(selector, context) {
const baseMatcher = (() => {
switch (selector.type) {
case "attribute": return attributeNodeToJSXElementMatcher(selector, context);
case "class": return classNameNodeToJSXElementMatcher(selector, context);
case "id": return identifierNodeToJSXElementMatcher(selector, context);
case "tag": return tagNodeToJSXElementMatcher(selector);
case "universal": return universalNodeToJSXElementMatcher(selector);
case "pseudo": return pseudoNodeToJSXElementMatcher(selector, context);
case "nesting": throw new SelectorError("Unsupported nesting selector.");
case "string": throw new SelectorError(`Unknown selector: ${selector.value}.`);
default: throw new SelectorError(`Unknown selector: ${selector.value}.`);
}
})();
return (element, subject) => {
if (isComponentElement(element)) return false;
return baseMatcher(element, subject);
};
}
/**
* Convert Attribute node to JSXElementMatcher
* @param {parser.Attribute} selector
* @returns {JSXElementMatcher}
*/
function attributeNodeToJSXElementMatcher(selector, context) {
const key = selector.attribute;
if (!selector.operator) return (element, _) => {
return hasAttribute(element, key, context);
};
const value = selector.value || "";
switch (selector.operator) {
case "=": return buildJSXElementMatcher(value, (attr, val) => attr === val);
case "~=": return buildJSXElementMatcher(value, (attr, val) => attr.split(/\s+/u).includes(val));
case "|=": return buildJSXElementMatcher(value, (attr, val) => attr === val || attr.startsWith(`${val}-`));
case "^=": return buildJSXElementMatcher(value, (attr, val) => attr.startsWith(val));
case "$=": return buildJSXElementMatcher(value, (attr, val) => attr.endsWith(val));
case "*=": return buildJSXElementMatcher(value, (attr, val) => attr.includes(val));
default: throw new SelectorError(`Unsupported operator: ${selector.operator}.`);
}
/**
* @param {string} selectorValue
* @param {(attrValue:string, selectorValue: string)=>boolean} test
* @returns {JSXElementMatcher}
*/
function buildJSXElementMatcher(selectorValue, test) {
const val = selector.insensitive ? selectorValue.toLowerCase() : selectorValue;
return (element) => {
const attr = getAttribute(element, key, context);
if (attr == null) return false;
if (attr.unknown || !attr.staticValue) return true;
const attrValue = attr.staticValue.value;
return test(selector.insensitive ? attrValue.toLowerCase() : attrValue, val);
};
}
}
/**
* Convert ClassName node to JSXElementMatcher
* @param {parser.ClassName} selector
* @returns {JSXElementMatcher}
*/
function classNameNodeToJSXElementMatcher(selector, context) {
const className = selector.value;
return (element) => {
const attr = getAttribute(element, "class", context);
if (attr == null) return false;
if (attr.unknown || !attr.staticValue) return true;
return attr.staticValue.value.split(/\s+/u).includes(className);
};
}
/**
* Convert Identifier node to JSXElementMatcher
* @param {parser.Identifier} selector
* @returns {JSXElementMatcher}
*/
function identifierNodeToJSXElementMatcher(selector, context) {
const id = selector.value;
return (element) => {
const attr = getAttribute(element, "id", context);
if (attr == null) return false;
if (attr.unknown || !attr.staticValue) return true;
return attr.staticValue.value === id;
};
}
/**
* Convert Tag node to JSXElementMatcher
* @param {parser.Tag} selector
* @returns {JSXElementMatcher}
*/
function tagNodeToJSXElementMatcher(selector) {
const name = selector.value;
return (element) => {
return getElementName(element.node) === name;
};
}
/**
* Convert Universal node to JSXElementMatcher
* @param {parser.Universal} _selector
* @returns {JSXElementMatcher}
*/
function universalNodeToJSXElementMatcher(_selector) {
return () => true;
}
/**
* Convert Pseudo node to JSXElementMatcher
* @param {parser.Pseudo} selector
* @returns {JSXElementMatcher}
*/
function pseudoNodeToJSXElementMatcher(selector, context) {
switch (selector.value) {
case ":is":
case ":where": return selectorsToJSXElementMatcher(selector.nodes, context);
case ":has": return pseudoHasSelectorsToJSXElementMatcher(selector.nodes, context);
case ":empty": return (element) => element.node.children.every((child) => child.type === "JSXText" && !child.value.trim() || child.type === "AstroHTMLComment");
case ":global": return () => true;
default: return () => true;
}
}
/**
* Convert :has() selector nodes to JSXElementMatcher
* @param {parser.Selector[]} selectorNodes
* @returns {JSXElementMatcher}
*/
function pseudoHasSelectorsToJSXElementMatcher(selectorNodes, context) {
const selectors = selectorNodes.map((n) => pseudoHasSelectorToJSXElementMatcher(n, context));
return (element, subject) => selectors.some((sel) => sel(element, subject));
}
/**
* Convert :has() selector node to JSXElementMatcher
* @param {parser.Selector} selector
* @returns {JSXElementMatcher}
*/
function pseudoHasSelectorToJSXElementMatcher(selector, context) {
const nodes = cleanSelectorChildren(selector);
const selectors = selectorToJSXElementMatcher(nodes, context);
const firstNode = nodes[0];
if (firstNode.type === "combinator" && (firstNode.value === "+" || firstNode.value === "~")) return buildJSXElementMatcher((element) => getAfterElements(element));
return buildJSXElementMatcher((element) => element.childElements);
/**
* @param {(element: JSXElementTreeNode) => JSXElementTreeNode[]} getStartElements
* @returns {JSXElementMatcher}
*/
function buildJSXElementMatcher(getStartElements) {
return (element) => {
const elements = [...getStartElements(element)];
let curr;
while (curr = elements.shift()) {
const el = curr;
if (selectors(el, element)) return true;
elements.push(...el.childElements);
}
return false;
};
}
}
/**
* @param {JSXElementTreeNode} element
*/
function getBeforeElement(element) {
return getBeforeElements(element).pop() || null;
}
/**
* @param {JSXElementTreeNode} element
*/
function getBeforeElements(element)