UNPKG

eslint-plugin-astro

Version:
1,565 lines 136 kB
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)