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 ``
-}
-
-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')
+ })
+})