├── .gitignore ├── .vscode └── launch.json ├── README.md ├── README_EN.md ├── mini-vite-example ├── .gitignore ├── .vscode │ └── extensions.json ├── README.md ├── index.html ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.vue │ ├── assets │ │ └── vue.svg │ ├── components │ │ └── HelloWorld.vue │ ├── main.ts │ ├── style.css │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── package.json ├── packages ├── create-vite │ └── package.json └── vite │ ├── bin │ └── mini-vite.js │ ├── package.json │ ├── rollup.config.ts │ ├── src │ ├── client │ │ ├── client.ts │ │ ├── env.ts │ │ ├── overlay.ts │ │ └── tsconfig.json │ ├── node │ │ ├── build.ts │ │ ├── cli.ts │ │ ├── config.ts │ │ ├── constants.ts │ │ ├── http.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ ├── optimizer │ │ │ ├── index.ts │ │ │ ├── optimizer.ts │ │ │ └── scan.ts │ │ ├── packages.ts │ │ ├── plugin.ts │ │ ├── pluginContainer.ts │ │ ├── plugins │ │ │ ├── asset.ts │ │ │ ├── clientInjections.ts │ │ │ ├── css.ts │ │ │ ├── esbuild.ts │ │ │ ├── html.ts │ │ │ ├── importAnalysis.ts │ │ │ ├── importMetaGlob.ts │ │ │ ├── index.ts │ │ │ └── resolve.ts │ │ ├── server │ │ │ ├── hmr.ts │ │ │ ├── index.ts │ │ │ ├── middlewares │ │ │ │ ├── htmlFallback.ts │ │ │ │ ├── indexHtml.ts │ │ │ │ ├── static.ts │ │ │ │ └── transform.ts │ │ │ ├── moduleGraph.ts │ │ │ ├── searchRoot.ts │ │ │ ├── send.ts │ │ │ ├── transformRequest.ts │ │ │ └── ws.ts │ │ ├── shortcuts.ts │ │ ├── tsconfig.json │ │ ├── utils.ts │ │ └── watch.ts │ └── types │ │ ├── alias.d.ts │ │ ├── anymatch.d.ts │ │ ├── chokidar.d.ts │ │ ├── connect.d.ts │ │ ├── http-proxy.d.ts │ │ ├── shims.d.ts │ │ └── ws.d.ts │ ├── tsconfig.base.json │ ├── tsconfig.check.json │ ├── tsconfig.json │ └── types │ ├── customEvent.d.ts │ ├── hmrPayload.d.ts │ ├── hot.d.ts │ ├── importGlob.d.ts │ └── ws.d.ts ├── patches └── sirv@2.0.2.patch ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | lerna-debug.log* 8 | 9 | node_modules 10 | dist 11 | dist-ssr 12 | *.local 13 | lib 14 | 15 | # Editor directories and files 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Launch via NPM", 5 | "request": "launch", 6 | "runtimeArgs": [ 7 | "run-script", 8 | "debug" 9 | ], 10 | "runtimeExecutable": "npm", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "type": "node" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [EN](README_EN.md) 2 | ## mini-vite 3 | 通过此仓库,你能学习vite以下的几个功能 4 | - ⛪ 预构建依赖 5 | - 🌈 Vite的编译构建能力 6 | - 🌻 热更新 7 | 8 | ## 安装 9 | 进入packages/vite目录,打包mini-vite 10 | ```shell 11 | pnpm i 12 | cd .\packages\vite\ 13 | pnpm run dev 14 | ``` 15 | ## 启动 16 | 返回上级目录,安装并启动示例项目 17 | ````shell 18 | cd ../.. 19 | pnpm run i 20 | pnpm run serve # 使用mini-vite启动示例项目 21 | pnpm run dev # 使用vite启动示例项目 22 | ```` 23 | 24 | ## 调试 25 | ````shell 26 | pnpm run dev # 调试vite源码 27 | pnpm run serve # 调试mini-vite源码 28 | ```` 29 | ![image](https://github.com/lyk990/mini-vite/assets/83712416/15bb9617-0b2e-42c7-8dcd-ee9d8001c23c) 30 | 31 | ## 流程图 32 | ![image](https://github.com/lyk990/mini-vite/assets/83712416/247125f6-655e-42dd-88aa-96eeae564998) 33 | ![image](https://github.com/lyk990/mini-vite/assets/83712416/020331fc-1450-45aa-a2af-ebcf18411f59) 34 | ![image](https://github.com/lyk990/mini-vite/assets/83712416/150727cf-6f04-49f6-bd87-049c78c7b22f) 35 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | [CN](README.md) 2 | # mini-vite 3 | This is a learning tool for vite, through this, you can learn vite 4 | - ⛪ How to build dependency and enable dependency scanning 5 | - 🌈 Implement the compilation and construction capabilities of Vite services 6 | - 🌻 Achieve HMR and realize server and client. 7 | -------------------------------------------------------------------------------- /mini-vite-example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /mini-vite-example/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /mini-vite-example/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /mini-vite-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-vite-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "serve": "mini-vite", 9 | "build": "vue-tsc && vite build", 10 | "preview": "vite preview", 11 | "rm": "rimraf node_modules", 12 | "i": "rimraf node_modules && pnpm i" 13 | }, 14 | "dependencies": { 15 | "mini-vite": "workspace:*", 16 | "vue": "^3.2.47" 17 | }, 18 | "devDependencies": { 19 | "@types/connect": "^3.4.35", 20 | "@vitejs/plugin-vue": "^4.1.0", 21 | "typescript": "^4.9.5", 22 | "vite": "^4.2.1", 23 | "vue-tsc": "^1.2.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mini-vite-example/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mini-vite-example/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 16 | 17 | 31 | -------------------------------------------------------------------------------- /mini-vite-example/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mini-vite-example/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /mini-vite-example/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css' 3 | import App from './App.vue' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /mini-vite-example/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | a:hover { 23 | color: #535bf2; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | display: flex; 29 | place-items: center; 30 | min-width: 320px; 31 | min-height: 100vh; 32 | } 33 | 34 | h1 { 35 | font-size: 3.2em; 36 | line-height: 1.1; 37 | } 38 | 39 | button { 40 | border-radius: 8px; 41 | border: 1px solid transparent; 42 | padding: 0.6em 1.2em; 43 | font-size: 1em; 44 | font-weight: 500; 45 | font-family: inherit; 46 | background-color: #1a1a1a; 47 | cursor: pointer; 48 | transition: border-color 0.25s; 49 | } 50 | button:hover { 51 | border-color: #646cff; 52 | } 53 | button:focus, 54 | button:focus-visible { 55 | outline: 4px auto -webkit-focus-ring-color; 56 | } 57 | 58 | .card { 59 | padding: 2em; 60 | } 61 | 62 | #app { 63 | max-width: 1280px; 64 | margin: 0 auto; 65 | padding: 2rem; 66 | text-align: center; 67 | } 68 | 69 | @media (prefers-color-scheme: light) { 70 | :root { 71 | color: #213547; 72 | background-color: #ffffff; 73 | } 74 | a:hover { 75 | color: #747bff; 76 | } 77 | button { 78 | background-color: #f9f9f9; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /mini-vite-example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /mini-vite-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": ["ESNext", "DOM"], 13 | "skipLibCheck": true, 14 | "noEmit": true 15 | }, 16 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 17 | "references": [{ "path": "./tsconfig.node.json" }] 18 | } 19 | -------------------------------------------------------------------------------- /mini-vite-example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /mini-vite-example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | 4 | export default defineConfig({ 5 | plugins: [vue()], 6 | }); 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-vite", 3 | "private": true, 4 | "type": "module", 5 | "version": "0.0.1", 6 | "author": "lyk990", 7 | "homepage": "https://github.com/lyk990/mini-vite", 8 | "description": "you can use mini-vite to learn vite", 9 | "engines": { 10 | "node": "^14.18.0 || >=16.0.0" 11 | }, 12 | "keywords": [ 13 | "vite", 14 | "mini-vite" 15 | ], 16 | "scripts": { 17 | "build": "pnpm -r --filter='./packages/*' run build", 18 | "test": "echo \"Error: no test specified\" && exit 1", 19 | "op": "pnpm run --filter ./ mini-vite-example op", 20 | "dev": "pnpm run --filter ./mini-vite-example dev", 21 | "serve": "pnpm run --filter ./mini-vite-example serve", 22 | "rm": "pnpm run --filter ./mini-vite-example rm", 23 | "i": "pnpm run --filter ./mini-vite-example i" 24 | }, 25 | "dependencies": { 26 | "@rollup/plugin-commonjs": "^24.0.1", 27 | "@rollup/plugin-json": "^6.0.0", 28 | "@rollup/plugin-node-resolve": "^15.0.2", 29 | "@rollup/plugin-typescript": "^11.1.0", 30 | "@types/sass": "~1.43.1", 31 | "convert-source-map": "^2.0.0", 32 | "fast-glob": "^3.2.12", 33 | "fs-extra": "^11.1.1", 34 | "picocolors": "^1.0.0", 35 | "resolve": "^1.22.2", 36 | "rimraf": "^4.4.1", 37 | "rollup": "^3.20.2", 38 | "typescript": "^4.9.5" 39 | }, 40 | "devDependencies": { 41 | "@types/convert-source-map": "^2.0.0", 42 | "@types/resolve": "^1.20.2" 43 | }, 44 | "pnpm": { 45 | "overrides": { 46 | "mini-vite": "workspace:*" 47 | }, 48 | "patchedDependencies": { 49 | "sirv@2.0.2": "patches/sirv@2.0.2.patch" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/create-vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mini-vite/create-vite", 3 | "version": "1.0.0", 4 | "description": "@mini-vite/create-vite", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /packages/vite/bin/mini-vite.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { performance } from 'node:perf_hooks' 3 | 4 | global.__vite_start_time = performance.now() 5 | import("../dist/node/cli.js"); 6 | -------------------------------------------------------------------------------- /packages/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-vite", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "description": "@mini-vite/vite", 6 | "author": "lyk990", 7 | "license": "MIT", 8 | "bin": { 9 | "mini-vite": "bin/mini-vite.js" 10 | }, 11 | "keywords": [ 12 | "vite", 13 | "mini-vite" 14 | ], 15 | "main": "index.js", 16 | "scripts": { 17 | "dev": "rimraf dist && pnpm run build-bundle -w", 18 | "build-bundle": "rollup --config rollup.config.ts --configPlugin typescript", 19 | "test": "echo \"Error: no test specified\" && exit 1", 20 | "rm": "rimraf node_modules && rimraf dist", 21 | "i": "rimraf node_modules && pnpm i" 22 | }, 23 | "dependencies": { 24 | "@ampproject/remapping": "^2.2.1", 25 | "@rollup/plugin-alias": "^4.0.4", 26 | "@rollup/pluginutils": "^5.0.2", 27 | "@types/estree": "^1.0.1", 28 | "acorn": "^8.8.2", 29 | "acorn-walk": "^8.2.0", 30 | "cac": "^6.7.14", 31 | "chokidar": "^3.5.3", 32 | "connect": "^3.7.0", 33 | "connect-history-api-fallback": "^2.0.0", 34 | "cross-spawn": "^7.0.3", 35 | "debug": "^4.3.4", 36 | "dotenv": "^16.0.3", 37 | "dotenv-expand": "^10.0.0", 38 | "es-module-lexer": "^1.2.1", 39 | "esbuild": "^0.17.16", 40 | "etag": "^1.8.1", 41 | "magic-string": "^0.30.0", 42 | "micromatch": "^4.0.5", 43 | "parse5": "^7.1.2", 44 | "picocolors": "^1.0.0", 45 | "picomatch": "^2.3.1", 46 | "resolve.exports": "^2.0.2", 47 | "rollup-plugin-license": "^3.0.1", 48 | "strip-literal": "^1.0.1", 49 | "tsconfck": "^2.1.1", 50 | "tslib": "^2.5.0", 51 | "ufo": "^1.1.2", 52 | "vite": "^4.2.1", 53 | "ws": "^8.13.0" 54 | }, 55 | "devDependencies": { 56 | "@types/connect": "^3.4.35", 57 | "@types/connect-history-api-fallback": "^1.5.0", 58 | "@types/cross-spawn": "^6.0.2", 59 | "@types/debug": "^4.1.7", 60 | "@types/etag": "^1.8.1", 61 | "@types/fs-extra": "^11.0.1", 62 | "@types/micromatch": "^4.0.2", 63 | "@types/node": "^18.15.11", 64 | "@types/picomatch": "^2.3.0", 65 | "@types/ws": "^8.5.4", 66 | "dep-types": "link:src\\types", 67 | "sirv": "^2.0.2", 68 | "types": "link:types" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/vite/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import nodeResolve from "@rollup/plugin-node-resolve"; 5 | import typescript from "@rollup/plugin-typescript"; 6 | import commonjs from "@rollup/plugin-commonjs"; 7 | import json from "@rollup/plugin-json"; 8 | import type { Plugin, RollupOptions } from "rollup"; 9 | import { defineConfig } from "rollup"; 10 | 11 | const pkg = JSON.parse( 12 | readFileSync(new URL("./package.json", import.meta.url)).toString() 13 | ); 14 | 15 | const __dirname = fileURLToPath(new URL(".", import.meta.url)); 16 | 17 | const envConfig = defineConfig({ 18 | input: path.resolve(__dirname, "src/client/env.ts"), 19 | plugins: [ 20 | typescript({ 21 | tsconfig: path.resolve(__dirname, "src/client/tsconfig.json"), 22 | }), 23 | ], 24 | output: { 25 | file: path.resolve(__dirname, "dist/client", "env.mjs"), 26 | sourcemap: true, 27 | sourcemapPathTransform(relativeSourcePath) { 28 | return path.basename(relativeSourcePath); 29 | }, 30 | sourcemapIgnoreList() { 31 | return true; 32 | }, 33 | }, 34 | }); 35 | 36 | const clientConfig = defineConfig({ 37 | input: path.resolve(__dirname, "src/client/client.ts"), 38 | external: ["./env", "@vite/env"], 39 | plugins: [ 40 | typescript({ 41 | tsconfig: path.resolve(__dirname, "src/client/tsconfig.json"), 42 | }), 43 | ], 44 | output: { 45 | file: path.resolve(__dirname, "dist/client", "client.mjs"), 46 | sourcemap: true, 47 | sourcemapPathTransform(relativeSourcePath) { 48 | return path.basename(relativeSourcePath); 49 | }, 50 | sourcemapIgnoreList() { 51 | return true; 52 | }, 53 | }, 54 | }); 55 | 56 | const sharedNodeOptions = defineConfig({ 57 | treeshake: { 58 | moduleSideEffects: "no-external", 59 | propertyReadSideEffects: false, 60 | tryCatchDeoptimization: false, 61 | }, 62 | output: { 63 | dir: "./dist", 64 | entryFileNames: `node/[name].js`, 65 | chunkFileNames: "node/chunks/dep-[hash].js", 66 | exports: "named", 67 | format: "esm", 68 | externalLiveBindings: false, 69 | freeze: false, 70 | }, 71 | onwarn(warning, warn) { 72 | if (warning.message.includes("Circular dependency")) { 73 | return; 74 | } 75 | warn(warning); 76 | }, 77 | }); 78 | 79 | function createNodePlugins( 80 | sourceMap: boolean, 81 | declarationDir: string | false 82 | ): (Plugin | false)[] { 83 | return [ 84 | nodeResolve({ preferBuiltins: true }), 85 | typescript({ 86 | tsconfig: path.resolve(__dirname, "src/node/tsconfig.json"), 87 | sourceMap, 88 | declaration: declarationDir !== false, 89 | declarationDir: declarationDir !== false ? declarationDir : undefined, 90 | }), 91 | 92 | commonjs({ 93 | extensions: [".js"], 94 | ignore: ["bufferutil", "utf-8-validate"], 95 | }), 96 | json(), 97 | ]; 98 | } 99 | 100 | function createNodeConfig(isProduction: boolean) { 101 | return defineConfig({ 102 | ...sharedNodeOptions, 103 | input: { 104 | index: path.resolve(__dirname, "src/node/index.ts"), 105 | cli: path.resolve(__dirname, "src/node/cli.ts"), 106 | constants: path.resolve(__dirname, "src/node/constants.ts"), 107 | }, 108 | output: { 109 | ...sharedNodeOptions.output, 110 | sourcemap: !isProduction, 111 | }, 112 | external: [ 113 | "fsevents", 114 | ...Object.keys(pkg.dependencies), 115 | ...(isProduction ? [] : Object.keys(pkg.devDependencies)), 116 | ], 117 | plugins: createNodePlugins( 118 | !isProduction, 119 | isProduction ? false : "./dist/node" 120 | ), 121 | }); 122 | } 123 | 124 | export default (commandLineArgs: any): RollupOptions[] => { 125 | const isDev = commandLineArgs.watch; 126 | const isProduction = !isDev; 127 | 128 | return defineConfig([ 129 | envConfig, 130 | clientConfig, 131 | createNodeConfig(isProduction), 132 | ]); 133 | }; 134 | 135 | -------------------------------------------------------------------------------- /packages/vite/src/client/env.ts: -------------------------------------------------------------------------------- 1 | declare const __MODE__: string 2 | declare const __DEFINES__: Record 3 | 4 | const context = (() => { 5 | if (typeof globalThis !== 'undefined') { 6 | return globalThis 7 | } else if (typeof self !== 'undefined') { 8 | return self 9 | } else if (typeof window !== 'undefined') { 10 | return window 11 | } else { 12 | return Function('return this')() 13 | } 14 | })() 15 | 16 | const defines = __DEFINES__ 17 | Object.keys(defines).forEach((key) => { 18 | const segments = key.split('.') 19 | let target = context 20 | for (let i = 0; i < segments.length; i++) { 21 | const segment = segments[i] 22 | if (i === segments.length - 1) { 23 | target[segment] = defines[key] 24 | } else { 25 | target = target[segment] || (target[segment] = {}) 26 | } 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /packages/vite/src/client/overlay.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorPayload } from "types/hmrPayload"; 2 | 3 | declare const __BASE__: string; 4 | 5 | const base = __BASE__ || "/"; 6 | 7 | const template = /*html*/ ` 8 | 118 |
119 |
120 |
121 |

122 |     

123 |     

