@web-features/tech
v0.0.1
Published
Detect the general technology a web-feature belongs to
Readme
@web-features/tech
Detect the general technology a web-feature belongs to.
Install
npm install @web-features/techUsage
import tech from "@web-features/tech";
tech("grid"); // Set { "css" }
tech("array-at"); // Set { "javascript" }
tech("import-maps"); // Set { "html", "javascript" }
tech("ad-selection"); // Set {} (real feature, no tech detected)
tech("not-a-feature"); // null (no such feature)tech(webFeatureId) takes a web-features ID and returns a Set of technology identifiers (spread it into an array with [...tech(id)] if you need one). The vocabulary is open: an identifier defaults to the web-features prefix or group name it came from — "css", "html", "javascript", plus area names like "webgl" or "webxr" — and rules only step in to fix names that are wrong or too coarse. Coarsen the output as you see fit with a remap map. It returns an empty Set for a real feature with no detected technology, and null only when the ID is unknown. For a shipped feature, any non-empty result includes at least one core tech (css, html, or javascript), with area identifiers like images alongside. Unshipped or retracted proposals often have no BCD data yet, so they may resolve to just a group area (install → Set { "progressive-web-app" }) or an empty Set — the library doesn't fabricate a classification for them. A moved/split redirect resolves to the union of its target features' technologies.
Technologies come from each feature's BCD compat_features, a few manual overrides, and its group ancestry — each feature inherits the technology of its group's root (e.g. a feature under the css group → "css", one under scrolling → "scrolling"), which is how features with no compat_features of their own (e.g. color-contrast → Set { "css" }) get classified. Group roots often name an area rather than a core tech, though, so whenever the signals so far leave a feature without a core tech, its spec hostname supplies one: image-function sits in the images area but its drafts.csswg.org spec makes it Set { "css", "images" }, and text-fit — with no other signal at all — becomes Set { "css" }. Only the feature itself is consulted, never its siblings, so results stay specific. The default rule sets live in src/defaults.js.
Output vocabulary
With the current web-features data and default rules, tech() resolves to one or more of 53 technology identifiers. The vocabulary is open (see Usage), so this list tracks the data — new features or groups can extend it.
Three core identifiers anchor the vocabulary — every shipped feature that resolves to anything includes at least one of them:
css,html,javascript
The rest name a specific technology area. Most are inherited from a feature's web-features group root (a feature under the scrolling group → "scrolling"); a handful — http, manifests, mathml, mediatypes, svg — come instead (or additionally) from a BCD prefix:
animation, canvas, clipboard, clipping-shapes-masking, compute-pressure, cookies, credential-management, dom, fetch, file-system, gamepad, geolocation, http, images, integrity, manifests, mathml, mediatypes, messaging, parsing-and-serialization, payments, performance, print, progressive-web-app, reading-order, reporting, resource-hints, ruby, scrolling, security, selection, sensors, speech, storage, streams, svg, text-fragments, view-transitions, web-audio, web-components, webassembly, webdriver, webgl, webgpu, webmcp, webrtc, webxr, workers, worklets, xml
webassembly and webdriver always appear alongside javascript, since both are surfaced to the web through it; a consumer who'd rather see a single coarse tech can fold them back with remap. The spec-hostname fallback only ever draws from techs already in this list (core techs, plus svg), so it adds nothing new.
Custom rules
Both tech() and inferFromBCD() accept custom rule sets, defaulting to the bundled ones — exported as defaults so you can build on them. Each option replaces its default wholesale (not merged), so spread the matching defaults.* in to extend rather than start from scratch — otherwise you'd drop rules like api → javascript that classify most JS features:
import tech, {
inferFromBCD, // (compatFeatures, { add, replace }?) => Set<Tech>
inferFromSpec, // (specs, { specHosts }?) => Set<Tech>
defaults, // the bundled rule sets: { add, replace, remap, overrides, specHosts }
} from "@web-features/tech";
// tech(webFeatureId, { add?, replace?, remap?, overrides?, specHosts? })
tech("some-feature", { overrides: { ...defaults.overrides, "some-feature": ["javascript"] } });
// Collapse the granular webassembly/webdriver techs into javascript:
tech("wasm-garbage-collection", {
remap: { webassembly: "javascript", webdriver: "javascript" },
});
// By default webgl.* → ["webgl"]; add a replace rule to reclassify it — spread
// `...defaults.replace` so the bundled rules (e.g. `api` → javascript) still apply:
inferFromBCD(["webgl.foo"], { replace: { ...defaults.replace, webgl: "javascript" } }); // Set { "javascript" }
// defaults.specHosts is a plain object keyed by hostname — extend it in place, or pass your own.
defaults.specHosts["tabatkins.github.io"] = ["css"];
inferFromSpec("https://tabatkins.github.io/specs/css-toggle-states/"); // Set { "css" }Custom add/replace passed to tech() govern both BCD inference and group-ancestry resolution — a group inherits its root's technology through the same rules (the css group → "css"). remap then rewrites the final output (e.g. webassembly → "javascript"), so you can coarsen granularity without touching inference. The specHosts map only applies as a fallback, when nothing else classifies a feature — see below.
An overrides entry is terminal: when a feature's exact ID is in the map, that value wins outright and BCD/group/spec inference is skipped entirely (remap still applies). This holds for moved/split redirect IDs too — an override on the alias replaces, rather than merges with, its target's techs.
Rule format
Two rule sets shape BCD inference, both keyed by prefix (the segment before the first .):
replacesets a prefix's base technology — only for prefixes whose own name isn't a tech ({ api: "javascript" }). A prefix with noreplacerule is its own technology:css.*→"css"needs no rule.addlayers extra techs on top of that base, for matching suffixes. Most refinements are additive:{ html: { "elements.script.type.": "javascript" } }keeps thehtmlbase and addsjavascript.
Both share the same rule grammar for the part of the key after the prefix. A rule is one of:
"css"— a tech, assigned wherever the rule is reached["css", "javascript"]— a list of rules{ test: /…/, tech }— a regex tested against the remaining suffix{ CSS: …, "*": … }— a nested map: each key is a literal prefix matched withstartsWithand extended as you nest deeper;"*"(the wildcard) matches at the current level
So replace: { api: "javascript" } with add: { api: { CSS: "css" } } makes every api.* key javascript (since api is not itself a tech), and any api.CSS… key (e.g. CSSStyleSheet) additionally css.
The "*" prefix is itself a wildcard: rules under it apply to every prefix, matched against the suffix. That's how markup segments are tagged in one place — add: { "*": { "elements.": "html", "global_attributes.": "html" } } makes html.elements.*, svg.elements.*, and mathml.elements.* all html, so each vocabulary only needs to name what's specific to it (e.g. svg/mathml presentation attributes → css).
Coarsening the output with remap
remap is not an inference rule like add/replace: it's a flat Tech → Tech map applied once to the final output, letting consumers fold fine-grained techs into coarser ones without touching how features are classified. Each returned tech is swapped for its mapping (if any); since the output is a Set, techs that collapse onto the same target automatically dedupe. Mapping a tech to a falsy value (e.g. null) removes it from the output entirely.
It's empty by default — the bundled rules keep every tech distinct, so webassembly, webdriver, svg, and the rest each come through on their own. Supply a map to collapse the granularity you don't need:
tech("wasm"); // Set { "webassembly", "javascript" }
tech("wasm", { remap: { webassembly: "javascript" } }); // Set { "javascript" }Only the keys you name are affected; every other tech passes through unchanged. Targets are just identifiers, so you can also route several techs into a coarse bucket of your own — { webgl: "graphics", webgpu: "graphics", canvas: "graphics" }.
Notes
Unshipped and unclassifiable features
A shipped feature always resolves to at least one core tech (see Usage). Unshipped or retracted proposals often have no BCD yet, so they fall back to whatever their group and spec provide — sometimes a bare area (install → Set { "progressive-web-app" }), sometimes nothing.
Features with no compat_features, no override, no group, and no recognized spec host carry no signal at all and return an empty Set. With the current web-features data these are:
ad-selectionportalusermedia
Shipped features that inference misses can be classified by adding an overrides entry; unshipped proposals are intentionally left alone.
