diff --git a/benchmark/renderer.bench.ts b/benchmark/renderer.bench.ts index ad05650..0cf8545 100644 --- a/benchmark/renderer.bench.ts +++ b/benchmark/renderer.bench.ts @@ -1,13 +1,26 @@ import { bench, describe } from 'vitest' import { createRenderer, renderStyles, renderScripts, renderResourceHints } from '../src/runtime' import { normalizeViteManifest, normalizeWebpackManifest } from '../src' +import { precomputeDependencies } from '../src/precompute' import viteManifest from '../test/fixtures/vite-manifest.json' import webpackManifest from '../test/fixtures/webpack-manifest.json' import largeViteManifest from './fixtures/large-vite-manifest.json' describe('createRenderer', () => { + // Precompute dependencies for benchmarks + const vitePrecomputed = precomputeDependencies(normalizeViteManifest(viteManifest)) + const webpackPrecomputed = precomputeDependencies(normalizeWebpackManifest(webpackManifest)) + const largeVitePrecomputed = precomputeDependencies(normalizeViteManifest(largeViteManifest)) + bench('vite', () => { + createRenderer(() => ({}), { + precomputed: vitePrecomputed, + renderToString: () => '
test
', + }) + }) + + bench('vite (manifest)', () => { createRenderer(() => ({}), { manifest: normalizeViteManifest(viteManifest), renderToString: () => '
test
', @@ -15,6 +28,13 @@ describe('createRenderer', () => { }) bench('webpack', () => { + createRenderer(() => ({}), { + precomputed: webpackPrecomputed, + renderToString: () => '
test
', + }) + }) + + bench('webpack (manifest)', () => { createRenderer(() => ({}), { manifest: normalizeWebpackManifest(webpackManifest), renderToString: () => '
test
', @@ -22,6 +42,13 @@ describe('createRenderer', () => { }) bench('vite (large)', () => { + createRenderer(() => ({}), { + precomputed: largeVitePrecomputed, + renderToString: () => '
test
', + }) + }) + + bench('vite (large) (manifest)', () => { createRenderer(() => ({}), { manifest: normalizeViteManifest(largeViteManifest), renderToString: () => '
test
', @@ -30,6 +57,12 @@ describe('createRenderer', () => { }) describe('rendering', () => { + // Precompute dependencies + const vitePrecomputed = precomputeDependencies(normalizeViteManifest(viteManifest)) + const webpackPrecomputed = precomputeDependencies(normalizeWebpackManifest(webpackManifest)) + const largeVitePrecomputed = precomputeDependencies(normalizeViteManifest(largeViteManifest)) + + // Legacy renderers (with manifest) const viteRenderer = createRenderer(() => ({}), { manifest: normalizeViteManifest(viteManifest), renderToString: () => '
test
', @@ -45,6 +78,22 @@ describe('rendering', () => { renderToString: () => '
test
', }) + // Precomputed renderers + const vitePrecomputedRenderer = createRenderer(() => ({}), { + precomputed: vitePrecomputed, + renderToString: () => '
test
', + }) + + const webpackPrecomputedRenderer = createRenderer(() => ({}), { + precomputed: webpackPrecomputed, + renderToString: () => '
test
', + }) + + const largeVitePrecomputedRenderer = createRenderer(() => ({}), { + precomputed: largeVitePrecomputed, + renderToString: () => '
test
', + }) + // Get actual module keys from manifests const viteModules = Object.keys(viteManifest) const webpackModules = Object.keys(webpackManifest) @@ -59,50 +108,98 @@ describe('rendering', () => { const largeLargeViteSet = new Set(largeViteModules.slice(0, 50)) bench('renderStyles - vite (small)', () => { + renderStyles({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext) + }) + + bench('renderStyles - vite (small) (manifest)', () => { renderStyles({ modules: smallViteSet }, viteRenderer.rendererContext) }) bench('renderStyles - vite (large)', () => { + renderStyles({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext) + }) + + bench('renderStyles - vite (large) (manifest)', () => { renderStyles({ modules: largeViteSet }, viteRenderer.rendererContext) }) bench('renderStyles - vite (very large)', () => { + renderStyles({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext) + }) + + bench('renderStyles - vite (very large) (manifest)', () => { renderStyles({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext) }) bench('renderScripts - vite (small)', () => { + renderScripts({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext) + }) + + bench('renderScripts - vite (small) (manifest)', () => { renderScripts({ modules: smallViteSet }, viteRenderer.rendererContext) }) bench('renderScripts - vite (large)', () => { + renderScripts({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext) + }) + + bench('renderScripts - vite (large) (manifest)', () => { renderScripts({ modules: largeViteSet }, viteRenderer.rendererContext) }) bench('renderScripts - vite (very large)', () => { + renderScripts({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext) + }) + + bench('renderScripts - vite (very large) (manifest)', () => { renderScripts({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext) }) bench('renderResourceHints - vite (small)', () => { + renderResourceHints({ modules: smallViteSet }, vitePrecomputedRenderer.rendererContext) + }) + + bench('renderResourceHints - vite (small) (manifest)', () => { renderResourceHints({ modules: smallViteSet }, viteRenderer.rendererContext) }) bench('renderResourceHints - vite (large)', () => { + renderResourceHints({ modules: largeViteSet }, vitePrecomputedRenderer.rendererContext) + }) + + bench('renderResourceHints - vite (large) (manifest)', () => { renderResourceHints({ modules: largeViteSet }, viteRenderer.rendererContext) }) bench('renderResourceHints - vite (very large)', () => { + renderResourceHints({ modules: largeLargeViteSet }, largeVitePrecomputedRenderer.rendererContext) + }) + + bench('renderResourceHints - vite (very large) (manifest)', () => { renderResourceHints({ modules: largeLargeViteSet }, largeViteRenderer.rendererContext) }) bench('renderStyles - webpack', () => { + renderStyles({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext) + }) + + bench('renderStyles - webpack (manifest)', () => { renderStyles({ modules: smallWebpackSet }, webpackRenderer.rendererContext) }) bench('renderScripts - webpack', () => { + renderScripts({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext) + }) + + bench('renderScripts - webpack (manifest)', () => { renderScripts({ modules: smallWebpackSet }, webpackRenderer.rendererContext) }) bench('renderResourceHints - webpack', () => { + renderResourceHints({ modules: smallWebpackSet }, webpackPrecomputedRenderer.rendererContext) + }) + + bench('renderResourceHints - webpack (manifest)', () => { renderResourceHints({ modules: smallWebpackSet }, webpackRenderer.rendererContext) }) }) diff --git a/src/index.ts b/src/index.ts index cca4e78..c04d6d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ export { normalizeViteManifest } from './vite' export { normalizeWebpackManifest } from './webpack' +export { precomputeDependencies } from './precompute' +export type { PrecomputedData } from './precompute' export * from './types' diff --git a/src/precompute.ts b/src/precompute.ts new file mode 100644 index 0000000..927a405 --- /dev/null +++ b/src/precompute.ts @@ -0,0 +1,130 @@ +import type { Manifest, ResourceMeta } from './types' +import type { ModuleDependencies } from './runtime' + +export interface PrecomputedData { + /** Pre-resolved dependencies for each module */ + dependencies: Record + /** List of entry point module IDs */ + entrypoints: string[] + /** Module metadata needed at runtime (file paths, etc.) */ + modules: Record> +} + +/** + * Build-time utility to precompute all module dependencies from a manifest. + * This eliminates recursive dependency resolution at runtime. + * + * @param manifest The build manifest + * @returns Serializable precomputed data for runtime use + */ +export function precomputeDependencies(manifest: Manifest): PrecomputedData { + const dependencies: Record = {} + const computing = new Set() + + function computeDependencies(id: string): ModuleDependencies { + if (dependencies[id]) { + return dependencies[id] + } + + if (computing.has(id)) { + // Circular dependency detected, return empty to break cycle + return { scripts: {}, styles: {}, preload: {}, prefetch: {} } + } + + computing.add(id) + + const deps: ModuleDependencies = { + scripts: {}, + styles: {}, + preload: {}, + prefetch: {}, + } + + const meta = manifest[id] + if (!meta) { + dependencies[id] = deps + computing.delete(id) + return deps + } + + // Add to scripts + preload + if (meta.file) { + deps.preload[id] = meta + if (meta.isEntry || meta.sideEffects) { + deps.scripts[id] = meta + } + } + + // Add styles + preload + for (const css of meta.css || []) { + const cssResource = manifest[css] + if (cssResource) { + deps.styles[css] = cssResource + deps.preload[css] = cssResource + deps.prefetch[css] = cssResource + } + } + + // Add assets as preload + for (const asset of meta.assets || []) { + const assetResource = manifest[asset] + if (assetResource) { + deps.preload[asset] = assetResource + deps.prefetch[asset] = assetResource + } + } + + // Resolve nested dependencies and merge + for (const depId of meta.imports || []) { + const depDeps = computeDependencies(depId) + Object.assign(deps.styles, depDeps.styles) + Object.assign(deps.preload, depDeps.preload) + Object.assign(deps.prefetch, depDeps.prefetch) + } + + // Filter preload based on preload flag + const filteredPreload: ModuleDependencies['preload'] = {} + for (const depId in deps.preload) { + const dep = deps.preload[depId] + if (dep.preload) { + filteredPreload[depId] = dep + } + } + deps.preload = filteredPreload + + dependencies[id] = deps + computing.delete(id) + return deps + } + + // Pre-compute dependencies for all modules in manifest + for (const moduleId of Object.keys(manifest)) { + computeDependencies(moduleId) + } + + // Extract entry points + const entrypoints = new Set() + for (const key in manifest) { + const meta = manifest[key] + if (meta?.isEntry) { + entrypoints.add(key) + } + } + + // Extract minimal module metadata needed at runtime + const modules: Record> = {} + for (const [moduleId, meta] of Object.entries(manifest)) { + modules[moduleId] = { + file: meta.file, + resourceType: meta.resourceType, + mimeType: meta.mimeType, + module: meta.module, + } + } + + return { + dependencies, + entrypoints: [...entrypoints], + modules, + } +} diff --git a/src/runtime.ts b/src/runtime.ts index e82a57b..bbc857a 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,5 +1,6 @@ import { withLeadingSlash } from 'ufo' import type { Manifest, ResourceMeta } from './types' +import type { PrecomputedData } from './precompute' export interface ModuleDependencies { scripts: Record @@ -23,10 +24,16 @@ export interface SSRContext { export interface RenderOptions { buildAssetsURL?: (id: string) => string - manifest: Manifest + /** @deprecated Use `precomputed` instead for better performance */ + manifest?: Manifest + /** Precomputed dependency data */ + precomputed?: PrecomputedData } -export interface RendererContext extends Required { +export interface RendererContext { + buildAssetsURL: (id: string) => string + manifest?: Manifest + precomputed?: PrecomputedData _dependencies: Record _dependencySets: Record _entrypoints: string[] @@ -41,16 +48,21 @@ interface LinkAttributes { crossorigin?: '' | null } -export function createRendererContext({ manifest, buildAssetsURL }: RenderOptions): RendererContext { +export function createRendererContext({ manifest, precomputed, buildAssetsURL }: RenderOptions): RendererContext { + if (!manifest && !precomputed) { + throw new Error('Either manifest or precomputed data must be provided') + } + const ctx: RendererContext = { - // Manifest + // Options buildAssetsURL: buildAssetsURL || withLeadingSlash, - manifest: undefined!, + manifest, + precomputed, updateManifest, // Internal cache - _dependencies: undefined!, - _dependencySets: undefined!, - _entrypoints: undefined!, + _dependencies: {}, + _dependencySets: {}, + _entrypoints: [], } function updateManifest(manifest: Manifest) { @@ -61,7 +73,13 @@ export function createRendererContext({ manifest, buildAssetsURL }: RenderOption ctx._entrypoints = manifestEntries.filter(e => e[1].isEntry).map(([module]) => module) } - updateManifest(manifest) + if (precomputed) { + ctx._dependencies = precomputed.dependencies + ctx._entrypoints = precomputed.entrypoints + } + else if (manifest) { + updateManifest(manifest) + } return ctx } @@ -78,6 +96,10 @@ export function getModuleDependencies(id: string, rendererContext: RendererConte prefetch: {}, } + if (!rendererContext.manifest) { + return dependencies + } + const meta = rendererContext.manifest[id] if (!meta) { @@ -103,9 +125,15 @@ export function getModuleDependencies(id: string, rendererContext: RendererConte // Resolve nested dependencies and merge for (const depId of meta.imports || []) { const depDeps = getModuleDependencies(depId, rendererContext) - Object.assign(dependencies.styles, depDeps.styles) - Object.assign(dependencies.preload, depDeps.preload) - Object.assign(dependencies.prefetch, depDeps.prefetch) + for (const key in depDeps.styles) { + dependencies.styles[key] = depDeps.styles[key] + } + for (const key in depDeps.preload) { + dependencies.preload[key] = depDeps.preload[key] + } + for (const key in depDeps.prefetch) { + dependencies.prefetch[key] = depDeps.prefetch[key] + } } const filteredPreload: ModuleDependencies['preload'] = {} for (const id in dependencies.preload) { @@ -120,7 +148,13 @@ export function getModuleDependencies(id: string, rendererContext: RendererConte } export function getAllDependencies(ids: Set, rendererContext: RendererContext): ModuleDependencies { - const cacheKey = Array.from(ids).sort().join(',') + let cacheKey = '' + const sortedIds = [...ids].sort() + for (let i = 0; i < sortedIds.length; i++) { + if (i > 0) cacheKey += ',' + cacheKey += sortedIds[i] + } + if (rendererContext._dependencySets[cacheKey]) { return rendererContext._dependencySets[cacheKey] } @@ -134,16 +168,30 @@ export function getAllDependencies(ids: Set, rendererContext: RendererCo for (const id of ids) { const deps = getModuleDependencies(id, rendererContext) - Object.assign(allDeps.scripts, deps.scripts) - Object.assign(allDeps.styles, deps.styles) - Object.assign(allDeps.preload, deps.preload) - Object.assign(allDeps.prefetch, deps.prefetch) + for (const key in deps.scripts) { + allDeps.scripts[key] = deps.scripts[key] + } + for (const key in deps.styles) { + allDeps.styles[key] = deps.styles[key] + } + for (const key in deps.preload) { + allDeps.preload[key] = deps.preload[key] + } + for (const key in deps.prefetch) { + allDeps.prefetch[key] = deps.prefetch[key] + } - for (const dynamicDepId of rendererContext.manifest[id]?.dynamicImports || []) { + for (const dynamicDepId of rendererContext.manifest?.[id]?.dynamicImports || []) { const dynamicDeps = getModuleDependencies(dynamicDepId, rendererContext) - Object.assign(allDeps.prefetch, dynamicDeps.scripts) - Object.assign(allDeps.prefetch, dynamicDeps.styles) - Object.assign(allDeps.prefetch, dynamicDeps.preload) + for (const key in dynamicDeps.scripts) { + allDeps.prefetch[key] = dynamicDeps.scripts[key] + } + for (const key in dynamicDeps.styles) { + allDeps.prefetch[key] = dynamicDeps.styles[key] + } + for (const key in dynamicDeps.preload) { + allDeps.prefetch[key] = dynamicDeps.preload[key] + } } } @@ -188,9 +236,12 @@ export function getRequestDependencies(ssrContext: SSRContext, rendererContext: export function renderStyles(ssrContext: SSRContext, rendererContext: RendererContext): string { const { styles } = getRequestDependencies(ssrContext, rendererContext) - return Object.values(styles).map(resource => - renderLinkToString({ rel: 'stylesheet', href: rendererContext.buildAssetsURL(resource.file), crossorigin: '' }), - ).join('') + let result = '' + for (const key in styles) { + const resource = styles[key]! + result += `` + } + return result } export function getResources(ssrContext: SSRContext, rendererContext: RendererContext): LinkAttributes[] { @@ -198,46 +249,139 @@ export function getResources(ssrContext: SSRContext, rendererContext: RendererCo } export function renderResourceHints(ssrContext: SSRContext, rendererContext: RendererContext): string { - return getResources(ssrContext, rendererContext).map(renderLinkToString).join('') + const { preload, prefetch } = getRequestDependencies(ssrContext, rendererContext) + let result = '' + + // Render preload links + for (const key in preload) { + const resource = preload[key]! + const href = rendererContext.buildAssetsURL(resource.file) + const rel = resource.module ? 'modulepreload' : 'preload' + const crossorigin = (resource.resourceType === 'style' || resource.resourceType === 'font' || resource.resourceType === 'script' || resource.module) ? ' crossorigin' : '' + + if (resource.resourceType && resource.mimeType) { + result += `` + } + else if (resource.resourceType) { + result += `` + } + else { + result += `` + } + } + // Render prefetch links + for (const key in prefetch) { + const resource = prefetch[key]! + const href = rendererContext.buildAssetsURL(resource.file) + const crossorigin = (resource.resourceType === 'style' || resource.resourceType === 'font' || resource.resourceType === 'script' || resource.module) ? ' crossorigin' : '' + + if (resource.resourceType && resource.mimeType) { + result += `` + } + else if (resource.resourceType) { + result += `` + } + else { + result += `` + } + } + + return result } -export function renderResourceHeaders(ssrContext: SSRContext, rendererContext: RendererContext): Record { +function renderResourceHeaders(ssrContext: SSRContext, rendererContext: RendererContext): Record { + const { preload, prefetch } = getRequestDependencies(ssrContext, rendererContext) + const links: string[] = [] + + // Render preload headers + for (const key in preload) { + const resource = preload[key]! + const href = rendererContext.buildAssetsURL(resource.file) + const rel = resource.module ? 'modulepreload' : 'preload' + let header = `<${href}>; rel="${rel}"` + + if (resource.resourceType) { + header += `; as="${resource.resourceType}"` + } + if (resource.mimeType) { + header += `; type="${resource.mimeType}"` + } + if (resource.resourceType === 'style' || resource.resourceType === 'font' || resource.resourceType === 'script' || resource.module) { + header += '; crossorigin' + } + + links.push(header) + } + + // Render prefetch headers + for (const key in prefetch) { + const resource = prefetch[key]! + const href = rendererContext.buildAssetsURL(resource.file) + let header = `<${href}>; rel="prefetch"` + + if (resource.resourceType) { + header += `; as="${resource.resourceType}"` + } + if (resource.mimeType) { + header += `; type="${resource.mimeType}"` + } + if (resource.resourceType === 'style' || resource.resourceType === 'font' || resource.resourceType === 'script' || resource.module) { + header += '; crossorigin' + } + + links.push(header) + } + return { - link: getResources(ssrContext, rendererContext).map(renderLinkToHeader).join(', '), + link: links.join(', '), } } export function getPreloadLinks(ssrContext: SSRContext, rendererContext: RendererContext): LinkAttributes[] { const { preload } = getRequestDependencies(ssrContext, rendererContext) - return Object.values(preload) - .map(resource => ({ + const result: LinkAttributes[] = [] + for (const key in preload) { + const resource = preload[key]! + result.push({ rel: resource.module ? 'modulepreload' : 'preload', as: resource.resourceType, type: resource.mimeType ?? null, crossorigin: resource.resourceType === 'style' || resource.resourceType === 'font' || resource.resourceType === 'script' || resource.module ? '' : null, href: rendererContext.buildAssetsURL(resource.file), - })) + }) + } + return result } export function getPrefetchLinks(ssrContext: SSRContext, rendererContext: RendererContext): LinkAttributes[] { const { prefetch } = getRequestDependencies(ssrContext, rendererContext) - return Object.values(prefetch).map(resource => ({ - rel: 'prefetch', - as: resource.resourceType, - type: resource.mimeType ?? null, - crossorigin: resource.resourceType === 'style' || resource.resourceType === 'font' || resource.resourceType === 'script' || resource.module ? '' : null, - href: rendererContext.buildAssetsURL(resource.file), - })) + const result: LinkAttributes[] = [] + for (const key in prefetch) { + const resource = prefetch[key]! + result.push({ + rel: 'prefetch', + as: resource.resourceType, + type: resource.mimeType ?? null, + crossorigin: resource.resourceType === 'style' || resource.resourceType === 'font' || resource.resourceType === 'script' || resource.module ? '' : null, + href: rendererContext.buildAssetsURL(resource.file), + }) + } + return result } export function renderScripts(ssrContext: SSRContext, rendererContext: RendererContext): string { const { scripts } = getRequestDependencies(ssrContext, rendererContext) - return Object.values(scripts).map(resource => renderScriptToString({ - type: resource.module ? 'module' : null, - src: rendererContext.buildAssetsURL(resource.file), - defer: resource.module ? null : '', - crossorigin: '', - })).join('') + let result = '' + for (const key in scripts) { + const resource = scripts[key]! + if (resource.module) { + result += `` + } + else { + result += `` + } + } + return result } export type RenderFunction = (ssrContext: SSRContext, rendererContext: RendererContext) => unknown @@ -271,18 +415,3 @@ export function createRenderer(createApp: ImportOf>, renderO }, } } - -// --- Internal --- - -// Utilities to render script and link tags, and link headers -function renderScriptToString(attrs: Record) { - return ` value === null ? '' : value ? ` ${key}="${value}"` : ' ' + key).join('')}>` -} - -function renderLinkToString(attrs: LinkAttributes) { - return ` value === null ? '' : value ? ` ${key}="${value}"` : ' ' + key).join('')}>` -} - -function renderLinkToHeader(attrs: LinkAttributes) { - return `<${attrs.href}>${Object.entries(attrs).map(([key, value]) => key === 'href' || value === null ? '' : value ? `; ${key}="${value}"` : `; ${key}`).join('')}` -} diff --git a/test/precompute.test.ts b/test/precompute.test.ts new file mode 100644 index 0000000..9373227 --- /dev/null +++ b/test/precompute.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest' +import { createRenderer } from '../src/runtime' +import { precomputeDependencies } from '../src/precompute' +import { normalizeViteManifest } from '../src/vite' +import viteManifest from './fixtures/vite-manifest.json' + +describe('precomputed dependencies', () => { + const normalizedManifest = normalizeViteManifest(viteManifest) + + it('should precompute dependencies correctly', () => { + const precomputed = precomputeDependencies(normalizedManifest) + + expect(precomputed).toHaveProperty('dependencies') + expect(precomputed).toHaveProperty('entrypoints') + expect(precomputed).toHaveProperty('modules') + + // Should have dependencies for all modules + expect(Object.keys(precomputed.dependencies).sort()).toEqual(Object.keys(normalizedManifest).sort()) + + // Should identify entry points + expect(precomputed.entrypoints).toContain('../packages/nuxt3/src/app/entry.ts') + }) + + it('should work with precomputed data instead of manifest', async () => { + const precomputed = precomputeDependencies(normalizedManifest) + + // Create renderer with precomputed data (no manifest needed!) + const renderer = createRenderer(() => ({}), { + precomputed, + renderToString: () => '
test
', + buildAssetsURL: id => `/_nuxt/${id}`, + }) + + const result = await renderer.renderToString({ + modules: new Set(['pages/index.vue']), + }) + + expect(result.renderStyles().includes('index.css')).toBe(true) + expect(result.renderScripts().includes('entry.mjs')).toBe(true) + }) + + it('should produce same results as manifest-based approach', async () => { + const precomputed = precomputeDependencies(normalizedManifest) + + // Renderer with manifest (legacy) + const manifestRenderer = createRenderer(() => ({}), { + manifest: normalizedManifest, + renderToString: () => '
test
', + buildAssetsURL: id => `/_nuxt/${id}`, + }) + + // Renderer with precomputed data + const precomputedRenderer = createRenderer(() => ({}), { + precomputed, + renderToString: () => '
test
', + buildAssetsURL: id => `/_nuxt/${id}`, + }) + + const modules = new Set(['pages/index.vue']) + + const manifestResult = await manifestRenderer.renderToString({ modules }) + const precomputedResult = await precomputedRenderer.renderToString({ modules }) + + // Should produce identical output + expect(precomputedResult.renderStyles()).toBe(manifestResult.renderStyles()) + expect(precomputedResult.renderScripts()).toBe(manifestResult.renderScripts()) + expect(precomputedResult.renderResourceHints()).toBe(manifestResult.renderResourceHints()) + }) + + it('should throw error when neither manifest nor precomputed provided', () => { + expect(() => { + createRenderer(() => ({}), { + renderToString: () => '
test
', + buildAssetsURL: id => `/_nuxt/${id}`, + }) + }).toThrow('Either manifest or precomputed data must be provided') + }) +})