| 1 | import type { ResolvedSlidevOptions } from '@slidev/types' |
| 2 | import type { Plugin, UserConfig } from 'vite' |
| 3 | import { fileURLToPath, pathToFileURL } from 'node:url' |
| 4 | import { slash, uniq } from '@antfu/utils' |
| 5 | import { createResolve } from 'mlly' |
| 6 | import { join } from 'pathe' |
| 7 | import { mergeConfig } from 'vite' |
| 8 | import { isInstalledGlobally, resolveImportPath, toAtFS } from '../resolver' |
| 9 | |
| 10 | const RE_SLIDEV_CLIENT = /^@slidev\/client$/ |
| 11 | const RE_SLIDEV_CLIENT_SUBPATH = /^@slidev\/client\/(.*)/ |
| 12 | const RE_SLIDEV_VIRTUAL = /^#slidev\/(.*)/ |
| 13 | const RE_MONACO_EDITOR = /\/monaco-editor(?:-core)?\// |
| 14 | |
| 15 | const INCLUDE_GLOBAL = [ |
| 16 | '@fix-webm-duration/fix', |
| 17 | '@typescript/ata', |
| 18 | 'file-saver', |
| 19 | 'lz-string', |
| 20 | 'recordrtc', |
| 21 | 'typescript', |
| 22 | 'yaml', |
| 23 | 'pptxgenjs', |
| 24 | 'ansis', |
| 25 | ] |
| 26 | |
| 27 | const INCLUDE_LOCAL = INCLUDE_GLOBAL.map(i => `@slidev/cli > @slidev/client > ${i}`) |
| 28 | |
| 29 | // @keep-sorted |
| 30 | const EXCLUDE_GLOBAL = [ |
| 31 | '@antfu/utils', |
| 32 | '@shikijs/magic-move/vue', |
| 33 | '@shikijs/monaco', |
| 34 | '@shikijs/vitepress-twoslash/client', |
| 35 | '@slidev/client', |
| 36 | '@slidev/client/constants', |
| 37 | '@slidev/client/context', |
| 38 | '@slidev/client/logic/dark', |
| 39 | '@slidev/parser', |
| 40 | '@slidev/parser/core', |
| 41 | '@slidev/rough-notation', |
| 42 | '@slidev/types', |
| 43 | '@unhead/vue', |
| 44 | '@unocss/reset', |
| 45 | '@vueuse/core', |
| 46 | '@vueuse/math', |
| 47 | '@vueuse/motion', |
| 48 | '@vueuse/shared', |
| 49 | 'drauu', |
| 50 | 'floating-vue', |
| 51 | 'fuse.js', |
| 52 | 'mermaid', |
| 53 | 'monaco-editor', |
| 54 | 'shiki', |
| 55 | 'shiki/core', |
| 56 | 'vue-demi', |
| 57 | 'vue-router', |
| 58 | 'vue', |
| 59 | ] |
| 60 | |
| 61 | const EXCLUDE_LOCAL = EXCLUDE_GLOBAL |
| 62 | |
| 63 | const ASYNC_MODULES = [ |
| 64 | 'file-saver', |
| 65 | 'vue', |
| 66 | '@vue', |
| 67 | ] |
| 68 | |
| 69 | export function createConfigPlugin(options: ResolvedSlidevOptions): Plugin { |
| 70 | const resolveClientDep = createResolve({ |
| 71 | // Same as Vite's default resolve conditions |
| 72 | conditions: ['import', 'module', 'browser', 'default', options.mode === 'build' ? 'production' : 'development'], |
| 73 | url: pathToFileURL(options.clientRoot), |
| 74 | }) |
| 75 | return { |
| 76 | name: 'slidev:config', |
| 77 | async config(config) { |
| 78 | const injection: UserConfig = { |
| 79 | define: options.utils.define, |
| 80 | resolve: { |
| 81 | alias: [ |
| 82 | { |
| 83 | find: RE_SLIDEV_CLIENT, |
| 84 | replacement: `${toAtFS(options.clientRoot)}/index.ts`, |
| 85 | }, |
| 86 | { |
| 87 | find: RE_SLIDEV_CLIENT_SUBPATH, |
| 88 | replacement: `${toAtFS(options.clientRoot)}/$1`, |
| 89 | }, |
| 90 | { |
| 91 | find: RE_SLIDEV_VIRTUAL, |
| 92 | replacement: '/@slidev/$1', |
| 93 | }, |
| 94 | { |
| 95 | find: 'vue', |
| 96 | replacement: await resolveImportPath('vue/dist/vue.esm-bundler.js', true), |
| 97 | }, |
| 98 | ...(isInstalledGlobally.value |
| 99 | ? await Promise.all(INCLUDE_GLOBAL.map(async dep => ({ |
| 100 | find: dep, |
| 101 | replacement: fileURLToPath(await resolveClientDep(dep)), |
| 102 | }))) |
| 103 | : [] |
| 104 | ), |
| 105 | ], |
| 106 | dedupe: ['vue'], |
| 107 | }, |
| 108 | optimizeDeps: isInstalledGlobally.value |
| 109 | ? { |
| 110 | exclude: EXCLUDE_GLOBAL, |
| 111 | include: INCLUDE_GLOBAL, |
| 112 | } |
| 113 | : { |
| 114 | // We need to specify the full deps path for non-hoisted modules |
| 115 | exclude: EXCLUDE_LOCAL, |
| 116 | include: INCLUDE_LOCAL, |
| 117 | }, |
| 118 | css: { |
| 119 | postcss: { |
| 120 | plugins: [ |
| 121 | await import('postcss-nested').then(r => (r.default || r)()) as any, |
| 122 | ], |
| 123 | }, |
| 124 | }, |
| 125 | server: { |
| 126 | fs: { |
| 127 | strict: true, |
| 128 | allow: uniq([ |
| 129 | options.userWorkspaceRoot, |
| 130 | options.clientRoot, |
| 131 | // Special case for PNPM global installation |
| 132 | isInstalledGlobally.value |
| 133 | ? slash(options.cliRoot).replace(/\/\.pnpm\/.*$/gi, '') |
| 134 | : options.cliRoot, |
| 135 | ...options.roots, |
| 136 | ]), |
| 137 | }, |
| 138 | }, |
| 139 | publicDir: join(options.userRoot, 'public'), |
| 140 | build: { |
| 141 | rollupOptions: { |
| 142 | output: { |
| 143 | chunkFileNames(chunkInfo) { |
| 144 | const DEFAULT = 'assets/[name]-[hash].js' |
| 145 | |
| 146 | // Already handled in manualChunks |
| 147 | if (chunkInfo.name.includes('/')) |
| 148 | return DEFAULT |
| 149 | |
| 150 | // Over 60% of the chunk is slidev client code, we put it into slidev chunk |
| 151 | if (chunkInfo.moduleIds.filter(i => isSlidevClient(i)).length > chunkInfo.moduleIds.length * 0.6) |
| 152 | return 'assets/slidev/[name]-[hash].js' |
| 153 | |
| 154 | // Monaco Editor |
| 155 | if (chunkInfo.moduleIds.filter(i => i.match(RE_MONACO_EDITOR)).length > chunkInfo.moduleIds.length * 0.6) |
| 156 | return 'assets/monaco/[name]-[hash].js' |
| 157 | |
| 158 | return DEFAULT |
| 159 | }, |
| 160 | manualChunks(id) { |
| 161 | if (id.startsWith('/@slidev-monaco-types/') || id.includes('/@slidev/monaco-types') || id.endsWith('?monaco-types&raw')) |
| 162 | return 'monaco/bundled-types' |
| 163 | if (id.includes('/shiki/') || id.includes('/@shikijs/')) |
| 164 | return `modules/shiki` |
| 165 | if (id.startsWith('~icons/')) |
| 166 | return 'modules/unplugin-icons' |
| 167 | // It seems that moving slides out will breaks the production build |
| 168 | // Would need to find a better way to handle this |
| 169 | // const slideMatch = id.match(/\/@slidev\/slides\/(\d+)/) |
| 170 | // if (slideMatch && !id.includes('.frontmatter')) |
| 171 | // return `slides/${slideMatch[1]}` |
| 172 | |
| 173 | const matchedAsyncModule = ASYNC_MODULES.find(i => id.includes(`/node_modules/${i}`)) |
| 174 | if (matchedAsyncModule) |
| 175 | return `modules/${matchedAsyncModule.replace('@', '').replace('/', '-')}` |
| 176 | }, |
| 177 | }, |
| 178 | }, |
| 179 | }, |
| 180 | cacheDir: isInstalledGlobally.value ? join(options.cliRoot, 'node_modules/.vite') : undefined, |
| 181 | } |
| 182 | |
| 183 | function isSlidevClient(id: string) { |
| 184 | return id.includes('/@slidev/') || id.includes('/slidev/packages/client/') || id.includes('/@vueuse/') |
| 185 | } |
| 186 | |
| 187 | // function getNodeModuleName(path: string) { |
| 188 | // const nodeModuelsMatch = [...path.matchAll(/node_modules\/(@[^/]+\/[^/]+|[^/]+)\//g)] |
| 189 | // if (nodeModuelsMatch.length) |
| 190 | // return nodeModuelsMatch[nodeModuelsMatch.length - 1][1] |
| 191 | // } |
| 192 | |
| 193 | return mergeConfig(injection, config) |
| 194 | }, |
| 195 | configureServer(server) { |
| 196 | // serve our index.html after vite history fallback |
| 197 | return () => { |
| 198 | server.middlewares.use(async (req, res, next) => { |
| 199 | if (req.url === '/index.html') { |
| 200 | const headers = server.config.server.headers ?? {} |
| 201 | |
| 202 | for (const header in headers) { |
| 203 | res.setHeader(header, headers[header]!) |
| 204 | } |
| 205 | |
| 206 | res.setHeader('Content-Type', 'text/html') |
| 207 | res.statusCode = 200 |
| 208 | res.end(options.utils.indexHtml) |
| 209 | return |
| 210 | } |
| 211 | next() |
| 212 | }) |
| 213 | } |
| 214 | }, |
| 215 | } |
| 216 | } |
| 217 |