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 {}