Can't build app when a custom template is passed to @customElement decorator

Hello,

For some reason, importing a custom template and passing it into the @customElement decorator causes an error when running npm run build. There are no errors when running npm start.

I am using Typescript and Vite. You can refer to this repository for a minimal reproduction of the problem.

import { customElement } from "aurelia";
import template from "./my-app.html"; //<-----------

@customElement({ name: "my-app", template }) //<--------
export class MyApp {
  public message = "Hello World!";
}

I have made sure that there is no invalid syntax in the template. This is the error I get when building the app:

Please help. Thank you.

You need to use the ?raw suffix on the import due to the way Vite treats imported resources. This will signal that the import should be imported as a string (which is what template expects): Static Asset Handling | Vite - I am going to update the docs to mention this too, as it is something that can catch a lot of devs offguard.

1 Like

To anyone else encountering this, I’ve got a PR up that handles this in the Aurelia Vite plugin so you don’t have to manually suffix ?raw to HTML imports.

1 Like

Here’s what I came up with which works so far:

import { PluginOption } from 'vite';

type HtmlCssImportPatchOptions = {
  ignorePattern?: {
    css?: Array<string | RegExp>;
    html?: Array<string | RegExp>;
  };
};

/**
 * This adds `?inline` at the end of all css/scss imports
 * and `?raw` at the end of all html imports inside `.ts` files
 *
 * This is needed so we can supply custom template and style on the `@capElement` decorator
 *
 * @example
 *
 * ```ts
 * import style from './index.css'
 * // becomes
 * // import style from './index.css?inline'
 *
 * import template from './index.template'
 * // becomes
 * // import template from './index.html?raw'
 * ```
 */
export function htmlCssImportPatch(options: HtmlCssImportPatchOptions = {}): PluginOption {
  return {
    name: 'vite-plugin-html-css-import-patch',
    enforce: 'pre',
    transform(code, id) {
      if (id.endsWith('.ts')) {
        const shouldIgnore = (patterns: Array<string | RegExp> | undefined, path: string) => {
          return patterns?.some(pattern => (typeof pattern === 'string' ? path.includes(pattern) : pattern.test(path)));
        };

        const newCode = code
          .replace(/import\s+([^\s]+)\s+from\s+(['"])(.*?\.html)\2/g, (match, variable, quote, path) => {
            if (shouldIgnore(options.ignorePattern?.html, path)) return match;
            return `import ${variable} from ${quote}${path}?raw${quote}`;
          })
          .replace(/import\s+([^\s]+)\s+from\s+(['"])(.*?\.(?:css|scss))\2/g, (match, variable, quote, path) => {
            if (shouldIgnore(options.ignorePattern?.css, path)) return match;
            return `import ${variable} from ${quote}${path}?inline${quote}`;
          });

        return {
          code: newCode,
          map: null
        };
      }
      return null;
    }
  };
}

Then, in my vite.config.ts,

import { htmlCssImportPatch } from './plugins/html-css-import-patch';

export default defineConfig(({ mode }) => {
  return {
    //...other configs,
    plugins: [
      htmlCssImportPatch(),
      aurelia({
        useDev: true,
        defaultShadowOptions: { mode: 'open' }
      }) as PluginOption[],
      // ....other plugins here
    ]
  };
});

NOTE: In our project, we needed the ?inline on css/scss imports so we can customize the styles as well. We have a @customElement decorator wrapper which looks like this:

import { type Constructable, type Key } from '@aurelia/kernel';
import { type PartialCustomElementDefinition } from '@aurelia/runtime-html';
import { customElement, ShadowDOMRegistry } from '@aurelia/runtime-html';

const shadowOptions = { mode: 'open' } as const;
export function xElement(
  name: string,
  def: Omit<PartialCustomElementDefinition, 'name'> & { style?: string },
  dependencies: Key[] = []
) {
  const { style, ..._def } = def;
  if (style) {
    dependencies.push(new ShadowDOMRegistry([style]));
  }

  return <T extends Constructable>(Type: T, context: ClassDecoratorContext) =>
    customElement({ ..._def, name, shadowOptions, dependencies })(Type, context);
}

Then, if we want to add a custom style and template to a component,

import { xElement } from 'lib/framework';
import template from './some-foo.html';
import style from './some-foo.css';

@xElement('some-foo', { template, style })
export class Foo {}