-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(astro): add Built-in SVG component support (#12067)
Co-authored-by: Sarah Rainsberger <[email protected]> Co-authored-by: Emanuele Stoppa <[email protected]>
- Loading branch information
1 parent
e246dc5
commit c48916c
Showing
35 changed files
with
2,121 additions
and
678 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
--- | ||
'astro': minor | ||
--- | ||
|
||
Adds experimental support for built-in SVG components. | ||
|
||
|
||
This feature allows you to import SVG files directly into your Astro project as components. By default, Astro will inline the SVG content into your HTML output. | ||
|
||
To enable this feature, set `experimental.svg` to `true` in your Astro config: | ||
|
||
```js | ||
{ | ||
experimental: { | ||
svg: true, | ||
}, | ||
} | ||
``` | ||
|
||
To use this feature, import an SVG file in your Astro project, passing any common SVG attributes to the imported component. Astro also provides a `size` attribute to set equal `height` and `width` properties: | ||
|
||
```astro | ||
--- | ||
import Logo from './path/to/svg/file.svg'; | ||
--- | ||
<Logo size={24} /> | ||
``` | ||
|
||
For a complete overview, and to give feedback on this experimental API, see the [Feature RFC](https://github.com/withastro/roadmap/pull/1035). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { | ||
createComponent, | ||
render, | ||
spreadAttributes, | ||
unescapeHTML, | ||
} from '../runtime/server/index.js'; | ||
import type { SSRResult } from '../types/public/index.js'; | ||
import type { ImageMetadata } from './types.js'; | ||
|
||
export interface SvgComponentProps { | ||
meta: ImageMetadata; | ||
attributes: Record<string, string>; | ||
children: string; | ||
} | ||
|
||
/** | ||
* Make sure these IDs are kept on the module-level so they're incremented on a per-page basis | ||
*/ | ||
const ids = new WeakMap<SSRResult, number>(); | ||
let counter = 0; | ||
|
||
export function createSvgComponent({ meta, attributes, children }: SvgComponentProps) { | ||
const rendered = new WeakSet<Response>(); | ||
const Component = createComponent((result, props) => { | ||
let id; | ||
if (ids.has(result)) { | ||
id = ids.get(result)!; | ||
} else { | ||
counter += 1; | ||
ids.set(result, counter); | ||
id = counter; | ||
} | ||
id = `a:${id}`; | ||
|
||
const { | ||
title: titleProp, | ||
viewBox, | ||
mode, | ||
...normalizedProps | ||
} = normalizeProps(attributes, props); | ||
const title = titleProp ? unescapeHTML(`<title>${titleProp}</title>`) : ''; | ||
|
||
if (mode === 'sprite') { | ||
// On the first render, include the symbol definition | ||
let symbol: any = ''; | ||
if (!rendered.has(result.response)) { | ||
// We only need the viewBox on the symbol definition, we can drop it everywhere else | ||
symbol = unescapeHTML(`<symbol${spreadAttributes({ viewBox, id })}>${children}</symbol>`); | ||
rendered.add(result.response); | ||
} | ||
|
||
return render`<svg${spreadAttributes(normalizedProps)}>${title}${symbol}<use href="#${id}" /></svg>`; | ||
} | ||
|
||
// Default to inline mode | ||
return render`<svg${spreadAttributes({ viewBox, ...normalizedProps })}>${title}${unescapeHTML(children)}</svg>`; | ||
}); | ||
|
||
if (import.meta.env.DEV) { | ||
// Prevent revealing that this is a component | ||
makeNonEnumerable(Component); | ||
|
||
// Maintaining the current `console.log` output for SVG imports | ||
Object.defineProperty(Component, Symbol.for('nodejs.util.inspect.custom'), { | ||
value: (_: any, opts: any, inspect: any) => inspect(meta, opts), | ||
}); | ||
} | ||
|
||
// Attaching the metadata to the component to maintain current functionality | ||
return Object.assign(Component, meta); | ||
} | ||
|
||
type SvgAttributes = Record<string, any>; | ||
|
||
/** | ||
* Some attributes required for `image/svg+xml` are irrelevant when inlined in a `text/html` document. We can save a few bytes by dropping them. | ||
*/ | ||
const ATTRS_TO_DROP = ['xmlns', 'xmlns:xlink', 'version']; | ||
const DEFAULT_ATTRS: SvgAttributes = { role: 'img' }; | ||
|
||
export function dropAttributes(attributes: SvgAttributes) { | ||
for (const attr of ATTRS_TO_DROP) { | ||
delete attributes[attr]; | ||
} | ||
|
||
return attributes; | ||
} | ||
|
||
function normalizeProps(attributes: SvgAttributes, { size, ...props }: SvgAttributes) { | ||
if (size !== undefined && props.width === undefined && props.height === undefined) { | ||
props.height = size; | ||
props.width = size; | ||
} | ||
|
||
return dropAttributes({ ...DEFAULT_ATTRS, ...attributes, ...props }); | ||
} | ||
|
||
function makeNonEnumerable(object: Record<string, any>) { | ||
for (const property in object) { | ||
Object.defineProperty(object, property, { enumerable: false }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { parse, renderSync } from 'ultrahtml'; | ||
import type { ImageMetadata } from '../types.js'; | ||
import type { SvgComponentProps } from '../runtime.js'; | ||
import { dropAttributes } from '../runtime.js'; | ||
|
||
function parseSvg(contents: string) { | ||
const root = parse(contents); | ||
const [{ attributes, children }] = root.children; | ||
const body = renderSync({ ...root, children }); | ||
|
||
return { attributes, body }; | ||
} | ||
|
||
export type SvgRenderMode = 'inline' | 'sprite'; | ||
|
||
export function makeSvgComponent(meta: ImageMetadata, contents: Buffer | string, options?: { mode?: SvgRenderMode }) { | ||
const file = typeof contents === 'string' ? contents : contents.toString('utf-8'); | ||
const { attributes, body: children } = parseSvg(file); | ||
const props: SvgComponentProps = { | ||
meta, | ||
attributes: dropAttributes({ mode: options?.mode, ...attributes }), | ||
children, | ||
}; | ||
|
||
return `import { createSvgComponent } from 'astro/assets/runtime'; | ||
export default createSvgComponent(${JSON.stringify(props)})`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.