vite esbuild 预打包

285 阅读3分钟

环境配置

默认的vite+vue+js

断点调试

由于第一次编译,不存在.vite/deps的缓存文件

需要重新收集依赖

depsOptimizer.scanProcessing = new Promise((resolve) => {
    // Ensure server listen is called before the scanner
    setTimeout(async () => {
        try {
            debuggerViteDeps(picocolorsExports.green(`scanning for dependencies...`));
            const deps = await discoverProjectDependencies(config);
            debuggerViteDeps(picocolorsExports.green(Object.keys(deps).length > 0
                ? `dependencies found by scanner: ${depsLogString(Object.keys(deps))}`
                : `no dependencies found by scanner`));
            // Add these dependencies to the discovered list, as these are currently
            // used by the preAliasPlugin to support aliased and optimized deps.
            // This is also used by the CJS externalization heuristics in legacy mode
            for (const id of Object.keys(deps)) {
                if (!metadata.discovered[id]) {
                    addMissingDep(id, deps[id]);
                }
            }
            const knownDeps = prepareKnownDeps();
            // For dev, we run the scanner and the first optimization
            // run on the background, but we wait until crawling has ended
            // to decide if we send this result to the browser or we need to
            // do another optimize step
            postScanOptimizationResult = runOptimizeDeps(config, knownDeps);
        }
        catch (e) {
            logger.error(e.message);
        }
        finally {
            resolve();
            depsOptimizer.scanProcessing = undefined;
        }
    }, 0);
    });

image.png

async function discoverProjectDependencies(config) {
    const { deps, missing } = await scanImports(config);
    const missingIds = Object.keys(missing);
    if (missingIds.length) {
        throw new Error(`The following dependencies are imported but could not be resolved:\n\n  ${missingIds
            .map((id) => `${picocolorsExports.cyan(id)} ${picocolorsExports.white(picocolorsExports.dim(`(imported by ${missing[id]})`))}`)
            .join(`\n  `)}\n\nAre they installed?`);
    }
    return deps;
}

默认入口是index.html

image.png

这是esbuild收集依赖的插件vite:dep-scan

