npm install connect es-module-lexer resolve check-is-array esbuild fast-glob fs-extra serve-static magic-string chokidar ws hash-sum --save
连接
在一起const connect = require("connect");
const http = require("http");
const middlewares = connect();
middlewares.use(function (req, res, next) {
console.log("middleware1");
next();
});
middlewares.use(function (req, res, next) {
console.log("middleware2");
next();
});
middlewares.use(function (req, res, next) {
res.end("Hello from Connect!");
});
http.createServer(middlewares).listen(3000);
const connect = require("connect");
const static = require("serve-static");
const http = require("http");
const middlewares = connect();
middlewares.use(static(__dirname));
http.createServer(middlewares).listen(3001);
const { init, parse } = require("es-module-lexer");
(async () => {
await init;
const [imports, exports] = parse(`import _ from 'lodash';\nexport var p = 5`);
console.log(imports);
console.log(exports);
})();
require.resolve()
算法const resolve = require("resolve");
const res = resolve.sync("check-is-array", { basedir: __dirname });
console.log(res);
Unix Bash shell
使用的规则返回与指定模式的定义集匹配的路径名const fg = require("fast-glob");
(async () => {
const entries = await fg(["**/*.js"]);
console.log(entries);
})();
const MagicString = require("magic-string");
const ms = new MagicString("var age = 10");
ms.overwrite(10, 12, "11");
console.log(ms.toString());
{
"bin": {
"vite3": "./bin/vite3.js"
}
}
bin\vite3.js
#!/usr/bin/env node
require("../lib/cli");
lib\cli.js
console.log("vite3");
lib\cli.js
+let { createServer } = require('./server');
+(async function () {
+ const server = await createServer();
+ server.listen(9999);
+})();
lib\server\index.js
const connect = require("connect");
async function createServer() {
const middlewares = connect();
const server = {
async listen(port) {
require("http")
.createServer(middlewares)
.listen(port, async () => {
console.log(`dev server running at: http://localhost:${port}`);
});
},
};
return server;
}
exports.createServer = createServer;
lib\server\index.js
const connect = require('connect');
+const serveStaticMiddleware = require('./middlewares/static');
+const resolveConfig = require('../config');
async function createServer() {
+ const config = await resolveConfig()
const middlewares = connect();
const server = {
async listen(port) {
require('http').createServer(middlewares)
.listen(port, async () => {
console.log(`dev server running at: http://localhost:${port}`)
})
}
}
+ middlewares.use(serveStaticMiddleware(config))
return server;
}
exports.createServer = createServer;
lib\server\middlewares\static.js
const static = require("serve-static");
function serveStaticMiddleware({ root }) {
return static(root);
}
module.exports = serveStaticMiddleware;
lib\config.js
const { normalizePath } = require("./utils");
async function resolveConfig() {
const root = normalizePath(process.cwd());
let config = {
root,
};
return config;
}
module.exports = resolveConfig;
lib\utils.js
function normalizePath(id) {
return id.replace(/\\/g, "/");
}
exports.normalizePath = normalizePath;
viteuse\index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vite2</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
viteuse\main.js
console.log("main");
src\main.js
+import { createApp } from 'vue'
+console.log(createApp);
lib\server\index.js
const connect = require('connect');
const serveStaticMiddleware = require('./middlewares/static');
const resolveConfig = require('../config');
+const { createOptimizeDepsRun } = require('../optimizer');
async function createServer() {
const config = await resolveConfig()
const middlewares = connect();
const server = {
async listen(port) {
+ await runOptimize(config, server)
require('http').createServer(middlewares)
.listen(port, async () => {
console.log(`dev server running at: http://localhost:${port}`)
})
}
}
middlewares.use(serveStaticMiddleware(config))
return server;
}
+async function runOptimize(config, server) {
+ await createOptimizeDepsRun(config)
+}
exports.createServer = createServer;
lib\optimizer\index.js
const scanImports = require("./scan");
async function createOptimizeDepsRun(config) {
const deps = await scanImports(config);
console.log(deps);
}
exports.createOptimizeDepsRun = createOptimizeDepsRun;
lib\optimizer\scan.js
const { build } = require("esbuild");
const esbuildScanPlugin = require("./esbuildScanPlugin");
const path = require("path");
async function scanImports(config) {
const depImports = {};
const esPlugin = await esbuildScanPlugin(config, depImports);
await build({
absWorkingDir: config.root,
entryPoints: [path.resolve("./index.html")],
bundle: true,
format: "esm",
outfile: "dist/index.js",
write: true,
plugins: [esPlugin],
});
return depImports;
}
module.exports = scanImports;
lib\optimizer\esbuildScanPlugin.js
const fs = require("fs-extra");
const path = require("path");
const { createPluginContainer } = require("../server/pluginContainer");
const resolvePlugin = require("../plugins/resolve");
const { normalizePath } = require("../utils");
const htmlTypesRE = /\.html$/;
const scriptModuleRE = /<script type="module" src\="(.+?)"><\/script>/;
const JS_TYPES_RE = /\.js$/;
async function esbuildScanPlugin(config, depImports) {
config.plugins = [resolvePlugin(config)];
const container = await createPluginContainer(config);
const resolve = async (id, importer) => {
return await container.resolveId(id, importer);
};
return {
name: "vite:dep-scan",
setup(build) {
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
const resolved = await resolve(path, importer);
if (resolved) {
return {
path: resolved.id || resolved,
namespace: "html",
};
}
});
build.onResolve({ filter: /.*/ }, async ({ path, importer }) => {
const resolved = await resolve(path, importer);
if (resolved) {
const id = resolved.id || resolved;
const included = id.includes("node_modules");
if (included) {
depImports[path] = normalizePath(id);
return {
path: id,
external: true,
};
}
return {
path: id,
};
}
return { path };
});
build.onLoad(
{ filter: htmlTypesRE, namespace: "html" },
async ({ path }) => {
let html = fs.readFileSync(path, "utf-8");
let [, scriptSrc] = html.match(scriptModuleRE);
let js = `import ${JSON.stringify(scriptSrc)};\n`;
return {
loader: "js",
contents: js,
};
}
);
build.onLoad({ filter: JS_TYPES_RE }, ({ path: id }) => {
let ext = path.extname(id).slice(1);
let contents = fs.readFileSync(id, "utf-8");
return {
loader: ext,
contents,
};
});
},
};
}
module.exports = esbuildScanPlugin;
lib\server\pluginContainer.js
const { normalizePath } = require("../utils");
const path = require("path");
async function createPluginContainer({ plugins, root }) {
class PluginContext {}
const container = {
async resolveId(id, importer) {
let ctx = new PluginContext();
let resolveId = id;
for (const plugin of plugins) {
if (!plugin.resolveId) continue;
const result = await plugin.resolveId.call(ctx, id, importer);
if (result) {
resolveId = result.id || result;
break;
}
}
return { id: normalizePath(resolveId) };
},
};
return container;
}
exports.createPluginContainer = createPluginContainer;
lib\plugins\resolve.js
const fs = require("fs");
const path = require("path");
const resolve = require("resolve");
function resolvePlugin(config) {
return {
name: "vite:resolve",
resolveId(id, importer) {
//如果/开头表示是绝对路径
if (id.startsWith("/")) {
return { id: path.resolve(config.root, id.slice(1)) };
}
//如果是绝对路径
if (path.isAbsolute(id)) {
return { id };
}
//如果是相对路径
if (id.startsWith(".")) {
const basedir = path.dirname(importer);
const fsPath = path.resolve(basedir, id);
return { id: fsPath };
}
//如果是第三方包
let res = tryNodeResolve(id, importer, config);
if (res) {
return res;
}
},
};
}
function tryNodeResolve(id, importer, config) {
const pkgPath = resolve.sync(`${id}/package.json`, { basedir: config.root });
const pkgDir = path.dirname(pkgPath);
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
const entryPoint = pkg.module;
const entryPointPath = path.join(pkgDir, entryPoint);
return { id: entryPointPath };
}
module.exports = resolvePlugin;
lib\server\index.js
const connect = require('connect');
const http = require('http');
const serveStaticMiddleware = require('./middlewares/static');
const resolveConfig = require('../config');
const { createOptimizeDepsRun } = require('../optimizer');
async function createServer() {
const config = await resolveConfig();
const middlewares = connect();
const server = {
async listen(port) {
+ await runOptimize(config, server)
http.createServer(middlewares).listen(port, async () => {
console.log(`server running at http://localhost:${port}`);
});
}
}
middlewares.use(serveStaticMiddleware(config));
return server;
}
+async function runOptimize(config, server) {
+ const optimizeDeps = await createOptimizeDepsRun(config);
+ server._optimizeDepsMetadata = optimizeDeps.metadata
}
exports.createServer = createServer;
lib\config.js
+const path = require('path');
const { normalizePath } = require('./utils');
async function resolveConfig() {
//当前的根目录 window \\ linux /
const root = normalizePath(process.cwd());
+ const cacheDir = normalizePath(path.resolve(`node_modules/.vite7`))
let config = {
root,
+ cacheDir
}
return config;
}
module.exports = resolveConfig;
lib\optimizer\index.js
const scanImports = require('./scan');
+const fs = require('fs-extra');
+const path = require('path');
+const { build } = require('esbuild');
+const { normalizePath } = require('../utils');
async function createOptimizeDepsRun(config) {
const deps = await scanImports(config);
+ const { cacheDir } = config;
+ const depsCacheDir = path.resolve(cacheDir, 'deps')
+ const metadataPath = path.join(depsCacheDir, '_metadata.json');
+ const metadata = {
+ optimized: {}
+ }
+ for (const id in deps) {
+ const entry = deps[id]
+ metadata.optimized[id] = {
+ file: normalizePath(path.resolve(depsCacheDir, id + '.js')),
+ src: entry
+ }
+ await build({
+ absWorkingDir: process.cwd(),
+ entryPoints: [deps[id]],
+ outfile: path.resolve(depsCacheDir, id + '.js'),
+ bundle: true,
+ write: true,
+ format: 'esm'
+ })
+ }
+ await fs.ensureDir(depsCacheDir);
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, (key, value) => {
+ if (key === 'file' || key === 'src') {
+ //optimized里存的是绝对路径,此处写入硬盘的是相对于缓存目录的相对路径
+ console.log(depsCacheDir, value);
+ return normalizePath(path.relative(depsCacheDir, value));
+ }
+ return value
+ }, 2));
+ return { metadata };
}
exports.createOptimizeDepsRun = createOptimizeDepsRun;
修改返回的 main.js
中的 vue
的路径
import { createApp } from 'vue'
变为import { createApp } from '/node_modules/.vite/deps/vue.js'
请求/src/main.js
,此请求先由transformMiddleware
中间件处理,通过isJSRequest
判断是 js 请求,走transformRequest
transformRequest
里执行pluginContainer.resolveId(url)
方法,方法内会由resolvePlugin
返回/src/main.js
的绝对路径pluginContainer.load(id)
方法返回 JS 文件内容,此处返回null
pluginContainer.transform(code, id)
方法,执行importAnalysisPlugin
里的transform
方法,里面会分析依赖的模块,获取依赖的vue
,重新执行PluginContext.resolve
,执行preAliasPlugin
,获取vue
相对路径/node_modules/.vite/deps/vue.js
,把vue
变为/node_modules/.vite/deps/vue.js
transformMiddleware
中间件transformRequest
pluginContainer.resolveId
lib\server\index.js
const connect = require('connect');
const http = require('http');
const serveStaticMiddleware = require('./middlewares/static');
const resolveConfig = require('../config');
const { createOptimizeDepsRun } = require('../optimizer');
+const transformMiddleware = require('./middlewares/transform');
+const { createPluginContainer } = require('./pluginContainer');
async function createServer() {
const config = await resolveConfig();
const middlewares = connect();
+ const pluginContainer = await createPluginContainer(config)
const server = {
+ pluginContainer,
async listen(port) {
await runOptimize(config, server)
http.createServer(middlewares).listen(port, async () => {
console.log(`server running at http://localhost:${port}`);
});
}
}
+ for (const plugin of config.plugins) {
+ if (plugin.configureServer) {
+ await plugin.configureServer(server)
+ }
+ }
+ middlewares.use(transformMiddleware(server))
middlewares.use(serveStaticMiddleware(config));
return server;
}
async function runOptimize(config, server) {
const optimizeDeps = await createOptimizeDepsRun(config);
server._optimizeDepsMetadata = optimizeDeps.metadata
}
exports.createServer = createServer;
lib\server\middlewares\transform.js
const { isJSRequest } = require("../../utils");
const send = require("../send");
const transformRequest = require("../transformRequest");
const { parse } = require("url");
function transformMiddleware(server) {
return async function (req, res, next) {
if (req.method !== "GET") {
return next();
}
let url = parse(req.url).pathname;
if (isJSRequest(url)) {
//切记这个地方要把req.url传给transformRequest,不是url,否则会丢失query
const result = await transformRequest(req.url, server);
if (result) {
const type = "js";
return send(req, res, result.code, type);
}
} else {
return next();
}
};
}
module.exports = transformMiddleware;
lib\utils.js
function normalizePath(id) {
return id.replace(/\\/g, '/')
}
exports.normalizePath = normalizePath;
+const knownJsSrcRE = /\.js/
+const isJSRequest = (url) => {
+ if (knownJsSrcRE.test(url)) {
+ return true
+ }
+ return false
+}
+exports.isJSRequest = isJSRequest;
lib\server\transformRequest.js
const fs = require("fs-extra");
async function transformRequest(url, server) {
const { pluginContainer } = server;
const { id } = await pluginContainer.resolveId(url); //获取此文件的绝对路径
const loadResult = await pluginContainer.load(id); //加载此文件的内容
let code;
if (loadResult) {
code = loadResult.code;
} else {
code = await fs.readFile(id, "utf-8");
}
//转换文件内容
const transformResult = await pluginContainer.transform(code, id);
return transformResult;
}
module.exports = transformRequest;
lib\server\pluginContainer.js
const { normalizePath } = require("../utils");
const path = require('path');
async function createPluginContainer({ plugins,root }) {
class PluginContext {
+ async resolve(id, importer = path.join(root, "index.html")) {
+ return await container.resolveId(id, importer);
+ }
}
//插件容器是一个用来执行插件的容器
const container = {
//resolve是一个方法,是一个根据标记符计算路径的方法
//vue=>vue在硬盘上对应路径
async resolveId(id, importer) {
let ctx = new PluginContext();
let resolveId = id;
for (const plugin of plugins) {
if (!plugin.resolveId) continue;
const result = await plugin.resolveId.call(ctx, id, importer);
if (result) {
resolveId = result.id || result;
break;
}
}
return { id: normalizePath(resolveId) }
},
+ async load(id) {
+ const ctx = new PluginContext()
+ for (const plugin of plugins) {
+ if (!plugin.load) continue
+ const result = await plugin.load.call(ctx, id)
+ if (result !== null) {
+ return result
+ }
+ }
+ return null
+ },
+ async transform(code, id) {
+ for (const plugin of plugins) {
+ if (!plugin.transform) continue
+ const ctx = new PluginContext()
+ const result = await plugin.transform.call(ctx, code, id)
+ if (!result) continue
+ code = result.code || result;
+ }
+ return { code }
+ }
}
return container;
}
exports.createPluginContainer = createPluginContainer;
lib\server\send.js
const alias = {
js: "application/javascript",
css: "text/css",
html: "text/html",
json: "application/json",
};
function send(_req, res, content, type) {
res.setHeader("Content-Type", alias[type] || type);
res.statusCode = 200;
return res.end(content);
}
module.exports = send;
lib\plugins\index.js
const importAnalysisPlugin = require("./importAnalysis");
const preAliasPlugin = require("./preAlias");
const resolvePlugin = require("./resolve");
async function resolvePlugins(config) {
return [
preAliasPlugin(config), //把vue=>vue.js
resolvePlugin(config),
importAnalysisPlugin(config),
];
}
exports.resolvePlugins = resolvePlugins;
const { init, parse } = require("es-module-lexer");
const MagicString = require("magic-string");
function importAnalysisPlugin(config) {
const { root } = config;
return {
name: "vite:import-analysis",
async transform(source, importer) {
await init;
let imports = parse(source)[0];
if (!imports.length) {
return source;
}
let ms = new MagicString(source);
const normalizeUrl = async (url) => {
//解析此导入的模块的路径
const resolved = await this.resolve(url, importer);
if (resolved.id.startsWith(root + "/")) {
//把绝对路径变成相对路径
url = resolved.id.slice(root.length);
}
return url;
};
for (let index = 0; index < imports.length; index++) {
const { s: start, e: end, n: specifier } = imports[index];
if (specifier) {
const normalizedUrl = await normalizeUrl(specifier);
if (normalizedUrl !== specifier) {
ms.overwrite(start, end, normalizedUrl);
}
}
}
return ms.toString();
},
};
}
module.exports = importAnalysisPlugin;
lib\plugins\preAlias.js
function preAliasPlugin() {
let server;
return {
name: "vite:pre-alias",
configureServer(_server) {
server = _server;
},
resolveId(id) {
//把vue=>vue.js
const metadata = server._optimizeDepsMetadata;
const isOptimized = metadata.optimized[id];
if (isOptimized) {
return {
id: isOptimized.file, //// vue => c:/node_modules/.vite/deps/vue.js
};
}
},
};
}
module.exports = preAliasPlugin;
lib\config.js
const path = require('path');
const { normalizePath } = require('./utils');
+const { resolvePlugins } = require('./plugins');
async function resolveConfig() {
const root = normalizePath(process.cwd());
const cacheDir = normalizePath(path.resolve(`node_modules/.vite`))
let config = {
root,
cacheDir
}
+ const plugins = await resolvePlugins(config);
+ config.plugins = plugins;
return config;
}
module.exports = resolveConfig;
lib\optimizer\esbuildDepPlugin.js
const path = require('path');
const fs = require('fs-extra');
const htmlTypesRE = /\.html$/;
const scriptModuleRE = /<script type="module" src\="(.+?)"><\/script>/;
const { createPluginContainer } = require('../server/pluginContainer');
const resolvePlugin = require('../plugins/resolve');
const jsRE = /\.js$/;
async function esBuildScanPlugin(config, deps) {
//在此处其实调用的vite插件系统
config.plugins = [resolvePlugin(config)];
const container = await createPluginContainer(config);
const resolve = async (id, importer) => {
return await container.resolveId(id, importer);
}
return {
name: 'vite:dep-scan',
setup(build) {
//X [ERROR] No loader is configured for ".vue" files: src/App.vue
+ build.onResolve(
+ {
+ filter: /\.vue$/
+ },
+ async ({ path: id, importer }) => {
+ const resolved = await resolve(id, importer)
+ if (resolved) {
+ return {
+ path: resolved.id,
+ external: true
+ }
+ }
+ }
+ )
//用来处理路径的
build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
//path=C:\aproject\vite5\doc\index.html importer 空
const resolved = await resolve(path, importer);
if (resolved) {
return {
path: resolved.id || resolved,
namespace: 'html' //为了更细化区分不同的文件类型,我可以给文件添加一个命名空间
}
}
});
//对于其它所有的类型文件我们也进行处理
build.onResolve({ filter: /.*/ }, async ({ path, importer }) => {
const resolved = await resolve(path, importer);
//返回值可能是 {id:xx} 或 xx
//C:\aproject\vite5\doc\main.js
if (resolved) {
const id = resolved.id || resolved;
const included = id.includes('node_modules');
if (included) {
//deps.vue = "C:/aproject/viteproject/node_modules/vue/dist/vue.runtime.esm-bundler.js"
deps[path] = id;
return {
path,
external: true //external设置为true的话说明这是一个外部模块,不会进行后续的打包分析,直接返回了
}
}
return { path: id }
}
return { path }
});
//用来处理读取内容 自定义读取器
build.onLoad({ filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => {
let html = fs.readFileSync(path, 'utf8');
let [, scriptSrc] = html.match(scriptModuleRE);
let js = `import ${JSON.stringify(scriptSrc)}`;//import "/main.js"
return {
loader: 'js',
contents: js
}
})
build.onLoad({ filter: jsRE }, async ({ path: id }) => {
let ext = path.extname(id).slice(1);// .js js
const contents = fs.readFileSync(id, 'utf8');
return {
loader: ext,
contents
}
})
}
}
}
module.exports = esBuildScanPlugin;
lib\plugins\index.js
const importAnalysisPlugin = require('./importAnalysis');
const preAliasPlugin = require('./preAlias');
const resolvePlugin = require('./resolve');
+async function resolvePlugins(config, userPlugins) {
return [
preAliasPlugin(config),
resolvePlugin(config),
+ ...userPlugins,
importAnalysisPlugin(config)
]
}
exports.resolvePlugins = resolvePlugins;
lib\utils.js
function normalizePath(id) {
return id.replace(/\\/g, '/')
}
exports.normalizePath = normalizePath;
+const knownJsSrcRE = /\.(js|vue)/
const isJSRequest = (url) => {
if (knownJsSrcRE.test(url)) {
return true
}
return false
}
exports.isJSRequest = isJSRequest;
lib\config.js
const path = require('path');
const { normalizePath } = require('./utils');
const { resolvePlugins } = require('./plugins');
+const fs = require('fs-extra');
async function resolveConfig() {
//当前的根目录 window \\ linux /
const root = normalizePath(process.cwd());
const cacheDir = normalizePath(path.resolve(`node_modules/.vite7`))
let config = {
root,
cacheDir
}
+ const jsconfigFile = path.resolve(root, 'vite.config.js')
+ const exists = await fs.pathExists(jsconfigFile)
+ if (exists) {
+ const userConfig = require(jsconfigFile);
+ config = { ...config, ...userConfig };
+ }
+ const userPlugins = config.plugins || [];
+ const plugins = await resolvePlugins(config, userPlugins);
config.plugins = plugins;
return config;
}
module.exports = resolveConfig;
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
src\main.js
import { createApp } from "vue";
import App from "/src/App.vue";
createApp(App).mount("#app");
src\App.vue
<template>
<h1>App</h1>
</template>
<script>
export default {
name: 'App'
}
</script>
plugins\vue.js
const {
parse,
compileScript,
rewriteDefault,
compileTemplate,
} = require("vue/compiler-sfc");
const fs = require("fs");
const descriptorCache = new Map();
function vue() {
return {
name: "vue",
async transform(code, id) {
const { filename } = parseVueRequest(id);
if (filename.endsWith(".vue")) {
let result = await transformMain(code, filename);
return result;
}
return null;
},
};
}
async function getDescriptor(filename) {
let descriptor = descriptorCache.get(filename);
if (descriptor) return descriptor;
const content = await fs.promises.readFile(filename, "utf8");
const result = parse(content, { filename });
descriptor = result.descriptor;
descriptorCache.set(filename, descriptor);
return descriptor;
}
async function transformMain(source, filename) {
const descriptor = await getDescriptor(filename);
const scriptCode = genScriptCode(descriptor, filename);
const templateCode = genTemplateCode(descriptor, filename);
let resolvedCode = [
templateCode,
scriptCode,
`_sfc_main['render'] = render`,
`export default _sfc_main`,
].join("\n");
return { code: resolvedCode };
}
function genScriptCode(descriptor, id) {
let scriptCode = "";
let script = compileScript(descriptor, { id });
if (!script.lang) {
scriptCode = rewriteDefault(script.content, "_sfc_main");
}
return scriptCode;
}
function genTemplateCode(descriptor, id) {
let content = descriptor.template.content;
const result = compileTemplate({ source: content, id });
return result.code;
}
function parseVueRequest(id) {
const [filename, querystring = ""] = id.split("?");
let query = new URLSearchParams(querystring);
return {
filename,
query,
};
}
module.exports = vue;
vite.config.js
const vue = require("./plugins/vue");
module.exports = {
plugins: [vue()],
};
lib\config.js
const path = require('path');
const { normalizePath } = require('./utils');
const { resolvePlugins } = require('./plugins');
const fs = require('fs-extra');
async function resolveConfig() {
//当前的根目录 window \\ linux /
const root = normalizePath(process.cwd());
const cacheDir = normalizePath(path.resolve(`node_modules/.vite7`))
let config = {
root,
cacheDir
}
const jsconfigFile = path.resolve(root, 'vite.config.js')
const exists = await fs.pathExists(jsconfigFile)
if (exists) {
const userConfig = require(jsconfigFile);
config = { ...config, ...userConfig };
}
const userPlugins = config.plugins || [];
+ for (const plugin of userPlugins) {
+ if (plugin.config) {
+ const res = await plugin.config(config)
+ if (res) {
+ config = { ...config, ...res }
+ }
+ }
+ }
const plugins = await resolvePlugins(config, userPlugins);
config.plugins = plugins;
return config;
}
module.exports = resolveConfig;
src\App.vue
<template>
<h1>App</h1>
</template>
<script>
export default {
name: 'App'
}
</script>
+<style>
+h1 {
+ color: red;
+}
+</style>
plugins\vue.js
npm install hash-sum --save
+const { parse, compileScript, rewriteDefault, compileTemplate, compileStyleAsync } = require('vue/compiler-sfc');
const fs = require('fs');
+const path = require('path');
+const hash = require('hash-sum');
+const descriptorCache = new Map();
function vue() {
+ let root;
return {
name: 'vue',
+ config(config) {
+ root = config.root;
+ },
+ async load(id) {
+ const { filename, query } = parseVueRequest(id);
+ if (query.has('vue')) {
+ const descriptor = await getDescriptor(filename, root);
+ if (query.get('type') === 'style') {
+ let block = descriptor.styles[Number(query.get('index'))];
+ if (block) {
+ return { code: block.content };
+ }
+ }
+ }
+ },
async transform(code, id) {
+ const { filename, query } = parseVueRequest(id);
+ if (filename.endsWith('.vue')) {
+ if (query.get('type') === 'style') {
+ const descriptor = await getDescriptor(filename, root);
+ let result = await transformStyle(code, descriptor, query.get('index'));
+ return result;
+ } else {
+ let result = await transformMain(code, filename,root);
+ return result;
+ }
+ }
return null;
}
}
}
+async function transformStyle(code, descriptor, index) {
+ const block = descriptor.styles[index];
+ //如果是CSS,其实翻译之后和翻译之前内容是一样的
+ const result = await compileStyleAsync({
+ filename: descriptor.filename,
+ source: code,
+ id: `data-v-${descriptor.id}`,//必须传递,不然报错
+ scoped: block.scoped
+ });
+ let styleCode = result.code;
+ const injectCode =
+ `\nvar style = document.createElement('style');` +
+ `\nstyle.innerHTML = ${JSON.stringify(styleCode)};` +
+ `\ndocument.head.appendChild(style);`
+ return {
+ code: injectCode
+ };
+}
+async function getDescriptor(filename, root) {
+ let descriptor = descriptorCache.get(filename);
+ if (descriptor) return descriptor;
+ const content = await fs.promises.readFile(filename, 'utf8');
+ const result = parse(content, { filename });
+ descriptor = result.descriptor;
+ descriptor.id = hash(path.relative(root, filename));
+ descriptorCache.set(filename, descriptor);
+ return descriptor;
+}
async function transformMain(source, filename,root) {
const descriptor = await getDescriptor(filename, root);
const scriptCode = genScriptCode(descriptor, filename)
const templateCode = genTemplateCode(descriptor, filename);
+ const stylesCode = genStyleCode(descriptor, filename);
let resolvedCode = [
+ stylesCode,
templateCode,
scriptCode,
`_sfc_main['render'] = render`,
`export default _sfc_main`
].join('\n');
return { code: resolvedCode }
}
+function genStyleCode(descriptor, filename) {
+ let styleCode = '';
+ if (descriptor.styles.length) {
+ descriptor.styles.forEach((style, index) => {
+ const query = `?vue&type=style&index=${index}&lang=css`;
+ const styleRequest = (filename + query).replace(/\\/g, '/');
+ styleCode += `\nimport ${JSON.stringify(styleRequest)}`;
+ });
+ return styleCode;
+ }
+}
function genScriptCode(descriptor, id) {
let scriptCode = ''
let script = compileScript(descriptor, { id });
if (!script.lang) {
scriptCode = rewriteDefault(
script.content,
'_sfc_main',
)
}
return scriptCode;
}
function genTemplateCode(descriptor, id) {
let content = descriptor.template.content;
const result = compileTemplate({ source: content, id });
return result.code;
}
+function parseVueRequest(id) {
+ const [filename, querystring = ''] = id.split('?');
+ let query = new URLSearchParams(querystring);
+ return {
+ filename, query
+ };
+}
module.exports = vue;
lib\plugins\index.js
const importAnalysisPlugin = require('./importAnalysis');
const preAliasPlugin = require('./preAlias');
const resolvePlugin = require('./resolve');
+const definePlugin = require('./define');
async function resolvePlugins(config, userPlugins) {
return [
preAliasPlugin(config),
resolvePlugin(config),
...userPlugins,
+ definePlugin(config),
importAnalysisPlugin(config)
]
}
exports.resolvePlugins = resolvePlugins;
lib\plugins\define.js
const MagicString = require("magic-string");
function definePlugin(config) {
return {
name: "vite:define",
transform(code) {
const replacements = config.define || {};
const replacementsKeys = Object.keys(replacements);
const pattern = new RegExp(
"(" + replacementsKeys.map((str) => str).join("|") + ")",
"g"
);
const s = new MagicString(code);
let hasReplaced = false;
let match;
while ((match = pattern.exec(code))) {
hasReplaced = true;
const start = match.index;
const end = start + match[0].length;
const replacement = "" + replacements[match[1]];
s.overwrite(start, end, replacement);
}
if (!hasReplaced) {
return null;
}
return { code: s.toString() };
},
};
}
module.exports = definePlugin;
plugins\vue.js
const { parse, compileScript, rewriteDefault, compileTemplate, compileStyleAsync } = require('vue/compiler-sfc');
const fs = require('fs');
const path = require('path');
const hash = require('hash-sum');
const descriptorCache = new Map();
function vue() {
let root;
return {
name: 'vue',
config(config) {
root = config.root;
+ return {
+ define: {
+ __VUE_OPTIONS_API__: true,
+ __VUE_PROD_DEVTOOLS__: false
+ }
+ }
},
async load(id) {
const { filename, query } = parseVueRequest(id);
if (query.has('vue')) {
const descriptor = await getDescriptor(filename, root);
if (query.get('type') === 'style') {
let block = descriptor.styles[Number(query.get('index'))];
if (block) {
return { code: block.content };
}
}
}
},
async transform(code, id) {
const { filename, query } = parseVueRequest(id);
if (filename.endsWith('.vue')) {
if (query.get('type') === 'style') {
const descriptor = await getDescriptor(filename, root);
let result = await transformStyle(code, descriptor, query.get('index'));
return result;
} else {
let result = await transformMain(code, filename,root);
return result;
}
}
return null;
}
}
}
async function transformStyle(code, descriptor, index) {
const block = descriptor.styles[index];
//如果是CSS,其实翻译之后和翻译之前内容是一样的,最终返回的JS靠packages\vite\src\node\plugins\css.ts
const result = await compileStyleAsync({
filename: descriptor.filename,
source: code,
id: `data-v-${descriptor.id}`,//必须传递,不然报错
scoped: block.scoped
});
let styleCode = result.code;
const injectCode =
`\nvar style = document.createElement('style');` +
`\nstyle.innerHTML = ${JSON.stringify(styleCode)};` +
`\ndocument.head.appendChild(style);`
return {
code: injectCode
};
}
async function getDescriptor(filename, root) {
let descriptor = descriptorCache.get(filename);
if (descriptor) return descriptor;
const content = await fs.promises.readFile(filename, 'utf8');
const result = parse(content, { filename });
descriptor = result.descriptor;
descriptor.id = hash(path.relative(root, filename));
descriptorCache.set(filename, descriptor);
return descriptor;
}
async function transformMain(source, filename,root) {
const { descriptor } = parse(source, { filename });
const scriptCode = genScriptCode(descriptor, filename)
const templateCode = genTemplateCode(descriptor, filename);
const stylesCode = genStyleCode(descriptor, filename);
let resolvedCode = [
stylesCode,
templateCode,
scriptCode,
`_sfc_main['render'] = render`,
`export default _sfc_main`
].join('\n');
return { code: resolvedCode }
}
function genStyleCode(descriptor, filename) {
let styleCode = '';
if (descriptor.styles.length) {
descriptor.styles.forEach((style, index) => {
const query = `?vue&type=style&index=${index}&lang=css`;
const styleRequest = (filename + query).replace(/\\/g, '/');
styleCode += `\nimport ${JSON.stringify(styleRequest)}`;
});
return styleCode;
}
}
function genScriptCode(descriptor, id) {
let scriptCode = ''
let script = compileScript(descriptor, { id });
if (!script.lang) {
scriptCode = rewriteDefault(
script.content,
'_sfc_main',
)
}
return scriptCode;
}
function genTemplateCode(descriptor, id) {
let content = descriptor.template.content;
const result = compileTemplate({ source: content, id });
return result.code;
}
function parseVueRequest(id) {
const [filename, querystring = ''] = id.split('?');
let query = new URLSearchParams(querystring);
return {
filename, query
};
}
module.exports = vue;
pnpm install vite -D
{
"scripts": {
"dev": "vite"
}
}
src\main.js
export function render() {
app.innerHTML = 'title';
}
render();
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
newModule.render();
});
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>hmr</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
src\render.js
export function render() {
app.innerHTML = 'title1';
}
src\main.js
import { render } from './render';
render();
if (import.meta.hot) {
import.meta.hot.accept(['./render'], ([renderMod]) => {
renderMod.render();
});
}
src\render.js
+let counter = { number: 0 };
+let timer = setInterval(() => {
+ console.log(counter.number++);
+}, 1000);
export function render() {
app.innerHTML = 'title';
}
+if (import.meta.hot) {
+ import.meta.hot.dispose(() => {
+ console.log('dispose render.js');
+ clearInterval(timer);
+ });
+}
src\render.js
+let counter = import.meta.hot.data.counter || { number: 0 };
let timer;
export function render() {
timer = setInterval(() => {
app.innerHTML = counter.number++;
}, 1000);
}
if (import.meta.hot) {
//每个模块有一个data属性,保存热更新前的状态
+ import.meta.hot.data.counter = counter;
import.meta.hot.dispose(() => {
console.log('dispose render.js');
clearInterval(timer);
});
}
src\render.js
+export let counter = import.meta.hot.data.counter || { number: 0 };
let timer;
export function render() {
timer = setInterval(() => {
app.innerHTML = counter.number++;
}, 1000);
}
if (import.meta.hot) {
import.meta.hot.data.counter = counter;
import.meta.hot.dispose(() => {
console.log('dispose render.js');
clearInterval(timer);
});
}
src\main.js
import { render } from './render';
render();
if (import.meta.hot) {
import.meta.hot.accept(['./render'], ([renderMod]) => {
+ if (renderMod.counter.number < 10) {
+ renderMod.render();
+ } else {
+ //强制刷新
+ import.meta.hot.invalidate();
}
});
+ //import.meta.hot.accept();
+ import.meta.hot.decline();
}
lib\server\index.js
const connect = require('connect');
const http = require('http');
const serveStaticMiddleware = require('./middlewares/static');
const resolveConfig = require('../config');
const { createOptimizeDepsRun } = require('../optimizer');
const transformMiddleware = require('./middlewares/transform');
const { createPluginContainer } = require('./pluginContainer');
+const { handleHMRUpdate } = require('./hmr');
+const { createWebSocketServer } = require('./ws');
+const { normalizePath } = require('../utils');
+const chokidar = require('chokidar');
+const { ModuleGraph } = require('./moduleGraph')
+const path = require('path');
async function createServer() {
const config = await resolveConfig();
const middlewares = connect();
+ const httpServer = require('http').createServer(middlewares)
+ const ws = createWebSocketServer(httpServer, config)
+ const watcher = chokidar.watch(path.resolve(config.root), {
+ ignored: [
+ '**/node_modules/**',
+ '**/.git/**'
+ ]
+ });
+ const moduleGraph = new ModuleGraph((url) =>
+ pluginContainer.resolveId(url)
+ )
+ const pluginContainer = await createPluginContainer(config)
const server = {
+ config,
+ ws,
+ watcher,
+ moduleGraph,
+ httpServer,
pluginContainer,
async listen(port) {
await runOptimize(config, server)
+ httpServer.listen(port, async () => {
console.log(`server running at http://localhost:${port}`);
});
}
}
+ watcher.on('change', async (file) => {
+ file = normalizePath(file)
+ await handleHMRUpdate(file, server)
+ })
for (const plugin of config.plugins) {
if (plugin.configureServer) {
await plugin.configureServer(server)
}
}
middlewares.use(transformMiddleware(server))
middlewares.use(serveStaticMiddleware(config));
return server;
}
async function runOptimize(config, server) {
const optimizeDeps = await createOptimizeDepsRun(config);
server._optimizeDepsMetadata = optimizeDeps.metadata
}
exports.createServer = createServer;
lib\server\ws.js
const { WebSocketServer } = require("ws");
const HMR_HEADER = "vite-hmr";
function createWebSocketServer(httpServer) {
const wss = new WebSocketServer({ noServer: true });
httpServer.on("upgrade", (req, socket, head) => {
if (req.headers["sec-websocket-protocol"] === HMR_HEADER) {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
}
});
wss.on("connection", (socket) => {
socket.send(JSON.stringify({ type: "connected" }));
});
return {
on: wss.on.bind(wss),
off: wss.off.bind(wss),
send(payload) {
const stringified = JSON.stringify(payload);
wss.clients.forEach((client) => {
if (client.readyState === 1) {
client.send(stringified);
}
});
},
};
}
exports.createWebSocketServer = createWebSocketServer;
lib\server\hmr.js
const LexerState = {
inCall: 0,
inQuoteString: 1,
};
async function handleHMRUpdate(file, server) {
const { moduleGraph, ws } = server;
//根据文件获取模块
const module = moduleGraph.getModuleById(file);
if (module) {
const updates = [];
const boundaries = new Set();
propagateUpdate(module, boundaries);
updates.push(
...[...boundaries].map(({ boundary, acceptedVia }) => ({
type: `${boundary.type}-update`,
path: boundary.url,
acceptedPath: acceptedVia.url,
}))
);
ws.send({
type: "update",
updates,
});
}
}
function updateModules(file, modules, { ws }) {}
function propagateUpdate(node, boundaries) {
if (!node.importers.size) {
return true;
}
for (const importer of node.importers) {
if (importer.acceptedHmrDeps.has(node)) {
boundaries.add({
boundary: importer,
acceptedVia: node,
});
continue;
}
}
return false;
}
function lexAcceptedHmrDeps(code, start, urls) {
let state = LexerState.inCall;
let prevState = LexerState.inCall;
let currentDep = "";
function addDep(index) {
urls.add({
url: currentDep,
start: index - currentDep.length - 1,
end: index + 1,
});
currentDep = "";
}
for (let i = start; i < code.length; i++) {
const char = code.charAt(i);
switch (state) {
case LexerState.inCall:
if (char === `'` || char === `"`) {
prevState = state;
state = LexerState.inQuoteString;
}
break;
case LexerState.inQuoteString:
if (char === `'` || char === `"`) {
addDep(i);
return false;
} else {
currentDep += char;
}
break;
default:
break;
}
}
return false;
}
exports.handleHMRUpdate = handleHMRUpdate;
exports.updateModules = updateModules;
exports.lexAcceptedHmrDeps = lexAcceptedHmrDeps;
lib\server\transformRequest.js
const fs = require('fs-extra');
+const { parse } = require("url");
async function transformRequest(url, server) {
const { pluginContainer } = server
const { id } = await pluginContainer.resolveId(url);
const loadResult = await pluginContainer.load(id)
let code;
if (loadResult) {
code = loadResult.code;;
} else {
+ let fsPath = parse(id).pathname;
+ code = await fs.readFile(fsPath, 'utf-8')
}
+ await server.moduleGraph.ensureEntryFromUrl(url)
const transformResult = await pluginContainer.transform(code, id)
return transformResult;
}
module.exports = transformRequest;
lib\server\moduleGraph.js
class ModuleNode {
//哪些模块导入了自己
importers = new Set();
//接收哪些子模块的修改
acceptedHmrDeps = new Set();
constructor(url) {
this.url = url;
this.type = "js";
}
}
class ModuleGraph {
constructor(resolveId) {
this.resolveId = resolveId;
}
idToModuleMap = new Map();
//通过ID查找模块
getModuleById(id) {
return this.idToModuleMap.get(id);
}
//把原始的URL添加到Map
async ensureEntryFromUrl(rawUrl) {
const [url, resolvedId] = await this.resolveUrl(rawUrl);
let mod = this.idToModuleMap.get(resolvedId); //通过文件URL查找模块
if (!mod) {
this.idToModuleMap.set(resolvedId, new ModuleNode(url)); //把绝对路径和模块的对应关系保存在idToModuleMap中
}
return mod;
}
async resolveUrl(url) {
const resolved = await this.resolveId(url);
const resolvedId = resolved.id || url;
return [url, resolvedId];
}
async updateModuleInfo(mod, importedModules, acceptedModules) {
for (const imported of importedModules) {
const dep = await this.ensureEntryFromUrl(imported);
dep.importers.add(mod); //render.js importerts main.js
}
const deps = (mod.acceptedHmrDeps = new Set()); //main.js acceptedHmrDeps render.js
for (const accepted of acceptedModules) {
const dep = await this.ensureEntryFromUrl(accepted);
deps.add(dep);
}
}
}
exports.ModuleGraph = ModuleGraph;
lib\plugins\importAnalysis.js
const { init, parse } = require('es-module-lexer')
const MagicString = require('magic-string');
+const { lexAcceptedHmrDeps } = require('../server/hmr');
+const path = require('path');
function importAnalysisPlugin(config) {
const { root } = config
+ let server
return {
name: 'vite:import-analysis',
+ configureServer(_server) {
+ server = _server
+ },
async transform(source, importer) {
await init
let imports = parse(source)[0]
if (!imports.length) {
return source
}
+ const { moduleGraph } = server
+ const importerModule = moduleGraph.getModuleById(importer)
+ const importedUrls = new Set()
+ const acceptedUrls = new Set()
let ms = new MagicString(source);
const normalizeUrl = async (url) => {
const resolved = await this.resolve(url, importer)
if (resolved.id.startsWith(root + '/')) {
url = resolved.id.slice(root.length)
}
+ await moduleGraph.ensureEntryFromUrl(url)
return url;
}
for (let index = 0; index < imports.length; index++) {
const { s: start, e: end, n: specifier } = imports[index]
+ const rawUrl = source.slice(start, end)
+ if (rawUrl === 'import.meta') {
+ const prop = source.slice(end, end + 4)
+ if (prop === '.hot') {
+ if (source.slice(end + 4, end + 11) === '.accept') {
+ lexAcceptedHmrDeps(source, source.indexOf('(', end + 11) + 1, acceptedUrls)
+ }
+ }
+ }
if (specifier) {
const normalizedUrl = await normalizeUrl(specifier)
if (normalizedUrl !== specifier) {
ms.overwrite(start, end, normalizedUrl)
}
+ importedUrls.add(normalizedUrl)
}
}
+ const normalizedAcceptedUrls = new Set()
+ const toAbsoluteUrl = (url) =>
+ path.posix.resolve(path.posix.dirname(importerModule.url), url)
+ for (const { url, start, end } of acceptedUrls) {
+ const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(url),)
+ normalizedAcceptedUrls.add(normalized)
+ ms.overwrite(start, end, JSON.stringify(normalized))
+ }
+ await moduleGraph.updateModuleInfo(
+ importerModule,
+ importedUrls,
+ normalizedAcceptedUrls
+ )
return ms.toString()
}
}
}
module.exports = importAnalysisPlugin;
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/client.js"></script>
</body>
</html>
src\main.js
import { render } from "./render.js";
render();
window.hotModulesMap = new Map();
var ownerPath = "/src/main.js";
import.meta.hot = {
accept(deps, callback) {
acceptDeps(deps, callback);
},
};
function acceptDeps(deps, callback) {
const mod = hotModulesMap.get(ownerPath) || {
id: ownerPath,
callbacks: [],
};
mod.callbacks.push({
deps,
fn: callback,
});
hotModulesMap.set(ownerPath, mod);
}
if (import.meta.hot) {
import.meta.hot.accept(["./render.js"], ([renderMod]) => {
renderMod.render();
});
}
src\render.js
export function render() {
app.innerHTML = "title1";
}
src\client.js
console.log("[vite] connecting...");
var socket = new WebSocket(`ws://${window.location.host}`, "vite-hmr");
socket.addEventListener("message", async ({ data }) => {
handleMessage(JSON.parse(data));
});
async function handleMessage(payload) {
switch (payload.type) {
case "connected":
console.log(`[vite] connected.`);
break;
case "update":
payload.updates.forEach((update) => {
if (update.type === "js-update") {
fetchUpdate(update);
}
});
break;
case "full-reload":
location.reload();
default:
break;
}
}
async function fetchUpdate({ path, acceptedPath }) {
const mod = window.hotModulesMap.get(path);
if (!mod) {
return;
}
const moduleMap = new Map();
const modulesToUpdate = new Set();
for (const { deps } of mod.callbacks) {
deps.forEach((dep) => {
if (acceptedPath === dep) {
modulesToUpdate.add(dep);
}
});
}
await Promise.all(
Array.from(modulesToUpdate).map(async (dep) => {
const newMod = await import(dep + "?ts=" + Date.now());
moduleMap.set(dep, newMod);
})
);
for (const { deps, fn } of mod.callbacks) {
fn(deps.map((dep) => moduleMap.get(dep)));
}
const loggedPath = `${acceptedPath} via ${path}`;
console.log(`[vite] hot updated: ${loggedPath}`);
}