Vite Stack Overflow Answers

Selected technical solutions from my Stack Overflow history. These cover bundling, library mode, plugins, and how Vite resolves modules — not a vote leaderboard.

Problem

A Vite/Rollup build with several inputs needed custom output names (root bundle vs per-module files), not the default hashed chunk names.

Solution

Use output.entryFileNames as a function of the chunk. Inspect chunk.facadeModuleId: map src/index.js to libname.js and anything under /module to module/<folder>.js.

Why it works

Rollup names entry chunks from facadeModuleId, the original user entry. A callback can return a different pattern per path without separate builds.

Code

output: {
    entryFileNames: chunk => {
        if (chunk.facadeModuleId.endsWith('src/index.js')) {
            return 'libname.js';
        }
        if (chunk.facadeModuleId.includes('/module')) {
            const dir = path.dirname(chunk.facadeModuleId);
            return 'module/' + path.basename(dir) + '.js';
        }
    }
}
9 score · Accepted · 4.1K question views
View on Stack Overflow ↗

Problem

A Firebase service worker (and similar single-file outputs) must not be split. inlineDynamicImports works for one input, but multiple inputs reject that option.

Solution

Build each input separately: pass the file after --, set inlineDynamicImports for that one input, and emptyOutDir: false so later builds do not delete earlier files. Separate config files are a valid variant.

Why it works

Rollup can inline dynamic imports only when there is a single entry. Multiple entries imply extra chunks. Sequential one-input builds sidestep that constraint.

Code

const input = process.argv[4]?.split('=')?.[1];
if (input) {
    // build only this input with inlineDynamicImports: true
}
3 score · Accepted · 17K question views
View on Stack Overflow ↗

Problem

The asker needed to import and debug an npm package (stacks-editor) from HTML, including edits inside node_modules. Bare <script type="module"> imports failed on inner CommonJS deps.

Solution

Use Vite. Dev pre-bundling still fights CommonJS excludes, so watch node_modules with entr and run vite build --watch-equivalent, with minify and treeshake off, then vite preview.

Why it works

Vite can resolve package entry points; the browser cannot. Rollup's watch of node_modules was unreliable here, so an external file watcher rebuilding is the practical loop.

Code

export default defineConfig({
    build: {
        minify: false,
        rollupOptions: { treeshake: false }
    }
});
2 score · Accepted
View on Stack Overflow ↗

Problem

Plugin components were loaded with import() whose path included a runtime directory variable. Dev worked; the production build did not include those modules.

Solution

Vite cannot see variables inside directory segments of dynamic import(). Put a fully static path in each import, like vue-router lazy routes: () => import("./components/debug/myButton/myButton.js").

Why it works

Rollup must enumerate files at build time. A dynamic directory is not a glob it can expand, so those modules never become chunks.

Code

{
    "name": "myButton",
    "component": () => import("./components/debug/myButton/myButton.js"),
    "enabled": true
}
2 score · Accepted · 2.8K question views
View on Stack Overflow ↗

Problem

A library build wanted Webpack-style code-split UMD chunks. Forcing dynamicInlineImports: false with format umd failed.

Solution

You cannot have both. UMD/IIFE output cannot be code-split. Choose ESM/CJS splitting or a single UMD file.

Why it works

UMD and IIFE assume one file with a single global/factory. Extra async chunks have no standard UMD loading story, which Rollup rejects up front.

Code

Invalid value "umd" for option "output.format" -
UMD and IIFE output formats are not supported for code-splitting builds
4 score · Accepted · 3.2K question views
View on Stack Overflow ↗

Problem

A multi-page site had about.html next to index.html, not about/index.html, and needed those HTML files bundled rather than copied as static public assets.

Solution

Treat them as Rollup inputs. Set build.rollupOptions.input to the HTML files (glob if needed) and map output.entryFileNames if the default paths are wrong.

Why it works

Vite MPA is Rollup multi-entry with HTML as the entry. The public/ folder only copies files; it does not bundle JS referenced from them.

Code

build: {
    rollupOptions: {
        input: ['index.html', 'about.html']
    }
}
4 score · Accepted · 5.5K question views
View on Stack Overflow ↗

