From db1b48dc174d4b2be823f71335f1efe188241a23 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 10 Apr 2026 18:40:45 +0100 Subject: [PATCH 1/4] fix: validate "simple" replacement id starts with `snippet::` --- scripts/validate-manifests.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/validate-manifests.js b/scripts/validate-manifests.js index 9795f7c..ff3cd91 100644 --- a/scripts/validate-manifests.js +++ b/scripts/validate-manifests.js @@ -53,6 +53,12 @@ export async function validateManifests() { `${manifestPath}: replacement "${id}" has compatKey "${compatKey}" not found in web-features feature "${featureId}"` ); } + + if (replacement.type === 'simple' && !id.startsWith('snippet::')) { + throw new Error( + `${manifestPath}: replacement "${id}" is type "simple" and must start with the "snippet::" prefix.` + ); + } } const usedReplacementIds = new Set(); From 90e72697baf54af66bb6d2539ec0c1ea0684f412 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 10 Apr 2026 18:45:47 +0100 Subject: [PATCH 2/4] fix --- manifests/micro-utilities.json | 12 ++++++------ scripts/validate-manifests.js | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/manifests/micro-utilities.json b/manifests/micro-utilities.json index 7efdf48..7994f2d 100644 --- a/manifests/micro-utilities.json +++ b/manifests/micro-utilities.json @@ -162,6 +162,12 @@ "description": "If the current environment is npm the `npm_config_user_agent` environment variable will be set and start with `\"npm\"`.", "example": "const isNpm = process.env.npm_config_user_agent?.startsWith(\"npm\")" }, + "snippet::is-number": { + "id": "snippet::is-number", + "type": "simple", + "description": "You can check if a value is a number by using `typeof` or coercing it to a number and using `Number.isFinite`.", + "example": "const isNumber = (v) => typeof v === \"number\" || (typeof v === \"string\" && Number.isFinite(+v));" + }, "snippet::is-object": { "id": "snippet::is-object", "type": "simple", @@ -305,12 +311,6 @@ "type": "simple", "description": "You can check the start of a path for the Windows extended-length path prefix and if it's not present, replace backslashes with forward slashes.", "example": "path.startsWith('\\\\\\\\?\\\\') ? path : path.replace(/\\\\/g, '/')" - }, - "snippet:is-number": { - "id": "snippet:is-number", - "type": "simple", - "description": "You can check if a value is a number by using `typeof` or coercing it to a number and using `Number.isFinite`.", - "example": "const isNumber = (v) => typeof v === \"number\" || (typeof v === \"string\" && Number.isFinite(+v));" } }, "mappings": { diff --git a/scripts/validate-manifests.js b/scripts/validate-manifests.js index ff3cd91..a0117db 100644 --- a/scripts/validate-manifests.js +++ b/scripts/validate-manifests.js @@ -37,6 +37,12 @@ export async function validateManifests() { ); } + if (replacement.type === 'simple' && !id.startsWith('snippet::')) { + throw new Error( + `${manifestPath}: replacement "${id}" is type "simple" and must start with the "snippet::" prefix.` + ); + } + if (!replacement.webFeatureId) continue; const {featureId, compatKey} = replacement.webFeatureId; @@ -53,12 +59,6 @@ export async function validateManifests() { `${manifestPath}: replacement "${id}" has compatKey "${compatKey}" not found in web-features feature "${featureId}"` ); } - - if (replacement.type === 'simple' && !id.startsWith('snippet::')) { - throw new Error( - `${manifestPath}: replacement "${id}" is type "simple" and must start with the "snippet::" prefix.` - ); - } } const usedReplacementIds = new Set(); From c82966007d13cb64cf418945ae515cc9e09a211d Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 10 Apr 2026 18:54:46 +0100 Subject: [PATCH 3/4] chore: do not loop twice in validation --- scripts/validate-manifests.js | 62 +++++++++++++++++------------------ 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/scripts/validate-manifests.js b/scripts/validate-manifests.js index a0117db..ee94984 100644 --- a/scripts/validate-manifests.js +++ b/scripts/validate-manifests.js @@ -30,37 +30,6 @@ export async function validateManifests() { throw new Error(`Validation for ${manifestPath} failed!`); } - for (const [id, replacement] of Object.entries(manifest.replacements)) { - if (replacement.id !== id) { - throw new Error( - `${manifestPath}: replacement key "${id}" does not match its id property "${replacement.id}"` - ); - } - - if (replacement.type === 'simple' && !id.startsWith('snippet::')) { - throw new Error( - `${manifestPath}: replacement "${id}" is type "simple" and must start with the "snippet::" prefix.` - ); - } - - if (!replacement.webFeatureId) continue; - - const {featureId, compatKey} = replacement.webFeatureId; - const feature = webFeatures[featureId]; - - if (!feature) { - throw new Error( - `${manifestPath}: replacement "${id}" has unknown webFeatureId.featureId "${featureId}"` - ); - } - - if (!feature.compat_features?.includes(compatKey)) { - throw new Error( - `${manifestPath}: replacement "${id}" has compatKey "${compatKey}" not found in web-features feature "${featureId}"` - ); - } - } - const usedReplacementIds = new Set(); for (const [key, mapping] of Object.entries(manifest.mappings)) { @@ -89,12 +58,41 @@ export async function validateManifests() { } } - for (const id of Object.keys(manifest.replacements)) { + for (const [id, replacement] of Object.entries(manifest.replacements)) { + if (replacement.id !== id) { + throw new Error( + `${manifestPath}: replacement key "${id}" does not match its id property "${replacement.id}"` + ); + } + if (!usedReplacementIds.has(id)) { throw new Error( `${manifestPath}: replacement "${id}" is defined but not used by any mapping.` ); } + + if (replacement.type === 'simple' && !id.startsWith('snippet::')) { + throw new Error( + `${manifestPath}: replacement "${id}" is type "simple" and must start with the "snippet::" prefix.` + ); + } + + if (!replacement.webFeatureId) continue; + + const {featureId, compatKey} = replacement.webFeatureId; + const feature = webFeatures[featureId]; + + if (!feature) { + throw new Error( + `${manifestPath}: replacement "${id}" has unknown webFeatureId.featureId "${featureId}"` + ); + } + + if (!feature.compat_features?.includes(compatKey)) { + throw new Error( + `${manifestPath}: replacement "${id}" has compatKey "${compatKey}" not found in web-features feature "${featureId}"` + ); + } } } console.log('OK'); From d3a7d1ca3d66bffaec7d347a8f785e40856a92ac Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 10 Apr 2026 18:58:07 +0100 Subject: [PATCH 4/4] feat: add `year` package to replacements --- manifests/micro-utilities.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/manifests/micro-utilities.json b/manifests/micro-utilities.json index 7994f2d..79681b3 100644 --- a/manifests/micro-utilities.json +++ b/manifests/micro-utilities.json @@ -311,6 +311,12 @@ "type": "simple", "description": "You can check the start of a path for the Windows extended-length path prefix and if it's not present, replace backslashes with forward slashes.", "example": "path.startsWith('\\\\\\\\?\\\\') ? path : path.replace(/\\\\/g, '/')" + }, + "snippet::year": { + "id": "snippet::year", + "type": "simple", + "description": "You can use `new Date().getUTCFullYear()` to get the current year.", + "example": "new Date().getUTCFullYear()" } }, "mappings": { @@ -482,12 +488,12 @@ "is-number": { "type": "module", "moduleName": "is-number", - "replacements": ["snippet:is-number"] + "replacements": ["snippet::is-number"] }, "is-number-object": { "type": "module", "moduleName": "is-number-object", - "replacements": ["snippet:is-number"] + "replacements": ["snippet::is-number"] }, "is-obj": { "type": "module", @@ -643,6 +649,11 @@ "type": "module", "moduleName": "upper-case", "replacements": ["snippet::to-uppercase"] + }, + "year": { + "type": "module", + "moduleName": "year", + "replacements": ["snippet::year"] } } }