Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(astro): add Built-in SVG component support #12067

Merged
merged 26 commits into from
Nov 19, 2024

Conversation

stramel
Copy link
Contributor

@stramel stramel commented Sep 25, 2024

Changes

This PR implements Built-in SVG Components following up on the discussions on withastro/roadmap#699

By default, SVG files will be components and can be imported by referencing their file path and then treating them like a component. Take this for example:

previously:

---
import Image from 'astro:assets'
import logo from './path/to/svg/file.svg'
---

<div set:html={logo}  style={{height: '24px', width: '24px' }} />
<!-- or -->
<Image src={logo} width={24} height={24} />

now:

---
import Logo from './path/to/svg/file.svg';
---

<Logo size={24} />

GOALS:

  • Allow users to import and render local .svg files as if they were an Astro component
  • Allow passing props onto the root <svg> element while overriding the existing props
  • Automate best practices for to minimize performance footguns
    • Including many inlined <svg> on a page can hurt browser parsing performance. We can automatically reduce the number of nodes by using <symbol> and <use>.
    • Inline <svg> often have xmlns and xmlns:xlink and version attributes. These are not needed in HTML5 and we can automatically drop them to save a few bytes.
  • Support this in a backwards-compatible way, meaning the current public interface of an .svg import remains unchanged
  • Stretch: support rendering .svg components in frameworks without inlining them into the client side JavaScript
    • Creating framework components for .svg icons is an anti-pattern. It’s a terrible idea that bloats your JavaScript and creates all sorts of headaches for HMR and clientside performance in production.
    • If we provide a great <Icon> component for each supported framework, we’ll make it easy for users to avoid this anti-pattern. It’s so common now because there aren’t many other good solutions.

This builds a foundation for us which can be extended in the future within the core astro and even by third-parties. For instance using astro-icon, which has been the critical plugin when it comes to icons, as a playground, I was able to extend the foundation built here to add dynamic icon import for Iconify.

FUTURE:

  • Optimization: Allow hooking in SVGO or other SVG Optimizers

/cc @natemoo-re

Testing

I have ensured that existing tests are functioning properly and added additional suite for SVG Components.

Manually, I have updated the blog example to include using the SVG components. I have also tested locally against astro-icon and its demo application. Will work on some testing for this change.

Docs

Docs added in withastro/docs#9911

Copy link

changeset-bot bot commented Sep 25, 2024

🦋 Changeset detected

Latest commit: 7b4a3a9

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added pkg: example Related to an example package (scope) pkg: astro Related to the core `astro` package (scope) semver: major Change triggers a `major` release labels Sep 25, 2024
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is blocked because it contains a major changeset. A reviewer will merge this at the next release if approved.

Copy link
Member

@natemoo-re natemoo-re left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few suggestions, but this looks really good!

Comment on lines 126 to 128
/**
* Bypasses automatic sprite optimization by directly inlinining the SVG
*/
inline?: boolean
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After sitting with this for a while, inline behavior should be the default. There are a few edge-cases that have come up with dynamic client-side behavior in astro-icon, so while I still think the optimization is very useful, we should prioritize flexibility by keeping the sprite behavior opt-in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I think especially with the SVG imports that developers wouldn't necessarily assume the optimization by default.

My first thought is to offer a "mode" that allows for "sprite" or "inline" ("inline" being the default) that is configurable in the Astro Config. I assume we'll later have some other properties under and "svg" scope such as an "image service" (like svgo).