Problem

A Vue+Vite app needed one JS file. Dynamic import() was still emitting extra chunks.

Solution

Set output.inlineDynamicImports: true. That requires a single input and changes execution order: previously lazy modules run immediately.

Why it works

Inlining copies the dynamically imported module into the entry instead of emitting a separate file. Rollup only allows that when there is one entry graph.

Code

output: {
    inlineDynamicImports: true
}
4 score · Accepted
View on Stack Overflow ↗

Problem

The SFC needed many CSS files injected into a scoped <style>, not as global JS imports. Vite has no built-in "glob into this style block".

Solution

A tiny Rollup load hook rewrites inject-css: "glob" in .vue files: globSync the pattern and splice the file contents into the SFC source before Vue compiles it.

Why it works

Scoped CSS is compiled per SFC. Concatenating files in load() makes them part of that SFC's style, so Vue's scoped transform applies to all of them.

Code

load(id) {
    if (id.endsWith('.vue')) {
        return fs.readFileSync(id).toString().replace(
            /inject-css:\s*"([^"]+)";/g,
            (_, pattern) => globSync(pattern, { absolute: true })
                .map(file => fs.readFileSync(file)).join(';')
        );
    }
}
2 score · Accepted
View on Stack Overflow ↗

Problem

An inline <script type="module"> in index.html worked in dev. After build the author expected the same inline script, or thought the JS was missing.

Solution

The build does emit a hashed /assets/index-….js and rewrites the HTML. There is no option to keep the module inline. entryFileNames: 'index.js' removes the hash if that is the complaint.

Why it works

Vite treats the HTML module as an entry and bundles it. Inlining the whole graph back into HTML is not a supported output.

Code

build: {
    rollupOptions: {
        output: { entryFileNames: 'index.js' }
    }
}
0 score · Accepted
View on Stack Overflow ↗

Problem

build.manifest wrote Vite's asset map. The project needed extra fields on those entries.

Solution

A plugin closeBundle hook reads dist/.vite/manifest.json, mutates it, and writes it back after Vite finishes.

Why it works

closeBundle runs after Rollup has written files. Patching the JSON then is stable; doing it earlier races the writer.

Code

closeBundle: () => {
    const path = './dist/.vite/manifest.json';
    const manifest = JSON.parse(fs.readFileSync(path).toString());
    manifest['index.html'].customProp = true;
    fs.writeFileSync(path, JSON.stringify(manifest, null, 2));
}
0 score · Accepted
View on Stack Overflow ↗

Problem

SCSS variables lived in the Vue <style> block. JS needed those values at runtime without duplicating them.

Solution

A load hook on .vue files parses the SCSS (scss-parser if needed) and replaces a sentinel like getScssVariables() with a JS object literal of the extracted vars.

Why it works

The SFC is still source when load() runs. String-replacing the sentinel compiles into real JS the component can import/call.

Code

load(id) {
    if (!id.endsWith('.vue')) return;
    const src = fs.readFileSync(id);
    return src.replace('getScssVariables()', '{ /* extracted vars */ }');
}
1 score · Accepted
View on Stack Overflow ↗

Problem

JS in .js files was downleveled to the build.target, but the same syntax in HTML attributes (onclick) was left as-is.

Solution

For Vue, put the handler in the template (@click) or <script>: the compiler emits a render function that esbuild transpiles. build.target (for example es6) applies to that output. Raw HTML attribute JS is not part of the module graph.

Why it works

Vite transpiles modules. An onclick string is HTML, not a JS module, so esbuild never sees it. Vue templates are compiled into JS modules, which is why @click is transformed.

Code

build: {
    target: 'es6'
}
0 score · 1.7K question views
View on Stack Overflow ↗

Problem

One app needed several libraries with different aliases, plugins, and folder layouts. Webpack multi-compiler exported an array of configs; Vite CLI takes one -c.

Solution

Keep vite.config.mjs, vite.lib.config.js, vite.vendor.config.js and run them in parallel: find vite*.config.*js | xargs -P0 -n1 npx vite build -c. One parameterized config is the other option.

