Skip to content

Commit

Permalink
test: move ssr tests from plugin-react (#12528)
Browse files Browse the repository at this point in the history
  • Loading branch information
sapphi-red committed Mar 22, 2023
1 parent abf536f commit 688a557
Show file tree
Hide file tree
Showing 19 changed files with 332 additions and 16 deletions.
35 changes: 35 additions & 0 deletions playground/ssr/__tests__/serve.ts
@@ -0,0 +1,35 @@
// this is automatically detected by playground/vitestSetup.ts and will replace
// the default e2e test serve behavior

import path from 'node:path'
import kill from 'kill-port'
import { hmrPorts, ports, rootDir } from '~utils'

export const port = ports.ssr

export async function serve(): Promise<{ close(): Promise<void> }> {
await kill(port)

const { createServer } = await import(path.resolve(rootDir, 'server.js'))
const { app, vite } = await createServer(rootDir, hmrPorts.ssr)

return new Promise((resolve, reject) => {
try {
const server = app.listen(port, () => {
resolve({
// for test teardown
async close() {
await new Promise((resolve) => {
server.close(resolve)
})
if (vite) {
await vite.close()
}
},
})
})
} catch (e) {
reject(e)
}
})
}
19 changes: 19 additions & 0 deletions playground/ssr/__tests__/ssr.spec.ts
@@ -0,0 +1,19 @@
import { expect, test } from 'vitest'
import { port } from './serve'
import { page } from '~utils'

const url = `http://localhost:${port}`

test(`circular dependencies modules doesn't throw`, async () => {
await page.goto(`${url}/circular-dep`)

expect(await page.textContent('.circ-dep-init')).toMatch(
'circ-dep-init-a circ-dep-init-b',
)
})

test(`deadlock doesn't happen`, async () => {
await page.goto(`${url}/forked-deadlock`)

expect(await page.textContent('.forked-deadlock')).toMatch('rendered')
})
12 changes: 12 additions & 0 deletions playground/ssr/index.html
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SSR</title>
</head>
<body>
<h1>SSR</h1>
<div><!--app-html--></div>
</body>
</html>
15 changes: 15 additions & 0 deletions playground/ssr/package.json
@@ -0,0 +1,15 @@
{
"name": "@vitejs/test-ssr",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "node server",
"serve": "NODE_ENV=production node server",
"debug": "node --inspect-brk server"
},
"dependencies": {},
"devDependencies": {
"express": "^4.18.2"
}
}
69 changes: 69 additions & 0 deletions playground/ssr/server.js
@@ -0,0 +1,69 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const isTest = process.env.VITEST

export async function createServer(root = process.cwd(), hmrPort) {
const resolve = (p) => path.resolve(__dirname, p)

const app = express()

/**
* @type {import('vite').ViteDevServer}
*/
const vite = await (
await import('vite')
).createServer({
root,
logLevel: isTest ? 'error' : 'info',
server: {
middlewareMode: true,
watch: {
// During tests we edit the files too fast and sometimes chokidar
// misses change events, so enforce polling for consistency
usePolling: true,
interval: 100,
},
hmr: {
port: hmrPort,
},
},
appType: 'custom',
})
// use vite's connect instance as middleware
app.use(vite.middlewares)

app.use('*', async (req, res, next) => {
try {
const url = req.originalUrl

let template
template = fs.readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
const render = (await vite.ssrLoadModule('/src/app.js')).render

const appHtml = await render(url, __dirname)

const html = template.replace(`<!--app-html-->`, appHtml)

res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})

return { app, vite }
}

if (!isTest) {
createServer().then(({ app }) =>
app.listen(5173, () => {
console.log('http://localhost:5173')
}),
)
}
41 changes: 41 additions & 0 deletions playground/ssr/src/app.js
@@ -0,0 +1,41 @@
import { escapeHtml } from './utils'

const pathRenderers = {
'/': renderRoot,
'/circular-dep': renderCircularDep,
'/forked-deadlock': renderForkedDeadlock,
}

export async function render(url, rootDir) {
const pathname = url.replace(/#[^#]*$/, '').replace(/\?[^?]*$/, '')
const renderer = pathRenderers[pathname]
if (renderer) {
return await renderer(rootDir)
}
return '404'
}

async function renderRoot(rootDir) {
const paths = Object.keys(pathRenderers).filter((key) => key !== '/')
return `
<ul>
${paths
.map(
(path) =>
`<li><a href="${escapeHtml(path)}">${escapeHtml(path)}</a></li>`,
)
.join('\n')}
</ul>
`
}

async function renderCircularDep(rootDir) {
const { getValueAB } = await import('./circular-dep-init/circular-dep-init')
return `<div class="circ-dep-init">${escapeHtml(getValueAB())}</div>`
}

async function renderForkedDeadlock(rootDir) {
const { commonModuleExport } = await import('./forked-deadlock/common-module')
commonModuleExport()
return `<div class="forked-deadlock">rendered</div>`
}
1 change: 1 addition & 0 deletions playground/ssr/src/circular-dep-init/README.md
@@ -0,0 +1 @@
This test aim to find out wherever the modules with circular dependencies are correctly initialized
2 changes: 2 additions & 0 deletions playground/ssr/src/circular-dep-init/circular-dep-init.js
@@ -0,0 +1,2 @@
export * from './module-a'
export { getValueAB } from './module-b'
1 change: 1 addition & 0 deletions playground/ssr/src/circular-dep-init/module-a.js
@@ -0,0 +1 @@
export const valueA = 'circ-dep-init-a'
8 changes: 8 additions & 0 deletions playground/ssr/src/circular-dep-init/module-b.js
@@ -0,0 +1,8 @@
import { valueA } from './circular-dep-init'

export const valueB = 'circ-dep-init-b'
export const valueAB = valueA.concat(` ${valueB}`)

export function getValueAB() {
return valueAB
}
51 changes: 51 additions & 0 deletions playground/ssr/src/forked-deadlock/README.md
@@ -0,0 +1,51 @@
This test aim to check for a particular type of circular dependency that causes tricky deadlocks, **deadlocks with forked imports stack**

```
A -> B means: B is imported by A and B has A in its stack
A ... B means: A is waiting for B to ssrLoadModule()
H -> X ... Y
H -> X -> Y ... B
H -> A ... B
H -> A -> B ... X
```

### Forked deadlock description:

```
[X] is waiting for [Y] to resolve
↑ ↳ is waiting for [A] to resolve
│ ↳ is waiting for [B] to resolve
│ ↳ is waiting for [X] to resolve
└────────────────────────────────────────────────────────────────────────┘
```

This may seems a traditional deadlock, but the thing that makes this special is the import stack of each module:

```
[X] stack:
[H]
```

```
[Y] stack:
[X]
[H]
```

```
[A] stack:
[H]
```

```
[B] stack:
[A]
[H]
```

Even if `[X]` is imported by `[B]`, `[B]` is not in `[X]`'s stack because it's imported by `[H]` in first place then it's stack is only composed by `[H]`. `[H]` **forks** the imports **stack** and this make hard to be found.

### Fix description

Vite, when imports `[X]`, should check whether `[X]` is already pending and if it is, it must check that, when it was imported in first place, the stack of `[X]` doesn't have any module in common with the current module; in this case `[B]` has the module `[H]` is common with `[X]` and i can assume that a deadlock is going to happen.
10 changes: 10 additions & 0 deletions playground/ssr/src/forked-deadlock/common-module.js
@@ -0,0 +1,10 @@
import { stuckModuleExport } from './stuck-module'
import { deadlockfuseModuleExport } from './deadlock-fuse-module'

/**
* module H
*/
export function commonModuleExport() {
stuckModuleExport()
deadlockfuseModuleExport()
}
8 changes: 8 additions & 0 deletions playground/ssr/src/forked-deadlock/deadlock-fuse-module.js
@@ -0,0 +1,8 @@
import { fuseStuckBridgeModuleExport } from './fuse-stuck-bridge-module'

/**
* module A
*/
export function deadlockfuseModuleExport() {
fuseStuckBridgeModuleExport()
}
@@ -0,0 +1,8 @@
import { stuckModuleExport } from './stuck-module'

/**
* module C
*/
export function fuseStuckBridgeModuleExport() {
stuckModuleExport()
}
8 changes: 8 additions & 0 deletions playground/ssr/src/forked-deadlock/middle-module.js
@@ -0,0 +1,8 @@
import { deadlockfuseModuleExport } from './deadlock-fuse-module'

/**
* module Y
*/
export function middleModuleExport() {
void deadlockfuseModuleExport
}
8 changes: 8 additions & 0 deletions playground/ssr/src/forked-deadlock/stuck-module.js
@@ -0,0 +1,8 @@
import { middleModuleExport } from './middle-module'

/**
* module X
*/
export function stuckModuleExport() {
middleModuleExport()
}
16 changes: 16 additions & 0 deletions playground/ssr/src/utils.js
@@ -0,0 +1,16 @@
const escapeHtmlReplaceMap = {
'&': '&amp;',
"'": '&#x27;',
'`': '&#x60;',
'"': '&quot;',
'<': '&lt;',
'>': '&gt;',
}

/**
* @param {string} string
* @returns {string}
*/
export function escapeHtml(string) {
return string.replace(/[&'`"<>]/g, (match) => escapeHtmlReplaceMap[match])
}
20 changes: 11 additions & 9 deletions playground/test-utils.ts
Expand Up @@ -22,21 +22,23 @@ export const ports = {
lib: 9521,
'optimize-missing-deps': 9522,
'legacy/client-and-ssr': 9523,
'ssr-deps': 9600,
'ssr-html': 9601,
'ssr-noexternal': 9602,
'ssr-pug': 9603,
'ssr-webworker': 9606,
ssr: 9600,
'ssr-deps': 9601,
'ssr-html': 9602,
'ssr-noexternal': 9603,
'ssr-pug': 9604,
'ssr-webworker': 9605,
'css/postcss-caching': 5005,
'css/postcss-plugins-different-dir': 5006,
'css/dynamic-import': 5007,
}
export const hmrPorts = {
'optimize-missing-deps': 24680,
'ssr-deps': 24681,
'ssr-html': 24682,
'ssr-noexternal': 24683,
'ssr-pug': 24684,
ssr: 24681,
'ssr-deps': 24682,
'ssr-html': 24683,
'ssr-noexternal': 24684,
'ssr-pug': 24685,
}

const hexToNameMap: Record<string, string> = {}
Expand Down

0 comments on commit 688a557

Please sign in to comment.