diff --git a/package-lock.json b/package-lock.json index 11fdab0..6fd4926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "devDependencies": { "@codecov/vite-plugin": "^0.0.1-beta.8", "@codspeed/tinybench-plugin": "^3.1.0", + "@types/css-tree": "^2.3.8", "c8": "^9.1.0", "tinybench": "^2.8.0", "uvu": "^0.5.6", @@ -22,7 +23,7 @@ "vite-plugin-dts": "^3.9.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18.0.0" } }, "node_modules/@babel/parser": { @@ -1088,6 +1089,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/css-tree": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.8.tgz", + "integrity": "sha512-zABG3nI2UENsx7AQv63tI5/ptoAG/7kQR1H0OvG+WTWYHOR5pfAT3cGgC8SdyCrgX/TTxJBZNmx82IjCXs1juQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -3730,6 +3738,12 @@ "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", "dev": true }, + "@types/css-tree": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/css-tree/-/css-tree-2.3.8.tgz", + "integrity": "sha512-zABG3nI2UENsx7AQv63tI5/ptoAG/7kQR1H0OvG+WTWYHOR5pfAT3cGgC8SdyCrgX/TTxJBZNmx82IjCXs1juQ==", + "dev": true + }, "@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", diff --git a/package.json b/package.json index 147dc6a..35985d0 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "devDependencies": { "@codecov/vite-plugin": "^0.0.1-beta.8", "@codspeed/tinybench-plugin": "^3.1.0", + "@types/css-tree": "^2.3.8", "c8": "^9.1.0", "tinybench": "^2.8.0", "uvu": "^0.5.6", @@ -64,4 +65,4 @@ "mangle": { "regex": "^_[^_]" } -} \ No newline at end of file +} diff --git a/src/collection.js b/src/collection.js index f787443..0c92d8c 100644 --- a/src/collection.js +++ b/src/collection.js @@ -15,13 +15,13 @@ export class Collection { } /** - * @param {string} item - * @param {import('css-tree').CssLocation} node_location + * @param {string | number} item + * @param {import('css-tree').CssLocation | undefined} node_location */ p(item, node_location) { let index = this._total - if (this._useLocations) { + if (this._useLocations && node_location) { let start = node_location.start let start_offset = start.offset let position = index * 4 diff --git a/src/index.js b/src/index.js index 251f519..0edc162 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,8 @@ +// @ts-expect-error No types for sub-export `parse` and `walk` import parse from 'css-tree/parser' +// @ts-expect-error No types for sub-export `parse` and `walk` import walk from 'css-tree/walker' +// @ts-expect-error No types for sub-export `core` import { calculate } from '@bramus/specificity/core' import { isSupportsBrowserhack, isMediaBrowserhack } from './atrules/atrules.js' import { getCombinators, getComplexity, isAccessibility, isPrefixed } from './selectors/utils.js' @@ -46,6 +49,12 @@ let border_radius_properties = new KeywordSet([ 'border-end-start-radius', ]) +/** + * Safe ratio calculation to avoid division by zero errors + * @param {number} part + * @param {number} total + * @returns {number} + */ function ratio(part, total) { if (total === 0) return 0 return part / total @@ -79,7 +88,9 @@ export function analyze(css, options = {}) { return stringifyNodePlain(node).trim() } + /** @param {import('css-tree').CssNode} node */ function stringifyNodePlain(node) { + /** @type {import('css-tree').CssNode as NonNullable} */ let loc = node.loc return css.substring(loc.start.offset, loc.end.offset) } @@ -192,7 +203,7 @@ export function analyze(css, options = {}) { let valueKeywords = new Collection(useLocations) let borderRadiuses = new ContextCollection(useLocations) - walk(ast, function (node) { + walk(ast, function (/** @type {import('css-tree').CssNode} */node) { switch (node.type) { case Atrule: { totalAtRules++ @@ -203,10 +214,10 @@ export function analyze(css, options = {}) { let descriptors = {} if (useLocations) { - fontfaces_with_loc.p(node.loc.start.offset, node.loc) + fontfaces_with_loc.p(node.loc?.start.offset, node.loc) } - node.block.children.forEach(descriptor => { + node.block?.children.forEach(descriptor => { // Ignore 'Raw' nodes in case of CSS syntax errors if (descriptor.type === Declaration) { descriptors[descriptor.property] = stringifyNode(descriptor.value) @@ -228,7 +239,7 @@ export function analyze(css, options = {}) { if (atRuleName === 'media') { medias.p(preludeStr, loc) - if (isMediaBrowserhack(prelude)) { + if (prelude.type === 'AtrulePrelude' && isMediaBrowserhack(prelude)) { mediaBrowserhacks.p(preludeStr, loc) complexity++ } @@ -236,7 +247,7 @@ export function analyze(css, options = {}) { supports.p(preludeStr, loc) // TODO: analyze vendor prefixes in @supports // TODO: analyze complexity of @supports 'declaration' - if (isSupportsBrowserhack(prelude)) { + if (prelude.type === 'AtrulePrelude' && isSupportsBrowserhack(prelude)) { supportsBrowserhacks.p(preludeStr, loc) complexity++ } @@ -276,7 +287,7 @@ export function analyze(css, options = {}) { case Rule: { let prelude = node.prelude let block = node.block - let preludeChildren = prelude.children + let preludeChildren = prelude.type === 'SelectorList' ? prelude.children : new Set() let blockChildren = block.children let numSelectors = preludeChildren ? preludeChildren.size : 0 let numDeclarations = blockChildren ? blockChildren.size : 0 @@ -355,7 +366,7 @@ export function analyze(css, options = {}) { ids.p(selector, node.loc) } - getCombinators(node, function onCombinator(combinator) { + getCombinators(node, function onCombinator(/** @type {import('css-tree').Combinator} */ combinator) { combinators.p(combinator.name, combinator.loc) }) @@ -546,7 +557,7 @@ export function analyze(css, options = {}) { // no break here: potentially contains colors } - walk(node, function (valueNode) { + walk(node, function (/** @type {import('css-tree').CssNode} */ valueNode) { let nodeName = valueNode.name switch (valueNode.type) {