@preact-signals/safe-react
Version:
Manage state with style in React
303 lines (299 loc) • 10.8 kB
JavaScript
;
const core = require('@babel/core');
const helperModuleImports = require('@babel/helper-module-imports');
const debug = require('debug');
const optOutCommentIdentifier = /(^|\s)@noUseSignals(\s|$)/;
const optInCommentIdentifier = /(^|\s)@useSignals(\s|$)/;
const dataNamespace = "@preact-signals/safe-react/babel";
const defaultImportSource = "@preact-signals/safe-react/tracking";
const importName = "useSignals";
const getHookIdentifier = "getHookIdentifier";
const maybeUsesSignal = "maybeUsesSignal";
const containsJSX = "containsJSX";
const alreadyTransformed = "alreadyTransformed";
const logger = {
transformed: debug("signals:react-transform:transformed"),
skipped: debug("signals:react-transform:skipped")
};
const get = (pass, name) => pass.get(`${dataNamespace}/${name}`);
const set = (pass, name, v) => pass.set(`${dataNamespace}/${name}`, v);
const setData = (node, name, value) => node.setData(`${dataNamespace}/${name}`, value);
const getData = (node, name) => node.getData(`${dataNamespace}/${name}`);
function setOnFunctionScope(path, key, value) {
const functionScope = path.scope.getFunctionParent();
if (functionScope) {
setData(functionScope, key, value);
}
}
function basename(filename) {
return filename?.split(/[\\/]/).pop();
}
const DefaultExportSymbol = Symbol("DefaultExportSymbol");
function getObjectPropertyKey(node) {
if (node.key.type === "Identifier") {
return node.key.name;
} else if (node.key.type === "StringLiteral") {
return node.key.value;
}
return null;
}
function getFunctionNodeName(path) {
if ((path.node.type === "FunctionDeclaration" || path.node.type === "FunctionExpression") && path.node.id) {
return path.node.id.name;
} else if (path.node.type === "ObjectMethod") {
return getObjectPropertyKey(path.node);
}
return null;
}
function getFunctionNameFromParent(parentPath) {
if (parentPath.node.type === "VariableDeclarator" && parentPath.node.id.type === "Identifier") {
return parentPath.node.id.name;
} else if (parentPath.node.type === "AssignmentExpression") {
const left = parentPath.node.left;
if (left.type === "Identifier") {
return left.name;
} else if (left.type === "MemberExpression") {
let property = left.property;
while (property.type === "MemberExpression") {
property = property.property;
}
if (property.type === "Identifier") {
return property.name;
} else if (property.type === "StringLiteral") {
return property.value;
}
return null;
} else {
return null;
}
} else if (parentPath.node.type === "ObjectProperty") {
return getObjectPropertyKey(parentPath.node);
} else if (parentPath.node.type === "ExportDefaultDeclaration") {
return DefaultExportSymbol;
} else if (parentPath.node.type === "CallExpression" && parentPath.parentPath != null) {
return getFunctionNameFromParent(parentPath.parentPath);
} else {
return null;
}
}
function getFunctionName(path) {
let nodeName = getFunctionNodeName(path);
if (nodeName) {
return nodeName;
}
return getFunctionNameFromParent(path.parentPath);
}
function isComponentName(name) {
return name?.match(/^[A-Z]/) != null;
}
function hasLeadingComment(path, comment) {
const comments = path.node.leadingComments;
return comments?.some((c) => c.value.match(comment) !== null) ?? false;
}
function hasLeadingOptInComment(path) {
return hasLeadingComment(path, optInCommentIdentifier);
}
function hasLeadingOptOutComment(path) {
return hasLeadingComment(path, optOutCommentIdentifier);
}
function isOptedIntoSignalTracking(path) {
if (!path) return false;
switch (path.node.type) {
case "ArrowFunctionExpression":
case "FunctionExpression":
case "FunctionDeclaration":
case "ObjectMethod":
case "ObjectExpression":
case "VariableDeclarator":
case "VariableDeclaration":
case "AssignmentExpression":
case "CallExpression":
return hasLeadingOptInComment(path) || isOptedIntoSignalTracking(path.parentPath);
case "ExportDefaultDeclaration":
case "ExportNamedDeclaration":
case "ObjectProperty":
case "ExpressionStatement":
return hasLeadingOptInComment(path);
default:
return false;
}
}
function isOptedOutOfSignalTracking(path) {
if (!path) return false;
switch (path.node.type) {
case "ArrowFunctionExpression":
case "FunctionExpression":
case "FunctionDeclaration":
case "ObjectMethod":
case "ObjectExpression":
case "VariableDeclarator":
case "VariableDeclaration":
case "AssignmentExpression":
case "CallExpression":
return hasLeadingOptOutComment(path) || isOptedOutOfSignalTracking(path.parentPath);
case "ExportDefaultDeclaration":
case "ExportNamedDeclaration":
case "ObjectProperty":
case "ExpressionStatement":
return hasLeadingOptOutComment(path);
default:
return false;
}
}
function isComponentFunction(path, functionName) {
return getData(path.scope, containsJSX) === true && // Function contains JSX
isComponentName(functionName);
}
function shouldTransform(path, functionName, options) {
if (isOptedOutOfSignalTracking(path)) return false;
if (isOptedIntoSignalTracking(path)) return true;
if (options.mode === "all") {
return isComponentFunction(path, functionName);
}
if (options.mode == null || options.mode === "auto") {
return getData(path.scope, maybeUsesSignal) === true && // Function appears to use signals;
isComponentFunction(path, functionName);
}
return false;
}
function isValueMemberExpression(path) {
return path.node.property.type === "Identifier" && path.node.property.name === "value" || path.node.property.type === "StringLiteral" && path.node.property.value === "value";
}
const tryCatchTemplate = core.template.statements`var STORE_IDENTIFIER = HOOK_IDENTIFIER();
try {
BODY
} finally {
STORE_IDENTIFIER.f();
}`;
function wrapInTryFinally(t, path, state) {
const stopTrackingIdentifier = path.scope.generateUidIdentifier("effect");
const newFunction = t.cloneNode(path.node);
newFunction.body = t.blockStatement(
tryCatchTemplate({
STORE_IDENTIFIER: stopTrackingIdentifier,
HOOK_IDENTIFIER: get(state, getHookIdentifier)(),
BODY: t.isBlockStatement(path.node.body) ? path.node.body.body : t.returnStatement(path.node.body)
})
);
return newFunction;
}
function transformFunction(t, options, path, functionName, state) {
const newFunction = wrapInTryFinally(t, path, state);
newFunction.leadingComments = newFunction.leadingComments?.filter(
(c) => !c.value.match(optOutCommentIdentifier) && !c.value.match(optInCommentIdentifier)
);
setData(path, alreadyTransformed, true);
path.replaceWith(newFunction);
}
function createImportLazily(t, pass, path, importName2, source) {
return () => {
if (helperModuleImports.isModule(path)) {
let reference = get(pass, `imports/${importName2}`);
if (reference) return t.cloneNode(reference);
reference = helperModuleImports.addNamed(path, importName2, source, {
importedInterop: "uncompiled",
importPosition: "after"
});
set(pass, `imports/${importName2}`, reference);
const matchesImportName = (s) => {
if (s.type !== "ImportSpecifier") return false;
return s.imported.type === "Identifier" && s.imported.name === importName2 || s.imported.type === "StringLiteral" && s.imported.value === importName2;
};
for (let statement of path.get("body")) {
if (statement.isImportDeclaration() && statement.node.source.value === source && statement.node.specifiers.some(matchesImportName)) {
path.scope.registerDeclaration(statement);
break;
}
}
return reference;
} else {
let reference = get(pass, `requires/${importName2}`);
if (reference) {
reference = t.cloneNode(reference);
} else {
reference = helperModuleImports.addNamed(path, importName2, source, {
importedInterop: "uncompiled"
});
set(pass, `requires/${importName2}`, reference);
}
return reference;
}
};
}
function log(transformed, path, functionName, currentFile) {
if (!logger.transformed.enabled && !logger.skipped.enabled) return;
let cwd = "";
if (typeof process !== void 0 && typeof process.cwd == "function") {
cwd = process.cwd().replace(/\\([^ ])/g, "/$1");
cwd = cwd.endsWith("/") ? cwd : cwd + "/";
}
const relativePath = currentFile?.replace(cwd, "") ?? "";
const lineNum = path.node.loc?.start.line;
functionName = functionName ?? "<anonymous>";
if (transformed) {
logger.transformed(`${functionName} (${relativePath}:${lineNum})`);
} else {
logger.skipped(`${functionName} (${relativePath}:${lineNum}) %o`, {
hasSignals: getData(path.scope, maybeUsesSignal) ?? false,
hasJSX: getData(path.scope, containsJSX) ?? false
});
}
}
function isComponentLike(path, functionName) {
return !getData(path, alreadyTransformed) && isComponentName(functionName);
}
function signalsTransform({ types: t }, options) {
options.mode ?? (options.mode = "all");
const visitFunction = {
exit(path, state) {
if (getData(path, alreadyTransformed) === true) return false;
let functionName = getFunctionName(path);
if (functionName === DefaultExportSymbol) {
functionName = basename(this.filename) ?? null;
}
if (shouldTransform(path, functionName, state.opts)) {
transformFunction(t, state.opts, path, functionName, state);
log(true, path, functionName, this.filename);
} else if (isComponentLike(path, functionName)) {
log(false, path, functionName, this.filename);
}
}
};
return {
name: dataNamespace,
visitor: {
Program: {
enter(path, state) {
set(
state,
getHookIdentifier,
createImportLazily(
t,
state,
path,
importName,
options.importSource ?? defaultImportSource
)
);
}
},
ArrowFunctionExpression: visitFunction,
FunctionExpression: visitFunction,
FunctionDeclaration: visitFunction,
ObjectMethod: visitFunction,
MemberExpression(path) {
if (isValueMemberExpression(path)) {
setOnFunctionScope(path, maybeUsesSignal, true);
}
},
JSXElement(path) {
setOnFunctionScope(path, containsJSX, true);
},
JSXFragment(path) {
setOnFunctionScope(path, containsJSX, true);
}
}
};
}
module.exports = signalsTransform;
//# sourceMappingURL=babel.cjs.map