import { describe, it, expect } from 'vitest';
import {
collectAssetRefs,
createAssetFetcher,
toDataUri,
inlineCssUrls,
inlineHtmlAssets,
} from '@/lib/export/inline-assets';
describe('collectAssetRefs', () => {
it('collects stylesheet link hrefs', () => {
const refs = collectAssetRefs('');
expect(refs).toContainEqual({ kind: 'link', url: 'https://cdn.example/a.css' });
});
it('collects script srcs', () => {
const refs = collectAssetRefs('');
expect(refs).toContainEqual({ kind: 'script', url: 'https://cdn.example/b.js' });
});
it('collects img srcs', () => {
const refs = collectAssetRefs('
');
expect(refs).toContainEqual({ kind: 'img', url: 'https://cdn.example/c.png' });
});
it('collects source srcs (video/audio)', () => {
const refs = collectAssetRefs('');
expect(refs).toContainEqual({ kind: 'source', url: 'https://cdn.example/d.mp4' });
});
it('collects url() refs inside ');
expect(refs).toContainEqual({ kind: 'css-url', url: 'https://cdn.example/e.png' });
});
it('collects importmap entry URLs', () => {
const html =
'';
const refs = collectAssetRefs(html);
expect(refs).toContainEqual({
kind: 'importmap',
url: 'https://unpkg.com/three@0.160.0/build/three.module.js',
});
});
it('IGNORES XML namespaces in xmlns (not a fetchable resource)', () => {
const refs = collectAssetRefs('');
expect(refs.map((r) => r.url)).not.toContain('http://www.w3.org/2000/svg');
});
it('IGNORES data: and relative URLs', () => {
const html =
'
';
const refs = collectAssetRefs(html);
expect(refs).toEqual([]);
});
it('only collects http(s) absolute URLs', () => {
const html = '';
const refs = collectAssetRefs(html);
expect(refs.map((r) => r.url).sort()).toEqual(['http://b/y.js', 'https://a/x.js']);
});
it('skips importmap scripts regardless of quote style or attribute order', () => {
const a = collectAssetRefs(``);
const b = collectAssetRefs(``);
expect(a).toEqual([]);
expect(b).toEqual([]);
});
});
describe('createAssetFetcher', () => {
function fakeFetch(map: Record) {
return (async (url: string) => {
const hit = map[String(url)];
if (!hit) return new Response('not found', { status: 404 });
return new Response(hit.body, {
status: hit.status ?? 200,
headers: { 'content-type': hit.contentType },
});
}) as unknown as typeof fetch;
}
it('fetches bytes + content-type', async () => {
const fetchAsset = createAssetFetcher({
fetchImpl: fakeFetch({
'https://x/a.js': { body: 'console.log(1)', contentType: 'text/javascript' },
}),
});
const got = await fetchAsset('https://x/a.js');
expect(got).not.toBeNull();
expect(new TextDecoder().decode(got!.bytes)).toBe('console.log(1)');
expect(got!.contentType).toBe('text/javascript');
});
it('returns null on 404 and caches the negative result', async () => {
let calls = 0;
const fetchImpl = (async () => {
calls++;
return new Response('', { status: 404 });
}) as unknown as typeof fetch;
const fetchAsset = createAssetFetcher({ fetchImpl });
expect(await fetchAsset('https://x/missing')).toBeNull();
expect(await fetchAsset('https://x/missing')).toBeNull();
expect(calls).toBe(1);
});
it('caches successful results (one network call per url)', async () => {
let calls = 0;
const fetchImpl = (async () => {
calls++;
return new Response('data', { status: 200, headers: { 'content-type': 'text/plain' } });
}) as unknown as typeof fetch;
const fetchAsset = createAssetFetcher({ fetchImpl });
await fetchAsset('https://x/a');
await fetchAsset('https://x/a');
expect(calls).toBe(1);
});
it('strips content-type parameters (charset) to the bare mime', async () => {
const fetchAsset = createAssetFetcher({
fetchImpl: (async () =>
new Response('x', {
status: 200,
headers: { 'content-type': 'text/css; charset=utf-8' },
})) as unknown as typeof fetch,
});
const got = await fetchAsset('https://x/a.css');
expect(got!.contentType).toBe('text/css');
});
it('falls back to extension-based mime when content-type missing', async () => {
const fetchAsset = createAssetFetcher({
// Use a Uint8Array body so Node does not auto-inject "text/plain;charset=UTF-8"
fetchImpl: (async () =>
new Response(new Uint8Array([120]), { status: 200 })) as unknown as typeof fetch,
});
const got = await fetchAsset('https://x/font.woff2');
expect(got!.contentType).toBe('font/woff2');
});
it('skips assets larger than maxAssetBytes', async () => {
const big = 'x'.repeat(100);
const fetchAsset = createAssetFetcher({
fetchImpl: (async () =>
new Response(big, {
status: 200,
headers: { 'content-type': 'text/plain' },
})) as unknown as typeof fetch,
maxAssetBytes: 10,
});
expect(await fetchAsset('https://x/big')).toBeNull();
});
it('returns null when fetch throws (network error)', async () => {
const fetchAsset = createAssetFetcher({
fetchImpl: (async () => {
throw new Error('network down');
}) as unknown as typeof fetch,
});
expect(await fetchAsset('https://x/err')).toBeNull();
});
});
describe('toDataUri', () => {
it('encodes bytes as base64 data uri with content type', () => {
const uri = toDataUri(new TextEncoder().encode('hi'), 'text/plain');
expect(uri).toBe('data:text/plain;base64,aGk=');
});
});
describe('inlineCssUrls', () => {
it('inlines relative font url() resolved against the css base url', async () => {
const css = "@font-face{font-family:K;src:url(fonts/K.woff2) format('woff2')}";
const fetchAsset = async (url: string) => {
if (url === 'https://cdn.example/dist/fonts/K.woff2') {
return { bytes: new TextEncoder().encode('FONT'), contentType: 'font/woff2' };
}
return null;
};
const { css: out } = await inlineCssUrls(
css,
'https://cdn.example/dist/katex.min.css',
fetchAsset,
);
expect(out).toContain('data:font/woff2;base64,');
expect(out).not.toContain('fonts/K.woff2');
});
it('inlines whatever url() is referenced (ttf etc.)', async () => {
const css = 'src:url(a.ttf)';
const fetchAsset = async () => ({ bytes: new Uint8Array([1]), contentType: 'font/ttf' });
const { css: out } = await inlineCssUrls(css, 'https://x/base.css', fetchAsset);
expect(out).toContain('data:font/ttf;base64,');
});
it('resolves absolute http url() too', async () => {
const css = 'background:url(https://img.example/bg.png)';
const fetchAsset = async (url: string) =>
url === 'https://img.example/bg.png'
? { bytes: new Uint8Array([2]), contentType: 'image/png' }
: null;
const { css: out } = await inlineCssUrls(css, 'https://x/base.css', fetchAsset);
expect(out).toContain('data:image/png;base64,');
});
it('leaves url() unmodified when fetch fails', async () => {
const css = 'src:url(missing.woff2)';
const fetchAsset = async () => null;
const { css: out } = await inlineCssUrls(css, 'https://x/base.css', fetchAsset);
expect(out).toContain('missing.woff2');
});
it('leaves data: url() untouched and does not fetch it', async () => {
const css = 'src:url(data:font/woff2;base64,AAAA)';
const fetchAsset = async () => {
throw new Error('should not fetch');
};
const { css: out } = await inlineCssUrls(css, 'https://x/base.css', fetchAsset);
expect(out).toContain('data:font/woff2;base64,AAAA');
});
it('handles quoted url() and multiple refs, fetching each unique once', async () => {
let calls = 0;
const css = `src:url("a.woff2"),url('a.woff2'),url(b.woff2)`;
const fetchAsset = async (_url: string) => {
calls++;
return { bytes: new Uint8Array([9]), contentType: 'font/woff2' };
};
const { css: out } = await inlineCssUrls(css, 'https://x/base.css', fetchAsset);
expect(out).not.toContain('a.woff2');
expect(out).not.toContain('b.woff2');
// a.woff2 (quoted twice, same resolved url) fetched once + b.woff2 once = 2
expect(calls).toBe(2);
});
it('inlines only woff2 and drops sibling woff/ttf to about:invalid within an @font-face that has woff2', async () => {
const css =
'@font-face{font-family:K;src:url(K.woff2) format("woff2"),url(K.woff) format("woff"),url(K.ttf) format("truetype")}';
const calls: string[] = [];
const fetchAsset = async (url: string) => {
calls.push(url);
return { bytes: new Uint8Array([1]), contentType: 'font/woff2' };
};
const { css: out } = await inlineCssUrls(css, 'https://x/base.css', fetchAsset);
expect(out).toContain('url(data:font/woff2;base64,'); // woff2 inlined
expect(out).toContain('url(about:invalid) format("woff")'); // woff dropped
expect(out).toContain('url(about:invalid) format("truetype")'); // ttf dropped
expect(out).not.toContain('K.woff2');
expect(out).not.toContain('K.woff)');
expect(out).not.toContain('K.ttf');
// only the woff2 was fetched
expect(calls.filter((u) => u.endsWith('.woff') || u.endsWith('.ttf'))).toEqual([]);
expect(calls.some((u) => u.endsWith('.woff2'))).toBe(true);
});
it('falls back to inlining a ttf-only @font-face (no woff2 present)', async () => {
const css = '@font-face{font-family:T;src:url(T.ttf) format("truetype")}';
const fetchAsset = async () => ({ bytes: new Uint8Array([2]), contentType: 'font/ttf' });
const { css: out } = await inlineCssUrls(css, 'https://x/base.css', fetchAsset);
expect(out).toContain('url(data:font/ttf;base64,'); // ttf inlined since no woff2 sibling
expect(out).not.toContain('about:invalid');
});
it('still inlines non-font url() (images) outside @font-face', async () => {
const css =
'.bg{background:url(https://img/x.png)} @font-face{font-family:K;src:url(K.woff2) format("woff2"),url(K.ttf) format("truetype")}';
const fetchAsset = async (url: string) => ({
bytes: new Uint8Array([3]),
contentType: url.includes('.png') ? 'image/png' : 'font/woff2',
});
const { css: out } = await inlineCssUrls(css, 'https://x/base.css', fetchAsset);
expect(out).toContain('url(data:image/png;base64,'); // image inlined
expect(out).toContain('url(data:font/woff2;base64,'); // woff2 inlined
expect(out).toContain('url(about:invalid) format("truetype")'); // ttf dropped
});
it('reports nested url() assets that fail to fetch', async () => {
const css = '@font-face{src:url(https://cdn.example/missing.woff2) format("woff2")}';
const fetchAsset = async () => null; // all fetches fail
const { css: out, failed } = await inlineCssUrls(
css,
'https://cdn.example/base.css',
fetchAsset,
);
expect(out).toContain('https://cdn.example/missing.woff2'); // left as-is
expect(failed.map((f) => f.url)).toContain('https://cdn.example/missing.woff2');
});
});
describe('inlineHtmlAssets — importmap integration', () => {
it('inlines three + three/addons via importmap and retains the prefix as fallback', async () => {
const base = 'https://unpkg.com/three@0.160.0/examples/jsm/';
const fetchImpl = (async (url: string) => {
const map: Record = {
'https://unpkg.com/three@0.160.0/build/three.module.js': 'export const THREE=1',
[base + 'controls/OrbitControls.js']:
"import * as THREE from 'three'; export class OrbitControls{}",
};
const body = map[String(url)];
if (body === undefined) return new Response('', { status: 404 });
return new Response(body, { status: 200, headers: { 'content-type': 'text/javascript' } });
}) as unknown as typeof fetch;
const html = [
'',
"",
].join('');
const { html: out } = await inlineHtmlAssets(html, { fetchImpl });
expect(out).toContain('"three":"data:text/javascript;base64,');
expect(out).toContain('"three/addons/controls/OrbitControls.js":"data:text/javascript;base64,');
// prefix is retained as online fallback for sub-paths not seen during static analysis
expect(out).toContain('"three/addons/":');
});
it('keeps the three/addons/ prefix as fallback when an addon fails to fetch', async () => {
const base = 'https://unpkg.com/three@0.160.0/examples/jsm/';
const fetchImpl = (async (url: string) => {
// three OK, OrbitControls OK, but a second addon 404s
const map: Record = {
'https://unpkg.com/three@0.160.0/build/three.module.js': 'export const THREE=1',
[base + 'controls/OrbitControls.js']: "import 'three'; export class OrbitControls{}",
};
const body = map[String(url)];
if (body === undefined) return new Response('', { status: 404 });
return new Response(body, { status: 200, headers: { 'content-type': 'text/javascript' } });
}) as unknown as typeof fetch;
const html = [
'',
"",
].join('');
const { html: out } = await inlineHtmlAssets(html, { fetchImpl });
// OrbitControls inlined as explicit data: entry
expect(out).toContain('"three/addons/controls/OrbitControls.js":"data:');
// GLTFLoader failed → prefix retained as online fallback
expect(out).toContain('"three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"');
});
});
function fetchFromMap(map: Record): typeof fetch {
return (async (url: string) => {
const hit = map[String(url)];
if (!hit) return new Response('', { status: 404 });
return new Response(hit.body, { status: 200, headers: { 'content-type': hit.ct } });
}) as unknown as typeof fetch;
}
describe('inlineHtmlAssets', () => {
it('inlines a stylesheet link into a ';
const fetchImpl = fetchFromMap({ 'https://cdn/bg.png': { body: 'IMG', ct: 'image/png' } });
const { html: out } = await inlineHtmlAssets(html, { fetchImpl });
expect(out).toContain('data:image/png;base64,');
expect(out).not.toContain('cdn/bg.png');
});
it('preserves the media attribute when converting a stylesheet link to