UNPKG

@open-wc/dev-server-hmr

Version:
201 lines (176 loc) 6.4 kB
/** @typedef {import('@babel/core').PluginObj} PluginObj */ /** @typedef {import('@babel/types').ClassDeclaration} ClassDeclaration */ /** @typedef {import('@babel/types').Expression} Expression */ /** @template T @typedef {import('@babel/core').NodePath<T>} NodePath<T> */ /** * @typedef {object} BaseClass * @property {string} name * @property {string} [import] */ /** * @typedef {object} Decorator * @property {string} name * @property {string} [import] */ /** * @typedef {object} FunctionOption * @property {string} name * @property {string} [import] */ /** * @typedef {object} BabelPluginWcHmrOptions * @property {string} rootDir * @property {BaseClass[]} [baseClasses] * @property {Decorator[]} [decorators] * @property {FunctionOption[]} [functions] * @property {string[]} [patches] */ const path = require('path'); const { findDefinedCustomElement } = require('./customElementsDefine.js'); const { findDecoratedCustomElement } = require('./decorators.js'); const { injectRegisterClass, injectRuntime, isClassRegistered, findComponentName, } = require('./inject.js'); const { parseOptions, singlePath, addToSet } = require('./utils.js'); const { isFunctionComponent } = require('./functions.js'); const { getImportedVariableNames } = require('./getImportedVariableNames.js'); const { implementsBaseClass } = require('./class.js'); const { createError } = require('../utils.js'); /** @returns {PluginObj} */ function babelPluginWcHmr() { return { visitor: { Program(program) { if (!this.filename) throw createError('Missing filename'); const resolvedFilename = path.resolve(this.filename); const opts = parseOptions(/** @type {BabelPluginWcHmrOptions} */ (this.opts)); const baseClasses = opts.baseClasses || []; const decorators = opts.decorators || []; const functions = opts.functions || []; /** @type {Set<string>} */ const baseClassNames = new Set(); /** @type {Set<string>} */ const decoratorNames = new Set(); /** @type {Set<string>} */ const functionNames = new Set(); let injectedRegister = false; let anonymousClassCount = -1; baseClassNames.add('HTMLElement'); /** * @param {NodePath<ClassDeclaration> | NodePath<Expression>} nodePath */ function injectRegister(nodePath) { if (isClassRegistered(nodePath)) { return; } let componentName = findComponentName(nodePath); if (!componentName) { anonymousClassCount += 1; componentName = `anonymous${anonymousClassCount}`; } injectRegisterClass(nodePath, componentName); injectedRegister = true; } // add decorators that don't require their import to be checked addToSet( baseClassNames, baseClasses.filter(b => !b.import).map(b => b.name), ); addToSet( decoratorNames, decorators.filter(c => !c.import).map(d => d.name), ); addToSet( functionNames, functions.filter(c => !c.import).map(d => d.name), ); program.traverse({ ImportDeclaration(importDecl) { // find all base classes and decorators that match this import const result = getImportedVariableNames( baseClasses, decorators, functions, importDecl, resolvedFilename, opts.rootDir, ); addToSet(baseClassNames, result.baseClassNames); addToSet(decoratorNames, result.decoratorNames); addToSet(functionNames, result.functionNames); }, }); program.traverse({ CallExpression(callExpr) { if (callExpr.isCallExpression() && isFunctionComponent(callExpr, functionNames)) { injectRegister(/** @type {NodePath<Expression>} */ (callExpr)); return; } const callee = callExpr.get('callee'); const args = callExpr.get('arguments'); if (!singlePath(callee) || !Array.isArray(args)) { return; } if (callee.isMemberExpression()) { // this might be a customElements.define call const definedCustomElement = findDefinedCustomElement(callee, args); if (!definedCustomElement) { return; } injectRegister(definedCustomElement); return; } if (decoratorNames.size > 0) { if (callee.isIdentifier()) { // this might be a decorated class const decoratedCustomElement = findDecoratedCustomElement( decoratorNames, callee, args, ); if (!decoratedCustomElement) { return; } injectRegister(decoratedCustomElement); } } }, ClassDeclaration(classDecl) { // this is a class declaration like class A extends HTMLElement {} if (implementsBaseClass(classDecl, baseClassNames)) { injectRegister(classDecl); } }, ClassExpression(classExpr) { const { parent, parentPath: varDeclarator } = classExpr; if (!parent || !varDeclarator.isVariableDeclarator()) { return; } if (!varDeclarator || !varDeclarator.parentPath.isVariableDeclaration()) { return; } const id = varDeclarator.get('id'); if (!singlePath(id) || !id.isIdentifier()) { return; } const injectScope = varDeclarator.parentPath; if (!injectScope) { return; } // this is a class expression assignment like const A = class B extends HTMLElement {} if (classExpr.isExpression() && implementsBaseClass(classExpr, baseClassNames)) { injectRegister(classExpr); } }, }); if (injectedRegister) { injectRuntime(opts, program); } }, }, }; } module.exports = babelPluginWcHmr;