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