124 |     
125 | Click outside or fix the code to dismiss.
126 | You can also disable this overlay by setting 127 | server.hmr.overlay to false in vite.config.js. 128 |
129 |
130 |
131 | `; 132 | 133 | const fileRE = /(?:[a-zA-Z]:\\|\/).*?:\d+:\d+/g; 134 | const codeframeRE = /^(?:>?\s+\d+\s+\|.*|\s+\|\s*\^.*)\r?\n/gm; 135 | 136 | const { HTMLElement = class {} as typeof globalThis.HTMLElement } = globalThis; 137 | export class ErrorOverlay extends HTMLElement { 138 | root: ShadowRoot; 139 | 140 | constructor(err: ErrorPayload["err"], links = true) { 141 | super(); 142 | this.root = this.attachShadow({ mode: "open" }); 143 | this.root.innerHTML = template; 144 | 145 | codeframeRE.lastIndex = 0; 146 | const hasFrame = err.frame && codeframeRE.test(err.frame); 147 | const message = hasFrame 148 | ? err.message.replace(codeframeRE, "") 149 | : err.message; 150 | if (err.plugin) { 151 | this.text(".plugin", `[plugin:${err.plugin}] `); 152 | } 153 | this.text(".message-body", message.trim()); 154 | 155 | const [file] = (err.loc?.file || err.id || "unknown file").split(`?`); 156 | if (err.loc) { 157 | this.text(".file", `${file}:${err.loc.line}:${err.loc.column}`, links); 158 | } else if (err.id) { 159 | this.text(".file", file); 160 | } 161 | 162 | if (hasFrame) { 163 | this.text(".frame", err.frame!.trim()); 164 | } 165 | this.text(".stack", err.stack, links); 166 | 167 | this.root.querySelector(".window")!.addEventListener("click", (e) => { 168 | e.stopPropagation(); 169 | }); 170 | this.addEventListener("click", () => { 171 | this.close(); 172 | }); 173 | } 174 | 175 | text(selector: string, text: string, linkFiles = false): void { 176 | const el = this.root.querySelector(selector)!; 177 | if (!linkFiles) { 178 | el.textContent = text; 179 | } else { 180 | let curIndex = 0; 181 | let match: RegExpExecArray | null; 182 | fileRE.lastIndex = 0; 183 | while ((match = fileRE.exec(text))) { 184 | const { 0: file, index } = match; 185 | if (index != null) { 186 | const frag = text.slice(curIndex, index); 187 | el.appendChild(document.createTextNode(frag)); 188 | const link = document.createElement("a"); 189 | link.textContent = file; 190 | link.className = "file-link"; 191 | link.onclick = () => { 192 | fetch(`${base}__open-in-editor?file=` + encodeURIComponent(file)); 193 | }; 194 | el.appendChild(link); 195 | curIndex += frag.length + file.length; 196 | } 197 | } 198 | } 199 | } 200 | 201 | close(): void { 202 | this.parentNode?.removeChild(this); 203 | } 204 | } 205 | // TODO 什么是错误覆盖层? 206 | export const overlayId = "vite-error-overlay"; 207 | const { customElements } = globalThis; 208 | if (customElements && !customElements.get(overlayId)) { 209 | customElements.define(overlayId, ErrorOverlay); 210 | } 211 | -------------------------------------------------------------------------------- /packages/vite/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["./", "../types"], 4 | "compilerOptions": { 5 | "types": [], 6 | "target": "ES2019", 7 | "lib": ["ESNext", "DOM"], 8 | "declaration": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/vite/src/node/build.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { BuildOptions, ResolvedBuildOptions } from "vite"; 3 | import type { InternalModuleFormat } from "rollup"; 4 | /**build配置 */ 5 | export function resolveBuildOptions( 6 | raw: BuildOptions | undefined 7 | ): ResolvedBuildOptions { 8 | const defaultBuildOptions: BuildOptions = { 9 | outDir: "dist", 10 | assetsDir: "assets", 11 | assetsInlineLimit: 4096, 12 | cssCodeSplit: !raw?.lib, 13 | sourcemap: false, 14 | rollupOptions: {}, 15 | minify: "esbuild", 16 | terserOptions: {}, 17 | write: true, 18 | emptyOutDir: null, 19 | copyPublicDir: true, 20 | manifest: false, 21 | lib: false, 22 | ssrManifest: false, 23 | ssrEmitAssets: false, 24 | reportCompressedSize: true, 25 | chunkSizeWarningLimit: 500, 26 | watch: null, 27 | }; 28 | const userBuildOptions = defaultBuildOptions; 29 | const defaultModulePreload = { 30 | polyfill: true, 31 | }; 32 | // @ts-expect-error Fallback options instead of merging 33 | const resolved: ResolvedBuildOptions = { 34 | target: "modules", 35 | cssTarget: false, 36 | ...userBuildOptions, 37 | commonjsOptions: { 38 | include: [/node_modules/], 39 | extensions: [".js", ".cjs"], 40 | ...userBuildOptions.commonjsOptions, 41 | }, 42 | dynamicImportVarsOptions: { 43 | warnOnError: true, 44 | exclude: [/node_modules/], 45 | ...userBuildOptions.dynamicImportVarsOptions, 46 | }, 47 | modulePreload: defaultModulePreload, 48 | }; 49 | 50 | return resolved; 51 | } 52 | 53 | const getResolveUrl = (path: string, URL = "URL") => `new ${URL}(${path}).href`; 54 | const getFileUrlFromFullPath = (path: string) => 55 | `require('u' + 'rl').pathToFileURL(${path}).href`; 56 | 57 | const getFileUrlFromRelativePath = (path: string) => 58 | getFileUrlFromFullPath(`__dirname + '/${path}'`); 59 | 60 | const getRelativeUrlFromDocument = (relativePath: string, umd = false) => 61 | getResolveUrl( 62 | `'${escapeId(relativePath)}', ${ 63 | umd ? `typeof document === 'undefined' ? location.href : ` : "" 64 | }document.currentScript && document.currentScript.src || document.baseURI` 65 | ); 66 | 67 | const needsEscapeRegEx = /[\n\r'\\\u2028\u2029]/; 68 | const quoteNewlineRegEx = /([\n\r'\u2028\u2029])/g; 69 | const backSlashRegEx = /\\/g; 70 | function escapeId(id: string): string { 71 | if (!needsEscapeRegEx.test(id)) return id; 72 | return id.replace(backSlashRegEx, "\\\\").replace(quoteNewlineRegEx, "\\$1"); 73 | } 74 | 75 | const relativeUrlMechanisms: Record< 76 | InternalModuleFormat, 77 | (relativePath: string) => string 78 | > = { 79 | amd: (relativePath) => { 80 | if (relativePath[0] !== ".") relativePath = "./" + relativePath; 81 | return getResolveUrl(`require.toUrl('${relativePath}'), document.baseURI`); 82 | }, 83 | cjs: (relativePath) => 84 | `(typeof document === 'undefined' ? ${getFileUrlFromRelativePath( 85 | relativePath 86 | )} : ${getRelativeUrlFromDocument(relativePath)})`, 87 | es: (relativePath) => getResolveUrl(`'${relativePath}', import.meta.url`), 88 | iife: (relativePath) => getRelativeUrlFromDocument(relativePath), 89 | system: (relativePath) => getResolveUrl(`'${relativePath}', module.meta.url`), 90 | umd: (relativePath) => 91 | `(typeof document === 'undefined' && typeof location === 'undefined' ? ${getFileUrlFromRelativePath( 92 | relativePath 93 | )} : ${getRelativeUrlFromDocument(relativePath, true)})`, 94 | }; 95 | 96 | const customRelativeUrlMechanisms = { 97 | ...relativeUrlMechanisms, 98 | "worker-iife": (relativePath) => 99 | getResolveUrl(`'${relativePath}', self.location.href`), 100 | } as const satisfies Record string>; 101 | 102 | export function createToImportMetaURLBasedRelativeRuntime( 103 | format: InternalModuleFormat 104 | ): (filename: string, importer: string) => { runtime: string } { 105 | const formatLong = format; 106 | const toRelativePath = customRelativeUrlMechanisms[formatLong]; 107 | return (filename, importer) => ({ 108 | runtime: toRelativePath( 109 | path.posix.relative(path.dirname(importer), filename) 110 | ), 111 | }); 112 | } 113 | -------------------------------------------------------------------------------- /packages/vite/src/node/cli.ts: -------------------------------------------------------------------------------- 1 | import cac from "cac"; 2 | import colors from "picocolors"; 3 | import { performance } from "node:perf_hooks"; 4 | import { VERSION } from "./constants"; 5 | import { createLogger } from "vite"; 6 | 7 | const cli = cac("mini-vite"); 8 | cli 9 | .command("[root]", "Run the development server") 10 | .action(async (root, options) => { 11 | const { createServer } = await import("./server"); 12 | try { 13 | const server = await createServer({ 14 | root, 15 | base: options.base, 16 | mode: options.mode, 17 | configFile: options.config, 18 | logLevel: options.logLevel, 19 | optimizeDeps: { force: options.force }, 20 | server: {}, 21 | }); 22 | if (!server.httpServer) { 23 | throw new Error("HTTP server not available"); 24 | } 25 | await server.listen(); 26 | const info = server.config.logger.info; 27 | const viteStartTime = global.__vite_start_time ?? false; 28 | const startupDurationString = viteStartTime 29 | ? colors.dim( 30 | `ready in ${colors.reset( 31 | colors.bold(Math.ceil(performance.now() - viteStartTime)) 32 | )} ms` 33 | ) 34 | : ""; 35 | info( 36 | `\n ${colors.green( 37 | `${colors.bold("MINI-VITE")} v${VERSION}` 38 | )} ${startupDurationString}\n`, 39 | { clear: !server.config.logger.hasWarned } 40 | ); 41 | server.printUrls(); 42 | } catch (e) { 43 | const logger = createLogger(options.logLevel); 44 | logger.error(colors.red(`error when starting dev server:\n${e.stack}`), { 45 | error: e, 46 | }); 47 | process.exit(1); 48 | } 49 | }); 50 | 51 | cli.help(); 52 | cli.parse(); 53 | -------------------------------------------------------------------------------- /packages/vite/src/node/constants.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | import path, { resolve } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | export const VITE_PACKAGE_DIR = resolve( 6 | fileURLToPath(import.meta.url), 7 | "../../.." 8 | ); 9 | export const DEP_VERSION_RE = /[?&](v=[\w.-]+)\b/; 10 | export const OPTIMIZABLE_ENTRY_RE = /\.[cm]?[jt]s$/; 11 | export const ENV_ENTRY = resolve(VITE_PACKAGE_DIR, "dist/client/env.mjs"); 12 | 13 | const { version } = JSON.parse( 14 | readFileSync(new URL("../../package.json", import.meta.url)).toString() 15 | ); 16 | export const CSS_LANGS_RE = 17 | /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/; 18 | export const BARE_IMPORT_RE = /^[\w@][^:]/; 19 | export const DEFAULT_DEV_PORT = 5173; 20 | export const DEFAULT_HOST_NAME = "localhost"; 21 | export const VERSION = version as string; 22 | export const ESBUILD_MODULES_TARGET = [ 23 | "es2020", 24 | "edge88", 25 | "firefox78", 26 | "chrome87", 27 | "safari14", 28 | ]; 29 | 30 | export const CLIENT_ENTRY = resolve(VITE_PACKAGE_DIR, "dist/client/client.mjs"); 31 | export const CLIENT_DIR = path.dirname(CLIENT_ENTRY); 32 | 33 | export const DEFAULT_MAIN_FIELDS = ["module", "jsnext:main", "jsnext"]; 34 | export const DEFAULT_EXTENSIONS = [ 35 | ".mjs", 36 | ".js", 37 | ".mts", 38 | ".ts", 39 | ".jsx", 40 | ".tsx", 41 | ".json", 42 | ]; 43 | export const SPECIAL_QUERY_RE = /[?&](?:worker|sharedworker|raw|url)\b/; 44 | 45 | export const FS_PREFIX = `/@fs/`; 46 | 47 | export const VALID_ID_PREFIX = `/@id/`; 48 | export const NULL_BYTE_PLACEHOLDER = `__x00__`; 49 | export const CLIENT_PUBLIC_PATH = `/@vite/client`; 50 | export const ENV_PUBLIC_PATH = `/@vite/env`; 51 | 52 | export const DEFAULT_CONFIG_FILES = [ 53 | "vite.config.js", 54 | "vite.config.mjs", 55 | "vite.config.ts", 56 | "vite.config.cjs", 57 | "vite.config.mts", 58 | "vite.config.cts", 59 | ]; 60 | 61 | export const KNOWN_ASSET_TYPES = [ 62 | "png", 63 | "jpe?g", 64 | "jfif", 65 | "pjpeg", 66 | "pjp", 67 | "gif", 68 | "svg", 69 | "ico", 70 | "webp", 71 | "avif", 72 | 73 | "mp4", 74 | "webm", 75 | "ogg", 76 | "mp3", 77 | "wav", 78 | "flac", 79 | "aac", 80 | "opus", 81 | 82 | "woff2?", 83 | "eot", 84 | "ttf", 85 | "otf", 86 | 87 | "webmanifest", 88 | "pdf", 89 | "txt", 90 | ]; 91 | 92 | export const DEFAULT_ASSETS_RE = new RegExp( 93 | `\\.(` + KNOWN_ASSET_TYPES.join("|") + `)(\\?.*)?$` 94 | ); 95 | 96 | export const JS_TYPES_RE = /\.(?:j|t)sx?$|\.mjs$/; 97 | export const wildcardHosts = new Set([ 98 | "0.0.0.0", 99 | "::", 100 | "0000:0000:0000:0000:0000:0000:0000:0000", 101 | ]); 102 | 103 | export const loopbackHosts = new Set([ 104 | "localhost", 105 | "127.0.0.1", 106 | "::1", 107 | "0000:0000:0000:0000:0000:0000:0000:0001", 108 | ]); 109 | -------------------------------------------------------------------------------- /packages/vite/src/node/http.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from "./logger"; 2 | import type { Server as HttpServer } from "node:http"; 3 | import type { Connect } from "dep-types/connect"; 4 | 5 | /**创建http服务器,返回http.Server 的新实例。 */ 6 | export async function resolveHttpServer( 7 | app: Connect.Server 8 | ): Promise { 9 | const { createServer } = await import("node:http"); 10 | return createServer(app); 11 | } 12 | /**开启服务器,并监听服务器端口 */ 13 | export async function httpServerStart( 14 | httpServer: HttpServer, 15 | serverOptions: { 16 | port: number; 17 | strictPort: boolean | undefined; 18 | host: string | undefined; 19 | logger: Logger; 20 | } 21 | ): Promise { 22 | let { port, strictPort, host, logger } = serverOptions; 23 | return new Promise((resolve, reject) => { 24 | // 创建http服务器异常处理 25 | const onError = (e: Error & { code?: string }) => { 26 | if (e.code === "EADDRINUSE") { 27 | if (strictPort) { 28 | httpServer.removeListener("error", onError); 29 | reject(new Error(`Port ${port} is already in use`)); 30 | } else { 31 | logger.info(`Port ${port} is in use, trying another one...`); 32 | httpServer.listen(++port, host); 33 | } 34 | } else { 35 | httpServer.removeListener("error", onError); 36 | reject(e); 37 | } 38 | }; 39 | httpServer.on("error", onError); 40 | // 监听端口 41 | httpServer.listen(port, host, async () => { 42 | httpServer.removeListener("error", onError); 43 | resolve(port); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /packages/vite/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./config"; 2 | 3 | export type { 4 | ViteDevServer, 5 | ResolvedServerOptions, 6 | ResolvedServerUrls, 7 | } from "./server"; 8 | 9 | export type { 10 | AliasOptions, 11 | MapToFunction, 12 | ResolverFunction, 13 | ResolverObject, 14 | Alias, 15 | } from "dep-types/alias"; 16 | 17 | export type { 18 | DepOptimizationMetadata, 19 | DepOptimizationOptions, 20 | DepOptimizationConfig, 21 | DepOptimizationResult, 22 | OptimizedDepInfo, 23 | ExportsData, 24 | } from "./optimizer"; 25 | -------------------------------------------------------------------------------- /packages/vite/src/node/logger.ts: -------------------------------------------------------------------------------- 1 | import colors from "picocolors"; 2 | import type { RollupError } from "rollup"; 3 | import type { ResolvedServerUrls } from "./server"; 4 | export type LogType = "error" | "warn" | "info"; 5 | export type LogLevel = LogType | "silent"; 6 | export interface Logger { 7 | info(msg: string, options?: LogOptions): void; 8 | warn(msg: string, options?: LogOptions): void; 9 | warnOnce(msg: string, options?: LogOptions): void; 10 | error(msg: string, options?: LogErrorOptions): void; 11 | hasErrorLogged(error: Error | RollupError): boolean; 12 | hasWarned: boolean; 13 | } 14 | export interface LogOptions { 15 | clear?: boolean; 16 | timestamp?: boolean; 17 | } 18 | export interface LogErrorOptions extends LogOptions { 19 | error?: Error | RollupError | null; 20 | } 21 | /**控制台打印本地服务器url */ 22 | export function printServerUrls( 23 | urls: ResolvedServerUrls, 24 | optionsHost: string | boolean | undefined, 25 | info: Logger["info"] 26 | ): void { 27 | const colorUrl = (url: string) => 28 | colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`)); 29 | for (const url of urls.local) { 30 | info(` ${colors.green("➜")} ${colors.bold("Local")}: ${colorUrl(url)}`); 31 | } 32 | for (const url of urls.network) { 33 | info(` ${colors.green("➜")} ${colors.bold("Network")}: ${colorUrl(url)}`); 34 | } 35 | if (urls.network.length === 0 && optionsHost === undefined) { 36 | info( 37 | colors.dim(` ${colors.green("➜")} ${colors.bold("Network")}: use `) + 38 | colors.bold("--host") + 39 | colors.dim(" to expose") 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/vite/src/node/optimizer/optimizer.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedConfig } from "../config"; 2 | import { getHash } from "../utils"; 3 | import { 4 | loadCachedDepOptimizationMetadata, 5 | discoverProjectDependencies, 6 | runOptimizeDeps, 7 | initDepsOptimizerMetadata, 8 | getOptimizedDepPath, 9 | depsFromOptimizedDepInfo, 10 | extractExportsData, 11 | addOptimizedDepInfo, 12 | OptimizedDepInfo, 13 | } from "./index"; 14 | 15 | /**初始化预构建依赖 */ 16 | export async function initDepsOptimizer(config: ResolvedConfig): Promise { 17 | await createDepsOptimizer(config); 18 | } 19 | 20 | async function createDepsOptimizer(config: ResolvedConfig): Promise { 21 | const sessionTimestamp = Date.now().toString(); 22 | const cachedMetadata = await loadCachedDepOptimizationMetadata(config); 23 | // 有缓存的话,就直接使用缓存的metadata,否则就初始化metadata 24 | let metadata = 25 | cachedMetadata || initDepsOptimizerMetadata(config, sessionTimestamp); 26 | 27 | let discover; 28 | if (!cachedMetadata) { 29 | // 扫描node_modules中的依赖 30 | discover = discoverProjectDependencies(config); 31 | const deps = await discover.result; 32 | discover = undefined; 33 | for (const id of Object.keys(deps)) { 34 | addMissingDep(id, deps[id]); 35 | } 36 | 37 | const knownDeps = prepareKnownDeps(); 38 | // 将依赖信息写入metadata.json文件中 39 | (await runOptimizeDeps(config, knownDeps).result).commit(); 40 | } 41 | /** 42 | * 在编写代码时可能会忽略某些依赖项的引入, 43 | * 或者某些依赖项的引入被误删或错误修改。 44 | * addMissingDep 函数的作用就是检测模块中缺失的依赖项, 45 | * 并自动向模块添加这些缺失的依赖项。 46 | * */ 47 | function addMissingDep(id: string, resolved: string) { 48 | return addOptimizedDepInfo(metadata, "discovered", { 49 | id, 50 | file: getOptimizedDepPath(id, config), 51 | src: resolved, 52 | browserHash: getDiscoveredBrowserHash( 53 | metadata.hash, 54 | depsFromOptimizedDepInfo(metadata.optimized), 55 | depsFromOptimizedDepInfo(metadata.discovered) 56 | ), 57 | exportsData: extractExportsData(resolved, config), 58 | }); 59 | } 60 | /**将依赖项处理成所需要的结构 */ 61 | function prepareKnownDeps() { 62 | const knownDeps: Record = {}; 63 | for (const dep of Object.keys(metadata.optimized)) { 64 | knownDeps[dep] = { ...metadata.optimized[dep] }; 65 | } 66 | for (const dep of Object.keys(metadata.discovered)) { 67 | const { ...info } = metadata.discovered[dep]; 68 | knownDeps[dep] = info; 69 | } 70 | return knownDeps; 71 | } 72 | 73 | function getDiscoveredBrowserHash( 74 | hash: string, 75 | deps: Record, 76 | missing: Record 77 | ) { 78 | return getHash( 79 | hash + JSON.stringify(deps) + JSON.stringify(missing) + sessionTimestamp 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/vite/src/node/packages.ts: -------------------------------------------------------------------------------- 1 | import { safeRealpathSync } from "./utils"; 2 | import path from "node:path"; 3 | import fs from "node:fs"; 4 | export type PackageCache = Map; 5 | export interface PackageData { 6 | dir: string; 7 | hasSideEffects: (id: string) => boolean | "no-treeshake"; 8 | webResolvedImports: Record; 9 | nodeResolvedImports: Record; 10 | setResolvedCache: (key: string, entry: string, targetWeb: boolean) => void; 11 | getResolvedCache: (key: string, targetWeb: boolean) => string | undefined; 12 | data: { 13 | [field: string]: any; 14 | name: string; 15 | type: string; 16 | version: string; 17 | main: string; 18 | module: string; 19 | browser: string | Record; 20 | exports: string | Record | string[]; 21 | imports: Record; 22 | dependencies: Record; 23 | }; 24 | } 25 | 26 | /**生成预加载缓存的键值。 */ 27 | function getRpdCacheKey( 28 | pkgName: string, 29 | basedir: string, 30 | preserveSymlinks: boolean 31 | ) { 32 | return `rpd_${pkgName}_${basedir}_${preserveSymlinks}`; 33 | } 34 | 35 | export function resolvePackageData( 36 | pkgName: string, 37 | basedir: string, 38 | preserveSymlinks = false, 39 | packageCache?: PackageCache 40 | ): PackageData | null { 41 | const originalBasedir = basedir; 42 | while (basedir) { 43 | const pkg = path.join(basedir, "node_modules", pkgName, "package.json"); 44 | try { 45 | if (fs.existsSync(pkg)) { 46 | const pkgPath = preserveSymlinks ? pkg : safeRealpathSync(pkg); 47 | const pkgData = loadPackageData(pkgPath); 48 | 49 | if (packageCache) { 50 | setRpdCache( 51 | packageCache, 52 | pkgData, 53 | pkgName, 54 | basedir, 55 | originalBasedir, 56 | preserveSymlinks 57 | ); 58 | } 59 | 60 | return pkgData; 61 | } 62 | } catch (e) {} 63 | 64 | const nextBasedir = path.dirname(basedir); 65 | if (nextBasedir === basedir) break; 66 | basedir = nextBasedir; 67 | } 68 | 69 | return null; 70 | } 71 | 72 | export function findNearestMainPackageData( 73 | basedir: string, 74 | packageCache?: PackageCache 75 | ): PackageData | null { 76 | const nearestPackage = findNearestPackageData(basedir, packageCache); 77 | return ( 78 | nearestPackage && 79 | (nearestPackage.data.name 80 | ? nearestPackage 81 | : findNearestMainPackageData( 82 | path.dirname(nearestPackage.dir), 83 | packageCache 84 | )) 85 | ); 86 | } 87 | 88 | export function loadPackageData(pkgPath: string): PackageData { 89 | const data = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); 90 | const pkgDir = path.dirname(pkgPath); 91 | let hasSideEffects: (id: string) => boolean; 92 | 93 | hasSideEffects = () => true; 94 | 95 | const pkg: PackageData = { 96 | dir: pkgDir, 97 | data, 98 | hasSideEffects, 99 | webResolvedImports: {}, 100 | nodeResolvedImports: {}, 101 | setResolvedCache(key: string, entry: string, targetWeb: boolean) { 102 | if (targetWeb) { 103 | pkg.webResolvedImports[key] = entry; 104 | } else { 105 | pkg.nodeResolvedImports[key] = entry; 106 | } 107 | }, 108 | getResolvedCache(key: string, targetWeb: boolean) { 109 | if (targetWeb) { 110 | return pkg.webResolvedImports[key]; 111 | } else { 112 | return pkg.nodeResolvedImports[key]; 113 | } 114 | }, 115 | }; 116 | 117 | return pkg; 118 | } 119 | 120 | function traverseBetweenDirs( 121 | longerDir: string, 122 | shorterDir: string, 123 | cb: (dir: string) => void 124 | ) { 125 | while (longerDir !== shorterDir) { 126 | cb(longerDir); 127 | longerDir = path.dirname(longerDir); 128 | } 129 | } 130 | 131 | export function findNearestPackageData( 132 | basedir: string, 133 | packageCache?: PackageCache 134 | ): PackageData | null { 135 | const originalBasedir = basedir; 136 | while (basedir) { 137 | if (packageCache) { 138 | const cached = getFnpdCache(packageCache, basedir, originalBasedir); 139 | if (cached) return cached; 140 | } 141 | 142 | const pkgPath = path.join(basedir, "package.json"); 143 | try { 144 | if (fs.statSync(pkgPath, { throwIfNoEntry: false })?.isFile()) { 145 | const pkgData = loadPackageData(pkgPath); 146 | 147 | if (packageCache) { 148 | setFnpdCache(packageCache, pkgData, basedir, originalBasedir); 149 | } 150 | 151 | return pkgData; 152 | } 153 | } catch (e) {} 154 | 155 | const nextBasedir = path.dirname(basedir); 156 | if (nextBasedir === basedir) break; 157 | basedir = nextBasedir; 158 | } 159 | 160 | return null; 161 | } 162 | 163 | function getFnpdCache( 164 | packageCache: PackageCache, 165 | basedir: string, 166 | originalBasedir: string 167 | ) { 168 | const cacheKey = getFnpdCacheKey(basedir); 169 | const pkgData = packageCache.get(cacheKey); 170 | if (pkgData) { 171 | traverseBetweenDirs(originalBasedir, basedir, (dir) => { 172 | packageCache.set(getFnpdCacheKey(dir), pkgData); 173 | }); 174 | return pkgData; 175 | } 176 | } 177 | 178 | function getFnpdCacheKey(basedir: string) { 179 | return `fnpd_${basedir}`; 180 | } 181 | 182 | function setFnpdCache( 183 | packageCache: PackageCache, 184 | pkgData: PackageData, 185 | basedir: string, 186 | originalBasedir: string 187 | ) { 188 | packageCache.set(getFnpdCacheKey(basedir), pkgData); 189 | traverseBetweenDirs(originalBasedir, basedir, (dir) => { 190 | packageCache.set(getFnpdCacheKey(dir), pkgData); 191 | }); 192 | } 193 | 194 | function setRpdCache( 195 | packageCache: PackageCache, 196 | pkgData: PackageData, 197 | pkgName: string, 198 | basedir: string, 199 | originalBasedir: string, 200 | preserveSymlinks: boolean 201 | ) { 202 | packageCache.set(getRpdCacheKey(pkgName, basedir, preserveSymlinks), pkgData); 203 | traverseBetweenDirs(originalBasedir, basedir, (dir) => { 204 | packageCache.set(getRpdCacheKey(pkgName, dir, preserveSymlinks), pkgData); 205 | }); 206 | } 207 | -------------------------------------------------------------------------------- /packages/vite/src/node/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ConfigEnv, 3 | HmrContext, 4 | IndexHtmlTransform, 5 | ModuleNode, 6 | PreviewServerHook, 7 | UserConfig, 8 | } from "vite"; 9 | import { ViteDevServer } from "./server"; 10 | import type { 11 | CustomPluginOptions, 12 | LoadResult, 13 | ObjectHook, 14 | PluginContext, 15 | ResolveIdResult, 16 | Plugin as RollupPlugin, 17 | TransformPluginContext, 18 | TransformResult, 19 | } from "rollup"; 20 | import { ResolvedConfig } from "./config"; 21 | export type ServerHook = ( 22 | server: ViteDevServer 23 | ) => (() => void) | void | Promise<(() => void) | void>; 24 | 25 | export interface Plugin extends RollupPlugin { 26 | enforce?: "pre" | "post"; 27 | apply?: 28 | | "serve" 29 | | "build" 30 | | ((this: void, config: UserConfig, env: ConfigEnv) => boolean); 31 | config?: ObjectHook< 32 | ( 33 | this: void, 34 | config: UserConfig, 35 | env: ConfigEnv 36 | ) => UserConfig | null | void | Promise 37 | >; 38 | 39 | configResolved?: ObjectHook< 40 | (this: void, config: ResolvedConfig) => void | Promise 41 | >; 42 | configureServer?: ObjectHook; 43 | configurePreviewServer?: ObjectHook; 44 | transformIndexHtml?: IndexHtmlTransform; 45 | handleHotUpdate?: ObjectHook< 46 | ( 47 | this: void, 48 | ctx: HmrContext 49 | ) => Array | void | Promise | void> 50 | >; 51 | resolveId?: ObjectHook< 52 | ( 53 | this: PluginContext, 54 | source: string, 55 | importer: string | undefined, 56 | options: { 57 | assertions: Record; 58 | custom?: CustomPluginOptions; 59 | scan?: boolean; 60 | isEntry: boolean; 61 | } 62 | ) => Promise | ResolveIdResult 63 | >; 64 | load?: ObjectHook< 65 | ( 66 | this: PluginContext, 67 | id: string, 68 | options?: {} 69 | ) => Promise | LoadResult 70 | >; 71 | transform?: ObjectHook< 72 | ( 73 | this: TransformPluginContext, 74 | code: string, 75 | id: string, 76 | options?: {} 77 | ) => Promise | TransformResult 78 | >; 79 | } 80 | -------------------------------------------------------------------------------- /packages/vite/src/node/pluginContainer.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | LoadResult, 3 | PartialResolvedId, 4 | SourceDescription, 5 | PluginContext as RollupPluginContext, 6 | ResolvedId, 7 | CustomPluginOptions, 8 | InputOptions, 9 | ModuleInfo, 10 | MinimalPluginContext, 11 | PartialNull, 12 | ModuleOptions, 13 | SourceMap, 14 | TransformResult, 15 | NormalizedInputOptions, 16 | ParallelPluginHooks, 17 | AsyncPluginHooks, 18 | FunctionPluginHooks, 19 | } from "rollup"; 20 | import { ModuleGraph } from "vite"; 21 | import { ResolvedConfig } from "./config"; 22 | import { createPluginHookUtils } from "./plugins"; 23 | import { join } from "path"; 24 | import { VERSION as rollupVersion } from "rollup"; 25 | import { Plugin } from "./plugin"; 26 | import { 27 | createDebugger, 28 | isExternalUrl, 29 | isObject, 30 | normalizePath, 31 | prettifyUrl, 32 | timeFrom, 33 | } from "./utils"; 34 | import * as acorn from "acorn"; 35 | 36 | type PluginContext = Omit; 37 | 38 | const debugPluginResolve = createDebugger("vite:plugin-resolve", { 39 | onlyWhenFocused: "vite:plugin", 40 | }); 41 | 42 | export let parser = acorn.Parser; 43 | 44 | export interface PluginContainer { 45 | options: InputOptions; 46 | getModuleInfo(id: string): ModuleInfo | null; 47 | buildStart(options: InputOptions): Promise; 48 | resolveId( 49 | id: string, 50 | importer?: string, 51 | options?: { 52 | assertions?: Record; 53 | custom?: CustomPluginOptions; 54 | skip?: Set; 55 | scan?: boolean; 56 | isEntry?: boolean; 57 | } 58 | ): Promise; 59 | transform( 60 | code: string, 61 | id: string, 62 | options?: { 63 | inMap?: SourceDescription["map"]; 64 | } 65 | ): Promise; 66 | load(id: string, options?: {}): Promise; 67 | close(): Promise; 68 | } 69 | /**创建插件容器 */ 70 | export async function createPluginContainer( 71 | config: ResolvedConfig, 72 | moduleGraph?: ModuleGraph 73 | ): Promise { 74 | const { 75 | plugins, 76 | root, 77 | build: { rollupOptions }, 78 | } = config; 79 | const { getSortedPlugins, getSortedPluginHooks } = 80 | createPluginHookUtils(plugins); 81 | 82 | const minimalContext: MinimalPluginContext = { 83 | meta: { 84 | rollupVersion, 85 | watchMode: true, 86 | }, 87 | }; 88 | 89 | const ModuleInfoProxy: ProxyHandler = { 90 | get(info: any, key: string) { 91 | if (key in info) { 92 | return info[key]; 93 | } 94 | if (key === "then") { 95 | return undefined; 96 | } 97 | throw Error( 98 | `[vite] The "${key}" property of ModuleInfo is not supported.` 99 | ); 100 | }, 101 | }; 102 | 103 | const EMPTY_OBJECT = Object.freeze({}); 104 | 105 | function getModuleInfo(id: string) { 106 | const module = moduleGraph?.getModuleById(id); 107 | if (!module) { 108 | return null; 109 | } 110 | if (!module.info) { 111 | module.info = new Proxy( 112 | { id, meta: module.meta || EMPTY_OBJECT } as ModuleInfo, 113 | ModuleInfoProxy 114 | ); 115 | } 116 | return module.info; 117 | } 118 | 119 | function updateModuleInfo(id: string, { meta }: { meta?: object | null }) { 120 | if (meta) { 121 | const moduleInfo = getModuleInfo(id); 122 | if (moduleInfo) { 123 | moduleInfo.meta = { ...moduleInfo.meta, ...meta }; 124 | } 125 | } 126 | } 127 | 128 | class Context implements PluginContext { 129 | meta = minimalContext.meta; 130 | _scan = false; 131 | _activePlugin: Plugin | null; 132 | _activeId: string | null = null; 133 | _activeCode: string | null = null; 134 | _resolveSkips?: Set; 135 | _addedImports: Set | null = null; 136 | 137 | constructor(initialPlugin?: Plugin) { 138 | this._activePlugin = initialPlugin || null; 139 | } 140 | 141 | parse() { 142 | return {} as any; 143 | } 144 | 145 | async resolve( 146 | id: string, 147 | importer?: string, 148 | options?: { 149 | assertions?: Record; 150 | custom?: CustomPluginOptions; 151 | isEntry?: boolean; 152 | skipSelf?: boolean; 153 | } 154 | ) { 155 | let skip: Set | undefined; 156 | if (options?.skipSelf && this._activePlugin) { 157 | skip = new Set(this._resolveSkips); 158 | skip.add(this._activePlugin); 159 | } 160 | let out = await container.resolveId(id, importer, { 161 | assertions: options?.assertions, 162 | custom: options?.custom, 163 | isEntry: !!options?.isEntry, 164 | skip, 165 | scan: this._scan, 166 | }); 167 | if (typeof out === "string") out = { id: out }; 168 | return out as ResolvedId | null; 169 | } 170 | 171 | async load( 172 | options: { 173 | id: string; 174 | resolveDependencies?: boolean; 175 | } & Partial> 176 | ): Promise { 177 | await moduleGraph?.ensureEntryFromUrl(options.id); 178 | updateModuleInfo(options.id, options); 179 | 180 | await container.load(options.id); 181 | const moduleInfo = this.getModuleInfo(options.id); 182 | if (!moduleInfo) 183 | throw Error(`Failed to load module with id ${options.id}`); 184 | return moduleInfo; 185 | } 186 | 187 | getModuleInfo(id: string) { 188 | return {} as any; 189 | } 190 | 191 | getModuleIds() { 192 | return {} as any; 193 | } 194 | 195 | addWatchFile() { 196 | return {} as any; 197 | } 198 | 199 | getWatchFiles() { 200 | return {} as any; 201 | } 202 | 203 | emitFile() { 204 | return ""; 205 | } 206 | 207 | setAssetSource() {} 208 | 209 | getFileName() { 210 | return ""; 211 | } 212 | warn() {} 213 | 214 | error() { 215 | return {} as never; 216 | } 217 | } 218 | 219 | class TransformContext extends Context { 220 | filename: string; 221 | originalCode: string; 222 | originalSourcemap: SourceMap | null = null; 223 | sourcemapChain: NonNullable[] = []; 224 | combinedMap: SourceMap | null = null; 225 | 226 | constructor(filename: string, code: string, inMap?: SourceMap | string) { 227 | super(); 228 | this.filename = filename; 229 | this.originalCode = code; 230 | if (inMap) { 231 | this.sourcemapChain.push(inMap); 232 | } 233 | } 234 | 235 | _getCombinedSourcemap(createIfNull = false) { 236 | let combinedMap = this.combinedMap; 237 | for (let m of this.sourcemapChain) { 238 | if (typeof m === "string") m = JSON.parse(m); 239 | if (!("version" in (m as SourceMap))) { 240 | combinedMap = this.combinedMap = null; 241 | this.sourcemapChain.length = 0; 242 | break; 243 | } 244 | if (!combinedMap) { 245 | combinedMap = m as SourceMap; 246 | } 247 | } 248 | if (!combinedMap) { 249 | return null; 250 | } 251 | if (combinedMap !== this.combinedMap) { 252 | this.combinedMap = combinedMap; 253 | this.sourcemapChain.length = 0; 254 | } 255 | return this.combinedMap; 256 | } 257 | } 258 | 259 | const container: PluginContainer = { 260 | options: await (async () => { 261 | let options = rollupOptions; 262 | for (const optionsHook of getSortedPluginHooks("options")) { 263 | options = (await optionsHook.call(minimalContext, options)) || options; 264 | } 265 | return { 266 | acorn, 267 | ...options, 268 | }; 269 | })(), 270 | 271 | getModuleInfo, 272 | 273 | async buildStart() { 274 | await hookParallel( 275 | "buildStart", 276 | (plugin) => new Context(plugin), 277 | () => [container.options as NormalizedInputOptions] 278 | ); 279 | }, 280 | 281 | async resolveId(rawId, importer = join(root, "index.html"), options) { 282 | const skip = options?.skip; 283 | const scan = !!options?.scan; 284 | const ctx = new Context(); 285 | ctx._scan = scan; 286 | ctx._resolveSkips = skip; 287 | 288 | let id: string | null = null; 289 | const partial: Partial = {}; 290 | for (const plugin of getSortedPlugins("resolveId")) { 291 | if (!plugin.resolveId) continue; 292 | if (skip?.has(plugin)) continue; 293 | 294 | ctx._activePlugin = plugin; 295 | 296 | const pluginResolveStart = debugPluginResolve ? performance.now() : 0; 297 | const handler = 298 | "handler" in plugin.resolveId 299 | ? plugin.resolveId.handler 300 | : plugin.resolveId; 301 | const result = await handler.call(ctx as any, rawId, importer, { 302 | assertions: options?.assertions ?? {}, 303 | custom: options?.custom, 304 | isEntry: !!options?.isEntry, 305 | scan, 306 | }); 307 | if (!result) continue; 308 | 309 | if (typeof result === "string") { 310 | id = result; 311 | } else { 312 | id = result.id; 313 | Object.assign(partial, result); 314 | } 315 | 316 | debugPluginResolve?.( 317 | timeFrom(pluginResolveStart), 318 | plugin.name, 319 | prettifyUrl(id, root) 320 | ); 321 | 322 | break; 323 | } 324 | 325 | if (id) { 326 | partial.id = isExternalUrl(id) ? id : normalizePath(id); 327 | return partial as PartialResolvedId; 328 | } else { 329 | return null; 330 | } 331 | }, 332 | 333 | async load(id) { 334 | const ctx = new Context(); 335 | for (const plugin of getSortedPlugins("load")) { 336 | if (!plugin.load) continue; 337 | ctx._activePlugin = plugin; 338 | const handler = 339 | "handler" in plugin.load ? plugin.load.handler : plugin.load; 340 | const result = await handler.call(ctx as any, id); 341 | if (result != null) { 342 | if (isObject(result)) { 343 | updateModuleInfo(id, result); 344 | } 345 | return result; 346 | } 347 | } 348 | return null; 349 | }, 350 | 351 | async transform(code, id, options) { 352 | const inMap = options?.inMap; 353 | const ctx = new TransformContext(id, code, inMap as SourceMap); 354 | for (const plugin of getSortedPlugins("transform")) { 355 | if (!plugin.transform) continue; 356 | ctx._activePlugin = plugin; 357 | ctx._activeId = id; 358 | ctx._activeCode = code; 359 | let result: TransformResult | string | undefined; 360 | const handler = 361 | "handler" in plugin.transform 362 | ? plugin.transform.handler 363 | : plugin.transform; 364 | try { 365 | result = await handler.call(ctx as any, code, id); 366 | } catch (e) {} 367 | 368 | if (!result) continue; 369 | if (isObject(result)) { 370 | if (result.code !== undefined) { 371 | code = result.code; 372 | if (result.map) { 373 | ctx.sourcemapChain.push(result.map); 374 | } 375 | } 376 | updateModuleInfo(id, result); 377 | } else { 378 | code = result; 379 | } 380 | } 381 | return { 382 | code, 383 | map: ctx._getCombinedSourcemap(), 384 | }; 385 | }, 386 | 387 | async close() {}, 388 | }; 389 | 390 | return container; 391 | 392 | async function hookParallel( 393 | hookName: H, 394 | context: (plugin: Plugin) => ThisType, 395 | args: (plugin: Plugin) => Parameters 396 | ): Promise { 397 | const parallelPromises: Promise[] = []; 398 | for (const plugin of getSortedPlugins(hookName)) { 399 | const hook = plugin[hookName]; 400 | if (!hook) continue; 401 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 402 | // @ts-ignore hook is not a primitive 403 | const handler: Function = "handler" in hook ? hook.handler : hook; 404 | if ((hook as { sequential?: boolean }).sequential) { 405 | await Promise.all(parallelPromises); 406 | parallelPromises.length = 0; 407 | await handler.apply(context(plugin), args(plugin)); 408 | } else { 409 | parallelPromises.push(handler.apply(context(plugin), args(plugin))); 410 | } 411 | } 412 | await Promise.all(parallelPromises); 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /packages/vite/src/node/plugins/asset.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { ResolvedConfig } from "../config"; 3 | import { 4 | cleanUrl, 5 | joinUrlSegments, 6 | removeLeadingSlash, 7 | } from "../utils"; 8 | import type { Plugin } from "rollup"; 9 | 10 | export interface GeneratedAssetMeta { 11 | originalName: string; 12 | isEntry?: boolean; 13 | } 14 | 15 | export const generatedAssets = new WeakMap< 16 | ResolvedConfig, 17 | Map 18 | >(); 19 | 20 | export const publicAssetUrlCache = new WeakMap< 21 | ResolvedConfig, 22 | Map 23 | >(); 24 | const assetCache = new WeakMap>(); 25 | 26 | const urlRE = /(\?|&)url(?:&|$)/; 27 | const unnededFinalQueryCharRE = /[?&]$/; 28 | 29 | export async function fileToUrl( 30 | id: string, 31 | config: ResolvedConfig, 32 | ): Promise { 33 | return fileToDevUrl(id, config); 34 | } 35 | 36 | function fileToDevUrl(id: string, config: ResolvedConfig) { 37 | let rtn: string; 38 | rtn = "/" + path.posix.relative(config.root, id); 39 | const base = joinUrlSegments(config.server?.origin ?? "", config.base); 40 | return joinUrlSegments(base, removeLeadingSlash(rtn)); 41 | } 42 | 43 | export function assetPlugin(config: ResolvedConfig): Plugin { 44 | return { 45 | name: "vite:asset", 46 | 47 | buildStart() { 48 | assetCache.set(config, new Map()); 49 | generatedAssets.set(config, new Map()); 50 | }, 51 | 52 | resolveId(id) { 53 | if (!config.assetsInclude(cleanUrl(id))) { 54 | return; 55 | } 56 | }, 57 | 58 | async load(id) { 59 | if (id[0] === "\0") { 60 | return; 61 | } 62 | 63 | if (!config.assetsInclude(cleanUrl(id)) && !urlRE.test(id)) { 64 | return; 65 | } 66 | 67 | id = id.replace(urlRE, "$1").replace(unnededFinalQueryCharRE, ""); 68 | const url = await fileToUrl(id, config); 69 | return `export default ${JSON.stringify(url)}`; 70 | }, 71 | 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /packages/vite/src/node/plugins/clientInjections.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { Plugin } from "./../plugin"; 3 | import { ResolvedConfig } from "../config"; 4 | import { isObject, normalizePath, resolveHostname } from "../utils"; 5 | import { CLIENT_ENTRY, ENV_ENTRY } from "../constants"; 6 | 7 | const process_env_NODE_ENV_RE = 8 | /(\bglobal(This)?\.)?\bprocess\.env\.NODE_ENV\b/g; 9 | 10 | const normalizedClientEntry = normalizePath(CLIENT_ENTRY); 11 | const normalizedEnvEntry = normalizePath(ENV_ENTRY); 12 | 13 | export function clientInjectionsPlugin(config: ResolvedConfig): Plugin { 14 | let injectConfigValues: (code: string) => string; 15 | 16 | return { 17 | name: "vite:client-inject", 18 | async buildStart() { 19 | const resolvedServerHostname = (await resolveHostname(config.server.host)) 20 | .name; 21 | const resolvedServerPort = config.server.port!; 22 | const devBase = config.base; 23 | 24 | const serverHost = `${resolvedServerHostname}:${resolvedServerPort}${devBase}`; 25 | 26 | let hmrConfig = config.server.hmr; 27 | hmrConfig = isObject(hmrConfig) ? hmrConfig : undefined; 28 | const host = hmrConfig?.host || null; 29 | const protocol = hmrConfig?.protocol || null; 30 | const timeout = hmrConfig?.timeout || 30000; 31 | const overlay = hmrConfig?.overlay !== false; 32 | const isHmrServerSpecified = !!hmrConfig?.server; 33 | 34 | let port = hmrConfig?.clientPort || hmrConfig?.port || null; 35 | if (config.server.middlewareMode && !isHmrServerSpecified) { 36 | port ||= 24678; 37 | } 38 | 39 | let directTarget = hmrConfig?.host || resolvedServerHostname; 40 | directTarget += `:${hmrConfig?.port || resolvedServerPort}`; 41 | directTarget += devBase; 42 | 43 | let hmrBase = devBase; 44 | if (hmrConfig?.path) { 45 | hmrBase = path.posix.join(hmrBase, hmrConfig.path); 46 | } 47 | 48 | const serializedDefines = serializeDefine(config.define || {}); 49 | 50 | const modeReplacement = escapeReplacement(config.mode); 51 | const baseReplacement = escapeReplacement(devBase); 52 | const definesReplacement = () => serializedDefines; 53 | const serverHostReplacement = escapeReplacement(serverHost); 54 | const hmrProtocolReplacement = escapeReplacement(protocol); 55 | const hmrHostnameReplacement = escapeReplacement(host); 56 | const hmrPortReplacement = escapeReplacement(port); 57 | const hmrDirectTargetReplacement = escapeReplacement(directTarget); 58 | const hmrBaseReplacement = escapeReplacement(hmrBase); 59 | const hmrTimeoutReplacement = escapeReplacement(timeout); 60 | const hmrEnableOverlayReplacement = escapeReplacement(overlay); 61 | 62 | injectConfigValues = (code: string) => { 63 | return code 64 | .replace(`__MODE__`, modeReplacement) 65 | .replace(/__BASE__/g, baseReplacement) 66 | .replace(`__DEFINES__`, definesReplacement) 67 | .replace(`__SERVER_HOST__`, serverHostReplacement) 68 | .replace(`__HMR_PROTOCOL__`, hmrProtocolReplacement) 69 | .replace(`__HMR_HOSTNAME__`, hmrHostnameReplacement) 70 | .replace(`__HMR_PORT__`, hmrPortReplacement) 71 | .replace(`__HMR_DIRECT_TARGET__`, hmrDirectTargetReplacement) 72 | .replace(`__HMR_BASE__`, hmrBaseReplacement) 73 | .replace(`__HMR_TIMEOUT__`, hmrTimeoutReplacement) 74 | .replace(`__HMR_ENABLE_OVERLAY__`, hmrEnableOverlayReplacement); 75 | }; 76 | }, 77 | transform(code, id) { 78 | if (id === normalizedClientEntry || id === normalizedEnvEntry) { 79 | return injectConfigValues(code); 80 | } else if (code.includes("process.env.NODE_ENV")) { 81 | return code.replace( 82 | process_env_NODE_ENV_RE, 83 | config.define?.["process.env.NODE_ENV"] || 84 | JSON.stringify(process.env.NODE_ENV || config.mode) 85 | ); 86 | } 87 | }, 88 | }; 89 | } 90 | 91 | function escapeReplacement(value: string | number | boolean | null) { 92 | const jsonValue = JSON.stringify(value); 93 | return () => jsonValue; 94 | } 95 | 96 | function serializeDefine(define: Record): string { 97 | let res = `{`; 98 | for (const key in define) { 99 | const val = define[key]; 100 | res += `${JSON.stringify(key)}: ${ 101 | typeof val === "string" ? `(${val})` : JSON.stringify(val) 102 | }, `; 103 | } 104 | return res + `}`; 105 | } 106 | -------------------------------------------------------------------------------- /packages/vite/src/node/plugins/css.ts: -------------------------------------------------------------------------------- 1 | import { ResolvedConfig } from "../config"; 2 | import { 3 | CLIENT_PUBLIC_PATH, 4 | CSS_LANGS_RE, 5 | SPECIAL_QUERY_RE, 6 | } from "../constants"; 7 | import { Plugin } from "../plugin"; 8 | import path from "node:path"; 9 | import type { RenderedChunk } from "rollup"; 10 | import { stripBomTag } from "../utils"; 11 | import type { ViteDevServer } from "../"; 12 | import { dataToEsm } from "@rollup/pluginutils"; 13 | 14 | const cssModulesCache = new WeakMap< 15 | ResolvedConfig, 16 | Map> 17 | >(); 18 | 19 | export const removedPureCssFilesCache = new WeakMap< 20 | ResolvedConfig, 21 | Map 22 | >(); 23 | 24 | const cssModuleRE = new RegExp(`\\.module${CSS_LANGS_RE.source}`); 25 | const directRequestRE = /(?:\?|&)direct\b/; 26 | const htmlProxyRE = /(?:\?|&)html-proxy\b/; 27 | const commonjsProxyRE = /\?commonjs-proxy/; 28 | const inlineRE = /(?:\?|&)inline\b/; 29 | 30 | export const isCSSRequest = (request: string): boolean => 31 | CSS_LANGS_RE.test(request); 32 | 33 | export const isDirectCSSRequest = (request: string): boolean => 34 | CSS_LANGS_RE.test(request) && directRequestRE.test(request); 35 | 36 | export function cssPlugin(config: ResolvedConfig): Plugin { 37 | let server: ViteDevServer; 38 | let moduleCache: Map>; 39 | 40 | return { 41 | name: "vite:css", 42 | 43 | configureServer(_server) { 44 | server = _server; 45 | }, 46 | 47 | buildStart() { 48 | moduleCache = new Map>(); 49 | cssModulesCache.set(config, moduleCache); 50 | 51 | removedPureCssFilesCache.set(config, new Map()); 52 | }, 53 | 54 | async transform(raw, id) { 55 | if ( 56 | !isCSSRequest(id) || 57 | commonjsProxyRE.test(id) || 58 | SPECIAL_QUERY_RE.test(id) 59 | ) { 60 | return; 61 | } 62 | 63 | if (server) { 64 | const { moduleGraph } = server; 65 | const thisModule = moduleGraph.getModuleById(id); 66 | if (thisModule) { 67 | const isSelfAccepting = !inlineRE.test(id) && !htmlProxyRE.test(id); 68 | thisModule.isSelfAccepting = isSelfAccepting; 69 | } 70 | } 71 | 72 | return { 73 | code: raw, 74 | map: null, 75 | }; 76 | }, 77 | }; 78 | } 79 | 80 | export const isDirectRequest = (request: string): boolean => 81 | directRequestRE.test(request); 82 | 83 | export const isModuleCSSRequest = (request: string): boolean => 84 | cssModuleRE.test(request); 85 | 86 | export function cssPostPlugin(config: ResolvedConfig): Plugin { 87 | return { 88 | name: "vite:css-post", 89 | async transform(css, id) { 90 | if ( 91 | !isCSSRequest(id) || 92 | commonjsProxyRE.test(id) || 93 | SPECIAL_QUERY_RE.test(id) 94 | ) { 95 | return; 96 | } 97 | 98 | css = stripBomTag(css); 99 | 100 | const inlined = inlineRE.test(id); 101 | const modules = cssModulesCache.get(config)!.get(id); 102 | 103 | const modulesCode = 104 | modules && 105 | !inlined && 106 | dataToEsm(modules, { namedExports: true, preferConst: true }); 107 | 108 | if (config.command === "serve") { 109 | const getContentWithSourcemap = async (content: string) => { 110 | return content; 111 | }; 112 | 113 | if (isDirectCSSRequest(id)) { 114 | return null; 115 | } 116 | if (inlined) { 117 | return `export default ${JSON.stringify(css)}`; 118 | } 119 | 120 | const cssContent = await getContentWithSourcemap(css); 121 | const code = [ 122 | `import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify( 123 | path.posix.join(config.base, CLIENT_PUBLIC_PATH) 124 | )}`, 125 | `const __vite__id = ${JSON.stringify(id)}`, 126 | `const __vite__css = ${JSON.stringify(cssContent)}`, 127 | `__vite__updateStyle(__vite__id, __vite__css)`, 128 | `${ 129 | modulesCode || 130 | `import.meta.hot.accept()\nexport default __vite__css` 131 | }`, 132 | `import.meta.hot.prune(() => __vite__removeStyle(__vite__id))`, 133 | ].join("\n"); 134 | return { code, map: { mappings: "" } }; 135 | } 136 | }, 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /packages/vite/src/node/plugins/esbuild.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import colors from "picocolors"; 3 | import { ResolvedConfig } from "../config"; 4 | import type { Plugin } from "../plugin"; 5 | import type { 6 | Loader, 7 | Message, 8 | TransformOptions, 9 | TransformResult, 10 | } from "esbuild"; 11 | import { 12 | cleanUrl, 13 | createFilter, 14 | generateCodeFrame, 15 | } from "../utils"; 16 | import { ViteDevServer } from "../server"; 17 | import type { TSConfckParseOptions } from "tsconfck"; 18 | import type { SourceMap } from "rollup"; 19 | import { parse } from "tsconfck"; 20 | import { transform } from "esbuild"; 21 | 22 | const validExtensionRE = /\.\w+$/; 23 | const jsxExtensionsRE = /\.(?:j|t)sx\b/; 24 | 25 | export interface ESBuildOptions extends TransformOptions { 26 | include?: string | RegExp | string[] | RegExp[]; 27 | exclude?: string | RegExp | string[] | RegExp[]; 28 | jsxInject?: string; 29 | minify?: never; 30 | } 31 | 32 | export type ESBuildTransformResult = Omit & { 33 | map: SourceMap; 34 | }; 35 | 36 | type TSConfigJSON = { 37 | extends?: string; 38 | compilerOptions?: { 39 | alwaysStrict?: boolean; 40 | importsNotUsedAsValues?: "remove" | "preserve" | "error"; 41 | jsx?: "preserve" | "react" | "react-jsx" | "react-jsxdev"; 42 | jsxFactory?: string; 43 | jsxFragmentFactory?: string; 44 | jsxImportSource?: string; 45 | preserveValueImports?: boolean; 46 | target?: string; 47 | useDefineForClassFields?: boolean; 48 | }; 49 | [key: string]: any; 50 | }; 51 | 52 | type TSCompilerOptions = NonNullable; 53 | 54 | let tsconfckParseOptions: TSConfckParseOptions | Promise = 55 | { resolveWithEmptyIfConfigNotFound: true }; 56 | // @ts-ignore 57 | let server: ViteDevServer; 58 | export function esbuildPlugin(config: ResolvedConfig): Plugin { 59 | const options = config.esbuild as ESBuildOptions; 60 | const { jsxInject, include, exclude, ...esbuildTransformOptions } = options; 61 | 62 | const filter = createFilter( 63 | include || /\.(m?ts|[jt]sx)$/, 64 | exclude || /\.js$/ 65 | ); 66 | 67 | const transformOptions: TransformOptions = { 68 | target: "esnext", 69 | charset: "utf8", 70 | ...esbuildTransformOptions, 71 | minify: false, 72 | minifyIdentifiers: false, 73 | minifySyntax: false, 74 | minifyWhitespace: false, 75 | treeShaking: false, 76 | keepNames: false, 77 | }; 78 | 79 | return { 80 | name: "vite:esbuild", 81 | configureServer(_server) {}, 82 | buildEnd() { 83 | server = null as any; 84 | }, 85 | async transform(code, id) { 86 | if (filter(id) || filter(cleanUrl(id))) { 87 | const result = await transformWithEsbuild(code, id, transformOptions); 88 | if (result.warnings.length) { 89 | result.warnings.forEach((m) => { 90 | this.warn(prettifyMessage(m, code)); 91 | }); 92 | } 93 | if (jsxInject && jsxExtensionsRE.test(id)) { 94 | result.code = jsxInject + ";" + result.code; 95 | } 96 | return { 97 | code: result.code, 98 | map: result.map, 99 | }; 100 | } 101 | }, 102 | }; 103 | } 104 | /**使用 esbuild 将 JavaScript 或 TypeScript 模块进行转换*/ 105 | export async function transformWithEsbuild( 106 | code: string, 107 | filename: string, 108 | options?: TransformOptions 109 | ): Promise { 110 | let loader = options?.loader; 111 | 112 | if (!loader) { 113 | const ext = path 114 | .extname(validExtensionRE.test(filename) ? filename : cleanUrl(filename)) 115 | .slice(1); 116 | loader = ext as Loader; 117 | } 118 | 119 | let tsconfigRaw = options?.tsconfigRaw; 120 | 121 | if (typeof tsconfigRaw !== "string") { 122 | const meaningfulFields: Array = [ 123 | "alwaysStrict", 124 | "importsNotUsedAsValues", 125 | "jsx", 126 | "jsxFactory", 127 | "jsxFragmentFactory", 128 | "jsxImportSource", 129 | "preserveValueImports", 130 | "target", 131 | "useDefineForClassFields", 132 | ]; 133 | const compilerOptionsForFile: TSCompilerOptions = {}; 134 | // 判断处理文件是否是ts文件,是ts文件的话就读取tsconifg.json 135 | // 中的compilerOptions配置 136 | if (loader === "ts" || loader === "tsx") { 137 | const loadedTsconfig = await loadTsconfigJsonForFile(filename); 138 | const loadedCompilerOptions = loadedTsconfig.compilerOptions ?? {}; 139 | 140 | for (const field of meaningfulFields) { 141 | if (field in loadedCompilerOptions) { 142 | // @ts-expect-error TypeScript can't tell they are of the same type 143 | compilerOptionsForFile[field] = loadedCompilerOptions[field]; 144 | } 145 | } 146 | } 147 | 148 | const compilerOptions = { 149 | ...compilerOptionsForFile, 150 | ...tsconfigRaw?.compilerOptions, 151 | }; 152 | // config.build中的配置项存在时,就对compilerOptions 153 | // 中的配置项进行重置 154 | if (options) { 155 | options.jsx && (compilerOptions.jsx = undefined); 156 | options.jsxFactory && (compilerOptions.jsxFactory = undefined); 157 | options.jsxFragment && (compilerOptions.jsxFragmentFactory = undefined); 158 | options.jsxImportSource && (compilerOptions.jsxImportSource = undefined); 159 | options.target && (compilerOptions.target = undefined); 160 | } 161 | 162 | tsconfigRaw = { 163 | ...tsconfigRaw, 164 | compilerOptions, 165 | }; 166 | } 167 | 168 | const resolvedOptions = { 169 | sourcemap: true, 170 | sourcefile: filename, 171 | ...options, 172 | loader, 173 | tsconfigRaw, 174 | } as ESBuildOptions; 175 | 176 | delete resolvedOptions.include; 177 | delete resolvedOptions.exclude; 178 | delete resolvedOptions.jsxInject; 179 | 180 | try { 181 | const result = await transform(code, resolvedOptions); 182 | let map: SourceMap; 183 | map = 184 | resolvedOptions.sourcemap && resolvedOptions.sourcemap !== "inline" 185 | ? JSON.parse(result.map) 186 | : { mappings: "" }; 187 | return { 188 | ...result, 189 | map, 190 | }; 191 | } catch (e: any) { 192 | throw new Error("transformWithEsbuild failed"); 193 | } 194 | } 195 | /**打印警告信息 */ 196 | function prettifyMessage(m: Message, code: string): string { 197 | let res = colors.yellow(m.text); 198 | if (m.location) { 199 | const lines = code.split(/\r?\n/g); 200 | const line = Number(m.location.line); 201 | const column = Number(m.location.column); 202 | const offset = 203 | lines 204 | .slice(0, line - 1) 205 | .map((l) => l.length) 206 | .reduce((total, l) => total + l + 1, 0) + column; 207 | res += `\n` + generateCodeFrame(code, offset, offset + 1); 208 | } 209 | return res + `\n`; 210 | } 211 | /**根据给定的文件路径,查找并加载与该文件相关联的 tsconfig.json 文件内容。 */ 212 | async function loadTsconfigJsonForFile( 213 | filename: string 214 | ): Promise { 215 | try { 216 | const result = await parse(filename, await tsconfckParseOptions); 217 | return result.tsconfig; 218 | } catch (e) { 219 | throw e; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /packages/vite/src/node/plugins/html.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HtmlTagDescriptor, 3 | IndexHtmlTransformContext, 4 | IndexHtmlTransformHook, 5 | } from "vite"; 6 | import { Plugin } from "../plugin"; 7 | import { generateCodeFrame } from "../utils"; 8 | import { RollupError } from "rollup"; 9 | import { DefaultTreeAdapterMap, ParserError, Token } from "parse5"; 10 | import MagicString from "magic-string"; 11 | 12 | const headInjectRE = /([ \t]*)<\/head>/i; 13 | const headPrependInjectRE = /([ \t]*)]*>/i; 14 | 15 | const htmlInjectRE = /<\/html>/i; 16 | const htmlPrependInjectRE = /([ \t]*)]*>/i; 17 | 18 | const bodyInjectRE = /([ \t]*)<\/body>/i; 19 | const bodyPrependInjectRE = /([ \t]*)]*>/i; 20 | 21 | const doctypePrependInjectRE = //i; 22 | 23 | const unaryTags = new Set(["link", "meta", "base"]); 24 | 25 | const htmlProxyRE = /\?html-proxy=?(?:&inline-css)?&index=(\d+)\.(js|css)$/; 26 | 27 | /**获取plugin处理index.html的hook,并对hooks进行排序 */ 28 | export function resolveHtmlTransforms( 29 | plugins: readonly Plugin[] 30 | ): [ 31 | IndexHtmlTransformHook[], 32 | IndexHtmlTransformHook[], 33 | IndexHtmlTransformHook[] 34 | ] { 35 | const preHooks: IndexHtmlTransformHook[] = []; 36 | const normalHooks: IndexHtmlTransformHook[] = []; 37 | const postHooks: IndexHtmlTransformHook[] = []; 38 | 39 | for (const plugin of plugins) { 40 | const hook = plugin.transformIndexHtml; 41 | if (!hook) continue; 42 | 43 | if (typeof hook === "function") { 44 | normalHooks.push(hook); 45 | } else { 46 | const order = hook.order ?? (hook.enforce === "pre" ? "pre" : undefined); 47 | // @ts-expect-error union type 48 | const handler = hook.handler ?? hook.transform; 49 | if (order === "pre") { 50 | preHooks.push(handler); 51 | } else if (order === "post") { 52 | postHooks.push(handler); 53 | } else { 54 | normalHooks.push(handler); 55 | } 56 | } 57 | } 58 | 59 | return [preHooks, normalHooks, postHooks]; 60 | } 61 | /** 62 | * 遍历并执行tranformRequest中的hooks 63 | * 得到@vite/client的路径和index.html的内容 64 | * 并将其注入到head和body中 65 | */ 66 | export async function applyHtmlTransforms( 67 | html: string, 68 | hooks: IndexHtmlTransformHook[], 69 | ctx: IndexHtmlTransformContext 70 | ): Promise { 71 | for (const hook of hooks) { 72 | // 执行hook之后得到index.html的内容和需要注入到index.html中的标签(tags) 73 | // tags中有需要注入的@vite/client 74 | const res = await hook(html, ctx); 75 | if (!res) { 76 | continue; 77 | } 78 | if (typeof res === "string") { 79 | html = res; 80 | } else { 81 | let tags: HtmlTagDescriptor[]; 82 | if (Array.isArray(res)) { 83 | tags = res; 84 | } else { 85 | html = res.html || html; 86 | tags = res.tags; 87 | } 88 | 89 | const headTags: HtmlTagDescriptor[] = []; 90 | const headPrependTags: HtmlTagDescriptor[] = []; 91 | const bodyTags: HtmlTagDescriptor[] = []; 92 | const bodyPrependTags: HtmlTagDescriptor[] = []; 93 | 94 | for (const tag of tags) { 95 | if (tag.injectTo === "body") { 96 | bodyTags.push(tag); 97 | } else if (tag.injectTo === "body-prepend") { 98 | bodyPrependTags.push(tag); 99 | } else if (tag.injectTo === "head") { 100 | headTags.push(tag); 101 | } else { 102 | headPrependTags.push(tag); 103 | } 104 | } 105 | 106 | html = injectToHead(html, headPrependTags, true); 107 | html = injectToHead(html, headTags); 108 | html = injectToBody(html, bodyPrependTags, true); 109 | html = injectToBody(html, bodyTags); 110 | } 111 | } 112 | 113 | return html; 114 | } 115 | /** 116 | * const tags = [{ tag: 'script', attrs: { src: 'main.js' } ] 117 | * 将tags中的内容转换成html字符串 118 | * */ 119 | function serializeTags( 120 | tags: HtmlTagDescriptor["children"], 121 | indent: string = "" 122 | ): string { 123 | if (typeof tags === "string") { 124 | return tags; 125 | } else if (tags && tags.length) { 126 | return tags 127 | .map((tag) => `${indent}${serializeTag(tag, indent)}\n`) 128 | .join(""); 129 | } 130 | return ""; 131 | } 132 | /** 133 | * const attrs= { id: 'my-element', class: 'my-class' } 134 | * 将attrs中的内容转换成html字符串 135 | */ 136 | function serializeAttrs(attrs: HtmlTagDescriptor["attrs"]): string { 137 | let res = ""; 138 | for (const key in attrs) { 139 | if (typeof attrs[key] === "boolean") { 140 | res += attrs[key] ? ` ${key}` : ``; 141 | } else { 142 | res += ` ${key}=${JSON.stringify(attrs[key])}`; 143 | } 144 | } 145 | return res; 146 | } 147 | /**给字符串增加缩进 */ 148 | function incrementIndent(indent: string = "") { 149 | return `${indent}${indent[0] === "\t" ? "\t" : " "}`; 150 | } 151 | /**将ast转换成html字符串 */ 152 | function serializeTag( 153 | { tag, attrs, children }: HtmlTagDescriptor, 154 | indent: string = "" 155 | ): string { 156 | if (unaryTags.has(tag)) { 157 | return `<${tag}${serializeAttrs(attrs)}>`; 158 | } else { 159 | return `<${tag}${serializeAttrs(attrs)}>${serializeTags( 160 | children, 161 | incrementIndent(indent) 162 | )}`; 163 | } 164 | } 165 | /**指定的内容注入到head标签 */ 166 | function injectToHead( 167 | html: string, 168 | tags: HtmlTagDescriptor[], 169 | prepend = false 170 | ) { 171 | if (tags.length === 0) return html; 172 | 173 | if (prepend) { 174 | if (headPrependInjectRE.test(html)) { 175 | return html.replace( 176 | headPrependInjectRE, 177 | (match, p1) => `${match}\n${serializeTags(tags, incrementIndent(p1))}` 178 | ); 179 | } 180 | } else { 181 | if (headInjectRE.test(html)) { 182 | return html.replace( 183 | headInjectRE, 184 | (match, p1) => `${serializeTags(tags, incrementIndent(p1))}${match}` 185 | ); 186 | } 187 | if (bodyPrependInjectRE.test(html)) { 188 | return html.replace( 189 | bodyPrependInjectRE, 190 | (match, p1) => `${serializeTags(tags, p1)}\n${match}` 191 | ); 192 | } 193 | } 194 | return prependInjectFallback(html, tags); 195 | } 196 | /**指定的内容注入到body标签 */ 197 | function injectToBody( 198 | html: string, 199 | tags: HtmlTagDescriptor[], 200 | prepend = false 201 | ) { 202 | if (tags.length === 0) return html; 203 | 204 | if (prepend) { 205 | if (bodyPrependInjectRE.test(html)) { 206 | return html.replace( 207 | bodyPrependInjectRE, 208 | (match, p1) => `${match}\n${serializeTags(tags, incrementIndent(p1))}` 209 | ); 210 | } 211 | if (headInjectRE.test(html)) { 212 | return html.replace( 213 | headInjectRE, 214 | (match, p1) => `${match}\n${serializeTags(tags, p1)}` 215 | ); 216 | } 217 | return prependInjectFallback(html, tags); 218 | } else { 219 | if (bodyInjectRE.test(html)) { 220 | return html.replace( 221 | bodyInjectRE, 222 | (match, p1) => `${serializeTags(tags, incrementIndent(p1))}${match}` 223 | ); 224 | } 225 | if (htmlInjectRE.test(html)) { 226 | return html.replace(htmlInjectRE, `${serializeTags(tags)}\n$&`); 227 | } 228 | return html + `\n` + serializeTags(tags); 229 | } 230 | } 231 | /**如果没有head标签的话 */ 232 | function prependInjectFallback(html: string, tags: HtmlTagDescriptor[]) { 233 | if (htmlPrependInjectRE.test(html)) { 234 | return html.replace(htmlPrependInjectRE, `$&\n${serializeTags(tags)}`); 235 | } 236 | if (doctypePrependInjectRE.test(html)) { 237 | return html.replace(doctypePrependInjectRE, `$&\n${serializeTags(tags)}`); 238 | } 239 | return serializeTags(tags) + html; 240 | } 241 | /**将html文件转换成ast */ 242 | export async function traverseHtml( 243 | html: string, 244 | filePath: string, 245 | visitor: (node: DefaultTreeAdapterMap["node"]) => void 246 | ): Promise { 247 | const { parse } = await import("parse5"); 248 | const ast = parse(html, { 249 | scriptingEnabled: false, 250 | sourceCodeLocationInfo: true, 251 | onParseError: (e: ParserError) => { 252 | handleParseError(e, html, filePath); 253 | }, 254 | }); 255 | traverseNodes(ast, visitor); 256 | } 257 | /**捕获ast转换失败的错误,语法错误或无效的语法结构,就会抛出错误 */ 258 | function handleParseError( 259 | parserError: ParserError, 260 | html: string, 261 | filePath: string 262 | ) { 263 | switch (parserError.code) { 264 | case "missing-doctype": 265 | return; 266 | case "abandoned-head-element-child": 267 | return; 268 | case "duplicate-attribute": 269 | return; 270 | case "non-void-html-element-start-tag-with-trailing-solidus": 271 | return; 272 | } 273 | const parseError = formatParseError(parserError, filePath, html); 274 | throw new Error( 275 | `Unable to parse HTML; ${parseError.message}\n` + 276 | // @ts-ignore 277 | ` at ${parseError.loc.file}:${parseError.loc.line}:${parseError.loc.column}\n` + 278 | `${parseError.frame}` 279 | ); 280 | } 281 | /**对document根节点进行处理 */ 282 | function traverseNodes( 283 | node: DefaultTreeAdapterMap["node"], 284 | visitor: (node: DefaultTreeAdapterMap["node"]) => void 285 | ) { 286 | visitor(node); 287 | if ( 288 | nodeIsElement(node) || 289 | node.nodeName === "#document" || 290 | node.nodeName === "#document-fragment" 291 | ) { 292 | node.childNodes.forEach((childNode) => traverseNodes(childNode, visitor)); 293 | } 294 | } 295 | /**将ast解析抛出的错误进行格式化处理,方便定位问题 */ 296 | function formatParseError(parserError: ParserError, id: string, html: string) { 297 | const formattedError = { 298 | code: parserError.code, 299 | message: `parse5 error code ${parserError.code}`, 300 | frame: generateCodeFrame(html, parserError.startOffset), 301 | loc: { 302 | file: id, 303 | line: parserError.startLine, 304 | column: parserError.startCol, 305 | }, 306 | } as RollupError; 307 | return formattedError; 308 | } 309 | 310 | export function nodeIsElement( 311 | node: DefaultTreeAdapterMap["node"] 312 | ): node is DefaultTreeAdapterMap["element"] { 313 | // #text 314 | return node.nodeName[0] !== "#"; 315 | } 316 | /**获取script节点的相关信息 */ 317 | export function getScriptInfo(node: DefaultTreeAdapterMap["element"]): { 318 | src: Token.Attribute | undefined; 319 | sourceCodeLocation: Token.Location | undefined; 320 | isModule: boolean; 321 | isAsync: boolean; 322 | } { 323 | let src: Token.Attribute | undefined; 324 | let sourceCodeLocation: Token.Location | undefined; 325 | let isModule = false; 326 | let isAsync = false; 327 | for (const p of node.attrs) { 328 | if (p.prefix !== undefined) continue; 329 | if (p.name === "src") { 330 | if (!src) { 331 | src = p; 332 | sourceCodeLocation = node.sourceCodeLocation?.attrs!["src"]; 333 | } 334 | } else if (p.name === "type" && p.value && p.value === "module") { 335 | isModule = true; 336 | } else if (p.name === "async") { 337 | isAsync = true; 338 | } 339 | } 340 | return { src, sourceCodeLocation, isModule, isAsync }; 341 | } 342 | 343 | const attrValueStartRE = /=\s*(.)/; 344 | /** 将 标签的 href 属性值替换为新的值 */ 345 | export function overwriteAttrValue( 346 | s: MagicString, 347 | sourceCodeLocation: Token.Location, 348 | newValue: string 349 | ): MagicString { 350 | // 对link标签做切割, srcString ='href="/vite.svg"' 351 | // newValue = "/vite.svg" 352 | const srcString = s.slice( 353 | sourceCodeLocation.startOffset, 354 | sourceCodeLocation.endOffset 355 | ); 356 | const valueStart = srcString.match(attrValueStartRE); 357 | if (!valueStart) { 358 | throw new Error( 359 | `[vite:html] internal error, failed to overwrite attribute value` 360 | ); 361 | } 362 | const wrapOffset = valueStart[1] === '"' || valueStart[1] === "'" ? 1 : 0; 363 | const valueOffset = valueStart.index! + valueStart[0].length - 1; 364 | s.update( 365 | sourceCodeLocation.startOffset + valueOffset + wrapOffset, 366 | sourceCodeLocation.endOffset - wrapOffset, 367 | newValue 368 | ); 369 | return s; 370 | } 371 | 372 | export const assetAttrsConfig: Record = { 373 | link: ["href"], 374 | video: ["src", "poster"], 375 | source: ["src", "srcset"], 376 | img: ["src", "srcset"], 377 | image: ["xlink:href", "href"], 378 | use: ["xlink:href", "href"], 379 | }; 380 | 381 | export function getAttrKey(attr: Token.Attribute): string { 382 | return attr.prefix === undefined ? attr.name : `${attr.prefix}:${attr.name}`; 383 | } 384 | 385 | export const isHTMLProxy = (id: string): boolean => htmlProxyRE.test(id); 386 | 387 | export const htmlProxyResult = new Map(); 388 | -------------------------------------------------------------------------------- /packages/vite/src/node/plugins/importAnalysis.ts: -------------------------------------------------------------------------------- 1 | import { CLIENT_PUBLIC_PATH, FS_PREFIX } from "../constants"; 2 | import { 3 | cleanUrl, 4 | fsPathFromUrl, 5 | injectQuery, 6 | isDataUrl, 7 | isExternalUrl, 8 | isJSRequest, 9 | joinUrlSegments, 10 | removeImportQuery, 11 | stripBase, 12 | stripBomTag, 13 | unwrapId, 14 | wrapId, 15 | transformStableResult, 16 | } from "../utils"; 17 | import path from "node:path"; 18 | import { ViteDevServer } from "../server"; 19 | import { Plugin } from "../plugin"; 20 | import { ImportSpecifier } from "es-module-lexer"; 21 | import MagicString from "magic-string"; 22 | import { ResolvedConfig } from "../config"; 23 | import { lexAcceptedHmrDeps, normalizeHmrUrl } from "../server/hmr"; 24 | import { isCSSRequest, isDirectCSSRequest } from "./css"; 25 | import { init, parse as parseImports } from "es-module-lexer"; 26 | import fs from "node:fs"; 27 | 28 | const skipRE = /\.(?:map|json)(?:$|\?)/; 29 | export const canSkipImportAnalysis = (id: string): boolean => 30 | skipRE.test(id) || isDirectCSSRequest(id); 31 | 32 | interface UrlPosition { 33 | url: string; 34 | start: number; 35 | end: number; 36 | } 37 | 38 | export function importAnalysisPlugin(config: ResolvedConfig): Plugin { 39 | const { root, base } = config; 40 | //clientPublicPath = @vite/client 41 | const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH); 42 | let server: ViteDevServer; 43 | 44 | return { 45 | name: "vite:import-analysis", 46 | 47 | configureServer(_server) { 48 | server = _server; 49 | }, 50 | 51 | async transform(source, importer) { 52 | if (!server) { 53 | return null; 54 | } 55 | 56 | // NOTE 57 | // 在使用 es-module-lexer 之前, 58 | // 必须先调用 init 函数进行初始化,确保解析器正常工作。 59 | // 可用于加载WebAssembly 模块(使用 WebAssembly 字节码编译的二进制文件) 60 | // es-module-lexer 使用 WebAssembly 来解析和分析JavaScript 模块中的 import 和 export 语句。 61 | // 并提取相关的模块信息,如导入的模块路径、导出的标识符等 62 | await init; 63 | let imports!: readonly ImportSpecifier[]; 64 | source = stripBomTag(source); 65 | try { 66 | // 通过es-module-lexer提取导入导出代码信息 67 | [imports, exports] = parseImports(source); 68 | } catch (e: any) { 69 | this.error( 70 | `Failed to parse source for import analysis because the content ` + 71 | `contains invalid JS syntax. ` + 72 | e.idx 73 | ); 74 | } 75 | 76 | const { moduleGraph } = server; 77 | // 从idToModuleMap中找到对应的模块节点 78 | const importerModule = moduleGraph.getModuleById(importer)!; 79 | // 如果没有导入语句且没有添加过导入项 80 | // 则将当前模块的 isSelfAccepting 标志设为 false,并打印相关信息。 81 | if (!imports.length && !(this as any)._addedImports) { 82 | importerModule.isSelfAccepting = false; 83 | return source; 84 | } 85 | 86 | let hasHMR = false; 87 | let isSelfAccepting = false; 88 | // 用于处理源代码的字符串操作 89 | let s: MagicString | undefined; 90 | const str = () => s || (s = new MagicString(source)); 91 | // 用于存储导入的 URL,利用set去重 92 | const importedUrls = new Set(); 93 | let isPartiallySelfAccepting = false; 94 | // 用于存储导入的绑定信息 95 | const importedBindings = null; 96 | // 将 URL 转换为绝对路径 97 | const toAbsoluteUrl = (url: string) => 98 | path.posix.resolve(path.posix.dirname(importerModule.url), url); 99 | // 解析出对应的绝对路径 100 | const normalizeUrl = async ( 101 | url: string, 102 | pos: number, 103 | forceSkipImportAnalysis: boolean = false 104 | ): Promise<[string, string]> => { 105 | // 移除基础路径config.base 106 | url = stripBase(url, base); 107 | 108 | let importerFile = importer; 109 | const resolved = await this.resolve(url, importerFile); 110 | 111 | if (!resolved) { 112 | return this.error(`Failed to resolve import`); 113 | } 114 | // 如果resolved.id以 root + "/" 开头, 115 | // 说明是绝对路径,将 url 更新为相对于 root 的路径 116 | if (resolved.id.startsWith(root + "/")) { 117 | url = resolved.id.slice(root.length); 118 | // 如果resolved.id 在文件系统中存在(使用 fs.existsSync 判断), 119 | // 则将 url 更新为使用 FS_PREFIX 拼接解析结果的 ID 120 | } else if (fs.existsSync(cleanUrl(resolved.id))) { 121 | url = path.posix.join(FS_PREFIX, resolved.id); 122 | } else { 123 | url = resolved.id; 124 | } 125 | 126 | if (url[0] !== "." && url[0] !== "/") { 127 | // 给id加上前缀`/@id/` 128 | url = wrapId(resolved.id); 129 | } 130 | // 判断是否需要在url上添加查询参数`?import` 131 | url = markExplicitImport(url); 132 | try { 133 | // 将当前文件id存储到idToModuleMap中 134 | const depModule = await moduleGraph._ensureEntryFromUrl( 135 | unwrapId(url), 136 | canSkipImportAnalysis(url) || forceSkipImportAnalysis, 137 | resolved 138 | ); 139 | // 模块发生更新时,更新url上的时间戳参数 140 | // 避免浏览器使用旧的缓存内容,确保获取到最新的模块代码 141 | if (depModule.lastHMRTimestamp > 0) { 142 | url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`); 143 | } 144 | } catch (e: any) { 145 | e.pos = pos; 146 | throw e; 147 | } 148 | // 将base和url组合起来 149 | url = joinUrlSegments(base, url); 150 | return [url, resolved.id]; 151 | }; 152 | 153 | const orderedAcceptedUrls = new Array | undefined>( 154 | imports.length 155 | ); 156 | const orderedAcceptedExports = new Array | undefined>( 157 | imports.length 158 | ); 159 | 160 | await Promise.all( 161 | // 文件中所有的import已经被parseImports方法收集到imports里面了 162 | imports.map(async (importSpecifier, index) => { 163 | const { 164 | s: start, 165 | e: end, 166 | d: dynamicIndex, 167 | n: specifier, 168 | } = importSpecifier; 169 | 170 | const rawUrl = source.slice(start, end); 171 | // import.meta是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象。 172 | // 它包含了这个模块的信息,它带有一个null的原型对象。这个对象可以扩展 173 | if (rawUrl === "import.meta") { 174 | const prop = source.slice(end, end + 4); 175 | // 判断是否有热更新需求 176 | if (prop === ".hot") { 177 | hasHMR = true; 178 | const endHot = end + 4 + (source[end + 4] === "?" ? 1 : 0); 179 | // import.meta.accept用于控制模块是否接受热替换的机制。 180 | if (source.slice(endHot, endHot + 7) === ".accept") { 181 | const importAcceptedUrls = (orderedAcceptedUrls[index] = 182 | new Set()); 183 | // 调用lexAcceptedHmrDeps解析得到模块路径和位置 184 | // 同时判断模块是否能热更新 185 | if ( 186 | lexAcceptedHmrDeps( 187 | source, 188 | source.indexOf("(", endHot + 7) + 1, 189 | importAcceptedUrls 190 | ) 191 | ) { 192 | isSelfAccepting = true; 193 | } 194 | } 195 | } 196 | return; 197 | } 198 | 199 | const isDynamicImport = dynamicIndex > -1; 200 | // 是否有imports导入模块路径 201 | if (specifier) { 202 | // 如果导入路径与 clientPublicPath(@vite/client) 相同, 203 | // 直接返回,不进行后续处理,因为@vite/client已经做过处理了 204 | if (specifier === clientPublicPath) { 205 | return; 206 | } 207 | // TODO url为什么要这样解析 208 | const [url] = await normalizeUrl(specifier, start); 209 | 210 | server?.moduleGraph.safeModulesPath.add(fsPathFromUrl(url)); 211 | // 解析得到的 URL 与原始导入路径 specifier 不同, 212 | // 说明进行了重写操作,将原始导入路径替换为规范化后的 URL 213 | if (url !== specifier) { 214 | const rewrittenUrl = JSON.stringify(url); 215 | const s = isDynamicImport ? start : start - 1; 216 | const e = isDynamicImport ? end : end + 1; 217 | str().overwrite(s, e, rewrittenUrl, { 218 | contentOnly: true, 219 | }); 220 | } 221 | 222 | const hmrUrl = unwrapId(stripBase(url, base)); 223 | // 判断是否是外部链接引入的url 224 | const isLocalImport = !isExternalUrl(hmrUrl) && !isDataUrl(hmrUrl); 225 | if (isLocalImport) { 226 | importedUrls.add(hmrUrl); 227 | } 228 | 229 | if ( 230 | !isDynamicImport && 231 | isLocalImport && 232 | config.server.preTransformRequests 233 | ) { 234 | const url = removeImportQuery(hmrUrl); 235 | server.transformRequest(url).catch((e) => { 236 | config.logger.error(e.message); 237 | }); 238 | } 239 | } 240 | }) 241 | ); 242 | // 去重 243 | const acceptedUrls = mergeAcceptedUrls(orderedAcceptedUrls); 244 | const acceptedExports = mergeAcceptedUrls(orderedAcceptedExports); 245 | 246 | if (hasHMR) { 247 | // 注入热更新代码 248 | // import.meta.hot = __vite__createHotContext("/src/Index.vue"); 249 | str().prepend( 250 | `import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` + 251 | `import.meta.hot = __vite__createHotContext(${JSON.stringify( 252 | normalizeHmrUrl(importerModule.url) 253 | )});` 254 | ); 255 | } 256 | // 处理url,并替换原始字符串中对应的部分 257 | const normalizedAcceptedUrls = new Set(); 258 | for (const { url, start, end } of acceptedUrls) { 259 | const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(url)); 260 | normalizedAcceptedUrls.add(normalized); 261 | str().overwrite(start, end, JSON.stringify(normalized), { 262 | contentOnly: true, // 表示仅替换内容部分 263 | }); 264 | } 265 | // TODO css文件在热更新中是怎么被处理的 266 | // CSS 在构建过程中通常会被提取出来, 267 | // 并以独立的方式加载到页面中,而不是通过模块系统进行导入和解析 268 | if (!isCSSRequest(importer)) { 269 | // 更新当前模块的相关信息,包括导入的 URL、导入的绑定、接受的 URL 270 | await moduleGraph.updateModuleInfo( 271 | importerModule, 272 | importedUrls, 273 | importedBindings, 274 | normalizedAcceptedUrls, 275 | isPartiallySelfAccepting ? acceptedExports : null, 276 | isSelfAccepting 277 | ); 278 | } 279 | 280 | if (s) { 281 | return transformStableResult(s); 282 | } else { 283 | return source; 284 | } 285 | }, 286 | }; 287 | } 288 | /**是否是本地导入 */ 289 | export function isExplicitImportRequired(url: string): boolean { 290 | return !isJSRequest(cleanUrl(url)) && !isCSSRequest(url); 291 | } 292 | // NOTE 动态加载是为了解决什么问题 293 | /** 294 | * url = '/src/assets/vue.svg?import' 295 | * 在URL上注入查询参数 "import" ,作为标记 296 | * 判断根据条件是否动态加载 297 | * */ 298 | function markExplicitImport(url: string) { 299 | if (isExplicitImportRequired(url)) { 300 | return injectQuery(url, "import"); 301 | } 302 | return url; 303 | } 304 | 305 | /**url去重 */ 306 | function mergeAcceptedUrls(orderedUrls: Array | undefined>) { 307 | const acceptedUrls = new Set(); 308 | for (const urls of orderedUrls) { 309 | if (!urls) continue; 310 | for (const url of urls) acceptedUrls.add(url); 311 | } 312 | return acceptedUrls; 313 | } 314 | -------------------------------------------------------------------------------- /packages/vite/src/node/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { HookHandler } from "vite"; 2 | import { ResolvedConfig, PluginHookUtils } from "../config"; 3 | import { Plugin } from "../plugin"; 4 | import { clientInjectionsPlugin } from "./clientInjections"; 5 | import { cssPlugin, cssPostPlugin } from "./css"; 6 | import { importAnalysisPlugin } from "./importAnalysis"; 7 | import { resolvePlugin } from "./resolve"; 8 | import { esbuildPlugin } from "./esbuild"; 9 | import { assetPlugin } from "./asset"; 10 | import aliasPlugin from "@rollup/plugin-alias"; 11 | 12 | /**项目中所有的plugin */ 13 | export async function resolvePlugins( 14 | config: ResolvedConfig, 15 | prePlugins: Plugin[], 16 | normalPlugins: Plugin[], 17 | postPlugins: Plugin[] 18 | ): Promise { 19 | const isBuild = config.command === "build"; 20 | 21 | return [ 22 | aliasPlugin({ entries: config.resolve.alias }), 23 | ...prePlugins, 24 | resolvePlugin({ 25 | ...config.resolve, 26 | root: config.root, 27 | isBuild, 28 | packageCache: config.packageCache, 29 | asSrc: true, 30 | }), 31 | cssPlugin(config), 32 | esbuildPlugin(config), 33 | assetPlugin(config), 34 | ...normalPlugins, 35 | cssPostPlugin(config), 36 | ...postPlugins, 37 | clientInjectionsPlugin(config), 38 | importAnalysisPlugin(config), 39 | ].filter(Boolean) as Plugin[]; // NOTE Bolean 写法 40 | } 41 | /**创建plugn钩子函数 */ 42 | export function createPluginHookUtils( 43 | plugins: readonly Plugin[] 44 | ): PluginHookUtils { 45 | const sortedPluginsCache = new Map(); 46 | function getSortedPlugins(hookName: keyof Plugin): Plugin[] { 47 | if (sortedPluginsCache.has(hookName)) 48 | return sortedPluginsCache.get(hookName)!; 49 | const sorted = getSortedPluginsByHook(hookName, plugins); 50 | sortedPluginsCache.set(hookName, sorted); 51 | return sorted; 52 | } 53 | 54 | function getSortedPluginHooks( 55 | hookName: K 56 | ): NonNullable>[] { 57 | const plugins = getSortedPlugins(hookName); 58 | return plugins 59 | .map((p) => { 60 | const hook: any = p[hookName]!; 61 | return typeof hook === "object" && "handler" in hook 62 | ? hook.handler 63 | : hook; 64 | }) 65 | .filter(Boolean); 66 | } 67 | 68 | return { 69 | getSortedPlugins, 70 | getSortedPluginHooks, 71 | }; 72 | } 73 | 74 | export function getSortedPluginsByHook( 75 | hookName: keyof Plugin, 76 | plugins: readonly Plugin[] 77 | ): Plugin[] { 78 | const pre: Plugin[] = []; 79 | const normal: Plugin[] = []; 80 | const post: Plugin[] = []; 81 | for (const plugin of plugins) { 82 | const hook = plugin[hookName]; 83 | if (hook) { 84 | if (typeof hook === "object") { 85 | if (hook.order === "pre") { 86 | pre.push(plugin); 87 | continue; 88 | } 89 | if (hook.order === "post") { 90 | post.push(plugin); 91 | continue; 92 | } 93 | } 94 | normal.push(plugin); 95 | } 96 | } 97 | return [...pre, ...normal, ...post]; 98 | } 99 | -------------------------------------------------------------------------------- /packages/vite/src/node/plugins/resolve.ts: -------------------------------------------------------------------------------- 1 | import { PackageCache, PackageData } from "../packages"; 2 | import { Plugin } from "../plugin"; 3 | import { 4 | bareImportRE, 5 | cleanUrl, 6 | createDebugger, 7 | fsPathFromId, 8 | isInNodeModules, 9 | isNonDriveRelativeAbsolutePath, 10 | isTsRequest, 11 | normalizePath, 12 | safeRealpathSync, 13 | tryStatSync, 14 | } from "../utils"; 15 | import path from "path"; 16 | import { PartialResolvedId } from "rollup"; 17 | import { FS_PREFIX } from "../constants"; 18 | import colors from "picocolors"; 19 | import fs from "node:fs"; 20 | import { resolvePackageData } from "../packages"; 21 | import { exports, imports } from "resolve.exports"; 22 | 23 | export interface ResolveOptions { 24 | mainFields?: string[]; 25 | conditions?: string[]; 26 | extensions?: string[]; 27 | dedupe?: string[]; 28 | preserveSymlinks?: boolean; 29 | } 30 | 31 | export interface InternalResolveOptions extends Required { 32 | root: string; 33 | isBuild: boolean; 34 | packageCache?: PackageCache; 35 | asSrc?: boolean; 36 | tryIndex?: boolean; 37 | preferRelative?: boolean; 38 | isRequire?: boolean; 39 | isFromTsImporter?: boolean; 40 | tryEsmOnly?: boolean; 41 | scan?: boolean; 42 | idOnly?: boolean; 43 | } 44 | const startsWithWordCharRE = /^\w/; 45 | 46 | const debug = createDebugger("vite:resolve-details", { 47 | onlyWhenFocused: true, 48 | }); 49 | export const browserExternalId = "__vite-browser-external"; 50 | 51 | export type InternalResolveOptionsWithOverrideConditions = 52 | InternalResolveOptions & { 53 | overrideConditions?: string[]; 54 | }; 55 | 56 | export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin { 57 | const { root, asSrc, preferRelative = false } = resolveOptions; 58 | const rootInRoot = false; 59 | return { 60 | name: "vite:resolve", 61 | 62 | async resolveId(id, importer, resolveOpts) { 63 | // 虚拟模块通常是在构建过程中动态生成或处理的临时模块 64 | // 它们可能不对应于实际的物理文件 65 | if ( 66 | id[0] === "\0" || 67 | id.startsWith("virtual:") || 68 | id.startsWith("/virtual:") 69 | ) { 70 | return; 71 | } 72 | const targetWeb = true; 73 | 74 | const isRequire: boolean = 75 | resolveOpts?.custom?.["node-resolve"]?.isRequire ?? false; 76 | 77 | const options: InternalResolveOptions = { 78 | isRequire, 79 | ...resolveOptions, 80 | scan: resolveOpts?.scan ?? resolveOptions.scan, 81 | }; 82 | 83 | if (importer) { 84 | if ( 85 | isTsRequest(importer) || 86 | resolveOpts.custom?.depScan?.loader?.startsWith("ts") 87 | ) { 88 | options.isFromTsImporter = true; 89 | } else { 90 | const moduleLang = this.getModuleInfo(importer)?.meta?.vite?.lang; 91 | options.isFromTsImporter = 92 | moduleLang && isTsRequest(`.${moduleLang}`); 93 | } 94 | } 95 | 96 | let res: string | PartialResolvedId | undefined; 97 | 98 | if (asSrc && id.startsWith(FS_PREFIX)) { 99 | res = fsPathFromId(id); 100 | debug?.(`[@fs] ${colors.cyan(id)} -> ${colors.dim(res)}`); 101 | return res; 102 | } 103 | 104 | if (asSrc && id[0] === "/" && (rootInRoot || !id.startsWith(root))) { 105 | const fsPath = path.resolve(root, id.slice(1)); 106 | if ((res = tryFsResolve(fsPath, options))) { 107 | debug?.(`[url] ${colors.cyan(id)} -> ${colors.dim(res)}`); 108 | return res; 109 | } 110 | } 111 | 112 | if ( 113 | id[0] === "." || 114 | ((preferRelative || importer?.endsWith(".html")) && 115 | startsWithWordCharRE.test(id)) 116 | ) { 117 | const basedir = importer ? path.dirname(importer) : process.cwd(); 118 | const fsPath = path.resolve(basedir, id); 119 | 120 | if ((res = tryFsResolve(fsPath, options))) { 121 | debug?.(`[relative] ${colors.cyan(id)} -> ${colors.dim(res)}`); 122 | return res; 123 | } 124 | } 125 | if ( 126 | isNonDriveRelativeAbsolutePath(id) && 127 | (res = tryFsResolve(id, options)) 128 | ) { 129 | debug?.(`[fs] ${colors.cyan(id)} -> ${colors.dim(res)}`); 130 | return res; 131 | } 132 | 133 | if (bareImportRE.test(id)) { 134 | if ((res = tryNodeResolve(id, importer, options, targetWeb))) { 135 | return res; 136 | } 137 | } 138 | 139 | debug?.(`[fallthrough] ${colors.dim(id)}`); 140 | }, 141 | }; 142 | } 143 | /**指定是否尝试使用 Node.js 的模块解析算法来解析模块的依赖关系 */ 144 | export function tryNodeResolve( 145 | id: string, 146 | importer: string | null | undefined, 147 | options: InternalResolveOptionsWithOverrideConditions, 148 | targetWeb: boolean 149 | ): PartialResolvedId | undefined { 150 | const { root, preserveSymlinks, packageCache } = options; 151 | 152 | const pkgId = id; 153 | 154 | let basedir: string; 155 | if ( 156 | importer && 157 | path.isAbsolute(importer) && 158 | (importer[importer.length - 1] === "*" || fs.existsSync(cleanUrl(importer))) 159 | ) { 160 | basedir = path.dirname(importer); 161 | } else { 162 | basedir = root; 163 | } 164 | 165 | const pkg = resolvePackageData( 166 | pkgId, 167 | basedir, 168 | preserveSymlinks, 169 | packageCache 170 | ); 171 | if (!pkg) 172 | throw new Error(`cannot find package.json for module ${id} in ${basedir}`); 173 | const resolveId = resolvePackageEntry; 174 | const unresolvedId = pkgId; 175 | 176 | let resolved: string | undefined; 177 | try { 178 | resolved = resolveId(unresolvedId, pkg, targetWeb, options); 179 | } catch (err) { 180 | if (!options.tryEsmOnly) { 181 | throw err; 182 | } 183 | } 184 | if (!resolved) { 185 | throw new Error(`cannot resolve PackageData`); 186 | } 187 | return { id: resolved }; 188 | } 189 | 190 | function resolveExportsOrImports( 191 | pkg: PackageData["data"], 192 | key: string, 193 | options: InternalResolveOptionsWithOverrideConditions, 194 | targetWeb: boolean, 195 | type: "imports" | "exports" 196 | ) { 197 | const additionalConditions = new Set( 198 | options.overrideConditions || [ 199 | "production", 200 | "development", 201 | "module", 202 | ...options.conditions, 203 | ] 204 | ); 205 | 206 | const conditions = [...additionalConditions].filter((condition) => { 207 | switch (condition) { 208 | case "production": 209 | return false; 210 | case "development": 211 | return true; 212 | case "module": 213 | return !options.isRequire; 214 | } 215 | return true; 216 | }); 217 | 218 | const fn = type === "imports" ? imports : exports; 219 | const result = fn(pkg, key, { 220 | browser: targetWeb && !additionalConditions.has("node"), 221 | require: options.isRequire && !additionalConditions.has("import"), 222 | conditions, 223 | }); 224 | 225 | return result ? result[0] : undefined; 226 | } 227 | 228 | function splitFileAndPostfix(path: string) { 229 | const file = cleanUrl(path); 230 | return { file, postfix: path.slice(file.length) }; 231 | } 232 | /**尝试解析给定的文件系统路径 */ 233 | function tryFsResolve( 234 | fsPath: string, 235 | options: InternalResolveOptions 236 | ): string | undefined { 237 | const hashIndex = fsPath.indexOf("#"); 238 | if (hashIndex >= 0 && isInNodeModules(fsPath)) { 239 | const queryIndex = fsPath.indexOf("?"); 240 | if (queryIndex < 0 || queryIndex > hashIndex) { 241 | const file = 242 | queryIndex > hashIndex ? fsPath.slice(0, queryIndex) : fsPath; 243 | const res = tryCleanFsResolve(file, options); 244 | if (res) return res + fsPath.slice(file.length); 245 | } 246 | } 247 | 248 | const { file, postfix } = splitFileAndPostfix(fsPath); 249 | const res = tryCleanFsResolve(file, options); 250 | if (res) return res + postfix; 251 | } 252 | /**解析package.json的入口路径 */ 253 | export function resolvePackageEntry( 254 | id: string, 255 | { dir, data, setResolvedCache, getResolvedCache }: PackageData, 256 | targetWeb: boolean, 257 | options: InternalResolveOptions 258 | ): string | undefined { 259 | const cached = getResolvedCache(".", targetWeb); 260 | if (cached) { 261 | return cached; 262 | } 263 | try { 264 | let entryPoint: string | undefined; 265 | 266 | if (data.exports) { 267 | entryPoint = resolveExportsOrImports( 268 | data, 269 | ".", 270 | options, 271 | targetWeb, 272 | "exports" 273 | ); 274 | } 275 | 276 | const resolvedFromExports = !!entryPoint; 277 | 278 | if (!resolvedFromExports && (!entryPoint || entryPoint.endsWith(".mjs"))) { 279 | for (const field of options.mainFields) { 280 | if (field === "browser") continue; 281 | if (typeof data[field] === "string") { 282 | entryPoint = data[field]; 283 | break; 284 | } 285 | } 286 | } 287 | entryPoint ||= data.main; 288 | 289 | const entryPoints = entryPoint 290 | ? [entryPoint] 291 | : ["index.js", "index.json", "index.node"]; 292 | 293 | for (let entry of entryPoints) { 294 | const entryPointPath = path.join(dir, entry); 295 | const resolvedEntryPoint = tryFsResolve(entryPointPath, options); 296 | if (resolvedEntryPoint) { 297 | debug?.( 298 | `[package entry] ${colors.cyan(id)} -> ${colors.dim( 299 | resolvedEntryPoint 300 | )}` 301 | ); 302 | setResolvedCache(".", resolvedEntryPoint, targetWeb); 303 | return resolvedEntryPoint; 304 | } 305 | } 306 | } catch (e) { 307 | packageEntryFailure(id, e.message); 308 | } 309 | packageEntryFailure(id); 310 | } 311 | 312 | function packageEntryFailure(id: string, details?: string) { 313 | throw new Error( 314 | `Failed to resolve entry for package "${id}". ` + 315 | `The package may have incorrect main/module/exports specified in its package.json` + 316 | (details ? ": " + details : ".") 317 | ); 318 | } 319 | 320 | function getRealPath(resolved: string, preserveSymlinks?: boolean): string { 321 | if (!preserveSymlinks && browserExternalId !== resolved) { 322 | resolved = safeRealpathSync(resolved); 323 | } 324 | return normalizePath(resolved); 325 | } 326 | /** 327 | * 创建一个临时的干净文件系统,将模块路径映射到该文件系统中, 328 | * 并在该环境下进行解析,避免缓存和其他外部因素对解析过程的干扰, 329 | * 确保每次解析都是基于最新的文件状态。 330 | * */ 331 | function tryCleanFsResolve( 332 | file: string, 333 | options: InternalResolveOptions 334 | ): string | undefined { 335 | const fileStat = tryStatSync(file); 336 | 337 | if (fileStat?.isFile()) return getRealPath(file, options.preserveSymlinks); 338 | } 339 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/hmr.ts: -------------------------------------------------------------------------------- 1 | import { ViteDevServer } from "."; 2 | import { createDebugger, normalizePath, unique, wrapId } from "../utils"; 3 | import path from "node:path"; 4 | import colors from "picocolors"; 5 | import { CLIENT_DIR } from "../constants"; 6 | import { HmrContext, isCSSRequest, ModuleNode } from "vite"; 7 | import fsp from "node:fs/promises"; 8 | import type { Update } from "types/hmrPayload"; 9 | import { isExplicitImportRequired } from "../plugins/importAnalysis"; 10 | import { getAffectedGlobModules } from "../plugins/importMetaGlob"; 11 | import type { RollupError } from "rollup"; 12 | 13 | export function getShortName(file: string, root: string): string { 14 | return file.startsWith(root + "/") ? path.posix.relative(root, file) : file; 15 | } 16 | export const debugHmr = createDebugger("vite:hmr"); 17 | const normalizedClientDir = normalizePath(CLIENT_DIR); 18 | /**处理热更新 */ 19 | export async function handleHMRUpdate( 20 | file: string, 21 | server: ViteDevServer, 22 | configOnly: boolean 23 | ): Promise { 24 | const { ws, config, moduleGraph } = server; 25 | const shortFile = getShortName(file, config.root); 26 | 27 | if (configOnly) { 28 | return; 29 | } 30 | 31 | debugHmr?.(`[file change] ${colors.dim(shortFile)}`); 32 | // client脚本文件发生更改时 33 | // 通知浏览器重新reload,刷新页面 34 | if (file.startsWith(normalizedClientDir)) { 35 | ws.send({ 36 | type: "full-reload", 37 | path: "*", 38 | }); 39 | return; 40 | } 41 | // 获取文件变动的具体路径 42 | const mods = moduleGraph.getModulesByFile(file); 43 | 44 | const timestamp = Date.now(); 45 | // 初始化HMR上下文对象 46 | const hmrContext: HmrContext = { 47 | file, 48 | timestamp, 49 | modules: mods ? [...mods] : [], 50 | // 使用utf-8格式读取文件内容 51 | read: () => readModifiedFile(file), 52 | server: server as any, 53 | }; 54 | 55 | for (const hook of config.getSortedPluginHooks("handleHotUpdate")) { 56 | const filteredModules = await hook(hmrContext); 57 | if (filteredModules) { 58 | hmrContext.modules = filteredModules; 59 | } 60 | } 61 | // 更新热更新模块信息,同时给浏览器推送信息 62 | updateModules(shortFile, hmrContext.modules, timestamp, server); 63 | } 64 | 65 | /**给浏览器端推送消息 */ 66 | export function updateModules( 67 | file: string, 68 | modules: ModuleNode[], 69 | timestamp: number, 70 | { config, ws, moduleGraph }: ViteDevServer, 71 | afterInvalidation?: boolean 72 | ): void { 73 | // 存储更新的信息 74 | const updates: Update[] = []; 75 | // 失效的模块集合 76 | const invalidatedModules = new Set(); 77 | // 被遍历过的模块集合 78 | const traversedModules = new Set(); 79 | // 页面是否需要重新reload 80 | let needFullReload = false; 81 | 82 | for (const mod of modules) { 83 | // 将需要更新的模块标记为失效状态 84 | // 只有被标记为失效的模块才会被处理和更新,减少了不必要的计算和传播。 85 | moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true); 86 | if (needFullReload) { 87 | continue; 88 | } 89 | // 收集边界模块和接受更新的模块。如果遇到了没有接受更新的模块, 90 | const boundaries: { boundary: ModuleNode; acceptedVia: ModuleNode }[] = []; 91 | const hasDeadEnd = propagateUpdate(mod, traversedModules, boundaries); 92 | if (hasDeadEnd) { 93 | // 则将 needFullReload 标记为 true,并跳过当前模块的处理 94 | needFullReload = true; 95 | continue; 96 | } 97 | // 将每个边界模块和接受更新的模块 98 | updates.push( 99 | ...boundaries.map(({ boundary, acceptedVia }) => ({ 100 | type: `${boundary.type}-update` as const, 101 | timestamp, 102 | path: normalizeHmrUrl(boundary.url), 103 | explicitImportRequired: 104 | boundary.type === "js" 105 | ? isExplicitImportRequired(acceptedVia.url) 106 | : undefined, 107 | acceptedPath: normalizeHmrUrl(acceptedVia.url), 108 | })) 109 | ); 110 | } 111 | // 如果 needFullReload 为 true,表示页面需要重新加载。 112 | // 会向客户端发送一个类型为 "full-reload" 的消息 113 | if (needFullReload) { 114 | config.logger.info(colors.green(`page reload `) + colors.dim(file), { 115 | clear: !afterInvalidation, 116 | timestamp: true, 117 | }); 118 | ws.send({ 119 | type: "full-reload", 120 | }); 121 | return; 122 | } 123 | // 表示没有更新 124 | if (updates.length === 0) { 125 | debugHmr?.(colors.yellow(`no update happened `) + colors.dim(file)); 126 | return; 127 | } 128 | // 如果有更新信息,它会通过日志输出显示更新的信息, 129 | // 并将更新消息发送给客户端 130 | config.logger.info( 131 | colors.green(`hmr update `) + 132 | colors.dim([...new Set(updates.map((u) => u.path))].join(", ")), 133 | { clear: !afterInvalidation, timestamp: true } 134 | ); 135 | ws.send({ 136 | type: "update", 137 | updates, 138 | }); 139 | } 140 | 141 | export async function handleFileAddUnlink( 142 | file: string, 143 | server: ViteDevServer 144 | ): Promise { 145 | const modules = [...(server.moduleGraph.getModulesByFile(file) || [])]; 146 | 147 | modules.push(...getAffectedGlobModules(file, server)); 148 | 149 | if (modules.length > 0) { 150 | updateModules( 151 | getShortName(file, server.config.root), 152 | unique(modules), 153 | Date.now(), 154 | server 155 | ); 156 | } 157 | } 158 | /** 159 | * 读取文件内容,如果文件内容为空, 160 | * 意味着文件可能正在被写入,此时函数会进入等待状态,持续轮询文件的最后修改时间, 161 | * 直到文件内容不为空或达到最大轮询次数 162 | */ 163 | async function readModifiedFile(file: string): Promise { 164 | const content = await fsp.readFile(file, "utf-8"); 165 | if (!content) { 166 | const mtime = (await fsp.stat(file)).mtimeMs; 167 | await new Promise((r) => { 168 | let n = 0; 169 | const poll = async () => { 170 | n++; 171 | const newMtime = (await fsp.stat(file)).mtimeMs; 172 | if (newMtime !== mtime || n > 10) { 173 | r(0); 174 | } else { 175 | setTimeout(poll, 10); 176 | } 177 | }; 178 | setTimeout(poll, 10); 179 | }); 180 | return await fsp.readFile(file, "utf-8"); 181 | } else { 182 | return content; 183 | } 184 | } 185 | /**根据模块之间的依赖关系,找到热更新边界 */ 186 | function propagateUpdate( 187 | node: ModuleNode, 188 | traversedModules: Set, 189 | boundaries: { boundary: ModuleNode; acceptedVia: ModuleNode }[], 190 | currentChain: ModuleNode[] = [node] 191 | ): boolean /* hasDeadEnd */ { 192 | // 当前模块是否已经被遍历过了 193 | // 遍历过了就无需再被遍历 194 | if (traversedModules.has(node)) { 195 | return false; 196 | } 197 | traversedModules.add(node); 198 | // 判断模块是否已经被分析过,如果未被分析过,则返回 199 | // 当模块未被分析时,模块是否能够接受自身的热更新 200 | // false代表没找到热更新边界 201 | if (node.id && node.isSelfAccepting === undefined) { 202 | debugHmr?.( 203 | `[propagate update] stop propagation because not analyzed: ${colors.dim( 204 | node.id 205 | )}` 206 | ); 207 | return false; 208 | } 209 | 210 | if (node.isSelfAccepting) { 211 | // 添加到热更新边界列表中 212 | boundaries.push({ boundary: node, acceptedVia: node }); 213 | 214 | for (const importer of node.importers) { 215 | // TODO 为什么需要判断是不是css请求 216 | // 是 CSS 请求且不在热更新传播链中, 217 | // 则递归调用 propagateUpdate 来传播更新,同时将导入者添加到当前链中 218 | if (isCSSRequest(importer.url) && !currentChain.includes(importer)) { 219 | propagateUpdate( 220 | importer, 221 | traversedModules, 222 | boundaries, 223 | currentChain.concat(importer) 224 | ); 225 | } 226 | } 227 | } 228 | return false; 229 | } 230 | /**规范hmr文件路径 */ 231 | export function normalizeHmrUrl(url: string): string { 232 | if (url[0] !== "." && url[0] !== "/") { 233 | url = wrapId(url); 234 | } 235 | return url; 236 | } 237 | 238 | function error(pos: number) { 239 | const err = new Error( 240 | `import.meta.hot.accept() can only accept string literals or an ` + 241 | `Array of string literals.` 242 | ) as RollupError; 243 | err.pos = pos; 244 | throw err; 245 | } 246 | 247 | const enum LexerState { 248 | inCall, 249 | inSingleQuoteString, 250 | inDoubleQuoteString, 251 | inTemplateString, 252 | inArray, 253 | } 254 | const whitespaceRE = /\s/; 255 | // TODO AST是怎么解析的 256 | /**对热更新依赖模块进行处理 */ 257 | export function lexAcceptedHmrDeps( 258 | code: string, 259 | start: number, 260 | urls: Set<{ url: string; start: number; end: number }> 261 | ): boolean { 262 | let state: LexerState = LexerState.inCall; 263 | let prevState: LexerState = LexerState.inCall; 264 | let currentDep: string = ""; 265 | 266 | function addDep(index: number) { 267 | urls.add({ 268 | url: currentDep, 269 | start: index - currentDep.length - 1, 270 | end: index + 1, 271 | }); 272 | currentDep = ""; 273 | } 274 | 275 | for (let i = start; i < code.length; i++) { 276 | const char = code.charAt(i); 277 | switch (state) { 278 | case LexerState.inCall: 279 | case LexerState.inArray: 280 | if (char === `'`) { 281 | prevState = state; 282 | state = LexerState.inSingleQuoteString; 283 | } else if (char === `"`) { 284 | prevState = state; 285 | state = LexerState.inDoubleQuoteString; 286 | } else if (char === "`") { 287 | prevState = state; 288 | state = LexerState.inTemplateString; 289 | } else if (whitespaceRE.test(char)) { 290 | continue; 291 | } else { 292 | if (state === LexerState.inCall) { 293 | if (char === `[`) { 294 | state = LexerState.inArray; 295 | } else { 296 | return true; 297 | } 298 | } else if (state === LexerState.inArray) { 299 | if (char === `]`) { 300 | return false; 301 | } else if (char === ",") { 302 | continue; 303 | } else { 304 | error(i); 305 | } 306 | } 307 | } 308 | break; 309 | case LexerState.inSingleQuoteString: 310 | if (char === `'`) { 311 | addDep(i); 312 | if (prevState === LexerState.inCall) { 313 | return false; 314 | } else { 315 | state = prevState; 316 | } 317 | } else { 318 | currentDep += char; 319 | } 320 | break; 321 | case LexerState.inDoubleQuoteString: 322 | if (char === `"`) { 323 | addDep(i); 324 | if (prevState === LexerState.inCall) { 325 | return false; 326 | } else { 327 | state = prevState; 328 | } 329 | } else { 330 | currentDep += char; 331 | } 332 | break; 333 | case LexerState.inTemplateString: 334 | if (char === "`") { 335 | addDep(i); 336 | if (prevState === LexerState.inCall) { 337 | return false; 338 | } else { 339 | state = prevState; 340 | } 341 | } else if (char === "$" && code.charAt(i + 1) === "{") { 342 | error(i); 343 | } else { 344 | currentDep += char; 345 | } 346 | break; 347 | default: 348 | throw new Error("unknown import.meta.hot lexer state"); 349 | } 350 | } 351 | return false; 352 | } 353 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/middlewares/htmlFallback.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import history from "connect-history-api-fallback"; 4 | import type { Connect } from "dep-types/connect"; 5 | import { createDebugger } from "../../utils"; 6 | /**拦截无法匹配的URL请求,并将默认的入口HTML文件返回给浏览器 */ 7 | export function htmlFallbackMiddleware( 8 | root: string 9 | ): Connect.NextHandleFunction { 10 | const historyHtmlFallbackMiddleware = history({ 11 | logger: createDebugger("vite:html-fallback"), 12 | rewrites: [ 13 | { 14 | from: /\/$/, 15 | to({ parsedUrl }: any) { 16 | const rewritten = 17 | decodeURIComponent(parsedUrl.pathname) + "index.html"; 18 | 19 | if (fs.existsSync(path.join(root, rewritten))) { 20 | return rewritten; 21 | } 22 | 23 | return `/index.html`; 24 | }, 25 | }, 26 | ], 27 | }); 28 | 29 | return function viteHtmlFallbackMiddleware(req, res, next) { 30 | return historyHtmlFallbackMiddleware(req, res, next); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/middlewares/indexHtml.ts: -------------------------------------------------------------------------------- 1 | import { ViteDevServer } from ".."; 2 | import type { Connect } from "dep-types/connect"; 3 | import { cleanUrl, normalizePath, stripBase, unwrapId } from "../../utils"; 4 | import { CLIENT_PUBLIC_PATH } from "../../constants"; 5 | import path from "node:path"; 6 | import fs from "node:fs"; 7 | import fsp from "node:fs/promises"; 8 | import { send } from "../send"; 9 | import { 10 | applyHtmlTransforms, 11 | resolveHtmlTransforms, 12 | traverseHtml, 13 | nodeIsElement, 14 | getScriptInfo, 15 | overwriteAttrValue, 16 | assetAttrsConfig, 17 | getAttrKey, 18 | } from "../../plugins/html"; 19 | import { IndexHtmlTransformHook } from "vite"; 20 | import MagicString from "magic-string"; 21 | import type { Token } from "parse5"; 22 | import { ResolvedConfig } from "../../config"; 23 | 24 | /**index.html中间件,改造index.html,用来注入脚本,处理预加载资源等 */ 25 | export function indexHtmlMiddleware( 26 | server: ViteDevServer 27 | ): Connect.NextHandleFunction { 28 | return async function viteIndexHtmlMiddleware(req, res, next) { 29 | if (res.writableEnded) { 30 | return next(); 31 | } 32 | 33 | const url = req.url && cleanUrl(req.url); 34 | // 判断是否是index.html文件 35 | if (url?.endsWith(".html") && req.headers["sec-fetch-dest"] !== "script") { 36 | const filename = getHtmlFilename(url, server); 37 | if (fs.existsSync(filename)) { 38 | try { 39 | let html = await fsp.readFile(filename, "utf-8"); 40 | // 得到注入脚本之后的html文件 41 | html = await server.transformIndexHtml(url, html, req.originalUrl); 42 | // req: http请求 43 | // res: http响应 44 | // html: 要发送的内容 45 | // "html": 指定响应的类型 46 | return send(req, res, html, "html", { 47 | headers: server.config.server.headers, 48 | }); 49 | } catch (e) { 50 | return next(e); 51 | } 52 | } 53 | } 54 | next(); 55 | }; 56 | } 57 | 58 | /**去掉'/',得到文件名 */ 59 | function getHtmlFilename(url: string, server: ViteDevServer) { 60 | // decodeURIComponent处理URI组件的编码字符 61 | return decodeURIComponent( 62 | normalizePath(path.join(server.config.root, url.slice(1))) 63 | ); 64 | } 65 | 66 | /**改造index.html,注入脚本 */ 67 | export function createDevHtmlTransformFn( 68 | server: ViteDevServer 69 | ): (url: string, html: string, originalUrl: string) => Promise { 70 | const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms( 71 | server.config.plugins 72 | ); 73 | return (url: string, html: string, originalUrl: string): Promise => { 74 | return applyHtmlTransforms( 75 | html, 76 | [ 77 | ...preHooks, 78 | devHtmlHook, // 主要调用这个钩子,将client脚本注入到script标签中 79 | ...normalHooks, 80 | ...postHooks, 81 | ], 82 | { 83 | path: url, 84 | filename: getHtmlFilename(url, server), 85 | server, 86 | originalUrl, 87 | } as any 88 | ); 89 | }; 90 | } 91 | 92 | /**拦截index.html,注入脚本 */ 93 | const devHtmlHook: IndexHtmlTransformHook = async ( 94 | html, 95 | { path: htmlPath, filename, server } 96 | ) => { 97 | const { config } = server!; 98 | const base = config.base || "/"; 99 | // @ts-ignore 100 | let proxyModulePath: string; 101 | // htmlPath = '/index.html' 102 | const trailingSlash = htmlPath.endsWith("/"); 103 | // 当htmlpath不以/结尾,且文件路径存在时 104 | if (!trailingSlash && fs.existsSync(filename)) { 105 | proxyModulePath = htmlPath; 106 | } else { 107 | // 以\0开头的路径表示虚拟路径或特殊路径 108 | const validPath = `${htmlPath}${trailingSlash ? "index.html" : ""}`; 109 | proxyModulePath = `\0${validPath}`; 110 | } 111 | 112 | const s = new MagicString(html); 113 | // html: HTML内容 114 | // filename: html文件的路径 115 | // node: node节点 116 | await traverseHtml(html, filename, (node) => { 117 | // 不是node节点就直接返回 118 | if (!nodeIsElement(node)) { 119 | return; 120 | } 121 | // 当前节点是script标签时 122 | if (node.nodeName === "script") { 123 | const { src, sourceCodeLocation } = getScriptInfo(node); 124 | 125 | if (src) { 126 | processNodeUrl( 127 | src, 128 | sourceCodeLocation!, 129 | s, 130 | config as any, 131 | server as any 132 | ); 133 | } 134 | } 135 | 136 | const assetAttrs = assetAttrsConfig[node.nodeName]; 137 | if (assetAttrs) { 138 | for (const p of node.attrs) { 139 | const attrKey = getAttrKey(p); 140 | if (p.value && assetAttrs.includes(attrKey)) { 141 | processNodeUrl( 142 | p, 143 | node.sourceCodeLocation!.attrs![attrKey], 144 | s, 145 | config as any 146 | ); 147 | } 148 | } 149 | } 150 | }); 151 | 152 | html = s.toString(); 153 | 154 | return { 155 | html, 156 | tags: [ 157 | { 158 | tag: "script", 159 | attrs: { 160 | type: "module", 161 | // path.join 即会按照当前操作系统进行给定路径分隔符, 162 | // 而 path.posix.join 则始终是 / 163 | src: path.posix.join(base, CLIENT_PUBLIC_PATH), 164 | }, 165 | injectTo: "head-prepend", 166 | }, 167 | ], 168 | }; 169 | }; 170 | /** 171 | * 调用transformRequest方法,对请求进行预处理 172 | * 包括路径重写、资源注入、请求过滤 173 | * */ 174 | function preTransformRequest(server: ViteDevServer, url: string, base: string) { 175 | if (!server.config.server.preTransformRequests) return; 176 | url = unwrapId(stripBase(url, base)); 177 | server.transformRequest(url).catch((e) => { 178 | server.config.logger.error(e.message); 179 | }); 180 | } 181 | /**对node路径进行重写 */ 182 | const processNodeUrl = ( 183 | attr: Token.Attribute, 184 | sourceCodeLocation: Token.Location, 185 | s: MagicString, 186 | config: ResolvedConfig, 187 | server?: ViteDevServer 188 | ) => { 189 | let url = attr.value || ""; 190 | 191 | const devBase = config.base; 192 | if (url[0] === "/" && url[1] !== "/") { 193 | const fullUrl = path.posix.join(devBase, url); 194 | overwriteAttrValue(s, sourceCodeLocation, fullUrl); 195 | if (server) { 196 | preTransformRequest(server, fullUrl, devBase); 197 | } 198 | } 199 | }; 200 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/middlewares/static.ts: -------------------------------------------------------------------------------- 1 | import { ViteDevServer } from "../.."; 2 | import type { Connect } from "dep-types/connect"; 3 | import { Options } from "sirv"; // NOTE patchedDependencies 4 | import sirv from "sirv"; // NOTE vite中的核心插件,用于处理静态资源 5 | import { 6 | cleanUrl, 7 | fsPathFromUrl, 8 | isImportRequest, 9 | isInternalRequest, 10 | isParentDirectory, 11 | removeLeadingSlash, 12 | shouldServeFile, 13 | } from "../../utils"; 14 | import type { OutgoingHttpHeaders } from "node:http"; 15 | import path from "node:path"; 16 | 17 | const knownJavascriptExtensionRE = /\.[tj]sx?$/; 18 | 19 | const sirvOptions = ({ 20 | headers, 21 | shouldServe, 22 | }: { 23 | headers?: OutgoingHttpHeaders; 24 | shouldServe?: (p: string) => void; 25 | }): Options => { 26 | return { 27 | dev: true, 28 | etag: true, 29 | extensions: [], 30 | setHeaders(res, pathname) { 31 | if (knownJavascriptExtensionRE.test(pathname)) { 32 | res.setHeader("Content-Type", "application/javascript"); 33 | } 34 | if (headers) { 35 | for (const name in headers) { 36 | res.setHeader(name, headers[name]!); 37 | } 38 | } 39 | }, 40 | shouldServe, 41 | }; 42 | }; 43 | /**对public静态资源进行处理 */ 44 | export function servePublicMiddleware( 45 | dir: string, 46 | headers?: OutgoingHttpHeaders 47 | ): Connect.NextHandleFunction { 48 | // 不需要为sirv或指定默认端口,开发服务器会自动选择vite本地服务器端口。 49 | const serve = sirv( 50 | dir, 51 | sirvOptions({ 52 | headers, 53 | shouldServe: (filePath) => shouldServeFile(filePath, dir), 54 | }) 55 | ); 56 | 57 | return function viteServePublicMiddleware(req, res, next) { 58 | if (isImportRequest(req.url!) || isInternalRequest(req.url!)) { 59 | return next(); 60 | } 61 | serve(req, res, next); 62 | }; 63 | } 64 | 65 | /**静态资源中间件 */ 66 | export function serveStaticMiddleware( 67 | dir: string, 68 | server: ViteDevServer 69 | ): Connect.NextHandleFunction { 70 | const serve = sirv( 71 | dir, 72 | sirvOptions({ 73 | headers: server.config.server.headers, 74 | }) 75 | ); 76 | 77 | return function viteServeStaticMiddleware(req, res, next) { 78 | const cleanedUrl = cleanUrl(req.url!); 79 | if ( 80 | cleanedUrl[cleanedUrl.length - 1] === "/" || 81 | path.extname(cleanedUrl) === ".html" || 82 | isInternalRequest(req.url!) 83 | ) { 84 | return next(); 85 | } 86 | 87 | const url = new URL(req.url!, "http://example.com"); 88 | // 在 URL 中,某些字符是需要进行编码的,例如空格会被编码为 %20, 89 | // 需要将这些编码后的 URI 组件解码为原始的字符串形式, 90 | // 这时就可以使用 decodeURIComponent 函数 91 | const pathname = decodeURIComponent(url.pathname); 92 | 93 | let redirectedPathname: string | undefined; 94 | for (const { find, replacement } of server.config.resolve.alias) { 95 | const matches = 96 | typeof find === "string" 97 | ? pathname.startsWith(find) 98 | : find.test(pathname); 99 | if (matches) { 100 | redirectedPathname = pathname.replace(find, replacement); 101 | break; 102 | } 103 | } 104 | const resolvedPathname = redirectedPathname || pathname; 105 | let fileUrl = path.resolve(dir, removeLeadingSlash(resolvedPathname)); 106 | if ( 107 | resolvedPathname[resolvedPathname.length - 1] === "/" && 108 | fileUrl[fileUrl.length - 1] !== "/" 109 | ) { 110 | fileUrl = fileUrl + "/"; 111 | } 112 | serve(req, res, next); 113 | }; 114 | } 115 | /**是否有权限去处理文件 */ 116 | export function isFileServingAllowed( 117 | url: string, 118 | server: ViteDevServer 119 | ): boolean { 120 | if (!server.config.server.fs.strict) return true; 121 | 122 | const file = fsPathFromUrl(url); 123 | // 当前文件url的绝对路径有没有被加进safeModulesPath中 124 | if (server.moduleGraph.safeModulesPath.has(file)) return true; 125 | 126 | if (server.config.server.fs.allow.some((dir) => isParentDirectory(dir, file))) 127 | return true; 128 | 129 | return false; 130 | } 131 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/middlewares/transform.ts: -------------------------------------------------------------------------------- 1 | import type { Connect } from "dep-types/connect"; 2 | import { transformRequest } from "../transformRequest"; 3 | import { send } from "vite"; 4 | import { 5 | isCSSRequest, 6 | isDirectCSSRequest, 7 | isDirectRequest, 8 | } from "../../plugins/css"; 9 | import { 10 | createDebugger, 11 | injectQuery, 12 | isImportRequest, 13 | isJSRequest, 14 | prettifyUrl, 15 | removeImportQuery, 16 | removeTimestampQuery, 17 | unwrapId, 18 | } from "../../utils"; 19 | import { DEP_VERSION_RE, NULL_BYTE_PLACEHOLDER } from "../../constants"; 20 | import { ViteDevServer } from "../.."; 21 | import { isHTMLProxy } from "../../plugins/html"; 22 | 23 | const knownIgnoreList = new Set(["/", "/favicon.ico"]); 24 | const debugCache = createDebugger("vite:cache"); 25 | /**transform核心中间件,可以拦截请求,修改请求URL、添加请求头 */ 26 | export function transformMiddleware( 27 | server: ViteDevServer 28 | ): Connect.NextHandleFunction { 29 | const { 30 | config: { root, logger }, 31 | moduleGraph, 32 | } = server; 33 | 34 | return async function viteTransformMiddleware(req, res, next) { 35 | if (req.method !== "GET" || knownIgnoreList.has(req.url!)) { 36 | return next(); 37 | } 38 | 39 | let url: string; 40 | try { 41 | url = decodeURI(removeTimestampQuery(req.url!)).replace( 42 | NULL_BYTE_PLACEHOLDER, 43 | "\0" 44 | ); 45 | } catch (e) { 46 | return next(e); 47 | } 48 | 49 | try { 50 | if ( 51 | isJSRequest(url) || 52 | isImportRequest(url) || 53 | isCSSRequest(url) || 54 | isHTMLProxy(url) 55 | ) { 56 | url = removeImportQuery(url); 57 | url = unwrapId(url); 58 | if ( 59 | isCSSRequest(url) && 60 | !isDirectRequest(url) && 61 | req.headers.accept?.includes("text/css") 62 | ) { 63 | url = injectQuery(url, "direct"); 64 | } 65 | 66 | const ifNoneMatch = req.headers["if-none-match"]; 67 | if ( 68 | ifNoneMatch && 69 | (await moduleGraph.getModuleByUrl(url))?.transformResult?.etag === 70 | ifNoneMatch 71 | ) { 72 | debugCache?.(`[304] ${prettifyUrl(url, root)}`); 73 | res.statusCode = 304; 74 | return res.end(); 75 | } 76 | 77 | const result = await transformRequest(url, server, { 78 | html: req.headers.accept?.includes("text/html"), 79 | }); 80 | if (result) { 81 | const type = isDirectCSSRequest(url) ? "css" : "js"; 82 | const isDep = DEP_VERSION_RE.test(url); 83 | return send(req, res, result.code, type, { 84 | etag: result.etag, 85 | cacheControl: isDep ? "max-age=31536000,immutable" : "no-cache", 86 | headers: server.config.server.headers, 87 | map: result.map, 88 | }); 89 | } 90 | } 91 | } catch (e) { 92 | logger.error("transform error: " + e.message); 93 | return next(e); 94 | } 95 | 96 | next(); 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/moduleGraph.ts: -------------------------------------------------------------------------------- 1 | import { extname } from "node:path"; 2 | import type { ModuleInfo, PartialResolvedId } from "rollup"; 3 | import { isDirectCSSRequest } from "../plugins/css"; 4 | import { cleanUrl, removeImportQuery, removeTimestampQuery } from "../utils"; 5 | import type { TransformResult } from "./transformRequest"; 6 | 7 | export class ModuleNode { 8 | url: string; 9 | id: string | null = null; 10 | file: string | null = null; 11 | type: "js" | "css"; 12 | info?: ModuleInfo; 13 | meta?: Record; 14 | importers = new Set(); 15 | importedModules = new Set(); 16 | acceptedHmrDeps = new Set(); 17 | acceptedHmrExports: Set | null = null; 18 | importedBindings: Map> | null = null; 19 | isSelfAccepting?: boolean; 20 | transformResult: TransformResult | null = null; 21 | ssrTransformResult: TransformResult | null = null; 22 | ssrModule: Record | null = null; 23 | ssrError: Error | null = null; 24 | lastHMRTimestamp = 0; 25 | lastInvalidationTimestamp = 0; 26 | 27 | constructor(url: string, setIsSelfAccepting = true) { 28 | this.url = url; 29 | this.type = isDirectCSSRequest(url) ? "css" : "js"; 30 | if (setIsSelfAccepting) { 31 | this.isSelfAccepting = false; 32 | } 33 | } 34 | } 35 | 36 | export type ResolvedUrl = [ 37 | url: string, 38 | resolvedId: string, 39 | meta: object | null | undefined 40 | ]; 41 | 42 | export class ModuleGraph { 43 | // key: '/src/main.ts' 44 | // 将原始路径映射到模块对象中 45 | // 主要用于模块热更新、服务端渲染、将 URL 转换为对应的模块路径,并加载相应的模块资源 46 | urlToModuleMap = new Map(); 47 | // key: 'C:/Users/Administrator/Desktop/learn-Code/vite源码/mini-vite/mini-vite-example/src/main.ts' 48 | // resolveId之后的路径与模块对象的映射,可用来快速查找和访问对应的模块节点。 49 | 50 | // 主要用于查找对应的模块对象,获取模块的代码和其他相关信息 51 | // 根据模块 ID 查找对应的模块对象,并获取其依赖关系,从而构建整个模块依赖图。 52 | // 当一个模块发生变化时,可以根据模块 ID 从 idToModuleMap 中查找对应的模块对象, 53 | // 然后通知客户端更新相应的模块 54 | // 在开发环境下,存储已解析和加载的模块对象 55 | idToModuleMap = new Map(); 56 | // key: 'C:/Users/Administrator/Desktop/learn-Code/vite源码/mini-vite/mini-vite-example/src/main.ts' 57 | // 文件路径与模块对象的映射,用来跟踪具有相同文件路径的模块节点(文件路径只有一个) 58 | // 主要用于模块解析、模块依赖管理、模块热更新和模块缓存管理 59 | fileToModulesMap = new Map>(); 60 | // 主要包括src/App.vue, node_modules中的第三方依赖等 61 | // 主要作用是确保指定的模块路径是安全的,vite核心模块不会被修改或覆盖。 62 | // 可以指定这些第三方模块的路径,确保它们不会被修改。 63 | // 这有助于保护核心模块和第三方模块的完整性,并避免意外冲突和覆盖。 64 | safeModulesPath = new Set(); 65 | 66 | // key: '/src/main.ts' 67 | // 将未解析的 URL 与相应的模块进行关联 68 | // 主要用于模块解析、模块加载、模块热更新和模块缓存管理等功 69 | _unresolvedUrlToModuleMap = new Map< 70 | string, 71 | Promise | ModuleNode 72 | >(); 73 | 74 | constructor( 75 | private resolveId: (url: string) => Promise 76 | ) {} 77 | 78 | async getModuleByUrl(rawUrl: string): Promise { 79 | rawUrl = removeImportQuery(removeTimestampQuery(rawUrl)); 80 | const mod = this._getUnresolvedUrlToModule(rawUrl); 81 | if (mod) { 82 | return mod; 83 | } 84 | 85 | const [url] = await this._resolveUrl(rawUrl); 86 | return this.urlToModuleMap.get(url); 87 | } 88 | 89 | getModuleById(id: string): ModuleNode | undefined { 90 | return this.idToModuleMap.get(removeTimestampQuery(id)); 91 | } 92 | 93 | getModulesByFile(file: string): Set | undefined { 94 | return this.fileToModulesMap.get(file); 95 | } 96 | 97 | onFileChange(file: string): void { 98 | const mods = this.getModulesByFile(file); 99 | if (mods) { 100 | const seen = new Set(); 101 | mods.forEach((mod) => { 102 | // 对受到文件变化影响的模块的标记成失效状态 103 | // 为了确保模块系统能够及时更新和重新加载这些模块 104 | // 文件发生变化,或文件删除时,将其标记为失效状态 105 | // 然后,模块系统在下一次需要使用到这些模块时, 106 | // 会重新加载、解析和执行这些模块,从而使模块的最新状态得以反映出来 107 | this.invalidateModule(mod, seen); 108 | }); 109 | } 110 | } 111 | 112 | invalidateModule( 113 | mod: ModuleNode, 114 | seen: Set = new Set(), 115 | timestamp: number = Date.now(), 116 | isHmr: boolean = false 117 | ): void { 118 | if (seen.has(mod)) { 119 | return; 120 | } 121 | seen.add(mod); 122 | if (isHmr) { 123 | mod.lastHMRTimestamp = timestamp; 124 | } else { 125 | mod.lastInvalidationTimestamp = timestamp; 126 | } 127 | 128 | mod.transformResult = null; 129 | mod.ssrTransformResult = null; 130 | mod.ssrModule = null; 131 | mod.ssrError = null; 132 | mod.importers.forEach((importer) => { 133 | if (!importer.acceptedHmrDeps.has(mod)) { 134 | this.invalidateModule(importer, seen, timestamp, isHmr); 135 | } 136 | }); 137 | } 138 | 139 | async updateModuleInfo( 140 | mod: ModuleNode, 141 | importedModules: Set, 142 | importedBindings: Map> | null, 143 | acceptedModules: Set, 144 | acceptedExports: Set | null, 145 | isSelfAccepting: boolean 146 | ): Promise | undefined> { 147 | // 是否接收自身模块的热更新 148 | mod.isSelfAccepting = isSelfAccepting; 149 | // 将所有的imports模块存储到prevImports中 150 | const prevImports = mod.importedModules; 151 | // 不会再被import 152 | let noLongerImported: Set | undefined; 153 | // 存储异步解析的promise对象 154 | let resolvePromises = []; 155 | // 存储每个异步解析的结果 156 | let resolveResults = new Array(importedModules.size); 157 | let index = 0; 158 | // 绑定节点依赖关系 159 | for (const imported of importedModules) { 160 | const nextIndex = index++; 161 | if (typeof imported === "string") { 162 | resolvePromises.push( 163 | this.ensureEntryFromUrl(imported).then((dep) => { 164 | dep.importers.add(mod); 165 | resolveResults[nextIndex] = dep; 166 | }) 167 | ); 168 | } else { 169 | imported.importers.add(mod); 170 | resolveResults[nextIndex] = imported; 171 | } 172 | } 173 | 174 | if (resolvePromises.length) { 175 | // 等待所有的异步解析操作完成 176 | await Promise.all(resolvePromises); 177 | } 178 | // nextImports保存着所有解析出来的import模块的 179 | const nextImports = (mod.importedModules = new Set(resolveResults)); 180 | // prevImports存储着所有的imports模块 181 | // 与nextImports做对比,判断模块是否已经被解析了 182 | // 主要是为了更新模块的导入管理,移除不需要被导入的模块 183 | prevImports.forEach((dep) => { 184 | if (!nextImports.has(dep)) { 185 | dep.importers.delete(mod); 186 | if (!dep.importers.size) { 187 | (noLongerImported || (noLongerImported = new Set())).add(dep); 188 | } 189 | } 190 | }); 191 | 192 | resolvePromises = []; 193 | resolveResults = new Array(acceptedModules.size); 194 | index = 0; 195 | // 更新最新的热更新模块的信息 196 | for (const accepted of acceptedModules) { 197 | const nextIndex = index++; 198 | if (typeof accepted === "string") { 199 | resolvePromises.push( 200 | this.ensureEntryFromUrl(accepted).then((dep) => { 201 | resolveResults[nextIndex] = dep; 202 | }) 203 | ); 204 | } else { 205 | resolveResults[nextIndex] = accepted; 206 | } 207 | } 208 | 209 | if (resolvePromises.length) { 210 | await Promise.all(resolvePromises); 211 | } 212 | 213 | mod.acceptedHmrDeps = new Set(resolveResults); 214 | 215 | mod.acceptedHmrExports = acceptedExports; 216 | // 当前模块导入的绑定关系的映射 217 | mod.importedBindings = importedBindings; 218 | return noLongerImported; 219 | } 220 | 221 | async ensureEntryFromUrl( 222 | rawUrl: string, 223 | setIsSelfAccepting = true 224 | ): Promise { 225 | return this._ensureEntryFromUrl(rawUrl, setIsSelfAccepting); 226 | } 227 | 228 | async _ensureEntryFromUrl( 229 | rawUrl: string, 230 | setIsSelfAccepting = true, 231 | resolved?: PartialResolvedId 232 | ): Promise { 233 | // 移除url上的时间戳查询参数,得到一个经过处理后的url 234 | rawUrl = removeImportQuery(removeTimestampQuery(rawUrl)); 235 | // 通过url判断_unresolvedUrlToModuleMap是否存在未解析的模块 236 | // 有就直接返回,没有就接着往下处理 237 | let mod = this._getUnresolvedUrlToModule(rawUrl); 238 | if (mod) { 239 | return mod; 240 | } 241 | // 异步地解析 URL 并创建相应的模块节点 (mod) 242 | const modPromise = (async () => { 243 | // 获取解析后的 URL、解析后的标识符 resolvedId 和元数据 meta 244 | const [url, resolvedId, meta] = await this._resolveUrl(rawUrl, resolved); 245 | // 通过 resolvedId从idToModuleMap 中找到对应的模块(mod) 246 | mod = this.idToModuleMap.get(resolvedId); 247 | // 如果没有的话就创建一个新的mod 248 | if (!mod) { 249 | mod = new ModuleNode(url, setIsSelfAccepting); 250 | 251 | if (meta) mod.meta = meta; 252 | // 将url与模块(mod)关联起来并存储到urlToModuleMap中 253 | this.urlToModuleMap.set(url, mod); 254 | mod.id = resolvedId; 255 | // 模块id存到idToModuleMap中 256 | this.idToModuleMap.set(resolvedId, mod); 257 | // 解析出file(文件绝对路径) 258 | const file = (mod.file = cleanUrl(resolvedId)); 259 | // 判断file是否已经存储到fileToModulesMap中 260 | let fileMappedModules = this.fileToModulesMap.get(file); 261 | // 没有的话,file存到fileToModulesMap中 262 | if (!fileMappedModules) { 263 | fileMappedModules = new Set(); 264 | this.fileToModulesMap.set(file, fileMappedModules); 265 | } 266 | // fileToModulesMap是一个一对多的数据结构 267 | // 它的key为文件路径,value为模块(mode) 268 | // 有可能多个模块引用了同一个文件作为它们的依赖项 269 | // 或者模块被多个入口文件引用,或者在多个地方被动态引入。 270 | // 所以此处是将模块节点添加到与文件路径相关联的模块集合中, 271 | // 以便在文件更新时进行批量处理和更新 272 | fileMappedModules.add(mod); 273 | } else if (!this.urlToModuleMap.has(url)) { 274 | this.urlToModuleMap.set(url, mod); 275 | } 276 | 277 | this._setUnresolvedUrlToModule(rawUrl, mod); 278 | return mod; 279 | })(); 280 | // 调用 _setUnresolvedUrlToModule 两次的目的是 281 | // 为了确保未解析的 URL 在解析过程中能够正确关联到对应的模块节点, 282 | // 并且在需要获取模块的地方能够等待解析的 Promise 完成。 283 | this._setUnresolvedUrlToModule(rawUrl, modPromise); 284 | return modPromise; 285 | } 286 | 287 | async resolveUrl(url: string): Promise { 288 | url = removeImportQuery(removeTimestampQuery(url)); 289 | const mod = await this._getUnresolvedUrlToModule(url); 290 | if (mod?.id) { 291 | return [mod.url, mod.id, mod.meta]; 292 | } 293 | return this._resolveUrl(url); 294 | } 295 | 296 | _getUnresolvedUrlToModule( 297 | url: string 298 | ): Promise | ModuleNode | undefined { 299 | return this._unresolvedUrlToModuleMap.get(url); 300 | } 301 | 302 | _setUnresolvedUrlToModule( 303 | url: string, 304 | mod: Promise | ModuleNode 305 | ): void { 306 | this._unresolvedUrlToModuleMap.set(url, mod); 307 | } 308 | 309 | async _resolveUrl( 310 | url: string, 311 | alreadyResolved?: PartialResolvedId 312 | ): Promise { 313 | const resolved = alreadyResolved ?? (await this.resolveId(url)); 314 | const resolvedId = resolved?.id || url; 315 | if ( 316 | url !== resolvedId && 317 | !url.includes("\0") && 318 | !url.startsWith(`virtual:`) 319 | ) { 320 | const ext = extname(cleanUrl(resolvedId)); 321 | if (ext) { 322 | const pathname = cleanUrl(url); 323 | if (!pathname.endsWith(ext)) { 324 | url = pathname + ext + url.slice(pathname.length); 325 | } 326 | } 327 | } 328 | return [url, resolvedId, resolved?.meta]; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/searchRoot.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import { dirname, join } from "node:path"; 3 | import { isFileReadable } from "../utils"; 4 | 5 | const ROOT_FILES = ["pnpm-workspace.yaml", "lerna.json"]; 6 | 7 | function hasPackageJSON(root: string) { 8 | const path = join(root, "package.json"); 9 | return fs.existsSync(path); 10 | } 11 | 12 | export function searchForPackageRoot(current: string, root = current): string { 13 | if (hasPackageJSON(current)) return current; 14 | 15 | const dir = dirname(current); 16 | if (!dir || dir === current) return root; 17 | 18 | return searchForPackageRoot(dir, root); 19 | } 20 | /**搜索工作区根目录 */ 21 | export function searchForWorkspaceRoot( 22 | current: string, 23 | root = searchForPackageRoot(current) 24 | ): string { 25 | if (hasRootFile(current)) return current; 26 | if (hasWorkspacePackageJSON(current)) return current; 27 | 28 | const dir = dirname(current); 29 | if (!dir || dir === current) return root; 30 | 31 | return searchForWorkspaceRoot(dir, root); 32 | } 33 | 34 | function hasRootFile(root: string): boolean { 35 | return ROOT_FILES.some((file) => fs.existsSync(join(root, file))); 36 | } 37 | 38 | function hasWorkspacePackageJSON(root: string): boolean { 39 | const path = join(root, "package.json"); 40 | if (!isFileReadable(path)) { 41 | return false; 42 | } 43 | const content = JSON.parse(fs.readFileSync(path, "utf-8")) || {}; 44 | return !!content.workspaces; 45 | } 46 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/send.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IncomingMessage, 3 | OutgoingHttpHeaders, 4 | ServerResponse, 5 | } from "node:http"; 6 | import type { SourceMap } from "rollup"; 7 | import getEtag from "etag"; 8 | 9 | export interface SendOptions { 10 | etag?: string; 11 | cacheControl?: string; 12 | headers?: OutgoingHttpHeaders; 13 | map?: SourceMap | null; 14 | } 15 | 16 | const alias: Record = { 17 | js: "application/javascript", 18 | css: "text/css", 19 | html: "text/html", 20 | json: "application/json", 21 | }; 22 | /** 23 | * 将经过转换后的 HTML 响应发送给客户端, 24 | * 并指定内容类型和自定义的响应头部信息 25 | */ 26 | export function send( 27 | req: IncomingMessage, 28 | res: ServerResponse, 29 | content: string | Buffer, 30 | type: string, 31 | options: SendOptions 32 | ): void { 33 | const { 34 | etag = getEtag(content, { weak: true }), 35 | cacheControl = "no-cache", 36 | headers, 37 | } = options; 38 | 39 | if (res.writableEnded) { 40 | return; 41 | } 42 | 43 | if (req.headers["if-none-match"] === etag) { 44 | res.statusCode = 304; 45 | res.end(); 46 | return; 47 | } 48 | 49 | res.setHeader("Content-Type", alias[type] || type); 50 | res.setHeader("Cache-Control", cacheControl); 51 | res.setHeader("Etag", etag); 52 | 53 | if (headers) { 54 | for (const name in headers) { 55 | res.setHeader(name, headers[name]!); 56 | } 57 | } 58 | 59 | res.statusCode = 200; 60 | res.end(content); 61 | return; 62 | } 63 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/transformRequest.ts: -------------------------------------------------------------------------------- 1 | import { TransformOptions } from "vite"; 2 | import { ViteDevServer } from "."; 3 | import { 4 | blankReplacer, 5 | cleanUrl, 6 | createDebugger, 7 | ensureWatchedFile, 8 | isObject, 9 | prettifyUrl, 10 | removeTimestampQuery, 11 | timeFrom, 12 | } from "../utils"; 13 | import { promises as fs } from "node:fs"; 14 | import convertSourceMap from "convert-source-map"; 15 | import getEtag from "etag"; 16 | import type { SourceDescription, SourceMap } from "rollup"; 17 | import { isFileServingAllowed } from "./middlewares/static"; 18 | import colors from "picocolors"; 19 | import path from "node:path"; 20 | 21 | const debugLoad = createDebugger("vite:load"); 22 | const debugTransform = createDebugger("vite:transform"); 23 | const debugCache = createDebugger("vite:cache"); 24 | 25 | export interface TransformResult { 26 | code: string; 27 | map: SourceMap | null; 28 | etag?: string; 29 | deps?: string[]; 30 | dynamicDeps?: string[]; 31 | } 32 | 33 | /**对transformMiddleware拦截的内容进行transform和resolveId */ 34 | export function transformRequest( 35 | url: string, 36 | server: ViteDevServer, 37 | options: TransformOptions = {} 38 | ): Promise { 39 | const cacheKey = (options.html ? "html:" : "") + url; 40 | const timestamp = Date.now(); 41 | // 检查缓存中是否存在正在处理的请求 42 | // 如果存在,并且缓存的请求仍然有效,则直接使用缓存的请求结果; 43 | // 否则,中止缓存的请求,并重新处理该请求。 44 | // 可以避免重复处理相同的请求,并确保在模块状态发生变化时获取最新的结果。 45 | const pending = server._pendingRequests.get(cacheKey); 46 | if (pending) { 47 | return server.moduleGraph 48 | .getModuleByUrl(removeTimestampQuery(url)) 49 | .then((module) => { 50 | if (!module || pending.timestamp > module.lastInvalidationTimestamp) { 51 | return pending.request; 52 | } else { 53 | pending.abort(); 54 | return transformRequest(url, server, options); 55 | } 56 | }); 57 | } 58 | // 利用pluginContainer和moduleGraph对资源进行转换处理,并将转换后的结果返回 59 | const request = doTransform(url, server, options, timestamp); 60 | 61 | let cleared = false; 62 | const clearCache = () => { 63 | if (!cleared) { 64 | server._pendingRequests.delete(cacheKey); 65 | cleared = true; 66 | } 67 | }; 68 | 69 | server._pendingRequests.set(cacheKey, { 70 | request, 71 | timestamp, 72 | abort: clearCache, 73 | }); 74 | 75 | return request.finally(clearCache); 76 | } 77 | /**transform核心方法 */ 78 | async function doTransform( 79 | url: string, 80 | server: ViteDevServer, 81 | options: TransformOptions, 82 | timestamp: number 83 | ) { 84 | url = removeTimestampQuery(url); 85 | 86 | const { config, pluginContainer } = server; 87 | const prettyUrl = debugCache ? prettifyUrl(url, config.root) : ""; 88 | // 判断当前url有没有被加进模块信息中 89 | const module = await server.moduleGraph.getModuleByUrl(url); 90 | // 如果有就命中缓存 91 | const cached = module && module.transformResult; 92 | if (cached) { 93 | debugCache?.(`[memory] ${prettyUrl}`); 94 | return cached; 95 | } 96 | // 否则调用 PluginContainer 的 resolveId 和 load 方法对进行模块加载 97 | const id = 98 | module?.id ?? (await pluginContainer.resolveId(url, undefined))?.id ?? url; 99 | // 对文件进行transfomr和load,将处理过后的资源文件放到模块管理图中 100 | // 并将模块的文件添加到热更新监听器中 101 | const result = loadAndTransform(id, url, server, options, timestamp); 102 | return result; 103 | } 104 | /**tranform时,对文件资源进行处理,并添加到模块管理图中进行管理 */ 105 | async function loadAndTransform( 106 | id: string, 107 | url: string, 108 | server: ViteDevServer, 109 | options: TransformOptions, 110 | timestamp: number 111 | ) { 112 | const { config, pluginContainer, moduleGraph, watcher } = server; 113 | const { root, logger } = config; 114 | const prettyUrl = 115 | debugLoad || debugTransform ? prettifyUrl(url, config.root) : ""; 116 | 117 | const file = cleanUrl(id); 118 | 119 | let code: string | null = null; 120 | let map: SourceDescription["map"] = null; 121 | 122 | const loadStart = debugLoad ? performance.now() : 0; 123 | const loadResult = await pluginContainer.load(id); 124 | if (loadResult == null) { 125 | if (options.html && !id.endsWith(".html")) { 126 | return null; 127 | } 128 | if (isFileServingAllowed(file, server)) { 129 | try { 130 | code = await fs.readFile(file, "utf-8"); 131 | debugLoad?.(`${timeFrom(loadStart)} [fs] ${prettyUrl}`); 132 | } catch (e) { 133 | if (e.code !== "ENOENT") { 134 | throw e; 135 | } 136 | } 137 | } 138 | if (code) { 139 | try { 140 | map = ( 141 | convertSourceMap.fromSource(code) || 142 | (await convertSourceMap.fromMapFileSource( 143 | code, 144 | createConvertSourceMapReadMap(file) 145 | )) 146 | )?.toObject(); 147 | 148 | code = code.replace( 149 | convertSourceMap.mapFileCommentRegex, 150 | blankReplacer 151 | ); 152 | } catch (e) { 153 | logger.warn(`Failed to load source map for ${url}.`, { 154 | timestamp: true, 155 | }); 156 | } 157 | } 158 | } else { 159 | debugLoad?.(`${timeFrom(loadStart)} [plugin] ${prettyUrl}`); 160 | if (isObject(loadResult)) { 161 | code = loadResult.code; 162 | map = loadResult.map; 163 | } else { 164 | code = loadResult; 165 | } 166 | } 167 | if (code == null) { 168 | throw new Error(`Failed to load url`); 169 | } 170 | // 创建ModuleNode 171 | const mod = await moduleGraph.ensureEntryFromUrl(url); 172 | // 将模块添加进热更新监听列表中 173 | ensureWatchedFile(watcher, mod.file, root); 174 | const transformStart = debugTransform ? performance.now() : 0; 175 | const transformResult = await pluginContainer.transform(code, id, { 176 | inMap: map, 177 | }); 178 | if ( 179 | transformResult == null || 180 | (isObject(transformResult) && transformResult.code == null) 181 | ) { 182 | debugTransform?.( 183 | timeFrom(transformStart) + colors.dim(` [skipped] ${prettyUrl}`) 184 | ); 185 | } else { 186 | debugTransform?.(`${timeFrom(transformStart)} ${prettyUrl}`); 187 | code = transformResult.code!; 188 | map = transformResult.map; 189 | } 190 | 191 | if (map && mod.file) { 192 | map = (typeof map === "string" ? JSON.parse(map) : map) as SourceMap; 193 | 194 | if (path.isAbsolute(mod.file)) { 195 | for ( 196 | let sourcesIndex = 0; 197 | sourcesIndex < map.sources.length; 198 | ++sourcesIndex 199 | ) { 200 | const sourcePath = map.sources[sourcesIndex]; 201 | if (sourcePath) { 202 | if (path.isAbsolute(sourcePath)) { 203 | map.sources[sourcesIndex] = path.relative( 204 | path.dirname(mod.file), 205 | sourcePath 206 | ); 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | const result = { 214 | code, 215 | map, 216 | etag: getEtag(code, { weak: true }), // NOTE 协商缓存 217 | } as TransformResult; 218 | // 检查模块是否过时,是否需要更新 219 | if (timestamp > mod.lastInvalidationTimestamp) { 220 | mod.transformResult = result; 221 | } 222 | 223 | return result; 224 | } 225 | /**创建sourceMap */ 226 | function createConvertSourceMapReadMap(originalFileName: string) { 227 | return (filename: string) => { 228 | return fs.readFile( 229 | path.resolve(path.dirname(originalFileName), filename), 230 | "utf-8" 231 | ); 232 | }; 233 | } 234 | -------------------------------------------------------------------------------- /packages/vite/src/node/server/ws.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from "node:http"; 2 | import { STATUS_CODES, createServer as createHttpServer } from "node:http"; 3 | import { 4 | ErrorPayload, 5 | HMRPayload, 6 | InferCustomEventPayload, 7 | WebSocketClient, 8 | WebSocketCustomListener, 9 | WebSocket as WebSocketTypes, 10 | } from "vite"; 11 | import { ResolvedConfig } from "../config"; 12 | import { WebSocketServer as WebSocketServerRaw } from "ws"; 13 | import colors from "picocolors"; 14 | import type { WebSocket as WebSocketRaw } from "ws"; 15 | import { isObject } from "../utils"; 16 | import { Socket } from "node:net"; 17 | 18 | export const HMR_HEADER = "vite-hmr"; 19 | export interface WebSocketServer { 20 | listen(): void; 21 | clients: Set; 22 | send(payload: HMRPayload): void; 23 | send(event: T, payload?: InferCustomEventPayload): void; 24 | close(): Promise; 25 | on: WebSocketTypes.Server["on"] & { 26 | ( 27 | event: T, 28 | listener: WebSocketCustomListener> 29 | ): void; 30 | }; 31 | off: WebSocketTypes.Server["off"] & { 32 | (event: string, listener: Function): void; 33 | }; 34 | } 35 | const wsServerEvents = [ 36 | "connection", 37 | "error", 38 | "headers", 39 | "listening", 40 | "message", 41 | ]; 42 | /**创建websocket服务器,并将本地服务器与浏览器建立通信 */ 43 | export function createWebSocketServer( 44 | server: Server | null, 45 | config: ResolvedConfig 46 | ): WebSocketServer { 47 | let wss: WebSocketServerRaw; 48 | let wsHttpServer: Server | undefined = undefined; 49 | 50 | const hmr = isObject(config.server.hmr) && config.server.hmr; 51 | const hmrServer = hmr && hmr.server; 52 | const hmrPort = hmr && hmr.port; 53 | const portsAreCompatible = !hmrPort || hmrPort === config.server.port; 54 | const wsServer = hmrServer || (portsAreCompatible && server); 55 | const customListeners = new Map>>(); 56 | const clientsMap = new WeakMap(); 57 | const port = hmrPort || 24678; 58 | const host = (hmr && hmr.host) || undefined; 59 | 60 | if (wsServer) { 61 | wss = new WebSocketServerRaw({ noServer: true }); 62 | wsServer.on("upgrade", (req, socket, head) => { 63 | if (req.headers["sec-websocket-protocol"] === HMR_HEADER) { 64 | wss.handleUpgrade(req, socket as Socket, head, (ws) => { 65 | wss.emit("connection", ws, req); 66 | }); 67 | } 68 | }); 69 | } else { 70 | const route = ((_, res) => { 71 | const statusCode = 426; 72 | const body = STATUS_CODES[statusCode]; 73 | if (!body) 74 | throw new Error(`No body text found for the ${statusCode} status code`); 75 | 76 | res.writeHead(statusCode, { 77 | "Content-Length": body.length, 78 | "Content-Type": "text/plain", 79 | }); 80 | res.end(body); 81 | }) as Parameters[1]; 82 | wsHttpServer = createHttpServer(route); 83 | wss = new WebSocketServerRaw({ server: wsHttpServer }); 84 | } 85 | 86 | wss.on("connection", (socket) => { 87 | socket.on("message", (raw) => { 88 | if (!customListeners.size) return; 89 | let parsed: any; 90 | try { 91 | parsed = JSON.parse(String(raw)); 92 | } catch (e) {} 93 | if (!parsed || parsed.type !== "custom" || !parsed.event) return; 94 | const listeners = customListeners.get(parsed.event); 95 | if (!listeners?.size) return; 96 | const client = getSocketClient(socket); 97 | listeners.forEach((listener) => listener(parsed.data, client)); 98 | }); 99 | socket.on("error", (err) => { 100 | config.logger.error(`${colors.red(`ws error:`)}\n${err.stack}`, { 101 | timestamp: true, 102 | error: err, 103 | }); 104 | }); 105 | socket.send(JSON.stringify({ type: "connected" })); 106 | if (bufferedError) { 107 | socket.send(JSON.stringify(bufferedError)); 108 | bufferedError = null; 109 | } 110 | }); 111 | 112 | wss.on("error", (e: Error & { code: string }) => { 113 | if (e.code === "EADDRINUSE") { 114 | config.logger.error( 115 | colors.red(`WebSocket server error: Port is already in use`), 116 | { error: e } 117 | ); 118 | } else { 119 | config.logger.error( 120 | colors.red(`WebSocket server error:\n${e.stack || e.message}`), 121 | { error: e } 122 | ); 123 | } 124 | }); 125 | let bufferedError: ErrorPayload | null = null; 126 | /**使用 WebSocket,实时服务器和客户端之间建立通信 */ 127 | function getSocketClient(socket: WebSocketRaw) { 128 | if (!clientsMap.has(socket)) { 129 | clientsMap.set(socket, { 130 | send: (...args) => { 131 | let payload: HMRPayload; 132 | if (typeof args[0] === "string") { 133 | payload = { 134 | type: "custom", 135 | event: args[0], 136 | data: args[1], 137 | }; 138 | } else { 139 | payload = args[0]; 140 | } 141 | socket.send(JSON.stringify(payload)); 142 | }, 143 | socket, 144 | }); 145 | } 146 | return clientsMap.get(socket)!; 147 | } 148 | 149 | return { 150 | listen: () => { 151 | wsHttpServer?.listen(port, host); 152 | }, 153 | on: ((event: string, fn: () => void) => { 154 | if (wsServerEvents.includes(event)) wss.on(event, fn); 155 | else { 156 | if (!customListeners.has(event)) { 157 | customListeners.set(event, new Set()); 158 | } 159 | customListeners.get(event)!.add(fn); 160 | } 161 | }) as WebSocketServer["on"], 162 | off: ((event: string, fn: () => void) => { 163 | if (wsServerEvents.includes(event)) { 164 | wss.off(event, fn); 165 | } else { 166 | customListeners.get(event)?.delete(fn); 167 | } 168 | }) as WebSocketServer["off"], 169 | 170 | get clients() { 171 | return new Set(Array.from(wss.clients).map(getSocketClient)); 172 | }, 173 | 174 | send(...args: any[]) { 175 | let payload: HMRPayload; 176 | if (typeof args[0] === "string") { 177 | payload = { 178 | type: "custom", 179 | event: args[0], 180 | data: args[1], 181 | }; 182 | } else { 183 | payload = args[0]; 184 | } 185 | 186 | if (payload.type === "error" && !wss.clients.size) { 187 | bufferedError = payload; 188 | return; 189 | } 190 | 191 | const stringified = JSON.stringify(payload); 192 | wss.clients.forEach((client) => { 193 | if (client.readyState === 1) { 194 | client.send(stringified); 195 | } 196 | }); 197 | }, 198 | 199 | close() { 200 | return new Promise((resolve, reject) => { 201 | wss.clients.forEach((client) => { 202 | client.terminate(); 203 | }); 204 | wss.close((err) => { 205 | if (err) { 206 | reject(err); 207 | } else { 208 | if (wsHttpServer) { 209 | wsHttpServer.close((err) => { 210 | if (err) { 211 | reject(err); 212 | } else { 213 | resolve(); 214 | } 215 | }); 216 | } else { 217 | resolve(); 218 | } 219 | } 220 | }); 221 | }); 222 | }, 223 | }; 224 | } 225 | -------------------------------------------------------------------------------- /packages/vite/src/node/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import { ViteDevServer } from "./server"; 2 | import colors from "picocolors"; 3 | import { isDefined } from "./utils"; 4 | 5 | const BASE_SHORTCUTS: CLIShortcut[] = [ 6 | { 7 | key: "r", 8 | description: "restart the server", 9 | async action(server) { 10 | await server.restart(); 11 | }, 12 | }, 13 | { 14 | key: "u", 15 | description: "show server url", 16 | action(server) { 17 | server.config.logger.info(""); 18 | server.printUrls(); 19 | }, 20 | }, 21 | { 22 | key: "q", 23 | description: "quit", 24 | async action(server) { 25 | await server.close().finally(() => process.exit()); 26 | }, 27 | }, 28 | ]; 29 | 30 | export type BindShortcutsOptions = { 31 | print?: boolean; 32 | customShortcuts?: (CLIShortcut | undefined | null)[]; 33 | }; 34 | 35 | export type CLIShortcut = { 36 | key: string; 37 | description: string; 38 | action(server: ViteDevServer): void | Promise; 39 | }; 40 | 41 | export function bindShortcuts( 42 | server: ViteDevServer, 43 | opts: BindShortcutsOptions 44 | ): void { 45 | if (!server.httpServer || !process.stdin.isTTY || process.env.CI) { 46 | return; 47 | } 48 | server._shortcutsOptions = opts; 49 | 50 | if (opts.print) { 51 | server.config.logger.info( 52 | colors.dim(colors.green(" ➜")) + 53 | colors.dim(" press ") + 54 | colors.bold("h") + 55 | colors.dim(" to show help") 56 | ); 57 | } 58 | 59 | const shortcuts = (opts.customShortcuts ?? []) 60 | .filter(isDefined) 61 | .concat(BASE_SHORTCUTS); 62 | 63 | let actionRunning = false; 64 | 65 | const onInput = async (input: string) => { 66 | if (input === "\x03" || input === "\x04") { 67 | await server.close().finally(() => process.exit(1)); 68 | return; 69 | } 70 | 71 | if (actionRunning) return; 72 | 73 | if (input === "h") { 74 | server.config.logger.info( 75 | [ 76 | "", 77 | colors.bold(" Shortcuts"), 78 | ...shortcuts.map( 79 | (shortcut) => 80 | colors.dim(" press ") + 81 | colors.bold(shortcut.key) + 82 | colors.dim(` to ${shortcut.description}`) 83 | ), 84 | ].join("\n") 85 | ); 86 | } 87 | 88 | const shortcut = shortcuts.find((shortcut) => shortcut.key === input); 89 | if (!shortcut) return; 90 | 91 | actionRunning = true; 92 | await shortcut.action(server); 93 | actionRunning = false; 94 | }; 95 | 96 | process.stdin.setRawMode(true); 97 | 98 | process.stdin.on("data", onInput).setEncoding("utf8").resume(); 99 | 100 | server.httpServer.on("close", () => { 101 | process.stdin.off("data", onInput).pause(); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /packages/vite/src/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["./", "../dep-types", "../types"], 4 | "exclude": ["**/__tests__"], 5 | "compilerOptions": { 6 | "lib": ["ESNext", "DOM"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/vite/src/node/watch.ts: -------------------------------------------------------------------------------- 1 | import glob from "fast-glob"; 2 | import type { WatchOptions } from "dep-types/chokidar"; 3 | import type { ResolvedConfig } from "."; 4 | /**热更新Chokidar 配置项 */ 5 | export function resolveChokidarOptions( 6 | config: ResolvedConfig, 7 | options: WatchOptions | undefined 8 | ): WatchOptions { 9 | const { ignored = [], ...otherOptions } = options ?? {}; 10 | 11 | const resolvedWatchOptions: WatchOptions = { 12 | ignored: [ 13 | "**/.git/**", 14 | "**/node_modules/**", 15 | "**/test-results/**", 16 | glob.escapePath(config.cacheDir) + "/**", 17 | ...(Array.isArray(ignored) ? ignored : [ignored]), 18 | ], 19 | ignoreInitial: true, 20 | ignorePermissionErrors: true, 21 | ...otherOptions, 22 | }; 23 | 24 | return resolvedWatchOptions; 25 | } 26 | -------------------------------------------------------------------------------- /packages/vite/src/types/alias.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Types from https://github.com/rollup/plugins/blob/master/packages/alias/types/index.d.ts 3 | Inlined because the plugin is bundled. 4 | 5 | https://github.com/rollup/plugins/blob/master/LICENSE 6 | 7 | The MIT License (MIT) 8 | 9 | Copyright (c) 2019 RollupJS Plugin Contributors (https://github.com/rollup/plugins/graphs/contributors) 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | */ 29 | 30 | import type { PluginHooks } from 'rollup' 31 | 32 | export interface Alias { 33 | find: string | RegExp 34 | replacement: string 35 | /** 36 | * Instructs the plugin to use an alternative resolving algorithm, 37 | * rather than the Rollup's resolver. 38 | * @default null 39 | */ 40 | customResolver?: ResolverFunction | ResolverObject | null 41 | } 42 | 43 | export type MapToFunction = T extends Function ? T : never 44 | 45 | export type ResolverFunction = MapToFunction 46 | 47 | export interface ResolverObject { 48 | buildStart?: PluginHooks['buildStart'] 49 | resolveId: ResolverFunction 50 | } 51 | 52 | /** 53 | * Specifies an `Object`, or an `Array` of `Object`, 54 | * which defines aliases used to replace values in `import` or `require` statements. 55 | * With either format, the order of the entries is important, 56 | * in that the first defined rules are applied first. 57 | * 58 | * This is passed to \@rollup/plugin-alias as the "entries" field 59 | * https://github.com/rollup/plugins/tree/master/packages/alias#entries 60 | */ 61 | export type AliasOptions = readonly Alias[] | { [find: string]: string } 62 | -------------------------------------------------------------------------------- /packages/vite/src/types/anymatch.d.ts: -------------------------------------------------------------------------------- 1 | export type AnymatchFn = (testString: string) => boolean 2 | export type AnymatchPattern = string | RegExp | AnymatchFn 3 | type AnymatchMatcher = AnymatchPattern | AnymatchPattern[] 4 | 5 | export { AnymatchMatcher as Matcher } 6 | -------------------------------------------------------------------------------- /packages/vite/src/types/chokidar.d.ts: -------------------------------------------------------------------------------- 1 | // Inlined to avoid extra dependency (chokidar is bundled in the published build) 2 | 3 | // https://github.com/paulmillr/chokidar/blob/master/types/index.d.ts 4 | // MIT Licensed https://github.com/paulmillr/chokidar/blob/master/LICENSE 5 | 6 | /** 7 | The MIT License (MIT) 8 | 9 | Copyright (c) 2012-2019 Paul Miller (https://paulmillr.com), Elan Shanker 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the “Software”), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | */ 29 | /// 30 | 31 | import type * as fs from 'node:fs' 32 | import { EventEmitter } from 'node:events' 33 | import type { Matcher } from './anymatch' 34 | 35 | export class FSWatcher extends EventEmitter implements fs.FSWatcher { 36 | options: WatchOptions 37 | 38 | /** 39 | * Constructs a new FSWatcher instance with optional WatchOptions parameter. 40 | */ 41 | constructor(options?: WatchOptions) 42 | 43 | /** 44 | * Add files, directories, or glob patterns for tracking. Takes an array of strings or just one 45 | * string. 46 | */ 47 | add(paths: string | ReadonlyArray): this 48 | 49 | /** 50 | * Stop watching files, directories, or glob patterns. Takes an array of strings or just one 51 | * string. 52 | */ 53 | unwatch(paths: string | ReadonlyArray): this 54 | 55 | /** 56 | * Returns an object representing all the paths on the file system being watched by this 57 | * `FSWatcher` instance. The object's keys are all the directories (using absolute paths unless 58 | * the `cwd` option was used), and the values are arrays of the names of the items contained in 59 | * each directory. 60 | */ 61 | getWatched(): { 62 | [directory: string]: string[] 63 | } 64 | 65 | /** 66 | * Removes all listeners from watched files. 67 | */ 68 | close(): Promise 69 | 70 | on( 71 | event: 'add' | 'addDir' | 'change', 72 | listener: (path: string, stats?: fs.Stats) => void, 73 | ): this 74 | 75 | on( 76 | event: 'all', 77 | listener: ( 78 | eventName: 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir', 79 | path: string, 80 | stats?: fs.Stats, 81 | ) => void, 82 | ): this 83 | 84 | /** 85 | * Error occurred 86 | */ 87 | on(event: 'error', listener: (error: Error) => void): this 88 | 89 | /** 90 | * Exposes the native Node `fs.FSWatcher events` 91 | */ 92 | on( 93 | event: 'raw', 94 | listener: (eventName: string, path: string, details: any) => void, 95 | ): this 96 | 97 | /** 98 | * Fires when the initial scan is complete 99 | */ 100 | on(event: 'ready', listener: () => void): this 101 | 102 | on(event: 'unlink' | 'unlinkDir', listener: (path: string) => void): this 103 | 104 | on(event: string, listener: (...args: any[]) => void): this 105 | } 106 | 107 | export interface WatchOptions { 108 | /** 109 | * Indicates whether the process should continue to run as long as files are being watched. If 110 | * set to `false` when using `fsevents` to watch, no more events will be emitted after `ready`, 111 | * even if the process continues to run. 112 | */ 113 | persistent?: boolean 114 | 115 | /** 116 | * ([anymatch](https://github.com/micromatch/anymatch)-compatible definition) Defines files/paths to 117 | * be ignored. The whole relative or absolute path is tested, not just filename. If a function 118 | * with two arguments is provided, it gets called twice per path - once with a single argument 119 | * (the path), second time with two arguments (the path and the 120 | * [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object of that path). 121 | */ 122 | ignored?: Matcher 123 | 124 | /** 125 | * If set to `false` then `add`/`addDir` events are also emitted for matching paths while 126 | * instantiating the watching as chokidar discovers these file paths (before the `ready` event). 127 | */ 128 | ignoreInitial?: boolean 129 | 130 | /** 131 | * When `false`, only the symlinks themselves will be watched for changes instead of following 132 | * the link references and bubbling events through the link's path. 133 | */ 134 | followSymlinks?: boolean 135 | 136 | /** 137 | * The base directory from which watch `paths` are to be derived. Paths emitted with events will 138 | * be relative to this. 139 | */ 140 | cwd?: string 141 | 142 | /** 143 | * If set to true then the strings passed to .watch() and .add() are treated as literal path 144 | * names, even if they look like globs. 145 | * 146 | * @default false 147 | */ 148 | disableGlobbing?: boolean 149 | 150 | /** 151 | * Whether to use fs.watchFile (backed by polling), or fs.watch. If polling leads to high CPU 152 | * utilization, consider setting this to `false`. It is typically necessary to **set this to 153 | * `true` to successfully watch files over a network**, and it may be necessary to successfully 154 | * watch files in other non-standard situations. Setting to `true` explicitly on OS X overrides 155 | * the `useFsEvents` default. 156 | */ 157 | usePolling?: boolean 158 | 159 | /** 160 | * Whether to use the `fsevents` watching interface if available. When set to `true` explicitly 161 | * and `fsevents` is available this supersedes the `usePolling` setting. When set to `false` on 162 | * OS X, `usePolling: true` becomes the default. 163 | */ 164 | useFsEvents?: boolean 165 | 166 | /** 167 | * If relying upon the [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object that 168 | * may get passed with `add`, `addDir`, and `change` events, set this to `true` to ensure it is 169 | * provided even in cases where it wasn't already available from the underlying watch events. 170 | */ 171 | alwaysStat?: boolean 172 | 173 | /** 174 | * If set, limits how many levels of subdirectories will be traversed. 175 | */ 176 | depth?: number 177 | 178 | /** 179 | * Interval of file system polling. 180 | */ 181 | interval?: number 182 | 183 | /** 184 | * Interval of file system polling for binary files. ([see list of binary extensions](https://gi 185 | * thub.com/sindresorhus/binary-extensions/blob/master/binary-extensions.json)) 186 | */ 187 | binaryInterval?: number 188 | 189 | /** 190 | * Indicates whether to watch files that don't have read permissions if possible. If watching 191 | * fails due to `EPERM` or `EACCES` with this set to `true`, the errors will be suppressed 192 | * silently. 193 | */ 194 | ignorePermissionErrors?: boolean 195 | 196 | /** 197 | * `true` if `useFsEvents` and `usePolling` are `false`. Automatically filters out artifacts 198 | * that occur when using editors that use "atomic writes" instead of writing directly to the 199 | * source file. If a file is re-added within 100 ms of being deleted, Chokidar emits a `change` 200 | * event rather than `unlink` then `add`. If the default of 100 ms does not work well for you, 201 | * you can override it by setting `atomic` to a custom value, in milliseconds. 202 | */ 203 | atomic?: boolean | number 204 | 205 | /** 206 | * can be set to an object in order to adjust timing params: 207 | */ 208 | awaitWriteFinish?: AwaitWriteFinishOptions | boolean 209 | } 210 | 211 | export interface AwaitWriteFinishOptions { 212 | /** 213 | * Amount of time in milliseconds for a file size to remain constant before emitting its event. 214 | */ 215 | stabilityThreshold?: number 216 | 217 | /** 218 | * File size polling interval. 219 | */ 220 | pollInterval?: number 221 | } 222 | 223 | /** 224 | * produces an instance of `FSWatcher`. 225 | */ 226 | export function watch( 227 | paths: string | ReadonlyArray, 228 | options?: WatchOptions, 229 | ): FSWatcher 230 | -------------------------------------------------------------------------------- /packages/vite/src/types/connect.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as http from "node:http"; 3 | export namespace Connect { 4 | export type ServerHandle = HandleFunction | http.Server; 5 | 6 | export class IncomingMessage extends http.IncomingMessage { 7 | originalUrl?: http.IncomingMessage["url"] | undefined; 8 | } 9 | 10 | export type NextFunction = (err?: any) => void; 11 | 12 | export type SimpleHandleFunction = ( 13 | req: IncomingMessage, 14 | res: http.ServerResponse 15 | ) => void; 16 | export type NextHandleFunction = ( 17 | req: IncomingMessage, 18 | res: http.ServerResponse, 19 | next: NextFunction 20 | ) => void; 21 | export type ErrorHandleFunction = ( 22 | err: any, 23 | req: IncomingMessage, 24 | res: http.ServerResponse, 25 | next: NextFunction 26 | ) => void; 27 | export type HandleFunction = 28 | | SimpleHandleFunction 29 | | NextHandleFunction 30 | | ErrorHandleFunction; 31 | 32 | export interface ServerStackItem { 33 | route: string; 34 | handle: ServerHandle; 35 | } 36 | 37 | export interface Server extends NodeJS.EventEmitter { 38 | ( 39 | req: http.IncomingMessage, 40 | res: http.ServerResponse, 41 | next?: Function 42 | ): void; 43 | 44 | route: string; 45 | stack: ServerStackItem[]; 46 | 47 | /** 48 | * Utilize the given middleware `handle` to the given `route`, 49 | * defaulting to _/_. This "route" is the mount-point for the 50 | * middleware, when given a value other than _/_ the middleware 51 | * is only effective when that segment is present in the request's 52 | * pathname. 53 | * 54 | * For example if we were to mount a function at _/admin_, it would 55 | * be invoked on _/admin_, and _/admin/settings_, however it would 56 | * not be invoked for _/_, or _/posts_. 57 | */ 58 | use(fn: NextHandleFunction): Server; 59 | use(fn: HandleFunction): Server; 60 | use(route: string, fn: NextHandleFunction): Server; 61 | use(route: string, fn: HandleFunction): Server; 62 | 63 | /** 64 | * Handle server requests, punting them down 65 | * the middleware stack. 66 | */ 67 | handle( 68 | req: http.IncomingMessage, 69 | res: http.ServerResponse, 70 | next: Function 71 | ): void; 72 | 73 | /** 74 | * Listen for connections. 75 | * 76 | * This method takes the same arguments 77 | * as node's `http.Server#listen()`. 78 | * 79 | * HTTP and HTTPS: 80 | * 81 | * If you run your application both as HTTP 82 | * and HTTPS you may wrap them individually, 83 | * since your Connect "server" is really just 84 | * a JavaScript `Function`. 85 | * 86 | * var connect = require('connect') 87 | * , http = require('http') 88 | * , https = require('https'); 89 | * 90 | * var app = connect(); 91 | * 92 | * http.createServer(app).listen(80); 93 | * https.createServer(options, app).listen(443); 94 | */ 95 | listen( 96 | port: number, 97 | hostname?: string, 98 | backlog?: number, 99 | callback?: Function 100 | ): http.Server; 101 | listen(port: number, hostname?: string, callback?: Function): http.Server; 102 | listen(path: string, callback?: Function): http.Server; 103 | listen(handle: any, listeningListener?: Function): http.Server; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/vite/src/types/http-proxy.d.ts: -------------------------------------------------------------------------------- 1 | // Inlined to avoid extra dependency 2 | // MIT Licensed https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/LICENSE 3 | 4 | // Type definitions for node-http-proxy 1.17 5 | // Project: https://github.com/nodejitsu/node-http-proxy 6 | // Definitions by: Maxime LUCE 7 | // Florian Oellerich 8 | // Daniel Schmidt 9 | // Jordan Abreu 10 | // Samuel Bodin 11 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 12 | // TypeScript Version: 2.1 13 | 14 | /// 15 | 16 | import type * as net from 'node:net' 17 | import type * as http from 'node:http' 18 | import * as events from 'node:events' 19 | import type * as url from 'node:url' 20 | import type * as stream from 'node:stream' 21 | 22 | export namespace HttpProxy { 23 | export type ProxyTarget = ProxyTargetUrl | ProxyTargetDetailed 24 | 25 | export type ProxyTargetUrl = string | Partial 26 | 27 | export interface ProxyTargetDetailed { 28 | host: string 29 | port: number 30 | protocol?: string | undefined 31 | hostname?: string | undefined 32 | socketPath?: string | undefined 33 | key?: string | undefined 34 | passphrase?: string | undefined 35 | pfx?: Buffer | string | undefined 36 | cert?: string | undefined 37 | ca?: string | undefined 38 | ciphers?: string | undefined 39 | secureProtocol?: string | undefined 40 | } 41 | 42 | export type ErrorCallback = ( 43 | err: Error, 44 | req: http.IncomingMessage, 45 | res: http.ServerResponse, 46 | target?: ProxyTargetUrl, 47 | ) => void 48 | 49 | export class Server extends events.EventEmitter { 50 | /** 51 | * Creates the proxy server with specified options. 52 | * @param options - Config object passed to the proxy 53 | */ 54 | constructor(options?: ServerOptions) 55 | 56 | /** 57 | * Used for proxying regular HTTP(S) requests 58 | * @param req - Client request. 59 | * @param res - Client response. 60 | * @param options - Additional options. 61 | */ 62 | web( 63 | req: http.IncomingMessage, 64 | res: http.ServerResponse, 65 | options?: ServerOptions, 66 | callback?: ErrorCallback, 67 | ): void 68 | 69 | /** 70 | * Used for proxying regular HTTP(S) requests 71 | * @param req - Client request. 72 | * @param socket - Client socket. 73 | * @param head - Client head. 74 | * @param options - Additional options. 75 | */ 76 | ws( 77 | req: http.IncomingMessage, 78 | socket: unknown, 79 | head: unknown, 80 | options?: ServerOptions, 81 | callback?: ErrorCallback, 82 | ): void 83 | 84 | /** 85 | * A function that wraps the object in a webserver, for your convenience 86 | * @param port - Port to listen on 87 | */ 88 | listen(port: number): Server 89 | 90 | /** 91 | * A function that closes the inner webserver and stops listening on given port 92 | */ 93 | close(callback?: () => void): void 94 | 95 | /** 96 | * Creates the proxy server with specified options. 97 | * @param options - Config object passed to the proxy 98 | * @returns Proxy object with handlers for `ws` and `web` requests 99 | */ 100 | static createProxyServer(options?: ServerOptions): Server 101 | 102 | /** 103 | * Creates the proxy server with specified options. 104 | * @param options - Config object passed to the proxy 105 | * @returns Proxy object with handlers for `ws` and `web` requests 106 | */ 107 | static createServer(options?: ServerOptions): Server 108 | 109 | /** 110 | * Creates the proxy server with specified options. 111 | * @param options - Config object passed to the proxy 112 | * @returns Proxy object with handlers for `ws` and `web` requests 113 | */ 114 | static createProxy(options?: ServerOptions): Server 115 | 116 | addListener(event: string, listener: () => void): this 117 | on(event: string, listener: () => void): this 118 | on(event: 'error', listener: ErrorCallback): this 119 | on( 120 | event: 'start', 121 | listener: ( 122 | req: http.IncomingMessage, 123 | res: http.ServerResponse, 124 | target: ProxyTargetUrl, 125 | ) => void, 126 | ): this 127 | on( 128 | event: 'proxyReq', 129 | listener: ( 130 | proxyReq: http.ClientRequest, 131 | req: http.IncomingMessage, 132 | res: http.ServerResponse, 133 | options: ServerOptions, 134 | ) => void, 135 | ): this 136 | on( 137 | event: 'proxyRes', 138 | listener: ( 139 | proxyRes: http.IncomingMessage, 140 | req: http.IncomingMessage, 141 | res: http.ServerResponse, 142 | ) => void, 143 | ): this 144 | on( 145 | event: 'proxyReqWs', 146 | listener: ( 147 | proxyReq: http.ClientRequest, 148 | req: http.IncomingMessage, 149 | socket: net.Socket, 150 | options: ServerOptions, 151 | head: any, 152 | ) => void, 153 | ): this 154 | on( 155 | event: 'econnreset', 156 | listener: ( 157 | err: Error, 158 | req: http.IncomingMessage, 159 | res: http.ServerResponse, 160 | target: ProxyTargetUrl, 161 | ) => void, 162 | ): this 163 | on( 164 | event: 'end', 165 | listener: ( 166 | req: http.IncomingMessage, 167 | res: http.ServerResponse, 168 | proxyRes: http.IncomingMessage, 169 | ) => void, 170 | ): this 171 | on( 172 | event: 'close', 173 | listener: ( 174 | proxyRes: http.IncomingMessage, 175 | proxySocket: net.Socket, 176 | proxyHead: any, 177 | ) => void, 178 | ): this 179 | 180 | once(event: string, listener: () => void): this 181 | removeListener(event: string, listener: () => void): this 182 | removeAllListeners(event?: string): this 183 | getMaxListeners(): number 184 | setMaxListeners(n: number): this 185 | listeners(event: string): Array<() => void> 186 | emit(event: string, ...args: any[]): boolean 187 | listenerCount(type: string): number 188 | } 189 | 190 | export interface ServerOptions { 191 | /** URL string to be parsed with the url module. */ 192 | target?: ProxyTarget | undefined 193 | /** URL string to be parsed with the url module. */ 194 | forward?: ProxyTargetUrl | undefined 195 | /** Object to be passed to http(s).request. */ 196 | agent?: any 197 | /** Object to be passed to https.createServer(). */ 198 | ssl?: any 199 | /** If you want to proxy websockets. */ 200 | ws?: boolean | undefined 201 | /** Adds x- forward headers. */ 202 | xfwd?: boolean | undefined 203 | /** Verify SSL certificate. */ 204 | secure?: boolean | undefined 205 | /** Explicitly specify if we are proxying to another proxy. */ 206 | toProxy?: boolean | undefined 207 | /** Specify whether you want to prepend the target's path to the proxy path. */ 208 | prependPath?: boolean | undefined 209 | /** Specify whether you want to ignore the proxy path of the incoming request. */ 210 | ignorePath?: boolean | undefined 211 | /** Local interface string to bind for outgoing connections. */ 212 | localAddress?: string | undefined 213 | /** Changes the origin of the host header to the target URL. */ 214 | changeOrigin?: boolean | undefined 215 | /** specify whether you want to keep letter case of response header key */ 216 | preserveHeaderKeyCase?: boolean | undefined 217 | /** Basic authentication i.e. 'user:password' to compute an Authorization header. */ 218 | auth?: string | undefined 219 | /** Rewrites the location hostname on (301 / 302 / 307 / 308) redirects, Default: null. */ 220 | hostRewrite?: string | undefined 221 | /** Rewrites the location host/ port on (301 / 302 / 307 / 308) redirects based on requested host/ port.Default: false. */ 222 | autoRewrite?: boolean | undefined 223 | /** Rewrites the location protocol on (301 / 302 / 307 / 308) redirects to 'http' or 'https'.Default: null. */ 224 | protocolRewrite?: string | undefined 225 | /** rewrites domain of set-cookie headers. */ 226 | cookieDomainRewrite?: 227 | | false 228 | | string 229 | | { [oldDomain: string]: string } 230 | | undefined 231 | /** rewrites path of set-cookie headers. Default: false */ 232 | cookiePathRewrite?: 233 | | false 234 | | string 235 | | { [oldPath: string]: string } 236 | | undefined 237 | /** object with extra headers to be added to target requests. */ 238 | headers?: { [header: string]: string } | undefined 239 | /** Timeout (in milliseconds) when proxy receives no response from target. Default: 120000 (2 minutes) */ 240 | proxyTimeout?: number | undefined 241 | /** Timeout (in milliseconds) for incoming requests */ 242 | timeout?: number | undefined 243 | /** Specify whether you want to follow redirects. Default: false */ 244 | followRedirects?: boolean | undefined 245 | /** If set to true, none of the webOutgoing passes are called and it's your responsibility to appropriately return the response by listening and acting on the proxyRes event */ 246 | selfHandleResponse?: boolean | undefined 247 | /** Buffer */ 248 | buffer?: stream.Stream | undefined 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /packages/vite/src/types/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare var __vite_start_time: number | undefined; 2 | 3 | declare module "connect-history-api-fallback" { 4 | const plugin: any; 5 | export = plugin; 6 | } 7 | -------------------------------------------------------------------------------- /packages/vite/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "declaration": true, 8 | "noImplicitOverride": true, 9 | "noUnusedLocals": true, 10 | "esModuleInterop": true, 11 | "useUnknownInCatchVariables": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/vite/tsconfig.check.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "moduleResolution": "node", 5 | "noEmit": true, 6 | "strict": true, 7 | "exactOptionalPropertyTypes": true 8 | }, 9 | "include": [ 10 | "dist/**/*", 11 | "types/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [ 4 | "./rollup.config.ts", 5 | "scripts", 6 | "src/node/__tests__", 7 | "src/types/shims.d.ts" 8 | ], 9 | "compilerOptions": { 10 | "esModuleInterop": true, 11 | "declaration": false, 12 | "resolveJsonModule": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/vite/types/customEvent.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ErrorPayload, 3 | FullReloadPayload, 4 | PrunePayload, 5 | UpdatePayload, 6 | } from './hmrPayload' 7 | 8 | export interface CustomEventMap { 9 | 'vite:beforeUpdate': UpdatePayload 10 | 'vite:afterUpdate': UpdatePayload 11 | 'vite:beforePrune': PrunePayload 12 | 'vite:beforeFullReload': FullReloadPayload 13 | 'vite:error': ErrorPayload 14 | 'vite:invalidate': InvalidatePayload 15 | } 16 | 17 | export interface InvalidatePayload { 18 | path: string 19 | message: string | undefined 20 | } 21 | 22 | export type InferCustomEventPayload = 23 | T extends keyof CustomEventMap ? CustomEventMap[T] : any 24 | -------------------------------------------------------------------------------- /packages/vite/types/hmrPayload.d.ts: -------------------------------------------------------------------------------- 1 | export type HMRPayload = 2 | | ConnectedPayload 3 | | UpdatePayload 4 | | FullReloadPayload 5 | | CustomPayload 6 | | ErrorPayload 7 | | PrunePayload; 8 | 9 | export interface ConnectedPayload { 10 | type: "connected"; 11 | } 12 | 13 | export interface UpdatePayload { 14 | type: "update"; 15 | updates: Update[]; 16 | } 17 | 18 | export interface Update { 19 | type: "js-update" | "css-update"; 20 | path: string; 21 | acceptedPath: string; 22 | timestamp: number; 23 | /** 24 | * @experimental internal 25 | */ 26 | explicitImportRequired?: boolean | undefined; 27 | } 28 | 29 | export interface PrunePayload { 30 | type: "prune"; 31 | paths: string[]; 32 | } 33 | 34 | export interface FullReloadPayload { 35 | type: "full-reload"; 36 | path?: string; 37 | } 38 | 39 | export interface CustomPayload { 40 | type: "custom"; 41 | event: string; 42 | data?: any; 43 | } 44 | 45 | export interface ErrorPayload { 46 | type: "error"; 47 | err: { 48 | [name: string]: any; 49 | message: string; 50 | stack: string; 51 | id?: string; 52 | frame?: string; 53 | plugin?: string; 54 | pluginCode?: string; 55 | loc?: { 56 | file?: string; 57 | line: number; 58 | column: number; 59 | }; 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /packages/vite/types/hot.d.ts: -------------------------------------------------------------------------------- 1 | import type { InferCustomEventPayload } from './customEvent' 2 | 3 | export type ModuleNamespace = Record & { 4 | [Symbol.toStringTag]: 'Module' 5 | } 6 | 7 | export interface ViteHotContext { 8 | readonly data: any 9 | 10 | accept(): void 11 | accept(cb: (mod: ModuleNamespace | undefined) => void): void 12 | accept(dep: string, cb: (mod: ModuleNamespace | undefined) => void): void 13 | accept( 14 | deps: readonly string[], 15 | cb: (mods: Array) => void, 16 | ): void 17 | 18 | acceptExports( 19 | exportNames: string | readonly string[], 20 | cb?: (mod: ModuleNamespace | undefined) => void, 21 | ): void 22 | 23 | dispose(cb: (data: any) => void): void 24 | prune(cb: (data: any) => void): void 25 | invalidate(message?: string): void 26 | 27 | on( 28 | event: T, 29 | cb: (payload: InferCustomEventPayload) => void, 30 | ): void 31 | send(event: T, data?: InferCustomEventPayload): void 32 | } 33 | -------------------------------------------------------------------------------- /packages/vite/types/importGlob.d.ts: -------------------------------------------------------------------------------- 1 | export interface ImportGlobOptions< 2 | Eager extends boolean, 3 | AsType extends string 4 | > { 5 | /** 6 | * Import type for the import url. 7 | */ 8 | as?: AsType; 9 | /** 10 | * Import as static or dynamic 11 | * 12 | * @default false 13 | */ 14 | eager?: Eager; 15 | /** 16 | * Import only the specific named export. Set to `default` to import the default export. 17 | */ 18 | import?: string; 19 | /** 20 | * Custom queries 21 | */ 22 | query?: string | Record; 23 | /** 24 | * Search files also inside `node_modules/` and hidden directories (e.g. `.git/`). This might have impact on performance. 25 | * 26 | * @default false 27 | */ 28 | exhaustive?: boolean; 29 | } 30 | 31 | export type GeneralImportGlobOptions = ImportGlobOptions; 32 | 33 | export interface KnownAsTypeMap { 34 | raw: string; 35 | url: string; 36 | worker: Worker; 37 | } 38 | 39 | export interface ImportGlobFunction { 40 | /** 41 | * Import a list of files with a glob pattern. 42 | * 43 | * Overload 1: No generic provided, infer the type from `eager` and `as` 44 | */ 45 | < 46 | Eager extends boolean, 47 | As extends string, 48 | T = As extends keyof KnownAsTypeMap ? KnownAsTypeMap[As] : unknown 49 | >( 50 | glob: string | string[], 51 | options?: ImportGlobOptions 52 | ): (Eager extends true ? true : false) extends true 53 | ? Record 54 | : Record Promise>; 55 | /** 56 | * Import a list of files with a glob pattern. 57 | * 58 | * Overload 2: Module generic provided, infer the type from `eager: false` 59 | */ 60 | ( 61 | glob: string | string[], 62 | options?: ImportGlobOptions 63 | ): Record Promise>; 64 | /** 65 | * Import a list of files with a glob pattern. 66 | * 67 | * Overload 3: Module generic provided, infer the type from `eager: true` 68 | */ 69 | ( 70 | glob: string | string[], 71 | options: ImportGlobOptions 72 | ): Record; 73 | } 74 | 75 | export interface ImportGlobEagerFunction { 76 | /** 77 | * Eagerly import a list of files with a glob pattern. 78 | * 79 | * Overload 1: No generic provided, infer the type from `as` 80 | */ 81 | < 82 | As extends string, 83 | T = As extends keyof KnownAsTypeMap ? KnownAsTypeMap[As] : unknown 84 | >( 85 | glob: string | string[], 86 | options?: Omit, "eager"> 87 | ): Record; 88 | /** 89 | * Eagerly import a list of files with a glob pattern. 90 | * 91 | * Overload 2: Module generic provided 92 | */ 93 | ( 94 | glob: string | string[], 95 | options?: Omit, "eager"> 96 | ): Record; 97 | } 98 | -------------------------------------------------------------------------------- /patches/sirv@2.0.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/sirv.d.ts b/sirv.d.ts 2 | index c05040fc6ec504a1828a7badd39f669981acd0ee..e9597e8b5bf24613a09565f0e13024ae3ca8fa5e 100644 3 | --- a/sirv.d.ts 4 | +++ b/sirv.d.ts 5 | @@ -19,6 +19,8 @@ declare module 'sirv' { 6 | gzip?: boolean; 7 | onNoMatch?: (req: IncomingMessage, res: ServerResponse) => void; 8 | setHeaders?: (res: ServerResponse, pathname: string, stats: Stats) => void; 9 | + /** patched */ 10 | + shouldServe?: (absoluteFilePath: string) => void; 11 | } 12 | 13 | export default function(dir?: string, opts?: Options): RequestHandler; -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'playground/**' 4 | - 'mini-vite-example' 5 | - '!**/test/**' --------------------------------------------------------------------------------