前言
目前为止,我们已经完成了本地
本节我们需要将一部分能力的控制权交由用户管理
源码获取
传送门
更新进度
公众号:更新至第
博客:更新至第
源码分析
当配置过多时,向用户提供配置文件是一个明智的选择,在
import { defineConfig } from 'vite'; export default defineConfig({ ... });
之所以扩展名是
// packagesvitesrc odeconstants.ts export const DEFAULT_CONFIG_FILES = [ "vite.config.js", "vite.config.mjs", "vite.config.ts", "vite.config.cjs", "vite.config.mts", "vite.config.cts", ];
既然有动态可选的,就一定要有托底的配置项来保证
// packagesvitesrc odeserverindex.ts export async function _createServer( inlineConfig: InlineConfig = {}, options: { ws: boolean }, ): Promise<ViteDevServer> { const config = await resolveConfig(inlineConfig, 'serve') ... }
沿着
// packagesvitesrc odeconfig.ts export async function loadConfigFromFile( configEnv: ConfigEnv, configFile?: string, configRoot: string = process.cwd(), logLevel?: LogLevel ): Promise<{ path: string; config: UserConfig; dependencies: string[]; } | null> {}
在该函数中,
for (const filename of DEFAULT_CONFIG_FILES) { const filePath = path.resolve(configRoot, filename); if (!fs.existsSync(filePath)) continue; resolvedPath = filePath; break; }
找到配置文件后,尝试去获取文件类型,从如下逻辑可知,
let isESM = false; // 校验vite.config.xx配置文件的扩展名来识别使用的是哪一种模块规范 if (/.m[jt]s$/.test(resolvedPath)) { isESM = true; } else if (/.c[jt]s$/.test(resolvedPath)) { isESM = false; } else { // 如果无法从扩展名获取有用的信息,则找package.json,该文件的type字段也可以用以区分cjs和esm try { const pkg = lookupFile(configRoot, ["package.json"]); isESM = !!pkg && JSON.parse(fs.readFileSync(pkg, "utf-8")).type === "module"; } catch (e) {} }
下一步去读取配置文件,并且此时的配置文件是在用户侧未经过打包处理的,是不能直接拿来使用的,因此需要
const bundled = await bundleConfigFile(resolvedPath, isESM);
进入
async function bundleConfigFile( fileName: string, isESM: boolean, ): Promise<{ code: string; dependencies: string[] }> { ... const result = await build({ ... }) const { text } = result.outputFiles[0] return { code: text, dependencies: result.metafile ? Object.keys(result.metafile.inputs) : [], } }
回到
const userConfig = await loadConfigFromBundledFile( resolvedPath, bundled.code, isESM );
正常来说,
const extension = path.extname(fileName); const realFileName = await promisifiedRealpath(fileName); const loaderExt = extension in _require.extensions ? extension : ".js"; // 保存默认的require行为 const defaultLoader = _require.extensions[loaderExt]!; // 针对当前文件进行重写 _require.extensions[loaderExt] = (module: NodeModule, filename: string) => { if (filename === realFileName) { (module as NodeModuleWithCompile)._compile(bundledCode, filename); } else { defaultLoader(module, filename); } }; // 清除缓存 delete _require.cache[_require.resolve(fileName)]; const raw = _require(fileName); _require.extensions[loaderExt] = defaultLoader; return raw.__esModule ? raw.default : raw;
回到
const config = await( typeof userConfig === "function" ? userConfig(configEnv) : userConfig );
最后需要对用户配置文件中的配置的
export function defineConfig(config: UserConfigExport): UserConfigExport { return config; }
代码实现
首先,
进入
odeconfig.ts
export const DEFAULT_CONFIG_FILES = ["svite.config.ts"];
找到该文件下的
export async function resolveConfig(userConf: UserConfig) { const internalConf = {}; const conf = { ...userConf, ...internalConf, }; const userConfig = await parseConfigFile(conf); return { ...conf, ...userConfig, }; }
进入
let resolvedPath: string | undefined; for (const filename of DEFAULT_CONFIG_FILES) { const filePath = resolve(process.cwd(), filename); if (!existsSync(filePath)) continue; resolvedPath = filePath; break; }
如果我们的配置文件只有一个默认的
export default { name: "spp", };
那我直接使用
await import(resolvedPath);
但是现实是这会报错
Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only file and data URLs are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'c:'
这是由于
const code = readFileSync(resolvedPath, "utf-8"); const dynamicImport = new Function("file", "return import(file)"); const configTimestamp = `${resolvedPath}.timestamp:${Date.now()}-${Math.random() .toString(16) .slice(2)}`; const res = ( await dynamicImport( "data:text/javascript;base64," + Buffer.from(`${code} //${configTimestamp}`).toString("base64") ) ).default;
现在,新建一个
// svite.config.ts import { name } from "./other"; export default { name, };
针对这种情况,我们还需要对用户侧的
如下,我们将用户文件作为
async function buildBoundle(fileName: string) { const result = await build({ absWorkingDir: process.cwd(), entryPoints: [fileName], outfile: "out.js", write: false, target: ["node14.18", "node16"], platform: "node", bundle: true, format: "esm", mainFields: ["main"], sourcemap: "inline", metafile: false, }); const { text } = result.outputFiles[0]; return text; }
此时,只需要使用
async function loadConfigFromBoundled(code: string, resolvedPath: string) { const dynamicImport = new Function("file", "return import(file)"); const configTimestamp = `${resolvedPath}.timestamp:${Date.now()}-${Math.random() .toString(16) .slice(2)}`; return ( await dynamicImport( "data:text/javascript;base64," + Buffer.from(`${code} //${configTimestamp}`).toString("base64") ) ).default; }
接下来,只需要对
return typeof userConfigFile === "function" ? userConfigFile(conf) : userConfigFile;
总结
本节,为
在实现的过程中,稍微有点复杂的是配置文件打包和转