UNPKG

react-refresh-typescript

Version:
570 lines 27.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = default_1; /** * Create a ReactRefresh transformer for TypeScript. * * This transformer should run in the before stage. * * This transformer requires TypeScript to be at least 4.0. */ function default_1(opts = {}) { const ts = opts.ts; if (!ts) throw new Error('Please provide typescript by options.ts'); { const [major] = ts.version.split('.'); if (parseInt(major) < 4) throw new Error('TypeScript should be at least 4.0'); } return (context) => { const { factory } = context; const refreshReg = factory.createIdentifier(opts.refreshReg || '$RefreshReg$'); const refreshSig = factory.createIdentifier(opts.refreshSig || '$RefreshSig$'); return (file) => { if (file.isDeclarationFile) return file; const containHooksLikeOrJSX = file.languageVariant === ts.LanguageVariant.JSX || file.text.includes('use'); if (!containHooksLikeOrJSX) return file; // TODO: change to scan comment? const globalRequireForceRefresh = file.text.includes('@refresh reset'); const topLevelDeclaredName = new Set(); // Collect top level local declarations for (const node of file.statements) { if (ts.isFunctionDeclaration(node) && node.name) topLevelDeclaredName.add(node.name.text); if (ts.isVariableStatement(node)) { for (const decl of node.declarationList.declarations) { if (ts.isIdentifier(decl.name)) { topLevelDeclaredName.add(decl.name.text); } // ? skip for deconstructing pattern } } } // track all JSX usage and transform non-top level hooks const { nextFile, usedAsJSXElement, hooksSignatureMap } = visitDeep(file, topLevelDeclaredName, globalRequireForceRefresh); file = nextFile; return updateStatements(file, (statements) => ts.visitLexicalEnvironment(statements, (node) => visitTopLevel(usedAsJSXElement, hooksSignatureMap, node), context)); }; // Only visit top level declaration to find possible components function visitTopLevel(usedAsJSXElement, hooksSignatureMap, node) { if (ts.isFunctionDeclaration(node)) { if (!node.name || !node.body) return node; return [node, ...registerComponent(node.name)]; } else if (ts.isVariableStatement(node)) { const deferredStatements = []; const nextDeclarationList = ts.visitEachChild(node.declarationList, (declaration) => { if (!ts.isVariableDeclaration(declaration)) return declaration; const init = declaration.initializer; // Not handle complex declaration. e.g. [a, b] = [() => ..., () => ...] // or declaration without initializer if (!ts.isIdentifier(declaration.name) || !init) return declaration; const declarationUsedAsJSX = usedAsJSXElement.has(declaration.name.text); if (declarationUsedAsJSX || isFunctionExpressionLikeOrFunctionDeclaration(init)) { if (!unwantedComponentLikeDefinition(init)) { deferredStatements.push(...registerComponent(declaration.name)); } if (isFunctionExpressionLikeOrFunctionDeclaration(init) && hooksSignatureMap.has(init)) { /** * const Comp = () => <Comp /> * const Comp2 = function () { return <Comp /> } * * Reserve the function name * * See https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-assignment-operators-runtime-semantics-evaluation */ // this is a workaround to https://github.com/Jack-Works/react-refresh-transformer/issues/8 // I don't have time to refactor it yet. let oneShot = false; const sig = ts.visitEachChild(hooksSignatureMap.get(init), (node) => oneShot ? node : ts.isFunctionLike(node) ? (oneShot = declaration.name) : node, context); deferredStatements.push(factory.createExpressionStatement(sig)); } return declaration; } if (isHigherOrderComponentLike(init)) { const { registers, call } = registerHigherOrderComponent(hooksSignatureMap, init, declaration.name.text); deferredStatements.push(...registers, ...registerComponent(declaration.name)); return factory.updateVariableDeclaration(declaration, declaration.name, undefined, declaration.type, call); } return declaration; }, context); return [ factory.updateVariableStatement(node, node.modifiers, nextDeclarationList), ...deferredStatements, ]; } else if (ts.isExportAssignment(node)) { if (isHigherOrderComponentLike(node.expression)) { const { registers, call } = registerHigherOrderComponent(hooksSignatureMap, node.expression, '%default%'); const temp = createTempVariable(); return [ factory.updateExportAssignment(node, node.modifiers, factory.createAssignment(temp, call)), createComponentRegisterCall(temp, '%default%'), ...registers, ]; } else if (isFunctionExpressionLikeOrFunctionDeclaration(node.expression)) { const expr = hooksSignatureMap.get(node.expression); if (expr) { return factory.updateExportAssignment(node, node.modifiers, expr); } } } return node; } function registerComponent(name) { if (!startsWithLowerCase(name.text)) { const temp = createTempVariable(); // uniq = name const assignment = factory.createAssignment(temp, name); // $reg$(uniq, "name") return [factory.createExpressionStatement(assignment), createComponentRegisterCall(temp, name.text)]; } return []; } /** * Please call isHOCLike before call this function */ function registerHigherOrderComponent(hooksSignatureMap, callExpr, nameHint) { // Recursive case, if it is x(y(...)), recursive with y(...) to get inner expr const arg0 = callExpr.arguments[0]; if (ts.isCallExpression(arg0)) { const tempVar = createTempVariable(); const nextNameHint = nameHint + '$' + printNode(callExpr.expression); const { registers, call: innerResult } = registerHigherOrderComponent(hooksSignatureMap, arg0, nextNameHint); return { call: factory.updateCallExpression(callExpr, callExpr.expression, void 0, [ factory.createAssignment(tempVar, innerResult), ...callExpr.arguments.slice(1), ]), registers: registers.concat(createComponentRegisterCall(tempVar, nextNameHint)), }; } // Base case, it is x(function () {...}) or x(() => ...) or x(Identifier) if (!isFunctionExpressionLikeOrFunctionDeclaration(arg0) && !ts.isIdentifier(arg0)) { throw new Error('This is an error of react-refresh/typescript. Please report this problem: Call isHOC before register it'); } if (ts.isIdentifier(arg0)) return { call: callExpr, registers: [] }; const tempVar = createTempVariable(); return { call: factory.updateCallExpression(callExpr, callExpr.expression, void 0, [ factory.createAssignment(tempVar, hooksSignatureMap.get(arg0) || arg0), ...callExpr.arguments.slice(1), ]), registers: [createComponentRegisterCall(tempVar, nameHint + '$' + printNode(callExpr.expression))], }; } function createTempVariable() { const tempVariable = factory.createUniqueName('_react_refresh_temp'); context.hoistVariableDeclaration(tempVariable); return tempVariable; } function visitDeep(file, topLevelDeclaredName, globalRequireForceRefresh) { const usedAsJSXElement = new Set(); const containingHooksOldMap = new Map(); const hooksSignatureMap = new Map(); function trackHooks(comp, call) { const arr = containingHooksOldMap.get(comp) || []; arr.push(call); containingHooksOldMap.set(comp, arr); } function visitor(node) { // Collect JSX create info // <abc /> or <abc> if (ts.isJsxOpeningLikeElement(node)) { const tag = node.tagName; if (ts.isIdentifier(tag) && !isIntrinsicElement(tag)) { const name = tag.text; if (topLevelDeclaredName.has(name)) usedAsJSXElement.add(name); } // Not tracking other kinds of tagNames like <A.B /> or <A:B /> } else if (isJSXConstructingCallExpr(node)) { const arg0 = node.arguments[0]; if (arg0 && ts.isIdentifier(arg0)) { const name = arg0.text; if (topLevelDeclaredName.has(name)) usedAsJSXElement.add(name); } } if (isReactHooksCall(node)) { const parent = findAncestor(node, isFunctionExpressionLikeOrFunctionDeclaration); if (parent) trackHooks(parent, node); } const oldNode = node; // Collect hooks node = ts.visitEachChild(node, visitor, context); const hooksCalls = containingHooksOldMap.get(oldNode); if (hooksCalls && isFunctionExpressionLikeOrFunctionDeclaration(node) && node.body) { const hooksTracker = createTempVariable(); const createHooksTracker = factory.createExpressionStatement(factory.createBinaryExpression(hooksTracker, factory.createToken(ts.SyntaxKind.EqualsToken), factory.createCallExpression(refreshSig, undefined, []))); // @ts-ignore This is a private API. context.addInitializationStatement(createHooksTracker); const callTracker = factory.createCallExpression(hooksTracker, void 0, []); const nextBody = ts.isBlock(node.body) ? updateStatements(node.body, (r) => [factory.createExpressionStatement(callTracker), ...r]) : factory.createComma(callTracker, node.body); const newFunction = updateBody(node, nextBody); const hooksSignature = hooksCallsToSignature(hooksCalls); const { force: forceRefresh, hooks: hooksArray } = needForceRefresh(hooksCalls); const requireForceRefresh = forceRefresh || globalRequireForceRefresh; if (ts.isFunctionDeclaration(newFunction)) { if (newFunction.name) { hooksSignatureMap.set(newFunction, createHooksRegisterCall(hooksTracker, newFunction.name, hooksSignature, requireForceRefresh, hooksArray)); } node = newFunction; } else { const wrapped = createHooksRegisterCall(hooksTracker, newFunction, hooksSignature, requireForceRefresh, hooksArray); hooksSignatureMap.set(newFunction, wrapped); node = newFunction; // if it is an inner decl, we can update it safely if (findAncestor(oldNode.parent, ts.isFunctionLike)) node = wrapped; } } return updateStatements(node, addSignatureReport); } function addSignatureReport(statements) { const next = []; for (const statement of statements) { // Don't want to do a type guard here cause it is safe const signatureReport = hooksSignatureMap.get(statement); next.push(statement); if (signatureReport) next.push(factory.createExpressionStatement(signatureReport)); } return next; } const nextFile = updateStatements(ts.visitEachChild(file, visitor, context), addSignatureReport); return { nextFile, usedAsJSXElement, hooksSignatureMap, }; } function printNode(node) { try { return node.getText(); } catch { return ''; } } function hooksCallsToSignature(calls) { const signature = calls .map((x) => { let assignTarget = ''; if (x.parent && ts.isVariableDeclaration(x.parent)) { assignTarget = printNode(x.parent.name); } let hooksName = printNode(x.expression); let shouldCaptureArgs = 0; // bit-wise parameter position if (ts.isPropertyAccessExpression(x.expression)) { const left = x.expression.expression; if (ts.isIdentifier(left) && left.text === 'React') { hooksName = printNode(x.expression.name); } } if (hooksName === 'useState') shouldCaptureArgs = 1 << 0; else if (hooksName === 'useReducer') shouldCaptureArgs = 1 << 1; const args = x.arguments.reduce((last, val, index) => { if ((1 << index) & shouldCaptureArgs) { if (last) last += ','; last += printNode(val); } return last; }, ''); return `${hooksName}{${assignTarget}${args ? `(${args})` : ''}}`; }) .join('\n'); if (opts.emitFullSignatures !== true && opts.hashSignature) { try { return opts.hashSignature(signature); } catch (e) { } } return signature; } function needForceRefresh(calls) { const externalHooks = []; return { hooks: externalHooks, force: calls.some((x) => { const ownerFunction = findAncestor(x, isFunctionExpressionLikeOrFunctionDeclaration); const callee = x.expression; if (!ownerFunction) return true; if (ts.isPropertyAccessExpression(callee)) { const left = callee.expression; if (ts.isIdentifier(left)) { if (left.text === 'React') return false; const hasDecl = hasDeclarationInScope(ownerFunction, left.text); if (hasDecl) externalHooks.push(callee); return !hasDecl; } return true; } else if (ts.isIdentifier(callee)) { if (isBuiltinHook(callee.text)) return false; const hasDecl = hasDeclarationInScope(ownerFunction, callee.text); if (hasDecl) externalHooks.push(callee); return !hasDecl; } return true; }), }; } /** * @param instance The identifier of the sig instance * @param component The binding component * @param signature The signature of the function * @param forceRefresh Does forceRefresh enabled? * @param trackers A list of custom hooks references */ function createHooksRegisterCall(instance, component, signature, forceRefresh, trackers) { const args = [component]; if (signature.includes('\n')) args.push(factory.createNoSubstitutionTemplateLiteral(signature, signature)); else args.push(factory.createStringLiteral(signature)); if (forceRefresh || trackers.length) args.push(forceRefresh ? factory.createTrue() : factory.createFalse()); if (trackers.length) args.push(factory.createArrowFunction(void 0, void 0, [], void 0, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createArrayLiteralExpression(trackers))); return factory.createCallExpression(instance, void 0, args); } function createComponentRegisterCall(id, name) { return factory.createExpressionStatement(factory.createCallExpression(refreshReg, void 0, [id, factory.createStringLiteral(name)])); } function updateStatements(node, f) { if (ts.isSourceFile(node)) { const sf = factory.updateSourceFile(node, f(node.statements), node.isDeclarationFile, node.referencedFiles, node.typeReferenceDirectives, node.hasNoDefaultLib, node.libReferenceDirectives); return sf; } if (ts.isCaseClause(node)) { const caseClause = factory.updateCaseClause(node, node.expression, f(node.statements)); return caseClause; } if (ts.isDefaultClause(node)) { const defaultClause = factory.updateDefaultClause(node, f(node.statements)); return defaultClause; } if (ts.isModuleBlock(node)) { const modBlock = factory.updateModuleBlock(node, f(node.statements)); return modBlock; } if (ts.isBlock(node)) { const block = factory.updateBlock(node, f(node.statements)); return block; } return node; } function updateBody(node, nextBody) { if (ts.isFunctionDeclaration(node)) { if (!ts.isBlock(nextBody)) throw new TypeError(); return factory.updateFunctionDeclaration(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, nextBody); } else if (ts.isFunctionExpression(node)) { if (!ts.isBlock(nextBody)) throw new TypeError(); return factory.updateFunctionExpression(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, nextBody); } else if (ts.isArrowFunction(node)) { return factory.updateArrowFunction(node, node.modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, nextBody); } return node; } }; function isBuiltinHook(hookName) { switch (hookName) { case 'useState': case 'useReducer': case 'useEffect': case 'useLayoutEffect': case 'useMemo': case 'useCallback': case 'useRef': case 'useContext': case 'useImperativeHandle': case 'useDebugValue': case 'useId': case 'useDeferredValue': case 'useTransition': case 'useInsertionEffect': case 'useSyncExternalStore': case 'useFormState': case 'useActionState': case 'useOptimistic': return true; default: return false; } } function hasDeclarationInScope(node, name) { while (node) { if (ts.isSourceFile(node) && hasDeclaration(node.statements, name)) return true; if (ts.isBlock(node) && hasDeclaration(node.statements, name)) return true; node = node.parent; } return false; } // This function does not consider uncommon and unrecommended practice like declare use var in a inner scope function hasDeclaration(nodes, name) { for (const node of nodes) { if (ts.isVariableStatement(node)) { for (const decl of node.declarationList.declarations) { // binding pattern not checked if (ts.isIdentifier(decl.name) && decl.name.text === name) return true; } } else if (ts.isImportDeclaration(node)) { const clause = node.importClause; const defaultImport = clause && clause.name; const namedImport = clause && clause.namedBindings; if (defaultImport && defaultImport.text === name) return true; if (namedImport && ts.isNamespaceImport(namedImport)) { if (namedImport.name.text === name) return true; } else if (namedImport && ts.isNamedImports(namedImport)) { const hasBinding = namedImport.elements.some((x) => x.name.text === name); if (hasBinding) return true; } } else if (ts.isFunctionDeclaration(node)) { if (!node.body) continue; if (node.name && node.name.text === name) return true; } } return false; } function isIntrinsicElement(id) { return id.text.includes('-') || startsWithLowerCase(id.text) || id.text.includes(':'); } function isImportOrRequireLike(expr) { if (!ts.isCallExpression(expr)) return false; const callee = expr.expression; if (callee.kind === ts.SyntaxKind.ImportKeyword) return true; if (ts.isIdentifier(callee) && callee.text.includes('require')) return true; return false; } function isReactHooksCall(expr) { if (!ts.isCallExpression(expr)) return false; const callee = expr.expression; if (ts.isIdentifier(callee) && callee.text.startsWith('use')) return true; if (ts.isPropertyAccessExpression(callee) && callee.name.text.startsWith('use')) return true; return false; } function findAncestor(node, callback) { while (node) { const result = callback(node); if (result === 'quit') { return undefined; } else if (result) { return node; } node = node.parent; } return undefined; } /** * If it return true, don't track it even it is used as JSX component */ function unwantedComponentLikeDefinition(expr) { if (isImportOrRequireLike(expr)) return true; // `const A = B.X` or `const A = X` if (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr)) return true; if (ts.isConditionalExpression(expr)) return (unwantedComponentLikeDefinition(expr.condition) || unwantedComponentLikeDefinition(expr.whenFalse) || unwantedComponentLikeDefinition(expr.whenTrue)); return false; } function isHigherOrderComponentLike(outExpr) { let expr = outExpr; if (!ts.isCallExpression(outExpr)) return false; while (ts.isCallExpression(expr) && !isImportOrRequireLike(expr)) { const callee = expr.expression; // x.y() or x() const isValidCallee = ts.isPropertyAccessExpression(callee) || ts.isIdentifier(callee); if (isValidCallee) { expr = expr.arguments[0]; // check if arg is also a HOC if (!expr) return false; } else return false; } const isValidHOCArg = isFunctionExpressionLikeOrFunctionDeclaration(expr) || (ts.isIdentifier(expr) && !startsWithLowerCase(expr.text)); return isValidHOCArg; } function isFunctionExpressionLikeOrFunctionDeclaration(node) { if (ts.isFunctionDeclaration(node)) return true; if (ts.isArrowFunction(node)) return true; if (ts.isFunctionExpression(node)) return true; return false; } /** * If the call expression seems like "jsx(...)" or "xyz.jsx(...)" */ function isJSXConstructingCallExpr(call) { if (!ts.isCallExpression(call)) return false; const callee = call.expression; let f = ''; if (ts.isIdentifier(callee)) f = callee.text; if (ts.isPropertyAccessExpression(callee)) f = callee.name.text; if (['createElement', 'jsx', 'jsxs', 'jsxDEV'].includes(f)) return true; return false; } } function startsWithLowerCase(str) { return str[0].toLowerCase() === str[0]; } //# sourceMappingURL=core.js.map