spike: add svg component support
This commit is contained in:
parent
923a443cb0
commit
c86ae12557
5 changed files with 105 additions and 5 deletions
|
@ -55,6 +55,7 @@
|
|||
"./components": "./components/index.ts",
|
||||
"./components/*": "./components/*",
|
||||
"./assets": "./dist/assets/index.js",
|
||||
"./assets/runtime": "./dist/assets/runtime.js",
|
||||
"./assets/utils": "./dist/assets/utils/index.js",
|
||||
"./assets/image-endpoint": "./dist/assets/image-endpoint.js",
|
||||
"./assets/services/sharp": "./dist/assets/services/sharp.js",
|
||||
|
@ -167,6 +168,7 @@
|
|||
"string-width": "^6.1.0",
|
||||
"strip-ansi": "^7.1.0",
|
||||
"tsconfig-resolver": "^3.0.1",
|
||||
"ultrahtml": "^1.3.0",
|
||||
"undici": "^5.23.0",
|
||||
"unist-util-visit": "^4.1.2",
|
||||
"vfile": "^5.3.7",
|
||||
|
|
35
packages/astro/src/assets/runtime.ts
Normal file
35
packages/astro/src/assets/runtime.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import type { ImageMetadata } from './types.js';
|
||||
import { createComponent, render, spreadAttributes, unescapeHTML } from '../runtime/server/index.js';
|
||||
import { normalizeProps, makeNonEnumerable } from './utils/svg.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
|
||||
let ids = 0;
|
||||
export function createSvgComponent({ meta, attributes, children }: SvgComponentProps) {
|
||||
const id = `a:${ids++}`;
|
||||
const rendered = new WeakSet<Response>();
|
||||
const Component = createComponent((result, props) => {
|
||||
const { viewBox, title: titleProp, ...normalizedProps } = normalizeProps(attributes, props);
|
||||
const title = titleProp ? unescapeHTML(`<title>${titleProp}</title>`) : '';
|
||||
let symbol: any = '';
|
||||
// On first render, include the symbol definition
|
||||
if (!rendered.has(result.response)) {
|
||||
// We only need the viewBox on the symbol, 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 xlink:href="#${id}" /></svg>`;
|
||||
})
|
||||
makeNonEnumerable(Component);
|
||||
if (import.meta.env.DEV) {
|
||||
Object.defineProperty(Component, Symbol.for('nodejs.util.inspect.custom'), { value: (_: any, opts: any, inspect: any) => inspect(meta, opts) })
|
||||
}
|
||||
return Object.assign(Component, meta);
|
||||
}
|
||||
|
|
@ -5,11 +5,13 @@ import { prependForwardSlash, slash } from '../../core/path.js';
|
|||
import type { ImageMetadata } from '../types.js';
|
||||
import { imageMetadata } from './metadata.js';
|
||||
|
||||
type ImageMetadataWithContents = ImageMetadata & { contents?: Buffer };
|
||||
|
||||
export async function emitESMImage(
|
||||
id: string | undefined,
|
||||
watchMode: boolean,
|
||||
fileEmitter: any
|
||||
): Promise<ImageMetadata | undefined> {
|
||||
): Promise<ImageMetadata & { contents?: Buffer } | undefined> {
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -28,11 +30,15 @@ export async function emitESMImage(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const emittedImage: ImageMetadata = {
|
||||
const emittedImage: ImageMetadataWithContents = {
|
||||
src: '',
|
||||
...fileMetadata,
|
||||
};
|
||||
|
||||
if (fileMetadata.format === 'svg') {
|
||||
emittedImage.contents = fileData;
|
||||
}
|
||||
|
||||
// Build
|
||||
if (!watchMode) {
|
||||
const pathname = decodeURI(url.pathname);
|
||||
|
|
27
packages/astro/src/assets/utils/svg.ts
Normal file
27
packages/astro/src/assets/utils/svg.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
type SvgAttributes = Record<string, any>;
|
||||
|
||||
// Some attributes required for `image/svg+xml` are irrelevant when
|
||||
// inlined in a `text/html` doc. 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;
|
||||
}
|
||||
|
||||
export function normalizeProps(attributes: SvgAttributes, { size, ...props }: SvgAttributes) {
|
||||
if (size) {
|
||||
props.width = size;
|
||||
props.height = size;
|
||||
}
|
||||
return dropAttributes({ ...DEFAULT_ATTRS, ...attributes, ...props });
|
||||
}
|
||||
|
||||
export function makeNonEnumerable(object: Record<string, any>) {
|
||||
for (const property in object) {
|
||||
Object.defineProperty(object, property, { enumerable: false });
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
import MagicString from 'magic-string';
|
||||
import type * as vite from 'vite';
|
||||
import type { AstroPluginOptions, ImageTransform, ImageMetadata } from '../@types/astro';
|
||||
import type { SvgComponentProps } from './runtime';
|
||||
|
||||
import MagicString from 'magic-string';
|
||||
import { normalizePath } from 'vite';
|
||||
import type { AstroPluginOptions, ImageTransform } from '../@types/astro';
|
||||
import { parse, renderSync } from 'ultrahtml';
|
||||
import {
|
||||
appendForwardSlash,
|
||||
joinPaths,
|
||||
|
@ -10,6 +13,7 @@ import {
|
|||
} from '../core/path.js';
|
||||
import { VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js';
|
||||
import { emitESMImage } from './utils/emitAsset.js';
|
||||
import { dropAttributes } from './utils/svg.js';
|
||||
import { hashTransform, propsToFilename } from './utils/transformToPath.js';
|
||||
|
||||
const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
|
||||
|
@ -125,11 +129,37 @@ export default function assets({
|
|||
}
|
||||
|
||||
const cleanedUrl = removeQueryString(id);
|
||||
|
||||
if (/\.(jpeg|jpg|png|tiff|webp|gif|svg)$/.test(cleanedUrl)) {
|
||||
const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile);
|
||||
return `export default ${JSON.stringify(meta)}`;
|
||||
if (!meta) return;
|
||||
if (/\.svg$/.test(cleanedUrl)) {
|
||||
const { contents, ...metadata } = meta;
|
||||
return makeSvgComponent(metadata, contents!);
|
||||
} else {
|
||||
return `export default ${JSON.stringify(meta)}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function parseSVG(contents: string) {
|
||||
const root = parse(contents);
|
||||
const [{ attributes, children }] = root.children;
|
||||
const body = renderSync({ ...root, children });
|
||||
return { attributes, body };
|
||||
}
|
||||
|
||||
function makeSvgComponent(meta: ImageMetadata, contents: Buffer) {
|
||||
const file = contents.toString('utf-8');
|
||||
const { attributes, body: children } = parseSVG(file);
|
||||
const props: SvgComponentProps = {
|
||||
meta,
|
||||
attributes: dropAttributes(attributes),
|
||||
children
|
||||
}
|
||||
return `import { createSvgComponent } from 'astro/assets/runtime';
|
||||
export default createSvgComponent(${JSON.stringify(props)});`;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue