Trying to use a webpack raw-loader
for an HTML file inline resulted in an error: TS2307: Cannot find module
. Here’s how to fix it!
I’m using web components a lot! I really prefer them over frameworks. They are simple, native, and easy to use. I also like typescript
and webpack
. Combining them is a joy.
Thing is – I’d like to separate my html
files from my ts
files. I will then import the html
as a raw string to add to my component’s innerHTML (or shadowDOM).
I’ll first install the loader: yarn add -D raw-loader
. Once done, I can use it in my code.
So my code would look kind of like this:
import './app.element.scss';
import * as template from '!!raw-loader!./app.element.html';
export class AppElement extends HTMLElement {
public static observedAttributes = [];
connectedCallback() {
const title = 'my-component';
this.innerHTML = template.default.replace('${title}', title);
}
}
customElements.define('yonatan-root', AppElement);
Note that we are using template.default
here because we are importing a module that doesn’t have an explicit default export. In this case, it is translated to { default: <our content> }.
In a naive setup it will fail on this:
TS2307: Cannot find module '!!raw-loader!./app.element.html'
That’s because typescript
needs a module definition. Let’s set one up. Create a file called raw-loader.d.ts
and paste the following code:
declare module '!!raw-loader!*' {
const contents:{default: string}
export = contents
}
Note that here we set the contents
as {default: string}
because we expect to receive a module that has a default
property.
We will now use it in our tsConfig.json
and add a line inside our compilerOptions
:
"compilerOptions": {
"types": ["raw-loader.d.ts", "node"]
},
Trying to compile now will work as expected.
If we’d like to make our code a bit nicer, we can use the allowSyntheticDefaultImports
compiler option.
In our tsConfig.json
we will add allowSyntheticDefaultImports: true
to the compilerOptions
and our code can now look like this:
import './app.element.scss';
import template from '!!raw-loader!./app.element.html';
export class AppElement extends HTMLElement {
public static observedAttributes = [];
connectedCallback() {
const title = 'my-component';
this.innerHTML = template.replace('${title}', title);
}
}
customElements.define('yonatan-root', AppElement);
The differences are bold and underlined. We are now using the default
import syntax. This also results in the removal of the .default
from the template
usage.
This will now fail, because we’ve set our raw-loader
module to return a module. We will just replace {default: string}
with string
and we’re good to go:
declare module '!!raw-loader!*' {
const contents: string
export = contents
}
Here’s the full solution in a gist:
<h1>Welcome to ${title}</h1> |
import './app.element.scss'; | |
import template from '!!raw-loader!./app.element.html'; | |
export class AppElement extends HTMLElement { | |
public static observedAttributes = []; | |
connectedCallback() { | |
const title = 'My Component'; | |
this.innerHTML = template.replace('${title}', title); | |
} | |
} | |
customElements.define('yonatan-root', AppElement); |
declare module '!!raw-loader!*' { | |
const contents: string | |
export = contents | |
} |
{ | |
"compilerOptions": { | |
"types": ["raw-loader.d.ts", "node"], | |
"allowSyntheticDefaultImports": true | |
} |
Final words
If you found this helpful, I’ve written an article on how to tweak webpack configuration to do the same without inline loaders.
This didn’t quite work for me as written; TypeScript still couldn’t find the module. The following post details some tweaks I had to make to get it to work: https://medium.com/@thisisjimkeller/loading-custom-modules-in-typescript-4-ea9b5293137e
Thanks for the heads up.
Mind sharing your use case? I could not reproduce the error while using this, but it might be our
tsconfig
files differ in some way.