@preact/signals-react-transform
Version:
Manage state with style in React
523 lines (521 loc) • 21.8 kB
JavaScript
import {template,types}from'@babel/core';import {isModule,addNamed}from'@babel/helper-module-imports';import debug from'debug';function _taggedTemplateLiteralLoose(strings, raw) {
if (!raw) {
raw = strings.slice(0);
}
strings.raw = raw;
return strings;
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
return arr2;
}
function _createForOfIteratorHelperLoose(o, allowArrayLike) {
var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];
if (it) return (it = it.call(o)).next.bind(it);
if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {
if (it) o = it;
var i = 0;
return function () {
if (i >= o.length) return {
done: true
};
return {
done: false,
value: o[i++]
};
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}var _templateObject;
var optOutCommentIdentifier = /(^|\s)Signals(\s|$)/;
var optInCommentIdentifier = /(^|\s)@(use|track)Signals(\s|$)/;
var dataNamespace = "@preact/signals-react-transform";
var defaultImportSource = "@preact/signals-react/runtime";
var importName = "useSignals";
var getHookIdentifier = "getHookIdentifier";
var maybeUsesSignal = "maybeUsesSignal";
var containsJSX = "containsJSX";
var alreadyTransformed = "alreadyTransformed";
var jsxIdentifiers = "jsxIdentifiers";
var jsxObjects = "jsxObjects";
var UNMANAGED = "0";
var MANAGED_COMPONENT = "1";
var MANAGED_HOOK = "2";
var logger = {
transformed: debug("signals:react-transform:transformed"),
skipped: debug("signals:react-transform:skipped")
};
var get = function get(pass, name) {
return pass.get(dataNamespace + "/" + name);
};
var set = function set(pass, name, v) {
return pass.set(dataNamespace + "/" + name, v);
};
var setData = function setData(node, name, value) {
return node.setData(dataNamespace + "/" + name, value);
};
var getData = function getData(node, name) {
return node.getData(dataNamespace + "/" + name);
};
function getComponentFunctionDeclaration(path, filename, prev) {
var functionScope = path.scope.getFunctionParent();
if (functionScope) {
var parent = functionScope.path.parent;
var functionName = getFunctionName(functionScope.path);
if (functionName === DefaultExportSymbol) {
functionName = filename || null;
}
if (isComponentFunction(functionScope.path, functionName)) {
return functionScope;
} else if (parent.type === "CallExpression" && parent.callee.type === "Identifier" && parent.callee.name.startsWith("use") && parent.callee.name[3] === parent.callee.name[3].toUpperCase()) {
return null;
}
return getComponentFunctionDeclaration(functionScope.parent.path, filename, functionScope);
} else {
return prev || null;
}
}
function setOnFunctionScope(path, key, value, filename) {
var functionScope = getComponentFunctionDeclaration(path, filename);
if (functionScope) {
setData(functionScope, key, value);
}
}
/**
* Simple "best effort" to get the base name of a file path. Not fool proof but
* works in browsers and servers. Good enough for our purposes.
*/
function basename(filename) {
return filename == null ? void 0 : filename.split(/[\\/]/).pop();
}
var 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;
}
/**
* If the function node has a name (i.e. is a function declaration with a
* name), return that. Else return null.
*/
function getFunctionNodeName(node) {
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression") && node.id) {
return node.id.name;
} else if (node.type === "ObjectMethod") {
return getObjectPropertyKey(node);
}
return null;
}
/**
* Given a function path's parent path, determine the "name" associated with the
* function. If the function is an inline default export (e.g. `export default
* () => {}`), returns a symbol indicating it is a default export. If the
* function is an anonymous function wrapped in higher order functions (e.g.
* memo(() => {})) we'll climb through the higher order functions to find the
* name of the variable that the function is assigned to, if any. Other cases
* handled too (see implementation). Else returns 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") {
var left = parentPath.node.left;
if (left.type === "Identifier") {
return left.name;
} else if (left.type === "MemberExpression") {
var 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) {
// If our parent is a Call Expression, then this function expression is
// wrapped in some higher order functions. Recurse through the higher order
// functions to determine if this expression is assigned to a name we can
// use as the function name
return getFunctionNameFromParent(parentPath.parentPath);
} else {
return null;
}
}
/* Determine the name of a function */
function getFunctionName(path) {
var nodeName = getFunctionNodeName(path.node);
if (nodeName) {
return nodeName;
}
return getFunctionNameFromParent(path.parentPath);
}
function isComponentName(name) {
return (name == null ? void 0 : name.match(/^[A-Z]/)) != null;
}
function isCustomHookName(name) {
return (name == null ? void 0 : name.match(/^use[A-Z]/)) != null;
}
function hasLeadingComment(path, comment) {
var _comments$some;
var comments = path.node.leadingComments;
return (_comments$some = comments == null ? void 0 : comments.some(function (c) {
return c.value.match(comment) !== null;
})) != null ? _comments$some : 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 name indicates it's a component
;
}
function shouldTransform(path, functionName, options) {
// Opt-out takes first precedence
if (isOptedOutOfSignalTracking(path)) return false;
// Opt-in opts in to transformation regardless of mode
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) || isCustomHookName(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";
}
function isJSXAlternativeCall(path, state) {
var jsxIdentifierSet = get(state, jsxIdentifiers);
var jsxObjectMap = get(state, jsxObjects);
var callee = path.get("callee");
// Check direct function calls like _jsx("div", props) or createElement("div", props)
if (callee.isIdentifier()) {
var _jsxIdentifierSet$has;
return (_jsxIdentifierSet$has = jsxIdentifierSet == null ? void 0 : jsxIdentifierSet.has(callee.node.name)) != null ? _jsxIdentifierSet$has : false;
}
// Check member expression calls like React.createElement("div", props) or jsxRuntime.jsx("div", props)
if (callee.isMemberExpression()) {
var object = callee.get("object");
var property = callee.get("property");
if (object.isIdentifier() && property.isIdentifier()) {
var _allowedMethods$inclu;
var objectName = object.node.name;
var methodName = property.node.name;
var allowedMethods = jsxObjectMap == null ? void 0 : jsxObjectMap.get(objectName);
return (_allowedMethods$inclu = allowedMethods == null ? void 0 : allowedMethods.includes(methodName)) != null ? _allowedMethods$inclu : false;
}
}
return false;
}
function hasValuePropertyInPattern(pattern) {
for (var _iterator = _createForOfIteratorHelperLoose(pattern.properties), _step; !(_step = _iterator()).done;) {
var property = _step.value;
if (types.isObjectProperty(property)) {
var key = property.key;
if (types.isIdentifier(key, {
name: "value"
})) {
return true;
}
}
}
return false;
}
var tryCatchTemplate = template.statements(_templateObject || (_templateObject = _taggedTemplateLiteralLoose(["var STORE_IDENTIFIER = HOOK_IDENTIFIER(HOOK_USAGE);\ntry {\n\tBODY\n} finally {\n\tSTORE_IDENTIFIER.f();\n}"])));
function wrapInTryFinally(t, path, state, hookUsage) {
var stopTrackingIdentifier = path.scope.generateUidIdentifier("effect");
return t.blockStatement(tryCatchTemplate({
STORE_IDENTIFIER: stopTrackingIdentifier,
HOOK_IDENTIFIER: get(state, getHookIdentifier)(),
HOOK_USAGE: hookUsage,
BODY: t.isBlockStatement(path.node.body) ? path.node.body.body // TODO: Is it okay to elide the block statement here?
: t.returnStatement(path.node.body)
}));
}
function prependUseSignals(t, path, state) {
var body = t.blockStatement([t.expressionStatement(t.callExpression(get(state, getHookIdentifier)(), []))]);
if (t.isBlockStatement(path.node.body)) {
var _body$body;
// TODO: Is it okay to elide the block statement here?
(_body$body = body.body).push.apply(_body$body, path.node.body.body);
} else {
body.body.push(t.returnStatement(path.node.body));
}
return body;
}
function transformFunction(t, options, path, functionName, state) {
var _options$experimental;
var isHook = isCustomHookName(functionName);
var isComponent = isComponentName(functionName);
var hookUsage = (_options$experimental = options.experimental) != null && _options$experimental.noTryFinally ? UNMANAGED : isHook ? MANAGED_HOOK : isComponent ? MANAGED_COMPONENT : UNMANAGED;
var newBody;
if (hookUsage !== UNMANAGED) {
newBody = wrapInTryFinally(t, path, state, hookUsage);
} else {
newBody = prependUseSignals(t, path, state);
}
setData(path, alreadyTransformed, true);
path.get("body").replaceWith(newBody);
}
function createImportLazily(types, pass, path, importName, source) {
return function () {
if (isModule(path)) {
var reference = get(pass, "imports/" + importName);
if (reference) return types.cloneNode(reference);
reference = addNamed(path, importName, source, {
importedInterop: "uncompiled",
importPosition: "after"
});
set(pass, "imports/" + importName, reference);
/** Helper function to determine if an import declaration's specifier matches the given importName */
var matchesImportName = function matchesImportName(s) {
if (s.type !== "ImportSpecifier") return false;
return s.imported.type === "Identifier" && s.imported.name === importName || s.imported.type === "StringLiteral" && s.imported.value === importName;
};
for (var _iterator2 = _createForOfIteratorHelperLoose(path.get("body")), _step2; !(_step2 = _iterator2()).done;) {
var statement = _step2.value;
if (statement.isImportDeclaration() && statement.node.source.value === source && statement.node.specifiers.some(matchesImportName)) {
path.scope.registerDeclaration(statement);
break;
}
}
return reference;
} else {
// This code originates from
// https://github.com/XantreDev/preact-signals/blob/%40preact-signals/safe-react%400.6.1/packages/react/src/babel.ts#L390-L400
var _reference = get(pass, "requires/" + importName);
if (_reference) {
_reference = types.cloneNode(_reference);
} else {
_reference = addNamed(path, importName, source, {
importedInterop: "uncompiled"
});
set(pass, "requires/" + importName, _reference);
}
return _reference;
}
};
}
function detectJSXAlternativeImports(path, state) {
var jsxIdentifierSet = new Set();
var jsxObjectMap = new Map();
var jsxPackages = {
"react/jsx-runtime": ["jsx", "jsxs"],
"react/jsx-dev-runtime": ["jsxDEV"],
react: ["createElement"]
};
path.traverse({
ImportDeclaration: function ImportDeclaration(importPath) {
var packageName = importPath.node.source.value;
var jsxMethods = jsxPackages[packageName];
if (!jsxMethods) {
return;
}
for (var _iterator3 = _createForOfIteratorHelperLoose(importPath.node.specifiers), _step3; !(_step3 = _iterator3()).done;) {
var specifier = _step3.value;
if (specifier.type === "ImportSpecifier" && specifier.imported.type === "Identifier") {
// Check if this is a function we care about
if (jsxMethods.includes(specifier.imported.name)) {
jsxIdentifierSet.add(specifier.local.name);
}
} else if (specifier.type === "ImportDefaultSpecifier") {
// Handle default imports - add to objects map for member access
jsxObjectMap.set(specifier.local.name, jsxMethods);
}
}
},
VariableDeclarator: function VariableDeclarator(varPath) {
var init = varPath.get("init");
if (init.isCallExpression()) {
var callee = init.get("callee");
var args = init.get("arguments");
if (callee.isIdentifier() && callee.node.type === "Identifier" && callee.node.name === "require" && args.length > 0 && args[0].isStringLiteral()) {
var packageName = args[0].node.value;
var jsxMethods = jsxPackages[packageName];
if (jsxMethods) {
if (varPath.node.id.type === "Identifier") {
// Handle CJS require like: const React = require("react")
jsxObjectMap.set(varPath.node.id.name, jsxMethods);
} else if (varPath.node.id.type === "ObjectPattern") {
// Handle destructured CJS require like: const { createElement } = require("react")
for (var _iterator4 = _createForOfIteratorHelperLoose(varPath.node.id.properties), _step4; !(_step4 = _iterator4()).done;) {
var prop = _step4.value;
if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.value.type === "Identifier" && jsxMethods.includes(prop.key.name)) {
jsxIdentifierSet.add(prop.value.name);
}
}
}
}
}
}
}
});
set(state, jsxIdentifiers, jsxIdentifierSet);
set(state, jsxObjects, jsxObjectMap);
}
function log(transformed, path, functionName, currentFile) {
var _currentFile$replace, _path$node$loc, _functionName;
if (!logger.transformed.enabled && !logger.skipped.enabled) return;
var cwd = "";
if (typeof process !== undefined && typeof process.cwd == "function") {
cwd = process.cwd().replace(/\\([^ ])/g, "/$1");
cwd = cwd.endsWith("/") ? cwd : cwd + "/";
}
var relativePath = (_currentFile$replace = currentFile == null ? void 0 : currentFile.replace(cwd, "")) != null ? _currentFile$replace : "";
var lineNum = (_path$node$loc = path.node.loc) == null ? void 0 : _path$node$loc.start.line;
functionName = (_functionName = functionName) != null ? _functionName : "<anonymous>";
if (transformed) {
logger.transformed(functionName + " (" + relativePath + ":" + lineNum + ")");
} else {
var _getData, _getData2;
logger.skipped(functionName + " (" + relativePath + ":" + lineNum + ") %o", {
hasSignals: (_getData = getData(path.scope, maybeUsesSignal)) != null ? _getData : false,
hasJSX: (_getData2 = getData(path.scope, containsJSX)) != null ? _getData2 : false
});
}
}
function isComponentLike(path, functionName) {
return !getData(path, alreadyTransformed) && isComponentName(functionName);
}
function signalsTransform(_ref, options) {
var t = _ref.types;
// TODO: Consider alternate implementation, where on enter of a function
// expression, we run our own manual scan the AST to determine if the
// function uses signals and is a component. This manual scan once upon
// seeing a function would probably be faster than running an entire
// babel pass with plugins on components twice.
var visitFunction = {
exit: function exit(path, state) {
if (getData(path, alreadyTransformed) === true) return false;
var functionName = getFunctionName(path);
if (functionName === DefaultExportSymbol) {
var _basename;
functionName = (_basename = basename(this.filename)) != null ? _basename : 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: "@preact/signals-transform",
visitor: {
Program: {
enter: function enter(path, state) {
var _options$importSource;
// Following the pattern of babel-plugin-transform-react-jsx, we
// lazily create the import statement for the useSignalTracking hook.
// We create a function and store it in the PluginPass object, so that
// on the first usage of the hook, we can create the import statement.
set(state, getHookIdentifier, createImportLazily(t, state, path, importName, (_options$importSource = options.importSource) != null ? _options$importSource : defaultImportSource));
if (options.detectTransformedJSX) {
detectJSXAlternativeImports(path, state);
}
}
},
ArrowFunctionExpression: visitFunction,
FunctionExpression: visitFunction,
FunctionDeclaration: visitFunction,
ObjectMethod: visitFunction,
CallExpression: function CallExpression(path, state) {
if (options.detectTransformedJSX) {
if (isJSXAlternativeCall(path, state)) {
setOnFunctionScope(path, containsJSX, true, this.filename);
}
}
},
MemberExpression: function MemberExpression(path) {
if (isValueMemberExpression(path)) {
setOnFunctionScope(path, maybeUsesSignal, true, this.filename);
}
},
ObjectPattern: function ObjectPattern(path) {
if (hasValuePropertyInPattern(path.node)) {
setOnFunctionScope(path, maybeUsesSignal, true, this.filename);
}
},
JSXElement: function JSXElement(path) {
setOnFunctionScope(path, containsJSX, true, this.filename);
},
JSXFragment: function JSXFragment(path) {
setOnFunctionScope(path, containsJSX, true, this.filename);
}
}
};
}export{signalsTransform as default};//# sourceMappingURL=signals-transform.module.js.map