From ae6a1419fd08f44e7f91cb9d8021d10172bd3ac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20S=C3=B8derlind?= Date: Mon, 10 Aug 2026 14:04:23 +0200 Subject: [PATCH 1/2] browser-native: add Baseline status and article-driven replacements Adopt the Smashing Magazine 'ship less JavaScript' Baseline model: - add optional baseline field (widely/newly/limited) to the replacement DB, surfaced in table/markdown/json output alongside confidence - add packages: timeago.js, pluralize, numeral, accounting, humanize-duration, lodash.groupby, lodash.union/intersection/difference, and a UI Primitives cluster (a11y-dialog, focus-trap, body-scroll-lock, tippy.js) - document the three-question decision framework and progressive enhancement in SKILL.md; add a 'keep for now' Temporal note explaining why dayjs/date-fns are not flagged - bump skill version to 1.2.0 --- skills/browser-native/SKILL.md | 47 ++++- .../references/replacements-guide.md | 157 +++++++++++++++++ .../browser-native/scripts/formatters/json.js | 2 + .../scripts/formatters/markdown.js | 32 ++++ .../scripts/formatters/table.js | 18 +- skills/browser-native/scripts/replacements.js | 164 +++++++++++++++++- 6 files changed, 406 insertions(+), 14 deletions(-) diff --git a/skills/browser-native/SKILL.md b/skills/browser-native/SKILL.md index 72c8ee2..cdd94ee 100644 --- a/skills/browser-native/SKILL.md +++ b/skills/browser-native/SKILL.md @@ -1,8 +1,8 @@ --- name: browser-native -description: "Scan JavaScript dependencies for packages replaceable by browser/runtime native APIs. Use for dependency-modernization audits, or when another skill needs a native-replacement report with confidence and examples." +description: "Scan JavaScript dependencies for packages replaceable by browser/runtime native APIs. Use for dependency-modernization audits, or when another skill needs a native-replacement report with confidence, Baseline status, and examples." compatibility: "Node.js 18+. Filesystem-based — reads package.json. No network or browser access required." -version: "1.1.0" +version: "1.2.0" --- # Browser-Native Dependency Scanner @@ -73,7 +73,9 @@ Use this quick category map while reviewing: | --- | --- | --- | | HTTP | axios, node-fetch | `fetch()` | | URL / Query | query-string, qs | `URL`, `URLSearchParams` | -| Object / Array utils | lodash.* helpers | `structuredClone()`, `Object.*`, array methods | +| Object / Array utils | lodash.* helpers | `structuredClone()`, `Object.*`, `Object.groupBy()`, `Set` methods, array methods | +| Internationalization | numeral, pluralize, timeago.js, humanize-duration | `Intl.*` (`NumberFormat`, `PluralRules`, `RelativeTimeFormat`, `DurationFormat`) | +| UI primitives | tippy.js, focus-trap, body-scroll-lock, a11y-dialog | ``, Popover API, CSS anchor positioning | | UUID / Date | uuid, moment | `crypto.randomUUID()`, `Intl.*` | | Polyfills / APIs | abort-controller, resize-observer-polyfill | Native globals and browser APIs | @@ -94,6 +96,24 @@ For detailed reference on each replacement including before/after code and brows Completion criterion: Every recommendation in the user-facing output includes a confidence label and caveat handling for `partial` replacements. +### 3b) Interpret Baseline status + +Some replacements carry a `baseline` tag describing how safe the native API is to adopt across browsers ([webstatus.dev](https://webstatus.dev) / MDN Baseline badges): + +- **widely** — in all major engines for 30+ months. Adopt without much thought. +- **newly** — recently in all major engines. Works on up-to-date browsers, but check your audience or add a fallback before shipping to a broad audience. +- **limited** — not yet in all engines. Keep the library or polyfill for now. + +Confidence answers *"does the native API do what the library does?"*; Baseline answers *"can my users run it?"*. Treat them as independent — a `full` replacement can still be `newly` available (e.g. `Object.groupBy`), which means safe *functionally* but audience-dependent. + +Before recommending a swap, run the article's three questions: + +1. **Is it Baseline-safe for my audience?** `widely` is usually yes; for `newly`, check `browserslist`/analytics. +2. **What does the swap actually cost?** A heavier polyfill than the library it replaces is a net loss unless loaded conditionally (e.g. Temporal vs `dayjs`). +3. **Does the platform feature cover my real use case?** Libraries often do more (e.g. `axios` interceptors/retries). Check actual usage before assuming a drop-in. + +Completion criterion: `newly`/`limited` recommendations are gated on audience/cost, not presented as free wins. + ### 4) Present findings to the user When showing results: @@ -101,13 +121,24 @@ When showing results: 1. Lead with the summary count (e.g., "14 of 42 dependencies can be replaced") 2. Group by confidence: list full replacements first (easy wins), then partial 3. For each flagged package, show the before/after code snippet -4. Note any caveats from the `notes` field +4. Note any caveats from the `notes` field, and surface the `baseline` tag (`widely`/`newly`/`limited`) 5. If asked for a migration plan, prioritize: - Polyfills first (safest to remove — they just provide what's already built-in) - - Full confidence replacements next - - Partial replacements last (require careful review) + - `widely` + `full` confidence replacements next (easy wins) + - `newly` replacements behind an audience check or a feature-detect fallback + - `partial` replacements last (require careful usage review) + +For `newly`-available features, recommend shipping behind progressive enhancement rather than a hard swap: + +```js +if (typeof Intl.DurationFormat === "function") { + // use the native API +} else { + // keep the library, or a simpler fallback +} +``` -Completion criterion: Final response includes summary count, confidence-grouped findings, and explicit next migration priorities. +Completion criterion: Final response includes summary count, confidence- and Baseline-grouped findings, and explicit next migration priorities. ### 5) Monorepo support @@ -122,7 +153,7 @@ After presenting recommendations, the user can verify by: 1. Removing the flagged package from `package.json` 2. Replacing imports with the native API (using the "after" code example) 3. Running the project's test suite -4. Checking browser compatibility against their targets +4. Checking browser compatibility against their targets (compare the API's Baseline status on [webstatus.dev](https://webstatus.dev) against their `browserslist`) ## Failure modes diff --git a/skills/browser-native/references/replacements-guide.md b/skills/browser-native/references/replacements-guide.md index 22ecc43..d188d31 100644 --- a/skills/browser-native/references/replacements-guide.md +++ b/skills/browser-native/references/replacements-guide.md @@ -3,6 +3,28 @@ Quick-reference for every npm package in the scanner database, grouped by category. Each entry shows: the native API, minimum browser/Node.js version, confidence, caveats, and before/after code. +## Reading confidence vs. Baseline + +Two independent signals decide whether a swap is safe: + +- **Confidence** — does the native API *do what the library does*? `full` = drop-in; `partial` = covers common cases, check your usage. +- **Baseline** — can *your users run it*? ([webstatus.dev](https://webstatus.dev) / MDN badges) + - **🟢 widely** — in all major engines 30+ months. Adopt freely. + - **🟡 newly** — recently in all major engines. Check `browserslist`/analytics or ship behind a feature check + fallback. + - **🔴 limited** — not in all engines yet. Keep the library or polyfill. + +A `full` + `newly` API (e.g. `Object.groupBy`) is functionally a drop-in but still audience-dependent. Before any swap, ask: (1) Baseline-safe for my audience? (2) Is the polyfill/rewrite cost worth it? (3) Does the native feature cover how I actually use the library? + +For `newly` features, prefer progressive enhancement over a hard swap: + +```js +if (typeof Intl.DurationFormat === 'function') { + // native +} else { + // fallback / keep the library +} +``` + --- ## HTTP @@ -188,6 +210,32 @@ uniq([1, 2, 2, 3]); [0, 1, false, 2, '', 3].filter(Boolean); // [1, 2, 3] ``` +### lodash.groupby → `Object.groupBy()` / `Map.groupBy()` +- **Confidence:** full · **Baseline:** 🟡 newly (2024) +- **Min support:** Chrome 117, Firefox 119, Safari 17.4, Edge 117, Node 21 +- **Notes:** Check your audience or add a fallback. `Object.groupBy` returns a null-prototype object; use `Map.groupBy` for non-string keys. + +```js +// Before +const byCat = groupBy(products, p => p.category); + +// After +const byCat = Object.groupBy(products, p => p.category); +``` + +### lodash.union / lodash.intersection / lodash.difference → `Set` methods +- **Confidence:** partial · **Baseline:** 🟡 newly (2024) +- **Min support:** Chrome 122, Firefox 127, Safari 17, Edge 122, Node 22 +- **Notes:** Check your audience or add a fallback. Full set: `union`, `intersection`, `difference`, `symmetricDifference`, `isSubsetOf`, `isSupersetOf`, `isDisjointFrom`. Methods return `Set`s — spread back to an array if needed. + +```js +// Before +intersection([1, 2, 3], [2, 3, 4]); // [2, 3] + +// After +[...new Set([1, 2, 3]).intersection(new Set([2, 3, 4]))]; // [2, 3] +``` + ### array.prototype.flat, array.prototype.flatmap, array.from, array-from, array.prototype.find, array.prototype.findindex, array.prototype.at - **Confidence:** full - **Notes:** Polyfills — these methods are natively available. @@ -246,8 +294,117 @@ new Intl.DateTimeFormat('en-US', { }).format(new Date()); ``` +### timeago.js → `Intl.RelativeTimeFormat` +- **Confidence:** partial · **Baseline:** 🟢 widely +- **Notes:** Formats a value + unit; unlike timeago.js it does not pick the unit for you. Add a small helper that finds the largest unit that fits. + +```js +const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); +rtf.format(-1, 'day'); // 'yesterday' +rtf.format(3, 'hour'); // 'in 3 hours' +``` + +### humanize-duration → `Intl.DurationFormat` +- **Confidence:** partial · **Baseline:** 🟡 newly +- **Min support:** Chrome 129, Firefox 133, Safari 16.4, Node 22 +- **Notes:** Landed in all engines March 2025; on track for Widely in 2027. Fine for internal/modern-audience tools; for a broad audience check traffic or guard with a feature check. + +```js +const df = new Intl.DurationFormat('en', { style: 'long' }); +df.format({ hours: 1, minutes: 30 }); // '1 hour, 30 minutes' +``` + +--- + +## Internationalization + +### numeral, accounting → `Intl.NumberFormat` +- **Confidence:** partial · **Baseline:** 🟢 widely +- **Notes:** Covers thousands separators, currency, percent, and compact notation. Custom format strings map to option objects. + +```js +new Intl.NumberFormat('en-US').format(1234567.89); // '1,234,567.89' +new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(1234.5); // '$1,234.50' +new Intl.NumberFormat('en', { notation: 'compact' }).format(1200000); // '1.2M' +``` + +### pluralize → `Intl.PluralRules` +- **Confidence:** partial · **Baseline:** 🟢 widely +- **Notes:** Selects the plural category (`one`/`other`/…); it does not inflect the word. Map categories to your own word forms. + +```js +const pr = new Intl.PluralRules('en'); +const forms = { one: 'item', other: 'items' }; +forms[pr.select(3)]; // 'items' +``` + +### list-joining helpers → `Intl.ListFormat` +- **Confidence:** full · **Baseline:** 🟢 widely + +```js +const lf = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }); +lf.format(['Alice', 'Bob', 'Carol']); // 'Alice, Bob, and Carol' +``` + --- +## UI Primitives + +Platform features here are often *more* accessible than hand-rolled solutions. + +### a11y-dialog / modal libraries → `` element +- **Confidence:** partial · **Baseline:** 🟢 widely +- **Min support:** Chrome 37, Firefox 98, Safari 15.4, Edge 79 (browser-only) +- **Notes:** `showModal()` gives focus trapping, `Escape`-to-close, focus restore, top-layer render, and a `::backdrop`. + +```html +
+ + +
+``` + +```js +const dialog = document.querySelector('#confirm'); +dialog.showModal(); +dialog.addEventListener('close', () => console.log(dialog.returnValue)); +``` + +### focus-trap → modal `.showModal()` +- **Confidence:** partial · **Baseline:** 🟢 widely +- **Notes:** A modal `` makes the rest of the page inert and traps focus for you. + +### body-scroll-lock → CSS `:has(dialog:modal)` +- **Confidence:** partial · **Baseline:** 🟢 widely +- **Notes:** Use `:modal` (not `[open]`) so scroll only locks once the dialog is actually modal. + +```css +body:has(dialog:modal) { overflow: hidden; } +``` + +### tippy.js → Popover API + CSS anchor positioning +- **Confidence:** partial · **Baseline:** 🟡 newly +- **Notes:** Popover API (Newly, Jan 2025) gives light-dismiss + top layer with no JS; CSS anchor positioning (Newly, Jan 2026) replaces Popper. Keep a fallback for older browsers and advanced `position-try` features. + +```html + + +``` + +```css +#btn { anchor-name: --trigger; } +#menu { position-anchor: --trigger; position-area: top; margin: 0; } +``` + +--- + +## Keep for now (not Baseline yet) + +### Temporal — do **not** drop your date library yet +- **Baseline:** 🔴 limited (Safari has not shipped a stable release as of this writing) +- **Why wait:** the official polyfill (`@js-temporal/polyfill`, ~44 KB gz; a lighter one ~19 KB gz) is far heavier than a lean date library like `dayjs` (~3 KB gz). Swapping today *grows* your bundle unless you load the polyfill conditionally. +- **Revisit when:** Safari ships Temporal in a stable release and it reaches Baseline. Then use it natively and conditionally polyfill older browsers. This is why the scanner does not flag `dayjs`/`date-fns` for a Temporal swap. + ## Promises ### bluebird → `Promise` diff --git a/skills/browser-native/scripts/formatters/json.js b/skills/browser-native/scripts/formatters/json.js index 11ff36a..4391e8f 100644 --- a/skills/browser-native/scripts/formatters/json.js +++ b/skills/browser-native/scripts/formatters/json.js @@ -18,6 +18,7 @@ export function formatJson(results, totalDeps, packageJsons) { replaceableCount: results.length, fullReplacements: full, partialReplacements: results.length - full, + newlyBaseline: results.filter((r) => r.replacement.baseline === "newly").length, }, replaceable: results.map((r) => ({ package: r.name, @@ -26,6 +27,7 @@ export function formatJson(results, totalDeps, packageJsons) { category: r.replacement.category, browserApi: r.replacement.browserApi, confidence: r.replacement.confidence, + baseline: r.replacement.baseline ?? null, notes: r.replacement.notes ?? null, minBrowser: r.replacement.minBrowser, before: r.replacement.before, diff --git a/skills/browser-native/scripts/formatters/markdown.js b/skills/browser-native/scripts/formatters/markdown.js index 3a45c39..f2cefaf 100644 --- a/skills/browser-native/scripts/formatters/markdown.js +++ b/skills/browser-native/scripts/formatters/markdown.js @@ -32,6 +32,15 @@ export function formatMarkdown(results, totalDeps, packageJsons) { ); lines.push(""); + const newly = results.filter((r) => r.replacement.baseline === "newly").length; + if (newly > 0) { + lines.push( + `> **Baseline note:** ${newly} replacement(s) rely on *Baseline Newly available* features. ` + + `Confirm your audience (check \`browserslist\` / analytics) or guard them with a feature check and fallback before shipping.` + ); + lines.push(""); + } + // Group by category /** @type {Map} */ const byCategory = new Map(); @@ -55,6 +64,11 @@ export function formatMarkdown(results, totalDeps, packageJsons) { lines.push(`**${badge}** → **${r.replacement.browserApi}**`); lines.push(""); + if (r.replacement.baseline) { + lines.push(`**Baseline:** ${baselineLabel(r.replacement.baseline)}`); + lines.push(""); + } + if (r.replacement.notes) { lines.push(`> ${r.replacement.notes}`); lines.push(""); @@ -103,3 +117,21 @@ function fmtVer(ver) { if (ver === Infinity) return "N/A"; return String(ver) + "+"; } + +/** + * Human-readable label for a Baseline status. + * @param {"widely"|"newly"|"limited"} baseline + * @returns {string} + */ +function baselineLabel(baseline) { + switch (baseline) { + case "widely": + return "🟢 Widely available — safe to adopt."; + case "newly": + return "🟡 Newly available — check your audience (browserslist/analytics) or add a fallback."; + case "limited": + return "🔴 Limited availability — keep the library or polyfill for now."; + default: + return baseline; + } +} diff --git a/skills/browser-native/scripts/formatters/table.js b/skills/browser-native/scripts/formatters/table.js index 159d6f4..a603809 100644 --- a/skills/browser-native/scripts/formatters/table.js +++ b/skills/browser-native/scripts/formatters/table.js @@ -59,14 +59,14 @@ export function formatTable(results, totalDeps, packageJsons) { const colPkg = 28; const colCat = 20; const colApi = 32; - const colConf = 10; + const colConf = 20; // Table header const header = [ pad(`${BOLD}Package${RESET}`, colPkg + 8), // +8 for ANSI codes pad(`${BOLD}Category${RESET}`, colCat + 8), pad(`${BOLD}Replace with${RESET}`, colApi + 8), - pad(`${BOLD}Confidence${RESET}`, colConf + 8), + pad(`${BOLD}Confidence · Baseline${RESET}`, colConf + 8), ].join(" "); lines.push(header); @@ -85,6 +85,10 @@ export function formatTable(results, totalDeps, packageJsons) { const confColor = r.replacement.confidence === "full" ? GREEN : YELLOW; const confLabel = r.replacement.confidence === "full" ? "✓ full" : "◐ partial"; + const baseline = r.replacement.baseline; + const baseColor = + baseline === "widely" ? GREEN : baseline === "newly" ? YELLOW : RED; + const baseTag = baseline ? ` ${DIM}·${RESET} ${baseColor}${baseline}${RESET}` : ""; const typeLabel = r.type === "devDep" ? `${DIM}(dev)${RESET}` : ""; const row = [ @@ -94,7 +98,7 @@ export function formatTable(results, totalDeps, packageJsons) { `${CYAN}${truncate(r.replacement.browserApi, colApi)}${RESET}`, colApi + 8 ), - `${confColor}${confLabel}${RESET}`, + `${confColor}${confLabel}${RESET}${baseTag}`, ].join(" "); lines.push(row); @@ -109,11 +113,19 @@ export function formatTable(results, totalDeps, packageJsons) { // Summary const full = results.filter((r) => r.replacement.confidence === "full").length; const partial = results.length - full; + const newly = results.filter((r) => r.replacement.baseline === "newly").length; lines.push(""); lines.push( `${BOLD}Found ${results.length}${RESET} replaceable dep(s) of ${totalDeps} total ` + `(${GREEN}${full} full${RESET}, ${YELLOW}${partial} partial${RESET})` ); + if (newly > 0) { + lines.push( + `${DIM}Baseline:${RESET} ${GREEN}widely${RESET} = safe to adopt · ` + + `${YELLOW}newly${RESET} = check your audience or add a fallback ` + + `(${YELLOW}${newly}${RESET} flagged).` + ); + } lines.push( `${DIM}Run with --md for detailed before/after code examples.${RESET}` ); diff --git a/skills/browser-native/scripts/replacements.js b/skills/browser-native/scripts/replacements.js index 4cac719..459fb04 100644 --- a/skills/browser-native/scripts/replacements.js +++ b/skills/browser-native/scripts/replacements.js @@ -6,12 +6,18 @@ * browserApi – name of the native API replacement * minBrowser – minimum browser/Node.js versions required * confidence – "full" (drop-in safe) or "partial" (covers most cases) + * baseline – optional Baseline status of the native API: + * "widely" (in all engines 30+ months — safe to adopt), + * "newly" (in all engines recently — check your audience or add a fallback), + * "limited" (not yet in all engines — keep the library or polyfill) * notes – optional caveats * before – code example using the npm package * after – equivalent code using the native API + * + * Baseline reference: https://webstatus.dev / MDN Baseline badges. */ -/** @typedef {{ category: string, browserApi: string, minBrowser: { chrome: number, firefox: number, safari: number, edge: number, node: number }, confidence: "full"|"partial", notes?: string, before: string, after: string }} Replacement */ +/** @typedef {{ category: string, browserApi: string, minBrowser: { chrome: number, firefox: number, safari: number, edge: number, node: number }, confidence: "full"|"partial", baseline?: "widely"|"newly"|"limited", notes?: string, before: string, after: string }} Replacement */ /** @type {Record} */ const replacements = { @@ -176,7 +182,8 @@ const replacements = { browserApi: "structuredClone()", minBrowser: { chrome: 98, firefox: 94, safari: 15.4, edge: 98, node: 17 }, confidence: "full", - notes: "structuredClone does not clone functions or DOM nodes. Handles circular refs.", + baseline: "widely", + notes: "structuredClone does not clone functions, DOM nodes, or class instances (drops the prototype). Handles Date, Map, Set, ArrayBuffer, and circular refs.", before: `const cloneDeep = require('lodash.clonedeep');\nconst copy = cloneDeep(original);`, after: `const copy = structuredClone(original);` }, @@ -186,6 +193,7 @@ const replacements = { browserApi: "structuredClone()", minBrowser: { chrome: 98, firefox: 94, safari: 15.4, edge: 98, node: 17 }, confidence: "full", + baseline: "widely", before: `const cloneDeep = require('clone-deep');\nconst copy = cloneDeep(obj);`, after: `const copy = structuredClone(obj);` }, @@ -195,6 +203,7 @@ const replacements = { browserApi: "structuredClone()", minBrowser: { chrome: 98, firefox: 94, safari: 15.4, edge: 98, node: 17 }, confidence: "full", + baseline: "widely", notes: "rfdc is faster for hot paths; structuredClone is built-in and handles circular refs.", before: `const clone = require('rfdc')();\nconst copy = clone(obj);`, after: `const copy = structuredClone(obj);` @@ -480,6 +489,50 @@ const replacements = { after: `[0, 1, false, 2, '', 3].filter(Boolean); // [1, 2, 3]` }, + "lodash.groupby": { + category: "Array Utilities", + browserApi: "Object.groupBy()", + minBrowser: { chrome: 117, firefox: 119, safari: 17.4, edge: 117, node: 21 }, + confidence: "full", + baseline: "newly", + notes: "Baseline Newly available (2024) — check your audience or add a fallback. Use Map.groupBy() when keys are not strings. Object.groupBy returns a null-prototype object.", + before: `const groupBy = require('lodash.groupby');\nconst byCat = groupBy(products, p => p.category);`, + after: `const byCat = Object.groupBy(products, p => p.category);\n// or, for non-string keys:\nconst byCat2 = Map.groupBy(products, p => p.category);` + }, + + "lodash.union": { + category: "Array Utilities", + browserApi: "Set.prototype.union()", + minBrowser: { chrome: 122, firefox: 127, safari: 17, edge: 122, node: 22 }, + confidence: "partial", + baseline: "newly", + notes: "Baseline Newly available (2024) — check your audience or add a fallback. Set methods dedupe and return Sets; spread back to an array if you need one.", + before: `const union = require('lodash.union');\nunion([1, 2], [2, 3]); // [1, 2, 3]`, + after: `[...new Set([1, 2]).union(new Set([2, 3]))]; // [1, 2, 3]` + }, + + "lodash.intersection": { + category: "Array Utilities", + browserApi: "Set.prototype.intersection()", + minBrowser: { chrome: 122, firefox: 127, safari: 17, edge: 122, node: 22 }, + confidence: "partial", + baseline: "newly", + notes: "Baseline Newly available (2024) — check your audience or add a fallback. Also available: symmetricDifference, isSubsetOf, isSupersetOf, isDisjointFrom.", + before: `const intersection = require('lodash.intersection');\nintersection([1, 2, 3], [2, 3, 4]); // [2, 3]`, + after: `[...new Set([1, 2, 3]).intersection(new Set([2, 3, 4]))]; // [2, 3]` + }, + + "lodash.difference": { + category: "Array Utilities", + browserApi: "Set.prototype.difference()", + minBrowser: { chrome: 122, firefox: 127, safari: 17, edge: 122, node: 22 }, + confidence: "partial", + baseline: "newly", + notes: "Baseline Newly available (2024) — check your audience or add a fallback.", + before: `const difference = require('lodash.difference');\ndifference([1, 2, 3], [2, 3]); // [1]`, + after: `[...new Set([1, 2, 3]).difference(new Set([2, 3]))]; // [1]` + }, + "array.prototype.flat": { category: "Array Utilities", browserApi: "Array.prototype.flat()", @@ -595,7 +648,8 @@ const replacements = { browserApi: "Intl.DateTimeFormat / Date", minBrowser: { chrome: 24, firefox: 29, safari: 10, edge: 12, node: 13 }, confidence: "partial", - notes: "Intl replaces formatting/localization. Complex date math (add/subtract/diff) still needs a library like date-fns or Temporal (stage 3).", + baseline: "widely", + notes: "Intl replaces formatting/localization. Complex date math (add/subtract/diff) still needs a library like date-fns or Temporal (limited availability — see notes below).", before: `const moment = require('moment');\nmoment().format('MMMM Do YYYY, h:mm a');\nmoment().fromNow();`, after: `new Intl.DateTimeFormat('en-US', {\n year: 'numeric', month: 'long',\n day: 'numeric', hour: 'numeric', minute: 'numeric'\n}).format(new Date());\n\n// Relative time\nnew Intl.RelativeTimeFormat('en', { numeric: 'auto' }).format(-3, 'day');` }, @@ -605,11 +659,69 @@ const replacements = { browserApi: "Intl.DateTimeFormat (timeZone option)", minBrowser: { chrome: 24, firefox: 52, safari: 14.1, edge: 14, node: 13 }, confidence: "partial", + baseline: "widely", notes: "Intl supports IANA timezones natively. Complex timezone math may still need a library.", before: `const moment = require('moment-timezone');\nmoment().tz('America/New_York').format('h:mm a z');`, after: `new Intl.DateTimeFormat('en-US', {\n timeZone: 'America/New_York',\n hour: 'numeric', minute: 'numeric', timeZoneName: 'short'\n}).format(new Date());` }, + "timeago.js": { + category: "Date / Time", + browserApi: "Intl.RelativeTimeFormat", + minBrowser: { chrome: 71, firefox: 65, safari: 14, edge: 79, node: 12 }, + confidence: "partial", + baseline: "widely", + notes: "Intl.RelativeTimeFormat formats a value + unit; unlike timeago.js it does not pick the unit for you. Add a small helper that finds the largest unit that fits.", + before: `import { format } from 'timeago.js';\nformat(Date.now() - 3600 * 1000); // '1 hour ago'`, + after: `const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });\nrtf.format(-1, 'hour'); // '1 hour ago'\nrtf.format(-1, 'day'); // 'yesterday'` + }, + + "humanize-duration": { + category: "Date / Time", + browserApi: "Intl.DurationFormat", + minBrowser: { chrome: 129, firefox: 133, safari: 16.4, edge: 129, node: 22 }, + confidence: "partial", + baseline: "newly", + notes: "Baseline Newly available (landed in all engines March 2025; on track for Widely in 2027). Fine for internal/modern-audience tools; for a broad audience check traffic or guard with a feature check.", + before: `import humanizeDuration from 'humanize-duration';\nhumanizeDuration(5400000); // '1 hour, 30 minutes'`, + after: `const df = new Intl.DurationFormat('en', { style: 'long' });\ndf.format({ hours: 1, minutes: 30 }); // '1 hour, 30 minutes'` + }, + + // ───────────────────────── Internationalization ───────────────────────── + + "pluralize": { + category: "Internationalization", + browserApi: "Intl.PluralRules", + minBrowser: { chrome: 63, firefox: 58, safari: 13, edge: 18, node: 10 }, + confidence: "partial", + baseline: "widely", + notes: "Intl.PluralRules selects the plural category (one/other/…); it does not inflect the word itself. Map categories to your own word forms.", + before: `const pluralize = require('pluralize');\npluralize('item', 3); // 'items'`, + after: `const pr = new Intl.PluralRules('en');\nconst forms = { one: 'item', other: 'items' };\nconst word = forms[pr.select(3)]; // 'items'` + }, + + "numeral": { + category: "Internationalization", + browserApi: "Intl.NumberFormat", + minBrowser: { chrome: 24, firefox: 29, safari: 10, edge: 12, node: 0.12 }, + confidence: "partial", + baseline: "widely", + notes: "Intl.NumberFormat covers thousands separators, currency, percent, and compact notation. Custom format strings map to option objects instead.", + before: `const numeral = require('numeral');\nnumeral(1234567.89).format('0,0.00'); // '1,234,567.89'\nnumeral(1200000).format('0.0a'); // '1.2m'`, + after: `new Intl.NumberFormat('en-US').format(1234567.89); // '1,234,567.89'\nnew Intl.NumberFormat('en', { notation: 'compact' }).format(1200000); // '1.2M'` + }, + + "accounting": { + category: "Internationalization", + browserApi: "Intl.NumberFormat", + minBrowser: { chrome: 24, firefox: 29, safari: 10, edge: 12, node: 0.12 }, + confidence: "partial", + baseline: "widely", + notes: "Use the currency style. Custom symbol/precision options replace accounting.js format strings.", + before: `const accounting = require('accounting');\naccounting.formatMoney(1234.5); // '$1,234.50'`, + after: `new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(1234.5); // '$1,234.50'` + }, + // ───────────────────────── Promises ───────────────────────── "bluebird": { @@ -925,6 +1037,52 @@ const replacements = { after: `globalThis.myGlobal = 42;` }, + // ───────────────────────── UI Primitives ───────────────────────── + + "a11y-dialog": { + category: "UI Primitives", + browserApi: " element", + minBrowser: { chrome: 37, firefox: 98, safari: 15.4, edge: 79, node: Infinity }, + confidence: "partial", + baseline: "widely", + notes: "Browser-only. showModal() gives focus trapping, Escape-to-close, focus restore, top-layer render, and a ::backdrop. Migrate ARIA wiring to the native element.", + before: `import A11yDialog from 'a11y-dialog';\nconst dialog = new A11yDialog(el);\ndialog.show();`, + after: `
\n \n \n
\n\nconst dialog = document.querySelector('#confirm');\ndialog.showModal();\ndialog.addEventListener('close', () => console.log(dialog.returnValue));` + }, + + "focus-trap": { + category: "UI Primitives", + browserApi: ".showModal()", + minBrowser: { chrome: 37, firefox: 98, safari: 15.4, edge: 79, node: Infinity }, + confidence: "partial", + baseline: "widely", + notes: "Browser-only. A modal traps focus for you. Keep focus-trap only if you need trapping outside a .", + before: `import { createFocusTrap } from 'focus-trap';\nconst trap = createFocusTrap(el);\ntrap.activate();`, + after: `// A modal makes the rest of the page inert automatically\ndocument.querySelector('#modal').showModal();` + }, + + "body-scroll-lock": { + category: "UI Primitives", + browserApi: "CSS :has(dialog:modal)", + minBrowser: { chrome: 105, firefox: 121, safari: 15.4, edge: 105, node: Infinity }, + confidence: "partial", + baseline: "widely", + notes: "Browser-only. Lock background scroll with one CSS rule while a modal dialog is open.", + before: `import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';\ndisableBodyScroll(el);`, + after: `/* CSS — locks scroll only while a dialog is actually modal */\nbody:has(dialog:modal) {\n overflow: hidden;\n}` + }, + + "tippy.js": { + category: "UI Primitives", + browserApi: "Popover API + CSS anchor positioning", + minBrowser: { chrome: 125, firefox: 147, safari: 26, edge: 125, node: Infinity }, + confidence: "partial", + baseline: "newly", + notes: "Browser-only. Popover API (Baseline Newly, Jan 2025) gives light-dismiss + top layer with no JS; CSS anchor positioning (Baseline Newly, Jan 2026) replaces Popper. Check your audience and keep a fallback for advanced position-try features.", + before: `import tippy from 'tippy.js';\ntippy('#btn', { content: 'Hello' });`, + after: `\n
Hello
\n\n/* CSS — pin the popover to its trigger */\n#btn { anchor-name: --trigger; }\n#menu { position-anchor: --trigger; position-area: top; margin: 0; }` + }, + // ───────────────────────── Observers ───────────────────────── "intersection-observer": { From 4ebc80b8689ee701001e3e8bc80a3d31284d5bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20S=C3=B8derlind?= Date: Mon, 10 Aug 2026 14:07:29 +0200 Subject: [PATCH 2/2] docs: add repo CHANGELOG; relax MD024 for changelog headings --- .markdownlint.json | 1 + CHANGELOG.md | 130 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 CHANGELOG.md diff --git a/.markdownlint.json b/.markdownlint.json index 1a7db5f..63f1b90 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -1,6 +1,7 @@ { "MD013": false, "MD022": false, + "MD024": { "siblings_only": true }, "MD032": false, "MD033": false, "MD060": false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6cd4798 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,130 @@ +# Changelog + +All notable changes to this skills repository are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +The repository is not versioned as a whole; individual skills carry their own +version in their frontmatter, so entries below are grouped by date. + +## [Unreleased] + +### Added + +- browser-native: Baseline status (`widely` / `newly` / `limited`) on replacement + entries, surfaced alongside confidence in the table, markdown, and JSON output. +- browser-native: new replaceable packages — `timeago.js`, `pluralize`, `numeral`, + `accounting`, `humanize-duration`, `lodash.groupby`, + `lodash.union`/`intersection`/`difference`, and a UI Primitives cluster + (`a11y-dialog`, `focus-trap`, `body-scroll-lock`, `tippy.js`). +- browser-native: three-question decision framework and progressive-enhancement + guidance in SKILL.md, plus a "keep for now" Temporal note explaining why + `dayjs`/`date-fns` are not flagged. + +### Changed + +- browser-native: skill version bumped to 1.2.0. + +## [2026-08-08] + +### Added + +- CI: discovery index is built and committed by CI as the source of truth. + +### Changed + +- prepare-wordpress: installs agent skills from WordPress/agent-skills; default + install trimmed to essentials with block/performance/router skills optional + (plugin 1.2.0). +- prepare-wordpress: Brain Monkey PHP tests, `phpcs.xml`, and ESLint scaffolding; + Composer/npm hardening (plugin 1.1.0). +- CI: bumped checkout/setup-node to v7 (Node 24), clearing the Node 20 deprecation. + +### Fixed + +- CI: gzip non-reproducibility in the discovery index build. + +## [2026-08-07] + +### Added + +- wordpress-skills Agent Plugin with a mirror workflow, bundled CHANGELOG, and + README describing the bundled skills. + +## [2026-08-03] + +### Added + +- Published the agent-skills discovery index with an inline determinism checklist. + +### Changed + +- README: grouped Available Skills by category, reworked tables, and rewrote the + install and introduction sections. + +### Fixed + +- Portable script paths and consistent skill frontmatter. + +## [2026-08-02] + +### Added + +- wp-mutate: WordPress mutation-testing skill. + +## [2026-07-27] + +### Added + +- document-architecture and pre-launch-security-audit skills. + +## [2026-07-16] + +### Added + +- Cross-link to the just-bash-runner skill. + +## [2026-07-15] + +### Added + +- wp-pcp-local: Plugin Check skill for Local by Flywheel. + +### Fixed + +- wp-pcp-local: multisite handling and argument parsing. + +## [2026-06-30] + +### Fixed + +- wp-bump: detect tests from `package.json` and `composer.json` scripts. + +## [2026-06-26] + +### Added + +- add-apim-api skill with Bicep patterns reference. +- `skills.sh.json` for repo page customization and a skills.sh README badge. +- Version 1.1.0 frontmatter across all skills. + +### Security + +- Fixed a prompt-injection vulnerability in prepare-wordpress. +- Removed the third-party `jeffallan/claude-skills` skill to reduce risk. + +## [2026-06-20] + +### Added + +- browser-native skill set. + +### Changed + +- Hardened skill scripts, improved planner detection, and tightened invocation + semantics with determinism guardrails. + +## [2026-05-10] + +### Added + +- Initial skills repository with skills.sh page links and CLI discoverability.