UNPKG

@preact-signals/safe-react

Version:
307 lines (304 loc) 11.1 kB
import { template } from '@babel/core'; import { isModule, addNamed } from '@babel/helper-module-imports'; import debug from '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 == null ? void 0 : 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 == null ? void 0 : name.match(/^[A-Z]/)) != null; } function hasLeadingComment(path, comment) { var _a; const comments = path.node.leadingComments; return (_a = comments == null ? void 0 : comments.some((c) => c.value.match(comment) !== null)) != null ? _a : 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 = 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) { var _a; const newFunction = wrapInTryFinally(t, path, state); newFunction.leadingComments = (_a = newFunction.leadingComments) == null ? void 0 : _a.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 (isModule(path)) { let reference = get(pass, `imports/${importName2}`); if (reference) return t.cloneNode(reference); reference = 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 = addNamed(path, importName2, source, { importedInterop: "uncompiled" }); set(pass, `requires/${importName2}`, reference); } return reference; } }; } function log(transformed, path, functionName, currentFile) { var _a, _b, _c, _d; 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 = (_a = currentFile == null ? void 0 : currentFile.replace(cwd, "")) != null ? _a : ""; const lineNum = (_b = path.node.loc) == null ? void 0 : _b.start.line; functionName = functionName != null ? functionName : "<anonymous>"; if (transformed) { logger.transformed(`${functionName} (${relativePath}:${lineNum})`); } else { logger.skipped(`${functionName} (${relativePath}:${lineNum}) %o`, { hasSignals: (_c = getData(path.scope, maybeUsesSignal)) != null ? _c : false, hasJSX: (_d = getData(path.scope, containsJSX)) != null ? _d : false }); } } function isComponentLike(path, functionName) { return !getData(path, alreadyTransformed) && isComponentName(functionName); } function signalsTransform({ types: t }, options) { var _a; (_a = options.mode) != null ? _a : options.mode = "all"; const visitFunction = { exit(path, state) { var _a2; if (getData(path, alreadyTransformed) === true) return false; let functionName = getFunctionName(path); if (functionName === DefaultExportSymbol) { functionName = (_a2 = basename(this.filename)) != null ? _a2 : 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) { var _a2; set( state, getHookIdentifier, createImportLazily( t, state, path, importName, (_a2 = options.importSource) != null ? _a2 : 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); } } }; } export { signalsTransform as default }; //# sourceMappingURL=babel.mjs.map