diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a29de4c21..b838ca482 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -213,6 +213,7 @@ module.exports = { "unicorn/prefer-regexp-test": "error", "unicorn/prefer-set-has": "error", "unicorn/prefer-spread": "error", + "unicorn/prefer-string-replace-all": "error", "unicorn/prefer-string-slice": "error", "unicorn/prefer-string-starts-ends-with": "error", "unicorn/prefer-switch": "error", @@ -291,6 +292,7 @@ module.exports = { "logical-assignment-operators": "off", "unicorn/prefer-array-flat": "off", "unicorn/prefer-array-flat-map": "off", + "unicorn/prefer-string-replace-all": "off", }, globals: { run_spec: "readonly", diff --git a/scripts/build/shims/at.js b/scripts/build/shims/at.js index 263043086..adc7cd756 100644 --- a/scripts/build/shims/at.js +++ b/scripts/build/shims/at.js @@ -5,13 +5,13 @@ Note: 2. `postcss` contains `Container` class with `.at` method */ -const at = (isOptionalObject, object, index, ...args) => { +const at = (isOptionalObject, object, index) => { if (isOptionalObject && (object === undefined || object === null)) { return; } return object.at - ? object.at(index, ...args) + ? object.at(index) : object[index < 0 ? object.length + index : index]; }; diff --git a/scripts/build/shims/string-replace-all.js b/scripts/build/shims/string-replace-all.js new file mode 100644 index 000000000..d6a2bc55d --- /dev/null +++ b/scripts/build/shims/string-replace-all.js @@ -0,0 +1,19 @@ +const stringReplaceAll = (isOptionalObject, original, pattern, replacement) => { + if (isOptionalObject && (original === undefined || original === null)) { + return; + } + + if (original.replaceAll) { + return original.replaceAll(pattern, replacement); + } + + // Simple detection to check if pattern is a `RegExp` + if (pattern.global) { + return original.replace(pattern, replacement); + } + + // Doesn't work for substitutes, eg `.replaceAll("*", "\\$&")` + return original.split(pattern).join(replacement); +}; + +export default stringReplaceAll; diff --git a/scripts/build/transform/index.js b/scripts/build/transform/index.js index 704e1509a..6b5662ce9 100644 --- a/scripts/build/transform/index.js +++ b/scripts/build/transform/index.js @@ -7,6 +7,9 @@ import { SOURCE_DIR } from "../../utils/index.mjs"; const generate = babelGenerator.default; const atHelperPath = fileURLToPath(new URL("../shims/at.js", import.meta.url)); +const stringReplaceAllHelperPath = fileURLToPath( + new URL("../shims/string-replace-all.js", import.meta.url) +); /* Doesn't work for dependencies, optional call, computed property, and spread arguments */ @@ -59,33 +62,40 @@ function transformObjectHasOwnCall(node) { * @param {import("@babel/types").Node} node * @returns {boolean} */ -function transformRelativeIndexingCall(node) { +function transformMethodCall({ + node, + method, + replacement = `__${method}`, + argumentsLength, +}) { if ( !( (node.type === "CallExpression" || node.type === "OptionalCallExpression") && !node.optional && - node.arguments.length === 1 && + node.arguments.length === argumentsLength && node.arguments.every(({ type }) => type !== "SpreadElement") && (node.callee.type === "MemberExpression" || node.callee.type === "OptionalMemberExpression") && !node.callee.computed && node.callee.object.type !== "ThisExpression" && node.callee.property.type === "Identifier" && - node.callee.property.name === "at" + node.callee.property.name === method ) ) { return false; } + // `__at(isOptionalObject, object, ...arguments)` node.arguments.unshift( { type: "BooleanLiteral", value: node.callee.type === "OptionalMemberExpression", + leadingComments: [{ type: "CommentBlock", value: " isOptionalObject" }], }, node.callee.object ); - node.callee = { type: "Identifier", name: "__at" }; + node.callee = { type: "Identifier", name: replacement }; return true; } @@ -93,22 +103,40 @@ function transformRelativeIndexingCall(node) { function transform(original, file) { if ( file === atHelperPath || + file === stringReplaceAllHelperPath || !file.startsWith(SOURCE_DIR) || - (!original.includes(".at(") && !original.includes("Object.hasOwn(")) + (!original.includes(".at(") && + !original.includes(".replaceAll(") && + !original.includes("Object.hasOwn(")) ) { return original; } let changed = false; let shouldInjectRelativeIndexingHelper = false; + let shouldInjectStringReplaceAllHelperPath = false; + const ast = parse(original, { sourceType: "module" }); traverse(ast, (node) => { const hasObjectHasOwnCall = transformObjectHasOwnCall(node); - changed ||= hasObjectHasOwnCall; - const hasRelativeIndexingCall = transformRelativeIndexingCall(node); + const hasRelativeIndexingCall = transformMethodCall({ + node, + method: "at", + argumentsLength: 1, + }); shouldInjectRelativeIndexingHelper ||= hasRelativeIndexingCall; - changed ||= hasRelativeIndexingCall; + + const hasStringReplaceAllCall = transformMethodCall({ + node, + method: "replaceAll", + replacement: "__stringReplaceAll", + argumentsLength: 2, + }); + shouldInjectStringReplaceAllHelperPath ||= hasStringReplaceAllCall; + + changed ||= + hasObjectHasOwnCall || hasRelativeIndexingCall || hasStringReplaceAllCall; }); if (!changed) { @@ -117,9 +145,22 @@ function transform(original, file) { let { code } = generate(ast); - if (shouldInjectRelativeIndexingHelper) { + const helpers = [ + shouldInjectRelativeIndexingHelper && { name: "__at", path: atHelperPath }, + shouldInjectStringReplaceAllHelperPath && { + name: "__stringReplaceAll", + path: stringReplaceAllHelperPath, + }, + ] + .filter(Boolean) + .map( + (helper) => `import ${helper.name} from ${JSON.stringify(helper.path)};` + ) + .join("\n"); + + if (helpers) { code = outdent` - import __at from ${JSON.stringify(atHelperPath)}; + ${helpers} ${code} `; diff --git a/scripts/release/steps/update-dependents-count.js b/scripts/release/steps/update-dependents-count.js index 4f2c470f1..aaa96209e 100644 --- a/scripts/release/steps/update-dependents-count.js +++ b/scripts/release/steps/update-dependents-count.js @@ -25,7 +25,7 @@ async function update() { .match( /.*?<\/svg>\s*([\d,]+)\s*Repositories\s*<\/a>/ )[1] - .replace(/,/g, "") + .replaceAll(",", "") ); if (Number.isNaN(dependentsCountNpm)) { throw new TypeError( diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/jsx-identifier-case.js b/scripts/tools/eslint-plugin-prettier-internal-rules/jsx-identifier-case.js index adc149a08..e56e20335 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/jsx-identifier-case.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/jsx-identifier-case.js @@ -32,7 +32,7 @@ module.exports = { return; } - const fixed = name.replace(/JSX/g, "Jsx"); + const fixed = name.replaceAll("JSX", "Jsx"); context.report({ node, messageId: MESSAGE_ID, diff --git a/src/cli/expand-patterns.js b/src/cli/expand-patterns.js index 0dd821ae7..0f6108541 100644 --- a/src/cli/expand-patterns.js +++ b/src/cli/expand-patterns.js @@ -180,9 +180,9 @@ function sortPaths(paths) { function escapePathForGlob(path) { return fastGlob .escapePath( - path.replace(/\\/g, "\0") // Workaround for fast-glob#262 (part 1) + path.replaceAll("\\", "\0") // Workaround for fast-glob#262 (part 1) ) - .replace(/\\!/g, "@(!)") // Workaround for fast-glob#261 + .replaceAll("\\!", "@(!)") // Workaround for fast-glob#261 .replace(/\0/g, "@(\\\\)"); // Workaround for fast-glob#262 (part 2) } @@ -196,7 +196,7 @@ const isWindows = path.sep === "\\"; * @param {string} pattern */ function fixWindowsSlashes(pattern) { - return isWindows ? pattern.replace(/\\/g, "/") : pattern; + return isWindows ? pattern.replaceAll("\\", "/") : pattern; } export { expandPatterns, fixWindowsSlashes }; diff --git a/src/language-css/clean.js b/src/language-css/clean.js index 8ff868f11..5f3a8e8d8 100644 --- a/src/language-css/clean.js +++ b/src/language-css/clean.js @@ -67,7 +67,7 @@ function clean(ast, newObj, parent) { } if (ast.type === "media-feature") { - newObj.value = newObj.value.replace(/ /g, ""); + newObj.value = newObj.value.replaceAll(" ", ""); } if ( @@ -187,7 +187,7 @@ function clean(ast, newObj, parent) { clean.ignoredProperties = ignoredProperties; function cleanCSSStrings(value) { - return value.replace(/'/g, '"').replace(/\\([^\dA-Fa-f])/g, "$1"); + return value.replaceAll("'", '"').replace(/\\([^\dA-Fa-f])/g, "$1"); } export default clean; diff --git a/src/language-graphql/printer-graphql.js b/src/language-graphql/printer-graphql.js index 2a445fd15..909c05ae2 100644 --- a/src/language-graphql/printer-graphql.js +++ b/src/language-graphql/printer-graphql.js @@ -119,7 +119,7 @@ function genericPrint(path, options, print) { case "StringValue": if (node.block) { - const lines = node.value.replace(/"""/g, "\\$&").split("\n"); + const lines = node.value.replaceAll('"""', '\\"""').split("\n"); if (lines.length === 1) { lines[0] = lines[0].trim(); } diff --git a/src/language-html/embed.js b/src/language-html/embed.js index a309f352d..c0636a435 100644 --- a/src/language-html/embed.js +++ b/src/language-html/embed.js @@ -399,7 +399,7 @@ function embed(path, options) { '="', group( mapDoc(embeddedAttributeValueDoc, (doc) => - typeof doc === "string" ? doc.replace(/"/g, """) : doc + typeof doc === "string" ? doc.replaceAll('"', """) : doc ) ), '"', diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js index dad37cffa..af488405b 100644 --- a/src/language-html/printer-html.js +++ b/src/language-html/printer-html.js @@ -107,8 +107,8 @@ function genericPrint(path, options, print) { quote, replaceEndOfLine( quote === '"' - ? value.replace(/"/g, """) - : value.replace(/'/g, "'") + ? value.replaceAll('"', """) + : value.replaceAll("'", "'") ), quote, ]; diff --git a/src/language-html/utils/index.js b/src/language-html/utils/index.js index 9eeb978d7..66408e1b1 100644 --- a/src/language-html/utils/index.js +++ b/src/language-html/utils/index.js @@ -571,7 +571,7 @@ function countChars(text, char) { } function unescapeQuoteEntities(text) { - return text.replace(/'/g, "'").replace(/"/g, '"'); + return text.replaceAll("'", "'").replaceAll(""", '"'); } // top-level elements (excluding