Skip to content

Commit 3b2d90a

Browse files
committed
fix: fix build for vite 3 + "type": "module"
Background: pnpm injects `NODE_PATH` when installing npm script binaries in order to simulate flat install structure when running npm scripts. This previously made files outside of VitePress to be able to import transitive deps (e.g. `vue`), but this breaks when upgrading to Vite 3 or in esm mode, because: - "type": "module", aka ESM mode doesn't support `NODE_PATH`, so now project files can't resolve `vue` which is a transitive dep. - Vite 3 now auto-resolves SSR externals, but it requires the dep to be resolvable first. Since it can't resovle `vue`, the Rollup build will fail. The fix: detect if `vue` is resolvable from project root's node_modules. If not, create a symlink to the version of `vue` from VitePress' own deps.
1 parent e0c04f9 commit 3b2d90a

File tree

7 files changed

+126
-106
lines changed

7 files changed

+126
-106
lines changed

src/client/app/ssr.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// entry for SSR
2+
import { createApp } from './index.js'
3+
import { renderToString } from 'vue/server-renderer'
4+
5+
export async function render(path: string) {
6+
const { app, router } = createApp()
7+
await router.go(path)
8+
return renderToString(app)
9+
}

src/node/alias.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createRequire } from 'module'
22
import { resolve, join } from 'path'
33
import { fileURLToPath } from 'url'
44
import { Alias, AliasOptions } from 'vite'
5+
import { SiteConfig } from './config'
56

67
const require = createRequire(import.meta.url)
78
const PKG_ROOT = resolve(fileURLToPath(import.meta.url), '../..')
@@ -19,21 +20,15 @@ export const SITE_DATA_REQUEST_PATH = '/' + SITE_DATA_ID
1920

2021
const vueRuntimePath = 'vue/dist/vue.runtime.esm-bundler.js'
2122

