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 every responsive image candidate', () => { const refs = collectAssetRefs( '', ); expect(refs).toContainEqual({ kind: 'srcset', url: 'https://cdn.example/c.png' }); expect(refs).toContainEqual({ kind: 'srcset', url: 'https://cdn.example/c@2x.png' }); }); it('collects external video poster URLs', () => { const refs = collectAssetRefs(''); expect(refs).toContainEqual({ kind: 'poster', url: 'https://cdn.example/poster.jpg' }); }); it('collects unsupported embedded-document resources for rejection', () => { const refs = collectAssetRefs( '' + '' + '' + '' + '' + '', ); expect(refs).toEqual( expect.arrayContaining([ { kind: 'iframe-src', url: 'https://embed.example/frame' }, { kind: 'object-data', url: 'https://embed.example/object' }, { kind: 'embed-src', url: 'https://embed.example/plugin' }, { kind: 'iframe-src', url: 'data:text/html,%3Cp%3Eframe%3C/p%3E' }, { kind: 'object-data', url: 'data:image/svg+xml,%3Csvg%3E%3C/svg%3E' }, { kind: 'embed-src', url: 'blob:https://embed.example/plugin-id' }, ]), ); }); 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('collects external SVG image and use references but keeps local fragments local', () => { const refs = collectAssetRefs( '' + '' + '', ); expect(refs).toContainEqual({ kind: 'svg-image', url: 'https://cdn.example/image.png' }); expect(refs).toContainEqual({ kind: 'svg-use', url: 'https://cdn.example/icons.svg#star', }); expect(refs.map((ref) => ref.url)).not.toContain('#local'); }); it('collects CSS resources only from real url tokens', () => { const realUrl = 'https://cdn.example/real.png'; const stringUrl = 'https://cdn.example/string.png'; const commentUrl = 'https://cdn.example/comment.png'; const refs = collectAssetRefs( ``, { includeRelative: true }, ); expect(refs).toContainEqual({ kind: 'css-url', url: realUrl }); expect(refs.map((ref) => ref.url)).not.toContain(stringUrl); expect(refs.map((ref) => ref.url)).not.toContain(commentUrl); }); 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([]); }); it('collects direct module imports and CSS @import URLs', () => { const refs = collectAssetRefs( '' + '', ); expect(refs).toContainEqual({ kind: 'css-import', url: 'https://cdn.example/theme.css' }); expect(refs).toContainEqual({ kind: 'module-import', url: 'https://cdn.example/dep.js' }); }); }); 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('preserves @import layer, supports, and media conditions when inlining', async () => { const css = '@import "theme.css" layer(theme) supports(display: grid) screen and (min-width: 600px);'; const { css: out } = await inlineCssUrls( css, 'https://cdn.example/styles/base.css', async (url) => url === 'https://cdn.example/styles/theme.css' ? { bytes: new TextEncoder().encode('.card{display:grid}'), contentType: 'text/css' } : null, ); expect(out).toContain('@layer theme'); expect(out).toContain('@supports (display: grid)'); expect(out).toContain('@media screen and (min-width: 600px)'); expect(out).toContain('.card{display:grid}'); expect(out).not.toContain('@import'); }); it('preserves repeated imports of one stylesheet under different conditions', async () => { const css = '@import "theme.css" screen; @import "theme.css" print;'; const { css: out } = await inlineCssUrls( css, 'https://cdn.example/styles/base.css', async () => ({ bytes: new TextEncoder().encode('.theme{color:blue}'), contentType: 'text/css', }), ); expect(out).toContain('@media screen{.theme{color:blue}}'); expect(out).toContain('@media print{.theme{color:blue}}'); expect(out.match(/\.theme\{color:blue\}/g)).toHaveLength(2); }); 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('leaves local CSS fragments untouched and does not fetch them', async () => { const calls: string[] = []; const { css: out, failed } = await inlineCssUrls( 'svg{filter:url(#blur);mask:url("#mask")}', 'about:blank', async (url) => { calls.push(url); return null; }, ); expect(out).toContain('url(#blur)'); expect(out).toContain('url("#mask")'); expect(calls).toEqual([]); expect(failed).toEqual([]); }); it('preserves external SVG fragments while fetching only the resource URL', async () => { const calls: string[] = []; const { css: out, failed } = await inlineCssUrls( 'svg{filter:url(https://cdn.test/filters.svg#blur)}', 'about:blank', async (url) => { calls.push(url); return url === 'https://cdn.test/filters.svg' ? { bytes: new TextEncoder().encode(''), contentType: 'image/svg+xml', } : null; }, ); expect(calls).toEqual(['https://cdn.test/filters.svg']); expect(out).toContain('url(data:image/svg+xml;base64,'); expect(out).toContain('#blur)'); expect(failed).toEqual([]); }); it('ignores url-like text in CSS comments and quoted strings', async () => { const realUrl = 'https://cdn.test/real.png'; const stringUrl = 'https://cdn.test/string.png'; const commentUrl = 'https://cdn.test/comment.png'; const calls: string[] = []; const { css: out } = await inlineCssUrls( `.x{content:"url(${stringUrl})";background:url(${realUrl})}/* url(${commentUrl}) */`, 'about:blank', async (url) => { calls.push(url); return { bytes: new Uint8Array([1]), contentType: 'image/png' }; }, ); expect(out).toContain(`content:"url(${stringUrl})"`); expect(out).toContain(`/* url(${commentUrl}) */`); expect(out).toContain('background:url(data:image/png;base64,'); expect(calls).toContain(realUrl); expect(calls).not.toContain(stringUrl); expect(calls).not.toContain(commentUrl); }); 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('preserves supported top-level import-map fields while rewriting imports', async () => { const html = ''; const { html: out } = await inlineHtmlAssets(html, { fetcher: async (url) => url === 'https://cdn.test/dep.js' ? { bytes: new TextEncoder().encode('export const value = 42;'), contentType: 'text/javascript', } : null, }); expect(out).toContain('"dep":"data:text/javascript;base64,'); expect(out).toContain('"scopes":{}'); expect(out).toContain('"integrity":{"https://cdn.test/dep.js":"sha384-example"}'); }); 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('rewrites real elements without touching resource-like authored script text', async () => { const realUrl = 'https://cdn.test/real.png'; const inertUrl = 'https://cdn.test/inert.png'; const calls: string[] = []; const authoredScript = `const template = '';`; const { html: out } = await inlineHtmlAssets( ``, { fetcher: async (url) => { calls.push(url); return { bytes: new Uint8Array([1, 2, 3]), contentType: 'image/png' }; }, }, ); expect(out).toContain(``); expect(out).toContain('' + '' + '', { fetcher: async (url) => { calls.push(url); return { bytes: new TextEncoder().encode(url.endsWith('.svg') ? '' : 'PNG'), contentType: url.endsWith('.svg') ? 'image/svg+xml' : 'image/png', }; }, }, ); expect(out).toContain(' { const realUrl = 'https://cdn.test/real.css-image.png'; const inertUrl = 'https://cdn.test/inert.css-image.png'; const authoredScript = `const template = '';`; const calls: string[] = []; const { html: out } = await inlineHtmlAssets( ``, { fetcher: async (url) => { calls.push(url); return { bytes: new Uint8Array([1]), contentType: 'image/png' }; }, }, ); expect(out).toContain(``); expect(out).toContain('data:image/png;base64,'); expect(calls).toContain(realUrl); expect(calls).not.toContain(inertUrl); }); it('rewrites real script and link elements without touching authored tag text', async () => { const realScript = 'https://cdn.test/real.js'; const realCss = 'https://cdn.test/real.css'; const inertScript = 'https://cdn.test/inert.js'; const inertCss = 'https://cdn.test/inert.css'; const authoredScript = `const scriptTag = '` + `` + ``, { fetcher: async (url) => { calls.push(url); return { bytes: new TextEncoder().encode( url.endsWith('.css') ? '.real{color:red}' : 'window.real = true;', ), contentType: url.endsWith('.css') ? 'text/css' : 'text/javascript', }; }, }, ); expect(out).toContain(``); expect(out).toContain(''; const fetchImpl = fetchFromMap({ 'https://cdn/app.js': { body: 'export const a=1', ct: 'text/javascript' }, }); const { html: out } = await inlineHtmlAssets(html, { fetchImpl }); expect(out).toMatch(/]*type="module"[^>]*src="data:text\/javascript;base64,/); }); it('inlines an img src', async () => { const html = ''; const fetchImpl = fetchFromMap({ 'https://cdn/p.png': { body: 'PNG', ct: 'image/png' } }); const { html: out } = await inlineHtmlAssets(html, { fetchImpl }); expect(out).toContain('src="data:image/png;base64,'); }); it('inlines a Tailwind CDN runtime script', async () => { const html = ''; const fetchImpl = fetchFromMap({ 'https://cdn.tailwindcss.com': { body: '/*tw*/', ct: 'text/javascript' }, }); const { html: out, report } = await inlineHtmlAssets(html, { fetchImpl }); expect(out).toContain('data:text/javascript;base64,'); expect(report.inlined).toContain('https://cdn.tailwindcss.com'); }); it('inlines direct video and audio src attributes', async () => { const html = ''; const fetchImpl = fetchFromMap({ 'https://cdn.test/demo.mp4': { body: 'video', ct: 'video/mp4' }, 'https://cdn.test/demo.mp3': { body: 'audio', ct: 'audio/mpeg' }, }); const { html: out, report } = await inlineHtmlAssets(html, { fetchImpl }); expect(out).toContain('