I have also seen an interesting use-case (natemoo-re/astro-icon#238) that we might be able to support later for exporting symbols instead. This would be fairly trivial with a "mode" and might enable some better DX around framework usages.

I would like to discuss and perhaps iterate a bit on the way we allow developers to opt-in/out of inline/sprite usages. Will bring this up in the Stage 3 RFC.

packages/astro/src/assets/runtime.ts Show resolved Hide resolved
Comment on lines +46 to +49
// Attach file data for SVGs
if (fileMetadata.format === 'svg') {
emittedImage.contents = fileData;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love @Princesseuh's take on this. Any expected footguns with including the file content for SVGs in the image metadata?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have a lot of SVGs, this can be problematic for SSR since it'll get in your bundle, that's my only concerns

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Unfortunately that will be unavoidable for this feature.

I think the DX improvement is worth the tradeoff, though!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely outside my realm of expertise. I'll defer to whatever the consensus is here :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something we could do one day (definitely outside of this PR), though it'd make SVG icons somewhat slower in SSR is add a lazy data property or something, that's essentially async () => await fetch(this.src) in SSR and something else in SSG so that you can load the data without it being added to the bundle.

packages/astro/src/assets/utils/svg.ts Outdated Show resolved Hide resolved
packages/astro/test/core-image-svg.test.js Show resolved Hide resolved
packages/astro/test/core-image-svg.test.js Show resolved Hide resolved
packages/astro/src/assets/utils/svg.ts Show resolved Hide resolved
packages/astro/client.d.ts Outdated Show resolved Hide resolved
Comment on lines +46 to +49
// Attach file data for SVGs
if (fileMetadata.format === 'svg') {
emittedImage.contents = fileData;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have a lot of SVGs, this can be problematic for SSR since it'll get in your bundle, that's my only concerns

@github-actions github-actions bot removed the pkg: example Related to an example package (scope) label Oct 2, 2024
@Princesseuh Princesseuh added semver: minor Change triggers a `minor` release and removed semver: major Change triggers a `major` release labels Oct 4, 2024
@stramel
Copy link
Contributor Author

stramel commented Oct 5, 2024

I have added it behind an experiment flag called svg. With this change, I swapped the default implementation to be a more straightforward approach of inlining the SVG. In order to control this a bit better, I have a mode setting in the svg config to apply a default, which is defaulted to inline. I also changed the inline prop to a mode prop that will allow configuring between inline and sprite on specific SVGs.

@natemoo-re would love your opinion on this approach for configuring the sprite vs inline method

@stramel stramel force-pushed the add-svg-support branch 2 times, most recently from ad55d4e to bf5e98b Compare October 5, 2024 05:00
@ematipico
Copy link
Member

ematipico commented Oct 30, 2024

@stramel

Since we are about to send this PR to next, you'll have to send the docs PR to their respective branch, which is 5.0.0-beta.

As for where, I would start with the Images guide: https://docs.astro.build/en/guides/images/

https://github.com/withastro/docs/blob/5.0.0-beta/src/content/docs/en/guides/images.mdx

As for where in the page, I don't exactly know, but I would start with a new paragraph (maybe an h2 heading) and send the PR, then Sarah and team will take it from there, and will help you to craft the content.

packages/astro/src/assets/runtime.ts Outdated Show resolved Hide resolved
packages/astro/src/assets/runtime.ts Outdated Show resolved Hide resolved
Comment on lines 16 to 19
/**
* Make sure these IDs are kept on the module-level so they're incremented on a per-page basis
*/
let ids = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, if this is on the module-level (and also globally as a singleton), wouldn't this value not be incremented on a per-page basis? If per page, I think it should be bound to the result from createComponent as a single result is created when rendering per page. So maybe we can use a WeakMap here to track the ids per result, which we can also use it to track the first render instead of using WeakSet<Response>?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that WeakMap seems like a good idea here!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite following how to use the WeakMap here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestion here is to use const ids = new WeakMap<SSRResult, number> instead to track the id per result (from createComponent)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having trouble getting this to work. I'll give it a shot again tomorrow. Maybe I'm just tired 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll help you out :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I missed that we would still need to keep the counter/ids. I'm curious what the WeakMap adds in this case since we're still generating the ids by the counter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need the counter variable there. What I mean is that the WeakMap values are the counter itself that starts from 0 on first assigned.

if (!ids.has(result)) ids.set(result, 0)
const id = ids.get(result)
ids.set(result, id + 1)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give it a shot but I was having issues previously with this.

Copy link
Member

@natemoo-re natemoo-re left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good!

packages/astro/src/assets/runtime.ts Outdated Show resolved Hide resolved
Comment on lines 16 to 19
/**
* Make sure these IDs are kept on the module-level so they're incremented on a per-page basis
*/
let ids = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that WeakMap seems like a good idea here!

.changeset/blue-socks-doubt.md Outdated Show resolved Hide resolved
@ematipico
Copy link
Member

ematipico commented Nov 6, 2024

@stramel While I was working on the PR, I noticed that you added a new configuration in Astro, which is fine, however the RFC doesn't mention anything about it. We should update the RFC and explain the options.

Also, here's some feedback regarding experimental options. There aren't written rules, but in the past I was in your same situation and I went for this route:

export default defineConfig({
	svg: {
		mode: "inline"
	},
	experimental: {
		svg: true
	}
})

We still need to have the experimental flag, which stays a boolean every time. Then, you create a new section in the Astro configuration. You're still free to change it as you see fit, because it's part of the experimental feature, however I found that the maintenance of these two separate sections is easier for us maintainers.

Copy link
Member

@sarah11918 sarah11918 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for this amazing feature @stramel ! I think people are going to really enjoy this! 🙌

I've done a quick pass on the documentation (changeset and flag docs) and you'll find some suggestions below for your consideration!

I know Nate has a docs PR going, and it seems like the mode property is newer than that, so we'll just make sure that we've got everything we need in the docs PR, too, before docs totally signs off on this! 🚀

.changeset/blue-socks-doubt.md Outdated Show resolved Hide resolved
packages/astro/src/types/public/config.ts Outdated Show resolved Hide resolved
packages/astro/src/types/public/config.ts Outdated Show resolved Hide resolved
* - `inline`: Astro will inline the SVG content into your HTML output.
* - `sprite`: Astro will generate a sprite sheet with all imported SVG files.
*
* When using 'sprite' mode, it is important to note that the symbol definition will be included with the first instance of the SVG that is rendered. This means that if the first instance is hidden or removed from the DOM, all other references will break.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* When using 'sprite' mode, it is important to note that the symbol definition will be included with the first instance of the SVG that is rendered. This means that if the first instance is hidden or removed from the DOM, all other references will break.
* When using 'sprite' mode, it is important to note that the symbol definition will be included with the first instance of the SVG that is rendered. This means that if the first instance is hidden or removed from the DOM, all other references will break.

Question: is this unique to Astro's implementation, or would this be something common to using a sprite sheet in general? I get that it's a caveat, but if it's not an Astro-specific caveat then it's a little bit more for the reader to have to process before they even see how to use this feature.

I might suggest we either remove this paragraph entirely (we do say full details in the RFC!), or place it after the code example if this is a super key thing and people will have a lot of trouble using the feature if they don't know this immediately up front.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unique to the implementation that we chose to go with. Though it would probably apply to the spritesheet in general but is significantly less common if you just have a single spritesheet.

This is something I was debating about adding as an enhancement later or opt-in. Would need some help from the core team on it though.

I'll leave it up to you if we keep this caveat or remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, for this stage of the game, I say we omit the caveat in these docs here. We just get people up and running quickly and happily, and they can check the RFC when and if they run into trouble!

packages/astro/src/types/public/config.ts Outdated Show resolved Hide resolved
Copy link
Member

@sarah11918 sarah11918 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving for docs!

(Also noting, as I always do, that I don't see any new errors added with this feature. If that changes before the feature is released, then please loop me back in! 🙌 )

@ematipico ematipico merged commit c48916c into withastro:next Nov 19, 2024
14 checks passed
@stramel
Copy link
Contributor Author

stramel commented Nov 20, 2024

Thank you all for the help to get this in! I greatly appreciate all the reviews and feedback!

@stramel stramel deleted the add-svg-support branch November 23, 2024 01:00
@stramel
Copy link
Contributor Author

stramel commented Nov 23, 2024

Seems there is some kind of issue with the refactor of ids to use WeakMap. I'm just getting all the same icon from the integration with astro-icon through the makeSvgComponent. I'll open a new PR for it

@stramel
Copy link
Contributor Author

stramel commented Nov 23, 2024

Initial fix: #12511

Going to do some testing and ensure I add a test to fail on previous case. Tested with astro-icon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs pr pkg: astro Related to the core `astro` package (scope) semver: minor Change triggers a `minor` release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants