Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ssr): support ESM-only dependencies #4450

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/playground/ssr-esm/app/entry-server.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import cjs from 'cjs-package'
import { esm, cjsFromEsm } from 'esm-package'

export default { cjs, esm, cjsFromEsm }
1 change: 1 addition & 0 deletions packages/playground/ssr-esm/cjs-package/index.dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'cjs-dev'
1 change: 1 addition & 0 deletions packages/playground/ssr-esm/cjs-package/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'cjs-prod'
10 changes: 10 additions & 0 deletions packages/playground/ssr-esm/cjs-package/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "cjs-package",
"version": "0.0.0",
"exports": {
".": {
"development": "./index.dev.js",
"default": "./index.js"
}
}
}
4 changes: 4 additions & 0 deletions packages/playground/ssr-esm/esm-package/index.dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import cjs from 'cjs-package'

export const esm = 'esm-dev'
export const cjsFromEsm = cjs
4 changes: 4 additions & 0 deletions packages/playground/ssr-esm/esm-package/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import cjs from 'cjs-package'

export const esm = 'esm-prod'
export const cjsFromEsm = cjs
10 changes: 10 additions & 0 deletions packages/playground/ssr-esm/esm-package/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "esm-package",
"version": "0.0.0",
"exports": {
".": {
"development": "./index.dev.mjs",
"default": "./index.mjs"
}
}
}
14 changes: 14 additions & 0 deletions packages/playground/ssr-esm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "test-ssr-esm",
"private": true,
"version": "0.0.0",
"license": "MIT",
"dependencies": {
"cjs-package": "link:./cjs-package",
"esm-package": "link:./esm-package",
"vite": "link:../../vite"
},
"scripts": {
"serve": "node server.mjs"
}
}
22 changes: 22 additions & 0 deletions packages/playground/ssr-esm/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import path from 'path'
import { fileURLToPath } from 'url'

import { install } from 'source-map-support'
install()

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const vite = await import('vite')
const server = await vite.createServer({
root: path.join(__dirname, 'app'),
// mode: 'production',
ssr: {
external: ['cjs-package', 'esm-package']
},
server: {
middlewareMode: 'ssr'
}
})

const entryModule = await server.ssrLoadModule('/entry-server.jsx')
console.log(entryModule.default)
8 changes: 8 additions & 0 deletions packages/playground/ssr-esm/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @type {import('vite').UserConfig}
*/
module.exports = {
build: {
minify: false
}
}
9 changes: 8 additions & 1 deletion packages/playground/ssr-react/__tests__/ssr-react.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { editFile, getColor, isBuild, untilUpdated } from '../../testUtils'
import { editFile, untilUpdated } from '../../testUtils'
import { port } from './serve'
import fetch from 'node-fetch'

Expand Down Expand Up @@ -46,3 +46,10 @@ test('client navigation', async () => {
)
await untilUpdated(() => page.textContent('h1'), 'changed')
})

test(`circular dependecies modules doesn't throw`, async () => {
await page.goto(url)
expect(await page.textContent('.circ-dep-init')).toMatch(
'circ-dep-init-a circ-dep-init-b'
)
})
9 changes: 9 additions & 0 deletions packages/playground/ssr-react/src/add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { multiply } from './multiply'

export function add(a, b) {
return a + b
}

