feat: add automatic customElements.define/@customElement detection for lit

This commit is contained in:
Nate Moore 2021-11-01 12:45:25 -05:00
parent 2efee75989
commit 5d96f98564
4 changed files with 118 additions and 1 deletions

View file

@ -0,0 +1,6 @@
---
'@astrojs/renderer-lit': minor
'astro': patch
---
Add support for automatically detecting @customElement("my-element")`and`customElements.define("my-element", MyElement)` when using Lit SSR.

View file

@ -41,7 +41,7 @@ class Metadata {
const id = specifier.startsWith('.') ? new URL(specifier, this.fileURL).pathname : specifier;
for (const [key, value] of Object.entries(module)) {
if (isCustomElement) {
if (key === 'tagName' && Component === value) {
if ((key === 'tagName' || key === '__astroTagName') && Component === value) {
return {
componentExport: key,
componentUrl: id,

View file

@ -1,3 +1,5 @@
import pluginLit from './vite-plugin-lit.js';
// NOTE: @lit-labs/ssr uses syntax incompatible with anything < Node v13.9.0.
// Throw an error if using that Node version.
@ -13,6 +15,9 @@ export default {
hydrationPolyfills: ['./hydration-support.js'],
viteConfig() {
return {
plugins: [
pluginLit(),
],
optimizeDeps: {
include: [
'@astrojs/renderer-lit/client-shim.js',

View file

@ -0,0 +1,106 @@
import { parse } from 'acorn';
import { walk } from 'estree-walker';
import MagicString from 'magic-string';
/**
* Determine if Vite is in SSR mode based on options
* https://github.com/vitejs/vite/discussions/5109#discussioncomment-1450726
*
* @param options boolean | { ssr: boolean }
* @returns boolean
*/
function isSSR(options) {
if (options === undefined) {
return false;
}
if (typeof options === 'boolean') {
return options;
}
if (typeof options == 'object') {
return !!options.ssr;
}
return false;
}
// This matches any JS-like file (that we know of)
// See https://regex101.com/r/Cgofir/1
const SUPPORTED_FILES = /\.([cm]?js|jsx|[cm]?ts|tsx)$/;
const IGNORED_MODULES = [/astro\/dist\/runtime\/server/, /\/renderer-lit\/server/, /\/@lit\//];
function scanForTagName(code) {
const ast = parse(code, {
sourceType: 'module'
})
let tagName;
walk(ast, {
enter(node, parent) {
if (tagName) {
return this.skip();
}
// Matches `customElement("my-component")`, which is Lit's @customElement("my-component") decorator
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'customElement') {
const arg = node.arguments[0];
if (arg.type === 'Literal') {
tagName = arg.raw
}
}
// Matches `customElements.define("my-component", thing)`
if (node.type === 'MemberExpression' && node.object.name === 'customElements' && node.property.name === 'define') {
const arg = parent.arguments[0];
if (arg.type === 'Literal') {
tagName = arg.raw
} else {
tagName = arg.name
}
}
}
})
return tagName;
}
/**
* @returns {import('vite').Plugin}
*/
export default function pluginLit() {
return {
name: '@astrojs/vite-plugin-lit',
enforce: 'post',
async transform(code, id, opts) {
const ssr = isSSR(opts);
// If this isn't an SSR pass, `fetch` will already be available!
if (!ssr) {
return null;
}
// Only transform JS-like files
if (!id.match(SUPPORTED_FILES)) {
return null;
}
// Optimization: only run on probable matches
if (!code.includes('customElement') && !code.includes('lit')) {
return null;
}
// Ignore specific modules
for (const ignored of IGNORED_MODULES) {
if (id.match(ignored)) {
return null;
}
}
const tagName = scanForTagName(code);
if (!tagName) {
return null;
}
const s = new MagicString(code);
s.append(`export const __astroTagName = ${tagName};`);
const result = s.toString();
const map = s.generateMap({
source: id,
includeContent: true,
});
return { code: result, map };
},
};
}