Why it works

Each Vite process is one Rollup graph. Parallel CLI processes are the equivalent of Webpack's multi-compiler without a first-class config array.

Code

find -maxdepth 1 -name 'vite*.config.*js' | xargs -P0 -n1 npx vite build -c
3 score · 4.4K question views
View on Stack Overflow ↗

Problem

Dev (native ESM, no vite.config) felt slow. The asker wanted a bundled, non-ESM-style dev loop.

Solution

npx vite build --watch --minify=false together with vite preview, optionally treeshake: false. Reloading is manual unless something watches dist.

Why it works

Vite dev serves unbundled ESM on purpose. build --watch is the bundled pipeline; preview is a static server, so HMR is not in that loop.

Code

npx vite build --watch --minify=false &
npx vite preview
2 score
View on Stack Overflow ↗

Problem

Some Vue SFCs were optional. A missing import failed the whole Vite build.

Solution

A resolveId hook: if the id ends with .vue and cannot be resolved, return a Placeholder.vue path so Rollup continues.

Why it works

When resolveId returns a filename, Rollup loads that instead of erroring. The placeholder can render nothing or a stub.

Code

{
    resolveId(id) {
        if (id.endsWith('.vue')) {
            return './src/components/Placeholder.vue';
        }
    }
}
1 score · Accepted
View on Stack Overflow ↗

Problem

A file change needed to run custom transform logic, not only Vite's default HMR.

Solution

Write a Vite plugin (.mjs) with config, resolveId, load, and transform. Import it like any other plugin; plugin order controls what code transform sees.

Why it works

Those hooks are the documented extension points. load/transform run whenever the module graph invalidates a file, which is the "on change" event.

Code

export default function (pluginOptions = {}) {
    return {
        name: 'test-vite-plugin',
        async transform(code, id) {
            // transform here; order depends on plugin inject flags
        }
    };
}
0 score
View on Stack Overflow ↗

Problem

Product logos used a runtime path under src/assets. Vite could not resolve them; public/ worked but skipped the image optimizer.

Solution

globalThis._importMeta_.glob('/src/assets/images/*/*Logo.svg', { as: 'url', eager: true }) at build time, then pick the URL from that map in getProductLogoUrl.

Why it works

Glob without runtime directory variables is something Vite can enumerate. Fetching product names later cannot invent imports the bundler never saw.

Code

images: globalThis._importMeta_.glob('/src/assets/images/*/*Logo.svg', {
    as: 'url',
    eager: true
})
0 score · 97 question views
View on Stack Overflow ↗

Problem

Many JSON content files were needed at runtime, but only a title field, without loading every full file into the client bundle blindly.

Solution

globalThis._importMeta_.glob('./content/**/*.json', { eager: true, import: 'title' }) then reduce keys into a lookup map.

Why it works

The import option tree-shakes each JSON module down to the named export, so the glob stays cheap.

Code

const files = globalThis._importMeta_.glob('./content/**/*.json', {
    eager: true,
    import: 'title'
});
0 score
View on Stack Overflow ↗

Problem

Production bugs were hard to map back through minification and tree-shaking. Source maps were not always trustworthy.

Solution

Emit a readable build: vite build --minify=false and rollup treeshake: false. That is still bundled/transpiled output, not original sources, but names survive.

Why it works

Minify + treeshake can produce source maps that no longer line up. A non-minified bundle is the actual deployed code, which is what you step through.

Code

npx vite build --minify=false
0 score
View on Stack Overflow ↗

Problem

Unknown components only produced a Vue warn. The team wanted a hard, visible failure.

Solution

resolveId maps unresolved .vue ids to MissingPlaceholder.vue?id=…. load() emits a stub that console.error's the missing path and renders it.

Why it works

Unresolved SFCs never reach Vue's runtime warn if Rollup fails first. Returning a stub turns a resolve miss into an explicit runtime/build artifact.

Code

resolveId(id) {
    if (id.endsWith('.vue')) {
        return 'MissingPlaceholder.vue?id=' + id;
    }
}
1 score · 498 question views
View on Stack Overflow ↗
View All Answers ↗