Build: transform String.raw (#18134)

This commit is contained in:
fisker Cheung authored and GitHub committed 2025-10-27 23:24:02 +08:00
1 parent eca8348812
commit b99012b48c
10 files changed
+147 -45

No files matched your search

+2
View File
@@ -128,6 +128,7 @@
"@eslint/js": "9.38.0",
"@stylistic/eslint-plugin": "5.5.0",
"@types/estree": "1.0.8",
"@types/jsesc": "3.0.3",
"@typescript-eslint/eslint-plugin": "8.46.2",
"@typescript-eslint/parser": "8.46.2",
"browserslist": "4.27.0",
@@ -156,6 +157,7 @@
"jest-snapshot-serializer-ansi": "2.2.1",
"jest-snapshot-serializer-raw": "2.0.0",
"jest-watch-typeahead": "3.0.1",
"jsesc": "3.1.0",
"knip": "5.66.2",
"magic-string": "0.30.19",
"nano-spawn": "2.0.0",
+13 -2
View File
@@ -56,13 +56,16 @@ function transform(original, file) {
continue;
}
transform.transform(node);
changed ||= true;
if (transform.inject) {
injected.add(transform.inject);
}
changed ||= true;
const replacement = transform.transform(node);
if (replacement && replacement !== node) {
replaceNode(node, replacement);
}
}
});
@@ -95,4 +98,12 @@ function transform(original, file) {
return code;
}
function replaceNode(original, object) {
for (const key of Object.keys(original)) {
delete original[key];
}
Object.assign(original, object);
}
export default transform;
@@ -1,4 +1,5 @@
import { fileURLToPath } from "node:url";
import { createIdentifier, isIdentifier } from "./utilities.js";
/**
* @param {import("@babel/types").Node} node
@@ -15,8 +16,7 @@ function isMethodCall(node, { methodName, argumentsLength }) {
node.callee.type === "OptionalMemberExpression") &&
!node.callee.computed &&
node.callee.object.type !== "ThisExpression" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === methodName
isIdentifier(node.callee.property, methodName)
);
}
@@ -29,16 +29,22 @@ function isMethodCall(node, { methodName, argumentsLength }) {
*/
function transformMethodCallToFunctionCall(node, functionName) {
// `__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: functionName };
return {
...node,
callee: createIdentifier(functionName),
arguments: [
{
type: "BooleanLiteral",
value: node.callee.type === "OptionalMemberExpression",
leadingComments: [
{ type: "CommentBlock", value: " isOptionalObject " },
],
},
node.callee.object,
...node.arguments,
],
};
}
function createMethodCallTransform({
+4 -1
View File
@@ -3,6 +3,7 @@ import transformArrayFindLastIndex from "./transform-array-find-last-index.js";
import transformArrayToReversed from "./transform-array-to-reversed.js";
import transformObjectHasOwnCall from "./transform-object-has-own.js";
import transformRelativeIndexing from "./transform-relative-indexing.js";
import transformStringRaw from "./transform-string-raw.js";
import transformStringReplaceAll from "./transform-string-replace-all.js";
// These transforms are like Babel and core-js
@@ -15,10 +16,12 @@ export default [
transformArrayFindLastIndex,
// Node.js 16.9.0
transformObjectHasOwnCall,
// Node.js 16.6.0
// Node.js 16.6.0, and for performance
transformRelativeIndexing,
// Node.js 15.0.0
transformStringReplaceAll,
// Node.js 20
transformArrayToReversed,
// For performance
transformStringRaw,
];
@@ -1,3 +1,5 @@
import { createMemberExpression, isIdentifier } from "./utilities.js";
/*
`Object.hasOwn(foo, "bar")` -> `Object.prototype.hasOwnProperty.call(foo, "bar")`
*/
@@ -8,40 +10,24 @@ const transformObjectHasOwnCall = {
* @param {import("@babel/types").Node} node
* @returns {boolean}
*/
test(node) {
return (
node.type === "CallExpression" &&
!node.optional &&
node.arguments.length === 2 &&
node.arguments.every(({ type }) => type !== "SpreadElement") &&
node.callee.type === "MemberExpression" &&
!node.callee.optional &&
!node.callee.computed &&
node.callee.object.type === "Identifier" &&
node.callee.object.name === "Object" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "hasOwn"
);
},
test: (node) =>
node.type === "CallExpression" &&
!node.optional &&
node.arguments.length === 2 &&
node.arguments.every(({ type }) => type !== "SpreadElement") &&
node.callee.type === "MemberExpression" &&
!node.callee.optional &&
!node.callee.computed &&
isIdentifier(node.callee.object, "Object") &&
isIdentifier(node.callee.property, "hasOwn"),
/**
* @param {import("@babel/types").CallExpression} node
* @returns {import("@babel/types").CallExpression}
*/
transform(node) {
node.callee = {
type: "MemberExpression",
object: {
type: "MemberExpression",
object: {
type: "MemberExpression",
object: { type: "Identifier", name: "Object" },
property: { type: "Identifier", name: "prototype" },
},
property: { type: "Identifier", name: "hasOwnProperty" },
},
property: { type: "Identifier", name: "call" },
};
},
transform: (node) => ({
...node,
callee: createMemberExpression("Object.prototype.hasOwnProperty.call"),
}),
};
export default transformObjectHasOwnCall;
@@ -0,0 +1,47 @@
import jsesc from "jsesc";
import { isIdentifier } from "./utilities.js";
/**
* @param {import("@babel/types").Node} node
* @returns {boolean}
*/
function isStringRaw(node) {
return (
node.type === "TaggedTemplateExpression" &&
node.tag.type === "MemberExpression" &&
!node.tag.computed &&
!node.tag.optional &&
isIdentifier(node.tag.object, "String") &&
isIdentifier(node.tag.property, "raw")
);
}
/**
* `` String.raw`foo` `` -> `"foo"`
* `` String.raw`foo ${"bar"}` `` -> `` `foo ${"bar"}` ``
*
* @param {import("@babel/types").TaggedTemplateExpression} node
* @returns {import("@babel/types").TaggedTemplateExpression}
*/
function transformStringRaw(taggedTemplateExpression) {
const templateLiteral = taggedTemplateExpression.quasi;
if (templateLiteral.expressions.length === 0) {
const { raw } = templateLiteral.quasis[0].value;
const value = new Function(`return \`${jsesc(raw)}\``)();
return { type: "StringLiteral", value };
}
const quasis = templateLiteral.quasis.map((quasis) => {
const { raw } = quasis.value;
return { ...quasis, value: { ...quasis, raw: jsesc(raw) } };
});
return { ...templateLiteral, quasis };
}
export default {
shouldSkip: (text /* , file*/) => !text.includes("String.raw`"),
test: isStringRaw,
transform: transformStringRaw,
};
@@ -0,0 +1,25 @@
function isIdentifier(node, name) {
return node.type === "Identifier" && node.name === name;
}
function createIdentifier(name) {
return { type: "Identifier", name };
}
function createMemberExpression(path) {
let node;
for (const name of path.split(".")) {
node = node
? {
type: "MemberExpression",
object: node,
property: createIdentifier(name),
}
: createIdentifier(name);
}
return node;
}
export { createIdentifier, createMemberExpression, isIdentifier };
+4
View File
@@ -20,6 +20,10 @@ const restrictedSyntaxes = [
selector: String.raw`CallExpression[callee.name="require"][arguments.0.value=/^\..*?\.mjs$/]`,
message: ".mjs file can't be `require()`d",
},
{
selector: String.raw`TaggedTemplateExpression > MemberExpression.tag[object.name="String"][property.name="raw"]`,
message: "`String.raw` should be transformed.",
},
];
const browserRestrictedSyntaxes = [
+9
View File
@@ -109,3 +109,12 @@ test("Array#toReversed", () => {
`"foo.toReversed(extraArgument)"`,
);
});
test("String.raw", () => {
expect(transform("String.raw`\\\\\\uINVALID`")).toMatchInlineSnapshot(
String.raw`""\\\\\\uINVALID""`,
);
expect(transform("String.raw`\\uINVALID${'world'}`")).toMatchInlineSnapshot(
`" \`\\\\uINVALID\${'world'}\`"`,
);
});
+10 -1
View File
@@ -2433,6 +2433,13 @@ __metadata:
languageName: node
linkType: hard
"@types/jsesc@npm:3.0.3":
version: 3.0.3
resolution: "@types/jsesc@npm:3.0.3"
checksum: 10/8e97013e91fdbffc7ce23f5be7f0b8a1208e9794ccae887f1a9fd606633d8b570424ab2de81ada47ff9a062812191a7aa764a42a293a37b86fd76f13329a0c0f
languageName: node
linkType: hard
"@types/jsesc@npm:^2.5.0":
version: 2.5.1
resolution: "@types/jsesc@npm:2.5.1"
@@ -6111,7 +6118,7 @@ __metadata:
languageName: node
linkType: hard
"jsesc@npm:^3.0.2, jsesc@npm:^3.1.0, jsesc@npm:~3.1.0":
"jsesc@npm:3.1.0, jsesc@npm:^3.0.2, jsesc@npm:^3.1.0, jsesc@npm:~3.1.0":
version: 3.1.0
resolution: "jsesc@npm:3.1.0"
bin:
@@ -7256,6 +7263,7 @@ __metadata:
"@prettier/parse-srcset": "npm:3.1.0"
"@stylistic/eslint-plugin": "npm:5.5.0"
"@types/estree": "npm:1.0.8"
"@types/jsesc": "npm:3.0.3"
"@typescript-eslint/eslint-plugin": "npm:8.46.2"
"@typescript-eslint/parser": "npm:8.46.2"
"@typescript-eslint/typescript-estree": "npm:8.46.2"
@@ -7318,6 +7326,7 @@ __metadata:
jest-snapshot-serializer-ansi: "npm:2.2.1"
jest-snapshot-serializer-raw: "npm:2.0.0"
jest-watch-typeahead: "npm:3.0.1"
jsesc: "npm:3.1.0"
json5: "npm:2.2.3"
knip: "npm:5.66.2"
leven: "npm:4.1.0"