22-
export function resolveAliases(root: string, themeDir: string): AliasOptions {
23+
export function resolveAliases(
24+
{ root, themeDir }: SiteConfig,
25+
ssr: boolean
26+
): AliasOptions {
2327
const paths: Record<string, string> = {
2428
'@theme': themeDir,
2529
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
2630
}
2731

28-
// prioritize vue installed in project root and fallback to
29-
// vue that comes with vitepress itself.
30-
let vuePath
31-
try {
32-
vuePath = require.resolve(vueRuntimePath, { paths: [root] })
33-
} catch (e) {
34-
vuePath = require.resolve(vueRuntimePath)
35-
}
36-
3732
const aliases: Alias[] = [
3833
...Object.keys(paths).map((p) => ({
3934
find: p,
@@ -46,14 +41,25 @@ export function resolveAliases(root: string, themeDir: string): AliasOptions {
4641
{
4742
find: /^vitepress\/theme$/,
4843
replacement: join(DIST_CLIENT_PATH, '/theme-default/index.js')
49-
},
50-
// make sure it always use the same vue dependency that comes
51-
// with vitepress itself
52-
{
53-
find: /^vue$/,
54-
replacement: vuePath
5544
}
5645
]
5746

47+
if (!ssr) {
48+
// Prioritize vue installed in project root and fallback to
49+
// vue that comes with vitepress itself.
50+
// Only do this when not running SSR build, since `vue` needs to be
51+
// externalized during SSR
52+
let vuePath
53+
try {
54+
vuePath = require.resolve(vueRuntimePath, { paths: [root] })
55+
} catch (e) {
56+
vuePath = require.resolve(vueRuntimePath)
57+
}
58+
aliases.push({
59+
find: /^vue$/,
60+
replacement: vuePath
61+
})
62+
}
63+
5864
return aliases
5965
}

src/node/build/build.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@ import { OutputChunk, OutputAsset } from 'rollup'
66
import { resolveConfig } from '../config'
77
import { renderPage } from './render'
88
import { bundle, okMark, failMark } from './bundle'
9+
import { createRequire } from 'module'
10+
import { pathToFileURL } from 'url'
911

1012
export async function build(
11-
root: string,
13+
root?: string,
1214
buildOptions: BuildOptions & { base?: string; mpa?: string } = {}
1315
) {
1416
const start = Date.now()
1517

1618
process.env.NODE_ENV = 'production'
1719
const siteConfig = await resolveConfig(root, 'build', 'production')
20+
const unlinkVue = linkVue(siteConfig.root)
1821

1922
if (buildOptions.base) {
2023
siteConfig.site.base = buildOptions.base
@@ -32,6 +35,9 @@ export async function build(
3235
buildOptions
3336
)
3437

38+
const entryPath = path.join(siteConfig.tempDir, 'app.js')
39+
const { render } = await import(pathToFileURL(entryPath).toString())
40+
3541
const spinner = ora()
3642
spinner.start('rendering pages...')
3743

@@ -58,6 +64,7 @@ export async function build(
5864

5965
for (const page of pages) {
6066
await renderPage(
67+
render,
6168
siteConfig,
6269
page,
6370
clientResult,
@@ -84,6 +91,7 @@ export async function build(
8491
pageToHashMap
8592
)
8693
} finally {
94+
unlinkVue()
8795
if (!process.env.DEBUG)
8896
fs.rmSync(siteConfig.tempDir, { recursive: true, force: true })
8997
}
@@ -92,3 +100,16 @@ export async function build(
92100

93101
console.log(`build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.`)
94102
}
103+
104+
function linkVue(root: string) {
105+
const dest = path.resolve(root, 'node_modules/vue')
106+
// if user did not install vue by themselves, link VitePress' version
107+
if (!fs.existsSync(dest)) {
108+
const src = path.dirname(createRequire(import.meta.url).resolve('vue'))
109+
fs.ensureSymlinkSync(src, dest)
110+
return () => {
111+
fs.unlinkSync(dest)
112+
}
113+
}
114+
return () => {}
115+
}

src/node/build/bundle.ts

Lines changed: 63 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ export async function bundle(
2828
// this is a multi-entry build - every page is considered an entry chunk
2929
// the loading is done via filename conversion rules so that the
3030
// metadata doesn't need to be included in the main chunk.
31-
const input: Record<string, string> = {
32-
app: path.resolve(APP_PATH, 'index.js')
33-
}
31+
const input: Record<string, string> = {}
3432
config.pages.forEach((file) => {
3533
// page filename conversion
3634
// foo/bar.md -> foo_bar.md
@@ -40,66 +38,70 @@ export async function bundle(
4038
// resolve options to pass to vite
4139
const { rollupOptions } = options
4240

43-
const resolveViteConfig = async (ssr: boolean): Promise<ViteUserConfig> => ({
44-
root: config.srcDir,
45-
base: config.site.base,
46-
logLevel: 'warn',
47-
plugins: await createVitePressPlugin(
48-
config,
49-
ssr,
50-
pageToHashMap,
51-
clientJSMap
52-
),
53-
ssr: {
54-
noExternal: ['vitepress', '@docsearch/css']
55-
},
56-
build: {
57-
...options,
58-
emptyOutDir: true,
59-
ssr,
60-
outDir: ssr ? config.tempDir : config.outDir,
61-
cssCodeSplit: false,
62-
rollupOptions: {
63-
...rollupOptions,
64-
input,
65-
// important so that each page chunk and the index export things for each
66-
// other
67-
preserveEntrySignatures: 'allow-extension',
68-
output: {
69-
...rollupOptions?.output,
70-
...(ssr
71-
? {
72-
entryFileNames: `[name].js`,
73-
chunkFileNames: `[name].[hash].js`
74-
}
75-
: {
76-
chunkFileNames(chunk) {
77-
// avoid ads chunk being intercepted by adblock
78-
return /(?:Carbon|BuySell)Ads/.test(chunk.name)
79-
? `assets/chunks/ui-custom.[hash].js`
80-
: `assets/chunks/[name].[hash].js`
81-
},
82-
manualChunks(id, ctx) {
83-
// move known framework code into a stable chunk so that
84-
// custom theme changes do not invalidate hash for all pages
85-
if (id.includes('plugin-vue:export-helper')) {
86-
return 'framework'
87-
}
88-
if (
89-
isEagerChunk(id, ctx) &&
90-
(/@vue\/(runtime|shared|reactivity)/.test(id) ||
91-
/vitepress\/dist\/client/.test(id))
92-
) {
93-
return 'framework'
94-
}
95-
}
96-
})
97-
}
41+
const resolveViteConfig = async (ssr: boolean): Promise<ViteUserConfig> => {
42+
// use different entry based on ssr or not
43+
input['app'] = path.resolve(APP_PATH, ssr ? 'ssr.js' : 'index.js')
44+
return {
45+
root: config.srcDir,
46+
base: config.site.base,
47+
logLevel: 'warn',
48+
plugins: await createVitePressPlugin(
49+
config,
50+
ssr,
51+
pageToHashMap,
52+
clientJSMap
53+
),
54+
ssr: {
55+
noExternal: ['vitepress', '@docsearch/css']
9856
},
99-
// minify with esbuild in MPA mode (for CSS)
100-
minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG
57+
build: {
58+
...options,
59+
emptyOutDir: true,
60+
ssr,
61+
outDir: ssr ? config.tempDir : config.outDir,
62+
cssCodeSplit: false,
63+
rollupOptions: {
64+
...rollupOptions,
65+
input,
66+
// important so that each page chunk and the index export things for each
67+
// other
68+
preserveEntrySignatures: 'allow-extension',
69+
output: {
70+
...rollupOptions?.output,
71+
...(ssr
72+
? {
73+
entryFileNames: `[name].js`,
74+
chunkFileNames: `[name].[hash].js`
75+
}
76+
: {
77+
chunkFileNames(chunk) {
78+
// avoid ads chunk being intercepted by adblock
79+
return /(?:Carbon|BuySell)Ads/.test(chunk.name)
80+
? `assets/chunks/ui-custom.[hash].js`
81+
: `assets/chunks/[name].[hash].js`
82+
},
83+
manualChunks(id, ctx) {
84+
// move known framework code into a stable chunk so that
85+
// custom theme changes do not invalidate hash for all pages
86+
if (id.includes('plugin-vue:export-helper')) {
87+
return 'framework'
88+
}
89+
if (
90+
isEagerChunk(id, ctx) &&
91+
(/@vue\/(runtime|shared|reactivity)/.test(id) ||
92+
/vitepress\/dist\/client/.test(id))
93+
) {
94+
return 'framework'
95+
}
96+
}
97+
})
98+
}
99+
},
100+
// minify with esbuild in MPA mode (for CSS)
101+
minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG
102+
}
101103
}
102-
})
104+
}
103105

104106
let clientResult: RollupOutput
105107
let serverResult: RollupOutput

src/node/build/render.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { createRequire } from 'module'
21
import fs from 'fs-extra'
32
import path from 'path'
43
import { pathToFileURL } from 'url'
@@ -16,9 +15,8 @@ import {
1615
import { slash } from '../utils/slash'
1716
import { SiteConfig, resolveSiteDataByRoute } from '../config'
1817

19-
const require = createRequire(import.meta.url)
20-
2118
export async function renderPage(
19+
render: (path: string) => Promise<string>,
2220
config: SiteConfig,
2321
page: string, // foo.md
2422
result: RollupOutput | null,
@@ -27,28 +25,11 @@ export async function renderPage(
2725
pageToHashMap: Record<string, string>,
2826
hashMapString: string
2927
) {
30-
const entryPath = path.join(config.tempDir, 'app.js')
31-
const { createApp } = await import(pathToFileURL(entryPath).toString())
32-
const { app, router } = createApp()
3328
const routePath = `/${page.replace(/\.md$/, '')}`
3429
const siteData = resolveSiteDataByRoute(config.site, routePath)
35-
await router.go(routePath)
36-
37-
// lazy require server-renderer for production build
38-
// prioritize project root over vitepress' own dep
39-
let rendererPath
40-
try {
41-
rendererPath = require.resolve('vue/server-renderer', {
42-
paths: [config.root]
43-
})
44-
} catch (e) {
45-
rendererPath = require.resolve('vue/server-renderer')
46-
}
4730

4831
// render page
49-
const content = await import(pathToFileURL(rendererPath).toString()).then(
50-
(r) => r.renderToString(app)
51-
)
32+
const content = await render(routePath)
5233

5334
const pageName = page.replace(/\//g, '_')
5435
// server build doesn't need hash

src/node/config.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import c from 'picocolors'
44
import fg from 'fast-glob'
55
import {
66
normalizePath,
7-
AliasOptions,
87
UserConfig as ViteConfig,
98
mergeConfig as mergeViteConfig,
109
loadConfigFromFile
@@ -20,7 +19,7 @@ import {
2019
CleanUrlsMode,
2120
PageData
2221
} from './shared'
23-
import { resolveAliases, DEFAULT_THEME_PATH } from './alias'
22+
import { DEFAULT_THEME_PATH } from './alias'
2423
import { MarkdownOptions } from './markdown/markdown'
2524
import _debug from 'debug'
2625

@@ -138,7 +137,6 @@ export interface SiteConfig<ThemeConfig = any>
138137
themeDir: string
139138
outDir: string
140139
tempDir: string
141-
alias: AliasOptions
142140
pages: string[]
143141
}
144142

@@ -208,7 +206,6 @@ export async function resolveConfig(
208206
tempDir: resolve(root, '.temp'),
209207
markdown: userConfig.markdown,
210208
lastUpdated: userConfig.lastUpdated,
211-
alias: resolveAliases(root, themeDir),
212209
vue: userConfig.vue,
213210
vite: userConfig.vite,
214211
shouldPreload: userConfig.shouldPreload,

src/node/plugin.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import c from 'picocolors'
33
import { defineConfig, mergeConfig, Plugin, ResolvedConfig } from 'vite'
44
import { SiteConfig } from './config'
55
import { createMarkdownToVueRenderFn, clearCache } from './markdownToVue'
6-
import { DIST_CLIENT_PATH, APP_PATH, SITE_DATA_REQUEST_PATH } from './alias'
6+
import {
7+
DIST_CLIENT_PATH,
8+
APP_PATH,
9+
SITE_DATA_REQUEST_PATH,
10+
resolveAliases
11+
} from './alias'
712
import { slash } from './utils/slash'
813
import { OutputAsset, OutputChunk } from 'rollup'
914
import { staticDataPlugin } from './staticDataPlugin'
@@ -41,7 +46,6 @@ export async function createVitePressPlugin(
4146
srcDir,
4247
configPath,
4348
configDeps,
44-
alias,
4549
markdown,
4650
site,
4751
vue: userVuePluginOptions,
@@ -95,7 +99,7 @@ export async function createVitePressPlugin(
9599
config() {
96100
const baseConfig = defineConfig({
97101
resolve: {
98-
alias
102+
alias: resolveAliases(siteConfig, ssr)
99103
},
100104
define: {
101105
__ALGOLIA__: !!site.themeConfig.algolia,

0 commit comments

Comments
 (0)