function esbuildScanPlugin(config, container, depImports, missing, entries) {
    const seen = new Map();
    const resolve = async (id, importer, options) => {
        const key = id + (importer && path$o.dirname(importer));
        if (seen.has(key)) {
            return seen.get(key);
        }
        const resolved = await container.resolveId(id, importer && normalizePath$3(importer), {
            ...options,
            scan: true,
        });
        const res = resolved?.id;
        seen.set(key, res);
        return res;
    };
    const include = config.optimizeDeps?.include;
    const exclude = [
        ...(config.optimizeDeps?.exclude || []),
        '@vite/client',
        '@vite/env',
    ];
    const externalUnlessEntry = ({ path }) => ({
        path,
        external: !entries.includes(path),
    });
    const doTransformGlobImport = async (contents, id, loader) => {
        let transpiledContents;
        // transpile because `transformGlobImport` only expects js
        if (loader !== 'js') {
            transpiledContents = (await transform$2(contents, { loader })).code;
        }
        else {
            transpiledContents = contents;
        }
        const result = await transformGlobImport(transpiledContents, id, config.root, resolve, config.isProduction);
        return result?.s.toString() || transpiledContents;
    };
    return {
        name: 'vite:dep-scan',
        setup(build) {
            const scripts = {};
            // external urls
            build.onResolve({ filter: externalRE }, ({ path }) => ({
                path,
                external: true,
            }));
            // data urls
            build.onResolve({ filter: dataUrlRE }, ({ path }) => ({
                path,
                external: true,
            }));
            // local scripts (`<script>` in Svelte and `<script setup>` in Vue)
            build.onResolve({ filter: virtualModuleRE }, ({ path }) => {
                return {
                    // strip prefix to get valid filesystem path so esbuild can resolve imports in the file
                    path: path.replace(virtualModulePrefix, ''),
                    namespace: 'script',
                };
            });
            build.onLoad({ filter: /.*/, namespace: 'script' }, ({ path }) => {
                return scripts[path];
            });
            // html types: extract script contents -----------------------------------
            build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
                const resolved = await resolve(path, importer);
                if (!resolved)
                    return;
                // It is possible for the scanner to scan html types in node_modules.
                // If we can optimize this html type, skip it so it's handled by the
                // bare import resolve, and recorded as optimization dep.
                if (resolved.includes('node_modules') &&
                    isOptimizable(resolved, config.optimizeDeps))
                    return;
                return {
                    path: resolved,
                    namespace: 'html',
                };
            });
            // extract scripts inside HTML-like files and treat it as a js module
            build.onLoad({ filter: htmlTypesRE, namespace: 'html' }, async ({ path }) => {
                let raw = fs$l.readFileSync(path, 'utf-8');
                // Avoid matching the content of the comment
                raw = raw.replace(commentRE, '<!---->');
                const isHtml = path.endsWith('.html');
                const regex = isHtml ? scriptModuleRE : scriptRE;
                regex.lastIndex = 0;
                let js = '';
                let scriptId = 0;
                let match;
                while ((match = regex.exec(raw))) {
                    const [, openTag, content] = match;
                    const typeMatch = openTag.match(typeRE);
                    const type = typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3]);
                    const langMatch = openTag.match(langRE);
                    const lang = langMatch && (langMatch[1] || langMatch[2] || langMatch[3]);
                    // skip type="application/ld+json" and other non-JS types
                    if (type &&
                        !(type.includes('javascript') ||
                            type.includes('ecmascript') ||
                            type === 'module')) {
                        continue;
                    }
                    let loader = 'js';
                    if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {
                        loader = lang;
                    }
                    else if (path.endsWith('.astro')) {
                        loader = 'ts';
                    }
                    const srcMatch = openTag.match(srcRE);
                    if (srcMatch) {
                        const src = srcMatch[1] || srcMatch[2] || srcMatch[3];
                        js += `import ${JSON.stringify(src)}\n`;
                    }
                    else if (content.trim()) {
                        // The reason why virtual modules are needed:
                        // 1. There can be module scripts (`<script context="module">` in Svelte and `<script>` in Vue)
                        // or local scripts (`<script>` in Svelte and `<script setup>` in Vue)
                        // 2. There can be multiple module scripts in html
                        // We need to handle these separately in case variable names are reused between them
                        // append imports in TS to prevent esbuild from removing them
                        // since they may be used in the template
                        const contents = content +
                            (loader.startsWith('ts') ? extractImportPaths(content) : '');
                        const key = `${path}?id=${scriptId++}`;
                        if (contents.includes('import.meta.glob')) {
                            scripts[key] = {
                                loader: 'js',
                                contents: await doTransformGlobImport(contents, path, loader),
                                pluginData: {
                                    htmlType: { loader },
                                },
                            };
                        }
                        else {
                            scripts[key] = {
                                loader,
                                contents,
                                pluginData: {
                                    htmlType: { loader },
                                },
                            };
                        }
                        const virtualModulePath = JSON.stringify(virtualModulePrefix + key);
                        const contextMatch = openTag.match(contextRE);
                        const context = contextMatch &&
                            (contextMatch[1] || contextMatch[2] || contextMatch[3]);
                        // Especially for Svelte files, exports in <script context="module"> means module exports,
                        // exports in <script> means component props. To avoid having two same export name from the
                        // star exports, we need to ignore exports in <script>
                        if (path.endsWith('.svelte') && context !== 'module') {
                            js += `import ${virtualModulePath}\n`;
                        }
                        else {
                            js += `export * from ${virtualModulePath}\n`;
                        }
                    }
                }
                // This will trigger incorrectly if `export default` is contained
                // anywhere in a string. Svelte and Astro files can't have
                // `export default` as code so we know if it's encountered it's a
                // false positive (e.g. contained in a string)
                if (!path.endsWith('.vue') || !js.includes('export default')) {
                    js += '\nexport default {}';
                }
                return {
                    loader: 'js',
                    contents: js,
                };
            });
            // bare imports: record and externalize ----------------------------------
            build.onResolve({
                // avoid matching windows volume
                filter: /^[\w@][^:]/,
            }, async ({ path: id, importer, pluginData }) => {
                if (moduleListContains(exclude, id)) {
                    return externalUnlessEntry({ path: id });
                }
                if (depImports[id]) {
                    return externalUnlessEntry({ path: id });
                }
                const resolved = await resolve(id, importer, {
                    custom: {
                        depScan: { loader: pluginData?.htmlType?.loader },
                    },
                });
                if (resolved) {
                    if (shouldExternalizeDep(resolved, id)) {
                        return externalUnlessEntry({ path: id });
                    }
                    if (resolved.includes('node_modules') || include?.includes(id)) {
                        // dependency or forced included, externalize and stop crawling
                        if (isOptimizable(resolved, config.optimizeDeps)) {
                            depImports[id] = resolved;
                        }
                        return externalUnlessEntry({ path: id });
                    }
                    else if (isScannable(resolved)) {
                        const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined;
                        // linked package, keep crawling
                        return {
                            path: path$o.resolve(resolved),
                            namespace,
                        };
                    }
                    else {
                        return externalUnlessEntry({ path: id });
                    }
                }
                else {
                    missing[id] = normalizePath$3(importer);
                }
            });
            // Externalized file types -----------------------------------------------
            // these are done on raw ids using esbuild's native regex filter so it
            // should be faster than doing it in the catch-all via js
            // they are done after the bare import resolve because a package name
            // may end with these extensions
            // css
            build.onResolve({ filter: CSS_LANGS_RE }, externalUnlessEntry);
            // json & wasm
            build.onResolve({ filter: /\.(json|json5|wasm)$/ }, externalUnlessEntry);
            // known asset types
            build.onResolve({
                filter: new RegExp(`\\.(${KNOWN_ASSET_TYPES.join('|')})$`),
            }, externalUnlessEntry);
            // known vite query types: ?worker, ?raw
            build.onResolve({ filter: SPECIAL_QUERY_RE }, ({ path }) => ({
                path,
                external: true,
            }));
            // catch all -------------------------------------------------------------
            build.onResolve({
                filter: /.*/,
            }, async ({ path: id, importer, pluginData }) => {
                // use vite resolver to support urls and omitted extensions
                const resolved = await resolve(id, importer, {
                    custom: {
                        depScan: { loader: pluginData?.htmlType?.loader },
                    },
                });
                if (resolved) {
                    if (shouldExternalizeDep(resolved, id) || !isScannable(resolved)) {
                        return externalUnlessEntry({ path: id });
                    }
                    const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined;
                    return {
                        path: path$o.resolve(cleanUrl(resolved)),
                        namespace,
                    };
                }
                else {
                    // resolve failed... probably unsupported type
                    return externalUnlessEntry({ path: id });
                }
            });
            // for jsx/tsx, we need to access the content and check for
            // presence of import.meta.glob, since it results in import relationships
            // but isn't crawled by esbuild.
            build.onLoad({ filter: JS_TYPES_RE }, async ({ path: id }) => {
                let ext = path$o.extname(id).slice(1);
                if (ext === 'mjs')
                    ext = 'js';
                let contents = fs$l.readFileSync(id, 'utf-8');
                if (ext.endsWith('x') && config.esbuild && config.esbuild.jsxInject) {
                    contents = config.esbuild.jsxInject + `\n` + contents;
                }
                const loader = config.optimizeDeps?.esbuildOptions?.loader?.[`.${ext}`] ||
                    ext;
                if (contents.includes('import.meta.glob')) {
                    return {
                        loader: 'js',
                        contents: await doTransformGlobImport(contents, id, loader),
                    };
                }
                return {
                    loader,
                    contents,
                };
            });
        },
    };
}

由于入口文件是index.html,所以主要看html的解析

image.png

从html里面找到script的部分

image.png

完整的js

image.png

之后处理main.js的时候,将vue收集进依赖

这里如果是虚拟模块,虚拟模块为\0开头的,就不会进入依赖收集 image.png

image.png

main.js有import App from './App.vue'

image.png

虚拟模块不会进行依赖收集

helloWorld.vue有import { ref } from 'vue'

但是vue已经收集过依赖了,不会重新收集

image.png

收集完依赖后,就对依赖进行打包

image.png

5个vue的文件打包成了一个,还有一份sourcemap

将预处理结果存到.vite/deps

image.png