Stencil version:
@stencil/[email protected]
I'm submitting a:
[x] bug report
[ ] feature request
[ ] support request => Please do not submit support requests here, use one of these channels: https://stencil-worldwide.herokuapp.com/ or https://forum.ionicframework.com/
Current behavior:
I've created a component that essentially spits out an svg when used. So this component works well when using the test server but when the component has been built and published, it is unable to find the assets.
Expected behavior:
The expected behaviour is that the component should be able to output the icon the same way it does locally
Steps to reproduce:
Import library within Angular and try to utilize web component.
<test-icon icon="arrow-down"></test-icon>
Related code:
Component
import {
Component,
Host,
h,
State,
Prop,
Watch,
getAssetPath,
} from '@stencil/core';
import { getSvgContent } from './util';
@Component({
tag: 'test-icon',
styleUrl: 'icon.css',
assetsDir: 'svg',
shadow: true,
})
export class SmartIcons {
@Prop() icon: string;
@State() private svgContent?: string;
connectedCallback() {
this.loadIcon();
}
@Watch('icon')
loadIcon() {
const svg = `svg/test-${this.icon}.svg`;
const url = getAssetPath(svg);
if (this.icon) {
getSvgContent(url).then((content) => {
this.svgContent = content;
});
}
}
render() {
return (
<Host role="img">
{this.svgContent ? (
<div innerHTML={this.svgContent}></div>
) : (
<div></div>
)}
</Host>
);
}
}
Other information:
zone-evergreen.js:1042 GET http://localhost:4200/svg/smart-arrow-down.svg 404 (Not Found)
(anonymous) @ zone-evergreen.js:1042
getSvgContent @ smart-button_2.entry.js:18
loadIcon @ smart-button_2.entry.js:42
connectedCallback @ smart-button_2.entry.js:36
safeCall @ core-7cea914b.js:853
fireConnectedCallback @ core-7cea914b.js:1018
initializeComponent @ core-7cea914b.js:999
async function (async)
initializeComponent @ core-7cea914b.js:965
(anonymous) @ core-7cea914b.js:1044
invoke @ zone-evergreen.js:359
run @ zone-evergreen.js:124
(anonymous) @ zone-evergreen.js:855
invokeTask @ zone-evergreen.js:391
runTask @ zone-evergreen.js:168
drainMicroTaskQueue @ zone-evergreen.js:559
Promise.then (async)
scheduleMicroTask @ zone-evergreen.js:542
scheduleTask @ zone-evergreen.js:381
scheduleTask @ zone-evergreen.js:211
scheduleMicroTask @ zone-evergreen.js:231
scheduleResolveOrReject @ zone-evergreen.js:845
then @ zone-evergreen.js:955
bootstrapModule @ core.js:40599
./src/main.ts @ main.ts:14
__webpack_require__ @ bootstrap:84
0 @ main.ts:16
__webpack_require__ @ bootstrap:84
checkDeferredModules @ bootstrap:45
webpackJsonpCallback @ bootstrap:32
(anonymous) @ main.js:1
Well, this is due to you can't actually know where the file will be hosted when on production during development. We have the same problem because our assets have cache busting hashes applied to their names, so they differ from development. And also the dev environment doesn't need to be equal to the production one.
So you either slot the images or pass the image path as a property.
You can also adopt your server and serve those image request correctly. (Your component is looking for the file on root/svg..)
Easiest way is to host assets on an unique and consistent path, like a CDN, and use that.
Hmm I think I understand. I guess this is why it works when you include the library via a script tag vs as an npm module.
What is the intention of getAssetPath if it doesn't map to the assets directory that Stencil creates in the build process?
This is quite a pain point.
Yea, stencil could be killed because of this problem. Because the whole point of using stencil is to decouple components to applications, if the components' assets still need applications to provide, I could say that the component isn't fully wrapped.
I wonder why if this is the real thing (you can't serve publicly from inside node_modules, nor you can't copy on runtime to a public folder), it should be clearly explicited here https://stenciljs.com/docs/local-assets
I have this exact issue. I'm packaging my assets with my components using a copy task into the correct directory, but I wanted an easy way to reference it from the component since the component library will be included from the app using a script tag.
There is a workaround: add data-resource-url attribute with the value you want your resources to point to (same path as to where you have your javascript, minus the javascript part) onto the script tag(s) that points to your component library javascript to set the base url for the assets. Example:
<script type="module" src="https://some.cdn.com/some-web-components.esm.js" data-resource-url="https://some.cdn.com/" defer=""></script>
<script src="https://some.cdn.com/some-web-components.js" data-resource-url="https://some.cdn.com/" defer=""></script>
What I don't understand is why stencil doesn't make this the default behavior? This should be how all component libraries work except for some key exceptions where they can specify otherwise. Furthermore, Stencil is already doing all of the hard work in the patchBrowser function when the library starts but just doesn't set resourceUrl unless it's specified in the script attribute. This is super weird to me.
This should be the rule, not the exception.
Also, to add to this, I didn't like the idea that _every. single. addition._ of our component library needs to make sure that our developers knew to set the data-resource-url on the script tags. So, I made a convenience method within the library to get around this. Example is below.
I hope that the Stencil actually fixes this eventually so that I can remove this code once and for all.
``` index.ts
export const namespace = 'your-component-namespace'; // this should be imported into your stencil.config.ts file and used as the namespace for your project
let currentScriptUrl: string;
export function getCurrentScriptUrl():string {
if (!currentScriptUrl) {
const script = Array.from(document.querySelectorAll('script'))
.find(s => new RegExp(${namespace}(\.esm)?\.js$).test(s.src));
currentScriptUrl = script && script.src || document.baseURI;
}
return currentScriptUrl;
}
export function getURL(path: string): string {
return new URL(path, getCurrentScriptUrl()).href;
}
```
Then anywhere in your app, you can do something like <img src={getUrl('./path-to-icons/icon1.svg}/> which should resolve to wherever you're deploying your javascript to :)
I followed this tutorial https://stenciljs.com/docs/local-assets and instead of looking in the www folder, it is looking in the build folder for some reason. http://localhost:3333/build/assets/img/angry.png and I get a 404.
Yup I'm having the exact same issue with loading a svg from getAssetsPath. It just seems to be looking at app root
It seems this function doesn't do what it's described to do, You can't relatively import a file in a component, and use it as a NPM module on an application. Really wish someone from stencil would help figure out this issue.
So, strangely, it works if you have a leading slash only:
img src="/assets/img/profile.png"
No issues whatsoever for me now. But I don't remember it being very clear in the docs.
So, strangely, it works if you have a leading slash only:
img src="/assets/img/profile.png"
No issues whatsoever for me now. But I don't remember it being very clear in the docs.
Are you saying if you use a / in the getAssetsPath it will work when referenced from a npm module?
So, strangely, it works if you have a leading slash only:
img src="/assets/img/profile.png"
No issues whatsoever for me now. But I don't remember it being very clear in the docs.Are you saying if you use a / in the getAssetsPath it will work when referenced from a npm module?
Hmm, I am not sure. I just meant that I was having weird behavior with setting the src of images within my Stencil app. If a component was in a deeper folder, then src="./assets/img/profile.png" would not work, even if to me that makes sense as assets is at the root. But if I used "../assets/img/profile.png" in this component in a subfolder then the image would show. But once I changed all of them to "/assets/img/{whatever}" then they all worked, even without the getAssetsPath. 🤷♂
I guess my question is just, if I have a component that includes an asset, and it's in the dist. Someone who installs the component, and used it, that reference would be the node_module? or what? How can I safely include assets and package them up?
I ended up embedding both images and fonts as base64, some problems may arise with CSP, but it gets the work done.
also having the same issue. I don't get the point of using getAssetPath function
I found this way to get the assets of my component into the app where I used just by adding this config in my angular.json file. It might be similar to other frameworks:
...
assets: [ {
"glob": "**/*",
"input": "./node_modules/my-component/path/to/assets/",
"output": "/assets/"
}]
I found this way to get the assets of my component into the app where I used just by adding this config in my angular.json file. It might be similar to other frameworks:
... assets: [ { "glob": "**/*", "input": "./node_modules/my-component/path/to/assets/", "output": "/assets/" }]
So while this worked and was my solution as well for a library you're trying to distribute this just copies from node modules to their local app. You'd have to do this solution for react and other frameworks too. I wish stencil had a lazy loading dynamic import
There is no update on this from the Ionic/Stencil team?
Is it posible to encode the image to base64 on compile time? So that what the base64 string is included and wrapped inside the web component and doesn't rely on a URL.
It would be very useful for a small static asset like a logo.
Is there any workaround?
base64 embedding
Is there any workaround?
base64 embedding
…
Is there any workaround?
Ok, thank you!
I followed the documentation on https://stenciljs.com/docs/local-assets as well and was having issues. Finally I decide to try and restart the dev server with the code unchanged from how the documentation specifies it to be, and the asset loaded to my component. The assets directory show in both my output targets 'dist' and 'www'(build directory).
Yes the assets directory is included in www and dist, but are you able to get it to work if you include the component in an external application? The problem for me is that getAssetPath is then not returning the correct path, which I described in https://github.com/ionic-team/stencil/issues/2269
Yes it is working. If I copy the dist folder to another location, and reference the ./dist/"namespace"/"namespace".esm.js file in a script tag in the head of my html. I have no issues using the component along with the svg asset that I have in the component.
This is not the case for the 'www' output target though.
I use "Copy"
{
type: 'www',
copy: [
{ src: 'assets', dest: 'build/assets' } -> This copy my assets folder in to build
],
}
Ok. But as far as I understand, the point is that you should not have to hardcode the path to the assets (which is also hard, since it seems to vary between www and dist builds), and you should also not have to do any copy actions of your own. Instead, assets are supposed to be copied automatically and then you should be able to use getAssetPath to get the path of wherever assets were copied.
@bjolletz Exactly, i think that, this is a Stencil logic error, but we can use "Copy" while they solve it.
I believe the preferred way to handle this currently is to use a copy task in your stencil.config.ts file. For example, if a component ships a set of web fonts, this would be used to copy those out of that collection into the stencil site that will be hosting those assets:
export const config: Config = {
outputTargets: [
{
type: 'www',
copy: [
{ src: '../node_modules/my-collection/www/assets/fonts', dest: 'assets/fonts' }
]
},
],
}
You'd want to make sure those assets are being published to your npm package as well. This is how we've done it in the past here at Ionic for other shared collections.
Other solutions would require custom runtime approaches to loading files/svgs. We do this in Ionicons 5, for example: https://github.com/ionic-team/ionicons/blob/master/src/components/icon/icon.tsx
```
It's good that there are workarounds and I'm thankful for the suggestions and examples! However, the way the documentation is written, it sounds like this should not be necessary and that it should work with getAssetPath. If this is not the case, it would be good if documentation could be updated. I've spent many hours trying to get the documented way to work.
Any progress on this issue?
I'm facing the very same problem and wanted a more decoupled way to do this rather than workarounds. Though they might work i feel that docs really makes us think of something Stencil actually won't do.
At least fix the docs, so no one gets frustrated with Stencil as i'm.
Assuming you have an assets directory defined like this:
@Component({
tag: 'my-icon',
assetsDirs: ['icons']
})
If you're using the default dist output target (i.e. the lazy loader) then at build time, the assets will be copied automagically for you to this directory:
dist/your-lib-here/icons
And you'll be able to reference an asset like this:
import { getAssetPath } from '@stencil/core';
const iconPath = getAssetPath(`./icons/${name}.svg`)
However, if you're using the dist-custom-elements-bundle then assets will _not_ be copied automatically for you. In this case, you must add your own copy task to whatever bundler you're using (webpack, Rollup, etc.).
Stencil has no way of knowing how you're bundling your app and where it will ultimately exist, so if you're using this output target, you need to copy assets to the right location as part of your own build process.
I created example projects for webpack and Rollup that import the custom elements bundle for Shoelace (built with Stencil). The relevant copy tasks are highlighted:
Let me know if this makes things more clear and I'll update Stencil's docs so it's less confusing. 😄
Most helpful comment
What is the intention of
getAssetPathif it doesn't map to theassetsdirectory that Stencil creates in the build process?