export function addAndMultiply(a, b, c) {
return multiply(add(a, b), c)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This test aim to find out wherever the modules with circular dependencies are correctly initialized
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './module-a'
export { getValueAB } from './module-b'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const valueA = 'circ-dep-init-a'
Original file line number Diff line number Diff line change
@@ -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 packages/playground/ssr-react/src/forked-deadlock/README.md
Original file line number Diff line number Diff line change
@@ -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 packages/playground/ssr-react/src/forked-deadlock/common-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { stuckModuleExport } from './stuck-module'
import { deadlockfuseModuleExport } from './deadlock-fuse-module'

/**
* module H
*/
export function commonModuleExport() {
stuckModuleExport()
deadlockfuseModuleExport()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { fuseStuckBridgeModuleExport } from './fuse-stuck-bridge-module'

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

/**
* module C
*/
export function fuseStuckBridgeModuleExport() {
stuckModuleExport()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { deadlockfuseModuleExport } from './deadlock-fuse-module'

/**
* module Y
*/
export function middleModuleExport() {
void deadlockfuseModuleExport
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { middleModuleExport } from './middle-module'

/**
* module X
*/
export function stuckModuleExport() {
middleModuleExport()
}
9 changes: 9 additions & 0 deletions packages/playground/ssr-react/src/multiply.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { add } from './add'

export function multiply(a, b) {
return a * b
}

export function multiplyAndAdd(a, b, c) {
return add(multiply(a, b), c)
}
11 changes: 10 additions & 1 deletion packages/playground/ssr-react/src/pages/About.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import { addAndMultiply } from '../add'
import { multiplyAndAdd } from '../multiply'

export default function About() {
return <h1>About</h1>
return (
<>
<h1>About</h1>
<div>{addAndMultiply(1, 2, 3)}</div>
<div>{multiplyAndAdd(1, 2, 3)}</div>
</>
)
}
16 changes: 15 additions & 1 deletion packages/playground/ssr-react/src/pages/Home.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import { addAndMultiply } from '../add'
import { multiplyAndAdd } from '../multiply'
import { commonModuleExport } from '../forked-deadlock/common-module'
import { getValueAB } from '../circular-dep-init/circular-dep-init'

export default function Home() {
return <h1>Home</h1>
commonModuleExport()

return (
<>
<h1>Home</h1>
<div>{addAndMultiply(1, 2, 3)}</div>
<div>{multiplyAndAdd(1, 2, 3)}</div>
<div className="circ-dep-init">{getValueAB()}</div>
</>
)
}
2 changes: 2 additions & 0 deletions packages/vite/src/node/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { modulePreloadPolyfillPlugin } from './modulePreloadPolyfill'
import { webWorkerPlugin } from './worker'
import { preAliasPlugin } from './preAlias'
import { definePlugin } from './define'
import { ssrRequireHookPlugin } from './ssrRequireHook'

export async function resolvePlugins(
config: ResolvedConfig,
Expand Down Expand Up @@ -42,6 +43,7 @@ export async function resolvePlugins(
ssrTarget: config.ssr?.target,
asSrc: true
}),
config.build.ssr ? ssrRequireHookPlugin(config) : null,
htmlInlineScriptProxyPlugin(),
cssPlugin(config),
config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
Expand Down
17 changes: 11 additions & 6 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
normalizePath,
fsPathFromId,
ensureVolumeInPath,
resolveFrom,
isDataUrl,
cleanUrl,
slash
Expand All @@ -29,6 +28,7 @@ import { ViteDevServer, SSRTarget } from '..'
import { createFilter } from '@rollup/pluginutils'
import { PartialResolvedId } from 'rollup'
import { resolve as _resolveExports } from 'resolve.exports'
import resolve from 'resolve'

// special id for paths marked with browser: false
// https://github.com/defunctzombie/package-browser-field-spec#ignore-a-module
Expand Down Expand Up @@ -61,6 +61,7 @@ export interface InternalResolveOptions extends ResolveOptions {
tryPrefix?: string
preferRelative?: boolean
isRequire?: boolean
preserveSymlinks?: boolean
}

export function resolvePlugin(baseOptions: InternalResolveOptions): Plugin {
Expand Down Expand Up @@ -361,7 +362,7 @@ export const idToPkgMap = new Map<string, PackageData>()

export function tryNodeResolve(
id: string,
importer: string | undefined,
importer: string | null | undefined,
options: InternalResolveOptions,
targetWeb: boolean,
server?: ViteDevServer,
Expand All @@ -379,12 +380,12 @@ export function tryNodeResolve(
path.isAbsolute(importer) &&
fs.existsSync(cleanUrl(importer))
) {
basedir = path.dirname(importer)
basedir = fs.realpathSync.native(path.dirname(importer))
} else {
basedir = root
}

const pkg = resolvePackageData(pkgId, basedir)
const pkg = resolvePackageData(pkgId, basedir, options.preserveSymlinks)

if (!pkg) {
return
Expand Down Expand Up @@ -483,14 +484,18 @@ const packageCache = new Map<string, PackageData>()

export function resolvePackageData(
id: string,
basedir: string
basedir: string,
preserveSymlinks = false
): PackageData | undefined {
const cacheKey = id + basedir
if (packageCache.has(cacheKey)) {
return packageCache.get(cacheKey)
}
try {
const pkgPath = resolveFrom(`${id}/package.json`, basedir)
const pkgPath = resolve.sync(`${id}/package.json`, {
basedir,
preserveSymlinks
})
return loadPackageData(pkgPath, cacheKey)
} catch (e) {
isDebug && debug(`${chalk.red(`[failed loading package.json]`)} ${id}`)
Expand Down
Loading