developit / htm

Hyperscript Tagged Markup: JSX alternative using standard tagged templates, with compiler support.
Apache License 2.0
8.68k stars 170 forks source link

Add feature in babel-plugin-htm to replace import html with pragma #118

Closed zaygraveyard closed 4 years ago

zaygraveyard commented 5 years ago

First off, thanks for this truly awesome library.

When transpiling my code with the babel-plugin-htm plugin, I noticed that the pragma is not imported (in my case I'm using htm/preact and need h to be imported from preact)

So I created a wrapper for this plugin that does just that, it works for me but I know it's very rough around the edges.

And thought why not share it with the community, it might be included in upstream

async function getHtmPlugin() {
  const htm = (await import('babel-plugin-htm')).default;
  return function({ types: t }, options = {}) {
    const htmPlugin = htm({types: t}, options);
    const pragmaString = options.pragma || 'h';
    const htmlName = options.tag || 'html';
    const importDeclaration = pragmaImport(options.import || false);

    function pragmaImport(imp) {
      if (imp === false) {
        return null;
      }
      const pragmaRoot = t.identifier(pragmaString.split('.')[0]);
      const { module, export: export_ } = typeof imp !== 'string' ? imp : {
        module: imp,
        export: null
      };

      let specifier;
      if (export_ === '*') {
        specifier = t.importNamespaceSpecifier(pragmaRoot);
      }
      else if (export_ === 'default') {
        specifier = t.importDefaultSpecifier(pragmaRoot);
      }
      else {
        specifier = t.importSpecifier(pragmaRoot, export_ ? t.identifier(export_) : pragmaRoot);
      }
      return t.importDeclaration([specifier], t.stringLiteral(module));
    }

    function patternStringToRegExp(str) {
      const parts = str.split('/').slice(1);
      const end = parts.pop() || '';
      return new RegExp(parts.join('/'), end);
    }

    return {
      ...htmPlugin,
      visitor: {
        ...htmPlugin.visitor,
        Program: {
          exit(path, state) {
            if (state.get('hasHtm') && importDeclaration) {
              path.unshiftContainer('body', importDeclaration);
            }
          },
        },
        TaggedTemplateExpression(path, state) {
          const tag = path.node.tag.name;
          if (htmlName[0]==='/' ? patternStringToRegExp(htmlName).test(tag) : tag === htmlName) {
            state.set('hasHtm', true);
          }
          htmPlugin.visitor.TaggedTemplateExpression(path, state);
        },
        ImportDeclaration(path) {
          path.node.specifiers = path.node.specifiers.filter(
            specifier => specifier.local.name !== htmlName
          );
          if (path.node.specifiers.length === 0) {
            path.remove();
          }
        },
      }
    };
  };
}
zaygraveyard commented 5 years ago

I found a different simpler way to do more or less the same thing. It only handles imports from htm/react and htm/preact

async function getHtmPlugin() {
  const htm = (await import('babel-plugin-htm')).default;
  return function({types: t}, options = {}) {
    const pragmaString = options.pragma || 'h';
    const pragmaRoot = pragmaString.split('.')[0];
    const htmlName = options.tag || 'html';
    const preactHSpecifier = t.importSpecifier(
      t.identifier(pragmaRoot),
      t.identifier('h')
    );

    return {
      name: 'htm-importer',
      inherits: htm,
      visitor: {
        ImportDeclaration(path) {
          const {node} = path;
          if (node.source.value === 'htm/preact') {
            const pragmaImported = node.specifiers.some(
              (specifier) =>
                specifier.imported.name === 'h' &&
                specifier.local.name === pragmaRoot
            );
            if (pragmaImported) {
              node.specifiers = node.specifiers.filter(
                (specifier) =>
                  !(
                    specifier.imported.name === 'html' &&
                    specifier.local.name === htmlName
                  )
              );
            } else {
              node.specifiers = node.specifiers.map(function(specifier) {
                if (
                  specifier.imported.name === 'html' &&
                  specifier.local.name === htmlName
                ) {
                  return preactHSpecifier;
                }
                return specifier;
              });
            }
            if (node.specifiers.length === 0) {
              path.remove();
            } else if (
              node.specifiers.length === 1 &&
              node.specifiers[0].imported.name === 'h'
            ) {
              path.get('source').replaceWith(t.stringLiteral('preact'));
            }
          } else if (node.source.value === 'htm/react') {
            const htmlImported = node.specifiers.some(
              (specifier) =>
                specifier.imported.name === 'html' &&
                specifier.local.name === htmlName
            );
            if (htmlImported) {
              node.specifiers = node.specifiers.filter(
                (specifier) =>
                  !(
                    specifier.imported.name === 'html' &&
                    specifier.local.name === htmlName
                  )
              );
              path.insertBefore(
                t.importDeclaration(
                  [t.importDefaultSpecifier(t.identifier(pragmaRoot))],
                  t.stringLiteral('react')
                )
              );
              if (node.specifiers.length === 0) {
                path.remove();
              }
            }
          }
        },
      },
    };
  };
}
developit commented 4 years ago

@zaygraveyard this is awesome! I would love to have the functionality supported natively in babel-plugin-htm if we can!

I think the configuration for this could even be the same as the similar feature in babel-plugin-jsx-to-htm: https://github.com/developit/htm/tree/master/packages/babel-plugin-transform-jsx-to-htm#auto-importing-the-tag

zaygraveyard commented 4 years ago

@developit thank you! :smile: I was actually highly inspired by the "Auto-importing the tag" feature when creating the first version I'll submit a PR integrating the first version into babel-plugin-htm

zaygraveyard commented 4 years ago

@developit quick question Do I remove the import of the tag? Or leave it and let the tree-shaker take care of it?

NB: The version above removes the import but doesn't look for the declaration of the tag in the case of const html = htm.bind(h)

zaygraveyard commented 4 years ago

PR #131

zaygraveyard commented 4 years ago

Closed by #131