diff --git a/__tests__/plugin/generateHtml.spec.ts b/__tests__/plugin/generateHtml.spec.ts
new file mode 100644
index 0000000..1b08090
--- /dev/null
+++ b/__tests__/plugin/generateHtml.spec.ts
@@ -0,0 +1,392 @@
+import { describe, expect, it } from 'vitest'
+import { generateHtml } from '../../src/plugin/generateHtml'
+import { type SSRContext, renderToString } from 'vue/server-renderer'
+import { createSSRApp, defineComponent } from 'vue'
+import { createHead, useHead } from '@unhead/vue'
+import { parse } from 'node-html-parser'
+
+describe('generate html', () => {
+ const html = `
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
+`
+
+ it('should render html without modifications', async () => {
+ const app = createSSRApp({
+ template: `{{ count }}
`,
+ setup() {
+ const count = 0
+
+ return {
+ count,
+ }
+ }
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const rendered = await renderToString(app, {})
+
+ const output = await generateHtml(html, '', rendered, {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ expect(root.querySelector('body')?.querySelector('#teleports')).toBeNull()
+ expect(root.querySelector('title')?.textContent).toBe('Vite App')
+ expect(root.querySelector('#app')?.textContent).toContain('0')
+ })
+
+ it('should change the title to "Hey"', async () => {
+ const app = createSSRApp({
+ template: `{{ count }}
`,
+ setup() {
+ const count = 0
+
+ useHead({
+ title: 'Hey',
+ })
+
+ return {
+ count,
+ }
+ }
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const rendered = await renderToString(app, {})
+
+ const output = await generateHtml(html, '', rendered, {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ expect(root.querySelector('title')?.textContent).toBe('Hey')
+ })
+
+ it('should create a teleports div with teleports', async () => {
+ const teleported = defineComponent({
+ template: 'Im teleported into #teleports
',
+ })
+
+ const app = createSSRApp({
+ template: ``,
+ components: {
+ Teleported: teleported,
+ },
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ const teleportedElement = root.querySelector('div[data-testid="teleported"]')
+
+ expect(root.querySelector('#teleports')?.parentNode.rawTagName).toBe('body')
+ expect(teleportedElement?.parentNode.id).toBe('teleports')
+ expect(teleportedElement?.parentNode.rawTagName).toBe('div')
+ })
+
+ it('should add attributes to the body', async () => {
+ const app = createSSRApp({
+ template: `Hey
`,
+ setup: () => {
+ useHead({
+ bodyAttrs: {
+ class: 'm-4',
+ id: 'container',
+ },
+ })
+ },
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ const attributes = root.querySelector('body')?.attributes
+
+ expect(attributes).toHaveProperty('class')
+ expect(attributes).toHaveProperty('id')
+ expect(attributes?.id).toBe('container')
+ expect(attributes?.class).toBe('m-4')
+ })
+
+ it('should add attributes to the html', async () => {
+ const app = createSSRApp({
+ template: `Hey
`,
+ setup: () => {
+ useHead({
+ htmlAttrs: {
+ class: 'm-4',
+ id: 'container',
+ },
+ })
+ },
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ const attributes = root.querySelector('html')?.attributes
+
+ expect(attributes).toHaveProperty('class')
+ expect(attributes).toHaveProperty('id')
+ expect(attributes?.id).toBe('container')
+ expect(attributes?.class).toBe('m-4')
+ })
+
+ it('should add meta tag to the head', async () => {
+ const app = createSSRApp({
+ template: `Hey
`,
+ setup: () => {
+ useHead({
+ meta: [
+ {
+ name: 'description',
+ content: 'My page description',
+ },
+ ]
+ })
+ },
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ const meta = root.querySelector('head')?.childNodes
+ .filter((node => node.rawTagName === 'meta'))
+ .find(node => node.rawAttrs.includes('name="description" content="My page description"'))
+
+ expect(meta).not.toBeUndefined()
+ })
+
+ it('should add link tag to the head', async () => {
+ const app = createSSRApp({
+ template: `Hey
`,
+ setup: () => {
+ useHead({
+ link: [
+ {
+ rel: 'stylesheet',
+ href: 'styles.css',
+ },
+ ]
+ })
+ },
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ const meta = root.querySelector('head')?.childNodes
+ .filter((node => node.rawTagName === 'link'))
+ .find(node => node.rawAttrs.includes('rel="stylesheet" href="styles.css"'))
+
+ expect(meta).not.toBeUndefined()
+ })
+
+ it('should add noscript tag to the head', async () => {
+ const app = createSSRApp({
+ template: `Hey
`,
+ setup: () => {
+ useHead({
+ noscript: [
+ {
+ textContent: 'Javascript is required',
+ },
+ ]
+ })
+ },
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ const noscript = root.querySelector('head')?.childNodes
+ .filter((node => node.rawTagName === 'noscript'))
+
+ expect(noscript).not.toBeUndefined()
+ expect(noscript?.length).not.toBe(0)
+ expect(noscript[0].childNodes[0].text).toBe('Javascript is required')
+ })
+
+ it('should add style tag to the head', async () => {
+ const app = createSSRApp({
+ template: `Hey
`,
+ setup: () => {
+ useHead({
+ style: [
+ {
+ innerHTML: 'body {color: red}',
+ },
+ ]
+ })
+ },
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ const style = root.querySelector('head')?.childNodes
+ .filter((node => node.rawTagName === 'style'))
+
+ expect(style).not.toBeUndefined()
+ expect(style?.length).not.toBe(0)
+ expect(style[0]?.childNodes[0].text).toBe('body {color: red}')
+ })
+
+ it('should add script tag to the head', async () => {
+ const app = createSSRApp({
+ template: `Hey
`,
+ setup: () => {
+ useHead({
+ script: [
+ {
+ innerHTML: 'const foo = \'bar\';',
+ },
+ ]
+ })
+ },
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ const script = root.querySelector('head')?.childNodes
+ .filter((node => node.rawTagName === 'script'))
+
+ expect(script).not.toBeUndefined()
+ expect(script?.length).not.toBe(0)
+ expect(script[0]?.childNodes[0].text).toBe('const foo = \'bar\';')
+ })
+
+ it('should add base tag to the head', async () => {
+ const app = createSSRApp({
+ template: `Hey
`,
+ setup: () => {
+ useHead({
+ base: {
+ href: 'https://www.w3schools.com/',
+ target: '_blank',
+ },
+ })
+ },
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, {}, head)
+
+ const root = parse(output, { comment: true })
+
+ const base = root.querySelector('head')?.childNodes
+ .filter((node => node.rawTagName === 'base'))
+ .find(node => node.rawAttrs.includes('href="https://www.w3schools.com/" target="_blank"'))
+
+ expect(base).not.toBeUndefined()
+ })
+
+ it('should render a script block with state added to the window object', async () => {
+ const app = createSSRApp({
+ template: `Hey
`,
+ })
+
+ const head = createHead()
+ app.use(head)
+
+ const ctx: SSRContext = {}
+
+ const rendered = await renderToString(app, ctx)
+
+ const state = {
+ value: {
+ foo: 'bar',
+ },
+ }
+
+ const output = await generateHtml(html, '', rendered, ctx.teleports ?? {}, state, head)
+
+ const root = parse(output, { comment: true })
+
+ expect(root.querySelector('body > script#state')?.childNodes[0].text).toBe(`window.__INITIAL_STATE__ = {foo:"bar"}`)
+ })
+})
diff --git a/package.json b/package.json
index fff9f55..5eac07c 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"scripts": {
"dev": "unbuild --stub",
"build": "unbuild",
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "vitest"
},
"keywords": [
"vue",
@@ -40,9 +40,9 @@
"@babel/traverse": "^7.24.1",
"@babel/types": "^7.24.0",
"@types/express": "^4.17.21",
- "cheerio": "1.0.0-rc.12",
"cookie": "^0.6.0",
- "cookie-parser": "^1.4.6"
+ "cookie-parser": "^1.4.6",
+ "node-html-parser": "^6.1.13"
},
"devDependencies": {
"@types/node": "^20.12.7",
@@ -51,6 +51,7 @@
"typescript": "~5.3.0",
"unbuild": "^2.0.0",
"vite": "^5.2.10",
+ "vitest": "^1.6.0",
"vue": "^3.4.25",
"vue-router": "^4.3.2"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 402e42e..8891bea 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,15 +23,15 @@ importers:
'@types/express':
specifier: ^4.17.21
version: 4.17.21
- cheerio:
- specifier: 1.0.0-rc.12
- version: 1.0.0-rc.12
cookie:
specifier: ^0.6.0
version: 0.6.0
cookie-parser:
specifier: ^1.4.6
version: 1.4.6
+ node-html-parser:
+ specifier: ^6.1.13
+ version: 6.1.13
devDependencies:
'@types/node':
specifier: ^20.12.7
@@ -51,6 +51,9 @@ importers:
vite:
specifier: ^5.2.10
version: 5.2.10(@types/node@20.12.7)
+ vitest:
+ specifier: ^1.6.0
+ version: 1.6.0(@types/node@20.12.7)
vue:
specifier: ^3.4.25
version: 3.4.25(typescript@5.3.3)
@@ -669,6 +672,10 @@ packages:
cpu: [x64]
os: [win32]
+ '@jest/schemas@29.6.3':
+ resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
'@jridgewell/gen-mapping@0.3.5':
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
engines: {node: '>=6.0.0'}
@@ -828,6 +835,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@sinclair/typebox@0.27.8':
+ resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
+
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@@ -901,6 +911,21 @@ packages:
vite: ^5.0.0
vue: ^3.2.25
+ '@vitest/expect@1.6.0':
+ resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==}
+
+ '@vitest/runner@1.6.0':
+ resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==}
+
+ '@vitest/snapshot@1.6.0':
+ resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==}
+
+ '@vitest/spy@1.6.0':
+ resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==}
+
+ '@vitest/utils@1.6.0':
+ resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==}
+
'@vue/compiler-core@3.4.25':
resolution: {integrity: sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg==}
@@ -940,6 +965,10 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
+ acorn-walk@8.3.3:
+ resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==}
+ engines: {node: '>=0.4.0'}
+
acorn@8.11.2:
resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==}
engines: {node: '>=0.4.0'}
@@ -949,9 +978,16 @@ packages:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
+ assertion-error@1.1.0:
+ resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
+
autoprefixer@10.4.16:
resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -993,6 +1029,10 @@ packages:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
+ cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+
call-bind@1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
@@ -1002,6 +1042,10 @@ packages:
caniuse-lite@1.0.30001570:
resolution: {integrity: sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==}
+ chai@4.4.1:
+ resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==}
+ engines: {node: '>=4'}
+
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@@ -1010,12 +1054,8 @@ packages:
resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
- cheerio-select@2.1.0:
- resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
-
- cheerio@1.0.0-rc.12:
- resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
- engines: {node: '>= 6'}
+ check-error@1.0.3:
+ resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
citty@0.1.5:
resolution: {integrity: sha512-AS7n5NSc0OQVMV9v6wt3ByujNIrne0/cTjiC2MYqhvao57VNfiuVksTSr2p17nVOhEr2KtqiAkGwHcgMC/qUuQ==}
@@ -1074,6 +1114,10 @@ packages:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
+ cross-spawn@7.0.3:
+ resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
+ engines: {node: '>= 8'}
+
css-declaration-sorter@7.1.1:
resolution: {integrity: sha512-dZ3bVTEEc1vxr3Bek9vGwfB5Z6ESPULhcRvO472mfjVnj8jRcTnKO8/JTczlvxM10Myb+wBM++1MtdO76eWcaQ==}
engines: {node: ^14 || ^16 || >=18}
@@ -1142,6 +1186,10 @@ packages:
supports-color:
optional: true
+ deep-eql@4.1.4:
+ resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
+ engines: {node: '>=6'}
+
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
@@ -1160,6 +1208,10 @@ packages:
devalue@5.0.0:
resolution: {integrity: sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==}
+ diff-sequences@29.6.3:
+ resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
@@ -1220,10 +1272,17 @@ packages:
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
+ execa@8.0.1:
+ resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
+ engines: {node: '>=16.17'}
+
express@4.19.2:
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
engines: {node: '>= 0.10.0'}
@@ -1273,9 +1332,16 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
+ get-func-name@2.0.2:
+ resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
+
get-intrinsic@1.2.1:
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
+ get-stream@8.0.1:
+ resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
+ engines: {node: '>=16'}
+
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -1315,16 +1381,21 @@ packages:
resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==}
engines: {node: '>= 0.4'}
+ he@1.2.0:
+ resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
+ hasBin: true
+
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
- htmlparser2@8.0.2:
- resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
-
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
+ human-signals@5.0.0:
+ resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
+ engines: {node: '>=16.17.0'}
+
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -1368,6 +1439,13 @@ packages:
is-reference@1.2.1:
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
+ is-stream@3.0.0:
+ resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
jiti@1.21.0:
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
hasBin: true
@@ -1375,6 +1453,9 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ js-tokens@9.0.0:
+ resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==}
+
jsesc@2.5.2:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
engines: {node: '>=4'}
@@ -1395,12 +1476,19 @@ packages:
resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==}
engines: {node: '>=14'}
+ local-pkg@0.5.0:
+ resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
+ engines: {node: '>=14'}
+
lodash.memoize@4.1.2:
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
lodash.uniq@4.5.0:
resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
+ loupe@2.3.7:
+ resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
+
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -1424,6 +1512,9 @@ packages:
merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
+ merge-stream@2.0.0:
+ resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+
merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -1449,6 +1540,10 @@ packages:
engines: {node: '>=4'}
hasBin: true
+ mimic-fn@4.0.0:
+ resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
+ engines: {node: '>=12'}
+
minimatch@5.1.6:
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
engines: {node: '>=10'}
@@ -1495,6 +1590,9 @@ packages:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
+ node-html-parser@6.1.13:
+ resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==}
+
node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
@@ -1502,6 +1600,10 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
+ npm-run-path@5.3.0:
+ resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
@@ -1519,16 +1621,26 @@ packages:
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
- parse5-htmlparser2-tree-adapter@7.0.0:
- resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
+ onetime@6.0.0:
+ resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
+ engines: {node: '>=12'}
- parse5@7.1.2:
- resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
+ p-limit@5.0.0:
+ resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
+ engines: {node: '>=18'}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-key@4.0.0:
+ resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+ engines: {node: '>=12'}
+
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -1542,6 +1654,9 @@ packages:
pathe@1.1.1:
resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==}
+ pathval@1.1.1:
+ resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
+
picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@@ -1751,6 +1866,10 @@ packages:
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
engines: {node: ^14.13.1 || >=16.0.0}
+ pretty-format@29.7.0:
+ resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@@ -1770,6 +1889,9 @@ packages:
resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
engines: {node: '>= 0.8'}
+ react-is@18.3.1:
+ resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
@@ -1825,9 +1947,24 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
+ siginfo@2.0.0:
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
slash@4.0.0:
resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
engines: {node: '>=12'}
@@ -1840,10 +1977,23 @@ packages:
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
engines: {node: '>=0.10.0'}
+ stackback@0.0.2:
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
+
statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'}
+ std-env@3.7.0:
+ resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==}
+
+ strip-final-newline@3.0.0:
+ resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
+ engines: {node: '>=12'}
+
+ strip-literal@2.1.0:
+ resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==}
+
stylehacks@6.0.1:
resolution: {integrity: sha512-jTqG2aIoX2fYg0YsGvqE4ooE/e75WmaEjnNiP6Ag7irLtHxML8NJRxRxS0HyDpde8DRGuEXTFVHVfR5Tmbxqzg==}
engines: {node: ^14 || ^16 || >=18.0}
@@ -1863,6 +2013,17 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
+ tinybench@2.8.0:
+ resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==}
+
+ tinypool@0.8.4:
+ resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==}
+ engines: {node: '>=14.0.0'}
+
+ tinyspy@2.2.1:
+ resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
+ engines: {node: '>=14.0.0'}
+
to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
@@ -1875,6 +2036,10 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
+ type-detect@4.0.8:
+ resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
+ engines: {node: '>=4'}
+
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@@ -1931,6 +2096,11 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
+ vite-node@1.6.0:
+ resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+
vite@5.2.10:
resolution: {integrity: sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -1959,6 +2129,31 @@ packages:
terser:
optional: true
+ vitest@1.6.0:
+ resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@edge-runtime/vm': '*'
+ '@types/node': ^18.0.0 || >=20.0.0
+ '@vitest/browser': 1.6.0
+ '@vitest/ui': 1.6.0
+ happy-dom: '*'
+ jsdom: '*'
+ peerDependenciesMeta:
+ '@edge-runtime/vm':
+ optional: true
+ '@types/node':
+ optional: true
+ '@vitest/browser':
+ optional: true
+ '@vitest/ui':
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+
vue-demi@0.14.7:
resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'}
@@ -1983,12 +2178,26 @@ packages:
typescript:
optional: true
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ why-is-node-running@2.2.2:
+ resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==}
+ engines: {node: '>=8'}
+ hasBin: true
+
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+ yocto-queue@1.0.0:
+ resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
+ engines: {node: '>=12.20'}
+
zhead@2.2.4:
resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==}
@@ -2342,6 +2551,10 @@ snapshots:
'@esbuild/win32-x64@0.20.2':
optional: true
+ '@jest/schemas@29.6.3':
+ dependencies:
+ '@sinclair/typebox': 0.27.8
+
'@jridgewell/gen-mapping@0.3.5':
dependencies:
'@jridgewell/set-array': 1.2.1
@@ -2465,6 +2678,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.14.1':
optional: true
+ '@sinclair/typebox@0.27.8': {}
+
'@trysound/sax@0.2.0': {}
'@tsconfig/node20@20.1.4': {}
@@ -2550,6 +2765,35 @@ snapshots:
vite: 5.2.10(@types/node@20.12.7)
vue: 3.4.25(typescript@5.3.3)
+ '@vitest/expect@1.6.0':
+ dependencies:
+ '@vitest/spy': 1.6.0
+ '@vitest/utils': 1.6.0
+ chai: 4.4.1
+
+ '@vitest/runner@1.6.0':
+ dependencies:
+ '@vitest/utils': 1.6.0
+ p-limit: 5.0.0
+ pathe: 1.1.1
+
+ '@vitest/snapshot@1.6.0':
+ dependencies:
+ magic-string: 0.30.10
+ pathe: 1.1.1
+ pretty-format: 29.7.0
+
+ '@vitest/spy@1.6.0':
+ dependencies:
+ tinyspy: 2.2.1
+
+ '@vitest/utils@1.6.0':
+ dependencies:
+ diff-sequences: 29.6.3
+ estree-walker: 3.0.3
+ loupe: 2.3.7
+ pretty-format: 29.7.0
+
'@vue/compiler-core@3.4.25':
dependencies:
'@babel/parser': 7.24.4
@@ -2612,14 +2856,22 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
+ acorn-walk@8.3.3:
+ dependencies:
+ acorn: 8.11.2
+
acorn@8.11.2: {}
ansi-styles@3.2.1:
dependencies:
color-convert: 1.9.3
+ ansi-styles@5.2.0: {}
+
array-flatten@1.1.1: {}
+ assertion-error@1.1.0: {}
+
autoprefixer@10.4.16(postcss@8.4.31):
dependencies:
browserslist: 4.22.2
@@ -2672,6 +2924,8 @@ snapshots:
bytes@3.1.2: {}
+ cac@6.7.14: {}
+
call-bind@1.0.2:
dependencies:
function-bind: 1.1.2
@@ -2686,6 +2940,16 @@ snapshots:
caniuse-lite@1.0.30001570: {}
+ chai@4.4.1:
+ dependencies:
+ assertion-error: 1.1.0
+ check-error: 1.0.3
+ deep-eql: 4.1.4
+ get-func-name: 2.0.2
+ loupe: 2.3.7
+ pathval: 1.1.1
+ type-detect: 4.0.8
+
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1
@@ -2694,24 +2958,9 @@ snapshots:
chalk@5.3.0: {}
- cheerio-select@2.1.0:
+ check-error@1.0.3:
dependencies:
- boolbase: 1.0.0
- css-select: 5.1.0
- css-what: 6.1.0
- domelementtype: 2.3.0
- domhandler: 5.0.3
- domutils: 3.1.0
-
- cheerio@1.0.0-rc.12:
- dependencies:
- cheerio-select: 2.1.0
- dom-serializer: 2.0.0
- domhandler: 5.0.3
- domutils: 3.1.0
- htmlparser2: 8.0.2
- parse5: 7.1.2
- parse5-htmlparser2-tree-adapter: 7.0.0
+ get-func-name: 2.0.2
citty@0.1.5:
dependencies:
@@ -2766,6 +3015,12 @@ snapshots:
cookie@0.6.0: {}
+ cross-spawn@7.0.3:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
css-declaration-sorter@7.1.1(postcss@8.4.31):
dependencies:
postcss: 8.4.31
@@ -2849,6 +3104,10 @@ snapshots:
dependencies:
ms: 2.1.2
+ deep-eql@4.1.4:
+ dependencies:
+ type-detect: 4.0.8
+
deepmerge@4.3.1: {}
defu@6.1.3: {}
@@ -2859,6 +3118,8 @@ snapshots:
devalue@5.0.0: {}
+ diff-sequences@29.6.3: {}
+
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0
@@ -2974,8 +3235,24 @@ snapshots:
estree-walker@2.0.2: {}
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.5
+
etag@1.8.1: {}
+ execa@8.0.1:
+ dependencies:
+ cross-spawn: 7.0.3
+ get-stream: 8.0.1
+ human-signals: 5.0.0
+ is-stream: 3.0.0
+ merge-stream: 2.0.0
+ npm-run-path: 5.3.0
+ onetime: 6.0.0
+ signal-exit: 4.1.0
+ strip-final-newline: 3.0.0
+
express@4.19.2:
dependencies:
accepts: 1.3.8
@@ -3061,6 +3338,8 @@ snapshots:
gensync@1.0.0-beta.2: {}
+ get-func-name@2.0.2: {}
+
get-intrinsic@1.2.1:
dependencies:
function-bind: 1.1.2
@@ -3068,6 +3347,8 @@ snapshots:
has-proto: 1.0.1
has-symbols: 1.0.3
+ get-stream@8.0.1: {}
+
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -3106,14 +3387,9 @@ snapshots:
dependencies:
function-bind: 1.1.2
- hookable@5.5.3: {}
+ he@1.2.0: {}
- htmlparser2@8.0.2:
- dependencies:
- domelementtype: 2.3.0
- domhandler: 5.0.3
- domutils: 3.1.0
- entities: 4.5.0
+ hookable@5.5.3: {}
http-errors@2.0.0:
dependencies:
@@ -3123,6 +3399,8 @@ snapshots:
statuses: 2.0.1
toidentifier: 1.0.1
+ human-signals@5.0.0: {}
+
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
@@ -3160,10 +3438,16 @@ snapshots:
dependencies:
'@types/estree': 1.0.0
+ is-stream@3.0.0: {}
+
+ isexe@2.0.0: {}
+
jiti@1.21.0: {}
js-tokens@4.0.0: {}
+ js-tokens@9.0.0: {}
+
jsesc@2.5.2: {}
json5@2.2.3: {}
@@ -3178,10 +3462,19 @@ snapshots:
lilconfig@3.0.0: {}
+ local-pkg@0.5.0:
+ dependencies:
+ mlly: 1.4.2
+ pkg-types: 1.0.3
+
lodash.memoize@4.1.2: {}
lodash.uniq@4.5.0: {}
+ loupe@2.3.7:
+ dependencies:
+ get-func-name: 2.0.2
+
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -3202,6 +3495,8 @@ snapshots:
merge-descriptors@1.0.1: {}
+ merge-stream@2.0.0: {}
+
merge2@1.4.1: {}
methods@1.1.2: {}
@@ -3219,6 +3514,8 @@ snapshots:
mime@1.6.0: {}
+ mimic-fn@4.0.0: {}
+
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.1
@@ -3262,10 +3559,19 @@ snapshots:
negotiator@0.6.3: {}
+ node-html-parser@6.1.13:
+ dependencies:
+ css-select: 5.1.0
+ he: 1.2.0
+
node-releases@2.0.14: {}
normalize-range@0.1.2: {}
+ npm-run-path@5.3.0:
+ dependencies:
+ path-key: 4.0.0
+
nth-check@2.1.1:
dependencies:
boolbase: 1.0.0
@@ -3282,17 +3588,20 @@ snapshots:
dependencies:
wrappy: 1.0.2
- parse5-htmlparser2-tree-adapter@7.0.0:
+ onetime@6.0.0:
dependencies:
- domhandler: 5.0.3
- parse5: 7.1.2
+ mimic-fn: 4.0.0
- parse5@7.1.2:
+ p-limit@5.0.0:
dependencies:
- entities: 4.5.0
+ yocto-queue: 1.0.0
parseurl@1.3.3: {}
+ path-key@3.1.1: {}
+
+ path-key@4.0.0: {}
+
path-parse@1.0.7: {}
path-to-regexp@0.1.7: {}
@@ -3301,6 +3610,8 @@ snapshots:
pathe@1.1.1: {}
+ pathval@1.1.1: {}
+
picocolors@1.0.0: {}
picomatch@2.3.1: {}
@@ -3492,6 +3803,12 @@ snapshots:
pretty-bytes@6.1.1: {}
+ pretty-format@29.7.0:
+ dependencies:
+ '@jest/schemas': 29.6.3
+ ansi-styles: 5.2.0
+ react-is: 18.3.1
+
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
@@ -3512,6 +3829,8 @@ snapshots:
iconv-lite: 0.4.24
unpipe: 1.0.0
+ react-is@18.3.1: {}
+
resolve@1.22.8:
dependencies:
is-core-module: 2.13.1
@@ -3596,20 +3915,40 @@ snapshots:
setprototypeof@1.2.0: {}
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
side-channel@1.0.4:
dependencies:
call-bind: 1.0.2
get-intrinsic: 1.2.1
object-inspect: 1.12.3
+ siginfo@2.0.0: {}
+
+ signal-exit@4.1.0: {}
+
slash@4.0.0: {}
source-map-js@1.0.2: {}
source-map-js@1.2.0: {}
+ stackback@0.0.2: {}
+
statuses@2.0.1: {}
+ std-env@3.7.0: {}
+
+ strip-final-newline@3.0.0: {}
+
+ strip-literal@2.1.0:
+ dependencies:
+ js-tokens: 9.0.0
+
stylehacks@6.0.1(postcss@8.4.31):
dependencies:
browserslist: 4.22.2
@@ -3632,6 +3971,12 @@ snapshots:
csso: 5.0.5
picocolors: 1.0.0
+ tinybench@2.8.0: {}
+
+ tinypool@0.8.4: {}
+
+ tinyspy@2.2.1: {}
+
to-fast-properties@2.0.0: {}
to-regex-range@5.0.1:
@@ -3640,6 +3985,8 @@ snapshots:
toidentifier@1.0.1: {}
+ type-detect@4.0.8: {}
+
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
@@ -3718,6 +4065,23 @@ snapshots:
vary@1.1.2: {}
+ vite-node@1.6.0(@types/node@20.12.7):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.3.4
+ pathe: 1.1.1
+ picocolors: 1.0.0
+ vite: 5.2.10(@types/node@20.12.7)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vite@5.2.10(@types/node@20.12.7):
dependencies:
esbuild: 0.20.2
@@ -3727,6 +4091,39 @@ snapshots:
'@types/node': 20.12.7
fsevents: 2.3.3
+ vitest@1.6.0(@types/node@20.12.7):
+ dependencies:
+ '@vitest/expect': 1.6.0
+ '@vitest/runner': 1.6.0
+ '@vitest/snapshot': 1.6.0
+ '@vitest/spy': 1.6.0
+ '@vitest/utils': 1.6.0
+ acorn-walk: 8.3.3
+ chai: 4.4.1
+ debug: 4.3.4
+ execa: 8.0.1
+ local-pkg: 0.5.0
+ magic-string: 0.30.10
+ pathe: 1.1.1
+ picocolors: 1.0.0
+ std-env: 3.7.0
+ strip-literal: 2.1.0
+ tinybench: 2.8.0
+ tinypool: 0.8.4
+ vite: 5.2.10(@types/node@20.12.7)
+ vite-node: 1.6.0(@types/node@20.12.7)
+ why-is-node-running: 2.2.2
+ optionalDependencies:
+ '@types/node': 20.12.7
+ transitivePeerDependencies:
+ - less
+ - lightningcss
+ - sass
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vue-demi@0.14.7(vue@3.4.25(typescript@5.3.3)):
dependencies:
vue: 3.4.25(typescript@5.3.3)
@@ -3746,8 +4143,19 @@ snapshots:
optionalDependencies:
typescript: 5.3.3
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ why-is-node-running@2.2.2:
+ dependencies:
+ siginfo: 2.0.0
+ stackback: 0.0.2
+
wrappy@1.0.2: {}
yallist@3.1.1: {}
+ yocto-queue@1.0.0: {}
+
zhead@2.2.4: {}
diff --git a/src/plugin/generateHtml.ts b/src/plugin/generateHtml.ts
index 9022941..164ba47 100644
--- a/src/plugin/generateHtml.ts
+++ b/src/plugin/generateHtml.ts
@@ -1,140 +1,85 @@
-import type { SSRContext } from 'vue/server-renderer'
-import { load } from 'cheerio'
-import { ModuleNode } from 'vite'
+import { parse, HTMLElement } from 'node-html-parser'
import type { VueHeadClient, MergeHead } from '@unhead/vue'
-import { basename } from 'node:path'
import { State } from '../types'
-import { renderCssForSsr } from './renderCssForSsr'
-
-function renderPreloadLinks(modules: string[], manifest: any /* TODO */) {
- let links = ''
- const seen = new Set()
-
- modules.forEach(id => {
- const files = manifest[id]
-
- if (files) {
- files.forEach((file: any /* TODO */) => {
- if (!seen.has(file)) {
- seen.add(file)
-
- const filename = basename(file)
-
- if (manifest[filename]) {
- for (const depFile of manifest[filename]) {
- links += renderPreloadLink(depFile)
- seen.add(depFile)
- }
- }
-
- links += renderPreloadLink(file)
- }
- })
- }
- })
-
- return links
-}
-
-function renderPreloadLink(file: string) {
- if (file.endsWith('.js')) {
- return ``
- } else if (file.endsWith('.css')) {
- return ``
- } else if (file.endsWith('.woff')) {
- return ` `
- } else if (file.endsWith('.woff2')) {
- return ` `
- } else if (file.endsWith('.gif')) {
- return ` `
- } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
- return ` `
- } else if (file.endsWith('.png')) {
- return ` `
- } else {
- // TODO
- return ''
- }
-}
export async function generateHtml(template: string,
+ preloadLinks: string,
rendered: string,
- ctx: SSRContext,
+ teleports: Record,
state: State,
head: VueHeadClient,
- cssModules?: Set,
- manifest?: object) {
- const $ = load(template)
+ styles?: string) {
+ const root = parse(template, { comment: true })
+
+ root.getElementById('app')?.set_content(rendered)
- $('#app').html(rendered)
+ const htmlHead = root.querySelector('head')
- const preloadLinks = renderPreloadLinks(ctx.modules, manifest ?? {})
- $('head').append(preloadLinks)
+ htmlHead?.insertAdjacentHTML('beforeend', preloadLinks)
- if (cssModules !== undefined) {
- const styles = renderCssForSsr(cssModules)
- $('head').append(styles)
+ if (styles !== undefined) {
+ htmlHead?.insertAdjacentHTML('beforeend', styles)
}
+ const body = root.querySelector('body')
+
if (state.value !== undefined) {
const { uneval } = await import('devalue')
- $('body').append(``)
+ body?.insertAdjacentHTML('beforeend', ``)
}
- const teleports = ctx.teleports ?? {}
-
if (teleports['#teleports'] !== undefined) {
- $('body').append(`${teleports['#teleports']}
`)
+ body?.insertAdjacentHTML('beforeend', `${teleports['#teleports']}
`)
}
const resolvedTags = await head.resolveTags()
- let tags = ['title', 'meta', 'link', 'base', 'style', 'script', 'noscript']
+ const htmlTitle = root.querySelector('title')
- if ($('title').length === 1) {
- tags = tags.filter(t => t !== 'title')
+ if (htmlTitle !== null) {
const title = resolvedTags.find(t => t.tag === 'title')
if (title !== undefined) {
- // @ts-ignore
- $('title').text(title.textContent)
+ htmlTitle.textContent = title.textContent ?? ''
}
}
- tags.map(tag => {
- resolvedTags
- .filter(t => t.tag === tag)
- .map(t => {
- let props = ''
+ const allowedTags = ['meta', 'link', 'base', 'style', 'script', 'noscript']
+
+ resolvedTags
+ .filter(tag => tag.tag === allowedTags.find(allowed => allowed === tag.tag))
+ .forEach(tag => {
+ let props = ''
+
+ for (const [key, value] of Object.entries(tag.props)) {
+ props = `${props} ${key}="${value}"`
+ }
- for (const [key, value] of Object.entries(t.props)) {
- props = `${props} ${key}="${value}"`
- }
+ const el = new HTMLElement(tag.tag, {}, props)
+ el.textContent = tag.innerHTML
+ ?? tag.textContent
+ ?? ''
- if (t.innerHTML !== undefined) {
- $('head').append(`<${tag} ${props}>${t.innerHTML}${tag}>`)
- } else {
- $('head').append(`<${tag} ${props}>`)
- }
- })
- })
+ htmlHead?.appendChild(el)
+ })
const bodyAttrs = resolvedTags.find(t => t.tag === 'bodyAttrs')
if (bodyAttrs !== undefined) {
for (const [key, value] of Object.entries(bodyAttrs.props)) {
- $('body').attr(key, value)
+ body?.setAttribute(key, value)
}
}
const htmlAttrs = resolvedTags.find(t => t.tag === 'htmlAttrs')
+ const htmlRoot = root.querySelector('html')
if (htmlAttrs !== undefined) {
for (const [key, value] of Object.entries(htmlAttrs.props)) {
- $('html').attr(key, value)
+ htmlRoot?.setAttribute(key, value)
}
}
- return $.html()
+ return root.toString()
}
diff --git a/src/plugin/index.ts b/src/plugin/index.ts
index 991ab2a..b3076ee 100644
--- a/src/plugin/index.ts
+++ b/src/plugin/index.ts
@@ -13,6 +13,8 @@ import { transformEntrypoint } from './transformEntrypoint'
import { generateHtml } from './generateHtml'
import type { Params, CallbackFn } from '../types'
import { Router } from 'vue-router'
+import { renderCssForSsr } from './renderCssForSsr'
+import { renderPreloadLinks } from './renderPreloadLinks'
declare function vueSSRFn(App: App, params: Params, cb: CallbackFn): { App: App } & Params & { cb: CallbackFn }
@@ -87,27 +89,27 @@ export default function vueSsrPlugin(): Plugin {
let template: string | undefined = readFileSync(resolve(cwd(), 'index.html'), 'utf-8')
template = await server.transformIndexHtml(url, template)
-
+
const { App, routes, cb, scrollBehavior }: ReturnType = (await server.ssrLoadModule(resolve(cwd(), ssr as string))).default
-
+
const { vueSSR } = (await import('./vue'))
-
+
function callbackFn(request: Request, response: Response) {
return async function({ app, router, state }: { app: App, router: Router, state: any }) {
return await cb({ app, router, state, request, response })
}
}
-
+
// @ts-ignore
const { app, router, state, head } = await vueSSR(App, { routes, scrollBehavior }, callbackFn(request, response), true, true)
-
+
await router.push(url.replace(router.options.history.base, ''))
await router.isReady()
-
+
let redirect = null
-
+
const cookies = new Set()
-
+
const ctx: SSRContext = {
request,
response: {
@@ -121,30 +123,40 @@ export default function vueSsrPlugin(): Plugin {
redirect = `${router.options.history.base}${url}`
},
}
-
+
const rendered = await renderToString(app, ctx)
-
+
const loadedModules = server.moduleGraph.getModulesByFile(resolve(cwd(), ssr as string))
-
+
+ let styles
+
+ if (loadedModules !== undefined) {
+ styles = renderCssForSsr(loadedModules)
+ }
+
+ const preloadLinks = renderPreloadLinks(ctx.modules, {})
+
const html = await generateHtml(
template,
+ preloadLinks,
rendered,
- ctx,
+ ctx.teleports ?? {},
state,
head,
- loadedModules)
-
+ styles
+ )
+
response.setHeader('Set-Cookie', [...cookies])
-
+
if (redirect !== null) {
// https://github.com/vitejs/vite/discussions/6562#discussioncomment-1999566
response.writeHead(302, {
location: redirect,
}).end()
-
+
return
}
-
+
response.end(html)
} catch (e) {
server.ssrFixStacktrace(e as Error)
@@ -214,7 +226,9 @@ async function generateTemplate(
throw _err
}
- const html = await generateHtml(template, rendered, ctx, state, head, undefined, manifest)
+ const preloadLinks = renderPreloadLinks(ctx.modules, manifest ?? {})
+
+ const html = await generateHtml(template, preloadLinks, rendered, ctx.teleports ?? {}, state, head)
return {
html,
diff --git a/src/plugin/renderPreloadLinks.ts b/src/plugin/renderPreloadLinks.ts
new file mode 100644
index 0000000..87ae7aa
--- /dev/null
+++ b/src/plugin/renderPreloadLinks.ts
@@ -0,0 +1,52 @@
+import { basename } from 'node:path'
+
+export function renderPreloadLinks(modules: string[], manifest: any /* TODO */) {
+ let links = ''
+ const seen = new Set()
+
+ modules.forEach(id => {
+ const files = manifest[id]
+
+ if (files) {
+ files.forEach((file: any /* TODO */) => {
+ if (!seen.has(file)) {
+ seen.add(file)
+
+ const filename = basename(file)
+
+ if (manifest[filename]) {
+ for (const depFile of manifest[filename]) {
+ links += renderPreloadLink(depFile)
+ seen.add(depFile)
+ }
+ }
+
+ links += renderPreloadLink(file)
+ }
+ })
+ }
+ })
+
+ return links
+}
+
+function renderPreloadLink(file: string) {
+ if (file.endsWith('.js')) {
+ return ``
+ } else if (file.endsWith('.css')) {
+ return ``
+ } else if (file.endsWith('.woff')) {
+ return ` `
+ } else if (file.endsWith('.woff2')) {
+ return ` `
+ } else if (file.endsWith('.gif')) {
+ return ` `
+ } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
+ return ` `
+ } else if (file.endsWith('.png')) {
+ return ` `
+ } else {
+ // TODO
+ return ''
+ }
+}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..54cf6ec
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ reporters: process.env.GITHUB_ACTIONS ? ['dot', 'github-actions'] : ['dot'],
+ },
+})