Skip to content

Commit

Permalink
fix: fix build for vite 3 + "type": "module"
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yyx990803 committed Aug 17, 2022
1 parent e0c04f9 commit 3b2d90a
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 106 deletions.
9 changes: 9 additions & 0 deletions src/client/app/ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// entry for SSR
import { createApp } from './index.js'
import { renderToString } from 'vue/server-renderer'

export async function render(path: string) {
const { app, router } = createApp()
await router.go(path)
return renderToString(app)
}
38 changes: 22 additions & 16 deletions src/node/alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createRequire } from 'module'
import { resolve, join } from 'path'
import { fileURLToPath } from 'url'
import { Alias, AliasOptions } from 'vite'
import { SiteConfig } from './config'

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

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

export function resolveAliases(root: string, themeDir: string): AliasOptions {
export function resolveAliases(
{ root, themeDir }: SiteConfig,
ssr: boolean
): AliasOptions {
const paths: Record<string, string> = {
'@theme': themeDir,
[SITE_DATA_ID]: SITE_DATA_REQUEST_PATH
}

// prioritize vue installed in project root and fallback to
// vue that comes with vitepress itself.
let vuePath
try {
vuePath = require.resolve(vueRuntimePath, { paths: [root] })
} catch (e) {
vuePath = require.resolve(vueRuntimePath)
}

const aliases: Alias[] = [
...Object.keys(paths).map((p) => ({
find: p,
Expand All @@ -46,14 +41,25 @@ export function resolveAliases(root: string, themeDir: string): AliasOptions {
{
find: /^vitepress\/theme$/,
replacement: join(DIST_CLIENT_PATH, '/theme-default/index.js')
},
// make sure it always use the same vue dependency that comes
// with vitepress itself
{
find: /^vue$/,
replacement: vuePath
}
]

if (!ssr) {
// Prioritize vue installed in project root and fallback to
// vue that comes with vitepress itself.
// Only do this when not running SSR build, since `vue` needs to be
// externalized during SSR
let vuePath
try {
vuePath = require.resolve(vueRuntimePath, { paths: [root] })
} catch (e) {
vuePath = require.resolve(vueRuntimePath)
}
aliases.push({
find: /^vue$/,
replacement: vuePath
})
}

return aliases
}
23 changes: 22 additions & 1 deletion src/node/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import { OutputChunk, OutputAsset } from 'rollup'
import { resolveConfig } from '../config'
import { renderPage } from './render'
import { bundle, okMark, failMark } from './bundle'
import { createRequire } from 'module'
import { pathToFileURL } from 'url'

export async function build(
root: string,
root?: string,
buildOptions: BuildOptions & { base?: string; mpa?: string } = {}
) {
const start = Date.now()

process.env.NODE_ENV = 'production'
const siteConfig = await resolveConfig(root, 'build', 'production')
const unlinkVue = linkVue(siteConfig.root)

if (buildOptions.base) {
siteConfig.site.base = buildOptions.base
Expand All @@ -32,6 +35,9 @@ export async function build(
buildOptions
)

const entryPath = path.join(siteConfig.tempDir, 'app.js')
const { render } = await import(pathToFileURL(entryPath).toString())

const spinner = ora()
spinner.start('rendering pages...')

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

for (const page of pages) {
await renderPage(
render,
siteConfig,
page,
clientResult,
Expand All @@ -84,6 +91,7 @@ export async function build(
pageToHashMap
)
} finally {
unlinkVue()
if (!process.env.DEBUG)
fs.rmSync(siteConfig.tempDir, { recursive: true, force: true })
}
Expand All @@ -92,3 +100,16 @@ export async function build(

console.log(`build complete in ${((Date.now() - start) / 1000).toFixed(2)}s.`)
}

function linkVue(root: string) {
const dest = path.resolve(root, 'node_modules/vue')
// if user did not install vue by themselves, link VitePress' version
if (!fs.existsSync(dest)) {
const src = path.dirname(createRequire(import.meta.url).resolve('vue'))
fs.ensureSymlinkSync(src, dest)
return () => {
fs.unlinkSync(dest)
}
}
return () => {}
}
124 changes: 63 additions & 61 deletions src/node/build/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ export async function bundle(
// this is a multi-entry build - every page is considered an entry chunk
// the loading is done via filename conversion rules so that the
// metadata doesn't need to be included in the main chunk.
const input: Record<string, string> = {
app: path.resolve(APP_PATH, 'index.js')
}
const input: Record<string, string> = {}
config.pages.forEach((file) => {
// page filename conversion
// foo/bar.md -> foo_bar.md
Expand All @@ -40,66 +38,70 @@ export async function bundle(
// resolve options to pass to vite
const { rollupOptions } = options

const resolveViteConfig = async (ssr: boolean): Promise<ViteUserConfig> => ({
root: config.srcDir,
base: config.site.base,
logLevel: 'warn',
plugins: await createVitePressPlugin(
config,
ssr,
pageToHashMap,
clientJSMap
),
ssr: {
noExternal: ['vitepress', '@docsearch/css']
},
build: {
...options,
emptyOutDir: true,
ssr,
outDir: ssr ? config.tempDir : config.outDir,
cssCodeSplit: false,
rollupOptions: {
...rollupOptions,
input,
// important so that each page chunk and the index export things for each
// other
preserveEntrySignatures: 'allow-extension',
output: {
...rollupOptions?.output,
...(ssr
? {
entryFileNames: `[name].js`,
chunkFileNames: `[name].[hash].js`
}
: {
chunkFileNames(chunk) {
// avoid ads chunk being intercepted by adblock
return /(?:Carbon|BuySell)Ads/.test(chunk.name)
? `assets/chunks/ui-custom.[hash].js`
: `assets/chunks/[name].[hash].js`
},
manualChunks(id, ctx) {
// move known framework code into a stable chunk so that
// custom theme changes do not invalidate hash for all pages
if (id.includes('plugin-vue:export-helper')) {
return 'framework'
}
if (
isEagerChunk(id, ctx) &&
(/@vue\/(runtime|shared|reactivity)/.test(id) ||
/vitepress\/dist\/client/.test(id))
) {
return 'framework'
}
}
})
}
const resolveViteConfig = async (ssr: boolean): Promise<ViteUserConfig> => {
// use different entry based on ssr or not
input['app'] = path.resolve(APP_PATH, ssr ? 'ssr.js' : 'index.js')
return {
root: config.srcDir,
base: config.site.base,
logLevel: 'warn',
plugins: await createVitePressPlugin(
config,
ssr,
pageToHashMap,
clientJSMap
),
ssr: {
noExternal: ['vitepress', '@docsearch/css']
},
// minify with esbuild in MPA mode (for CSS)
minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG
build: {
...options,
emptyOutDir: true,
ssr,
outDir: ssr ? config.tempDir : config.outDir,
cssCodeSplit: false,
rollupOptions: {
...rollupOptions,
input,
// important so that each page chunk and the index export things for each
// other
preserveEntrySignatures: 'allow-extension',
output: {
...rollupOptions?.output,
...(ssr
? {
entryFileNames: `[name].js`,
chunkFileNames: `[name].[hash].js`
}
: {
chunkFileNames(chunk) {
// avoid ads chunk being intercepted by adblock
return /(?:Carbon|BuySell)Ads/.test(chunk.name)
? `assets/chunks/ui-custom.[hash].js`
: `assets/chunks/[name].[hash].js`
},
manualChunks(id, ctx) {
// move known framework code into a stable chunk so that
// custom theme changes do not invalidate hash for all pages
if (id.includes('plugin-vue:export-helper')) {
return 'framework'
}
if (
isEagerChunk(id, ctx) &&
(/@vue\/(runtime|shared|reactivity)/.test(id) ||
/vitepress\/dist\/client/.test(id))
) {
return 'framework'
}
}
})
}
},
// minify with esbuild in MPA mode (for CSS)
minify: ssr ? (config.mpa ? 'esbuild' : false) : !process.env.DEBUG
}
}
})
}

let clientResult: RollupOutput
let serverResult: RollupOutput
Expand Down
23 changes: 2 additions & 21 deletions src/node/build/render.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createRequire } from 'module'
import fs from 'fs-extra'
import path from 'path'
import { pathToFileURL } from 'url'
Expand All @@ -16,9 +15,8 @@ import {
import { slash } from '../utils/slash'
import { SiteConfig, resolveSiteDataByRoute } from '../config'

const require = createRequire(import.meta.url)

export async function renderPage(
render: (path: string) => Promise<string>,
config: SiteConfig,
page: string, // foo.md
result: RollupOutput | null,
Expand All @@ -27,28 +25,11 @@ export async function renderPage(
pageToHashMap: Record<string, string>,
hashMapString: string
) {
const entryPath = path.join(config.tempDir, 'app.js')
const { createApp } = await import(pathToFileURL(entryPath).toString())
const { app, router } = createApp()
const routePath = `/${page.replace(/\.md$/, '')}`
const siteData = resolveSiteDataByRoute(config.site, routePath)
await router.go(routePath)

// lazy require server-renderer for production build
// prioritize project root over vitepress' own dep
let rendererPath
try {
rendererPath = require.resolve('vue/server-renderer', {
paths: [config.root]
})
} catch (e) {
rendererPath = require.resolve('vue/server-renderer')
}

// render page
const content = await import(pathToFileURL(rendererPath).toString()).then(
(r) => r.renderToString(app)
)
const content = await render(routePath)

const pageName = page.replace(/\//g, '_')
// server build doesn't need hash
Expand Down
5 changes: 1 addition & 4 deletions src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import c from 'picocolors'
import fg from 'fast-glob'
import {
normalizePath,
AliasOptions,
UserConfig as ViteConfig,
mergeConfig as mergeViteConfig,
loadConfigFromFile
Expand All @@ -20,7 +19,7 @@ import {
CleanUrlsMode,
PageData
} from './shared'
import { resolveAliases, DEFAULT_THEME_PATH } from './alias'
import { DEFAULT_THEME_PATH } from './alias'
import { MarkdownOptions } from './markdown/markdown'
import _debug from 'debug'

Expand Down Expand Up @@ -138,7 +137,6 @@ export interface SiteConfig<ThemeConfig = any>
themeDir: string
outDir: string
tempDir: string
alias: AliasOptions
pages: string[]
}

Expand Down Expand Up @@ -208,7 +206,6 @@ export async function resolveConfig(
tempDir: resolve(root, '.temp'),
markdown: userConfig.markdown,
lastUpdated: userConfig.lastUpdated,
alias: resolveAliases(root, themeDir),
vue: userConfig.vue,
vite: userConfig.vite,
shouldPreload: userConfig.shouldPreload,
Expand Down
10 changes: 7 additions & 3 deletions src/node/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import c from 'picocolors'
import { defineConfig, mergeConfig, Plugin, ResolvedConfig } from 'vite'
import { SiteConfig } from './config'
import { createMarkdownToVueRenderFn, clearCache } from './markdownToVue'
import { DIST_CLIENT_PATH, APP_PATH, SITE_DATA_REQUEST_PATH } from './alias'
import {
DIST_CLIENT_PATH,
APP_PATH,
SITE_DATA_REQUEST_PATH,
resolveAliases
} from './alias'
import { slash } from './utils/slash'
import { OutputAsset, OutputChunk } from 'rollup'
import { staticDataPlugin } from './staticDataPlugin'
Expand Down Expand Up @@ -41,7 +46,6 @@ export async function createVitePressPlugin(
srcDir,
configPath,
configDeps,
alias,
markdown,
site,
vue: userVuePluginOptions,
Expand Down Expand Up @@ -95,7 +99,7 @@ export async function createVitePressPlugin(
config() {
const baseConfig = defineConfig({
resolve: {
alias
alias: resolveAliases(siteConfig, ssr)
},
define: {
__ALGOLIA__: !!site.themeConfig.algolia,
Expand Down

0 comments on commit 3b2d90a

Please sign in to comment.