vite
vite copied to clipboard
Unnecessary `export` in output
Describe the bug
I've reproduced this with a simple piece of code:
import orderBy from 'lodash-es/orderBy.js';
console.log(orderBy);
Vite puts an export
in the output. I'm a bit confused as to why it'd do this as it's unnecessary. We currently use Vite to compile SvelteKit's service workers. export
is not allowed in service workers so this causes them to break. We can work around that by compiling to iife
, but it does result in code that's slightly larger and not quite as nice to read and it could result in extra bytes in other situations as well
This error does not happen when using Rollup directly. The reproduction has build scripts for both Rollup and Vite to demonstrate it and I've checked in the dist directory so you can easily see what they both output.
Reproduction
https://github.com/benmccann/vite-export-reproduction
Steps to reproduce
git clone [email protected]:benmccann/vite-export-reproduction.git
cd vite-export-reproduction
npm install
npm run build
System Info
npmPackages:
rollup: ^4.6.1 => 4.6.1
vite: ^5.0.3 => 5.0.3
Used Package Manager
npm
Logs
No response
Validations
- [X] Follow our Code of Conduct
- [X] Read the Contributing Guidelines.
- [X] Read the docs.
- [X] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- [X] Make sure this is a Vite issue and not a framework-specific issue. For example, if it's a Vue SFC related bug, it should likely be reported to vuejs/core instead.
- [X] Check that this is a concrete bug. For Q&A open a GitHub Discussion or join our Discord Chat Server.
- [X] The provided reproduction is a minimal reproducible example of the bug.
adding target: 'esnext'
to the build option seems to work
EDIT: it is weird, I had minify: false
and the output is fine, without it, the export is back...
it is esbuild
in the minify option (default?), using terser
works.
https://vitejs.dev/config/build-options.html#build-minify
It looks like lodash-es/isBuffer
is a simpler dependency to reproduce the issue. While looking at the source, I found that global exports
variable could be a minimal repro:
https://stackblitz.com/edit/github-cnfnjx?file=repro-esbuild.mjs
// node repro-esbuild.mjs
import * as esbuild from "esbuild";
const input = `console.log(typeof exports === 'object');`;
const output = await esbuild.transform(input, {
format: "esm",
});
console.log(output.code);
which produces a following output:
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var require_stdin = __commonJS({
"<stdin>"(exports) {
console.log(typeof exports === "object");
}
});
export default require_stdin();
This looks like esbuld's bug to me. I'll check their doc/issues to see if there's anything relevant.
Though this behavior looks surprising, It seems this is an expected behavior from esbuild's standpoint when they guess input code is whether cjs or not by existence of exports
, this
etc... https://github.com/evanw/esbuild/issues/1878#issuecomment-999765754 https://github.com/evanw/esbuild/issues/2807#issuecomment-1376618503
They suggest to add dummy export {}
to flag input code as ESM and this indeed seems to work without affecting the actual output:
https://stackblitz.com/edit/github-cnfnjx?file=repro-esbuild-workaround.mjs
// input
console.log(typeof exports === 'object');
export {}
// output
console.log(typeof exports === "object");
I was testing this trick in user code (i.e. vite config lib.entry
), but it doesn't seem to work probably because rollup would strip out such no-op export {}
before reaching esbuild's transform step during Vite build.
But, then I'm not sure if injecting this dummy export automatically on Vite side is a safe thing to do. (It might be safe because all the bundling is essentially done by rollup already?).
https://github.com/vitejs/vite/blob/5684fcd8d27110d098b3e1c19d851f44251588f1/packages/vite/src/node/plugins/esbuild.ts#L306
EDIT: One quick trick for Vite users would be to use rollupOptions.output.banner
to inject export {}
. This will make dummy export to reach esbuild transform:
https://stackblitz.com/edit/github-cnfnjx-zjgxr6?file=vite.config.js
Thank you for looking at this! Though I'm afraid I don't understand why esbuild should have to guess the format based on the file contents. Both the reproduction repository and lodash-es
have "type": "module"
in the package.json
so it should obviously be ESM
Though I'm afraid I don't understand why esbuild should have to guess the format based on the file contents. E.g.
lodash-es
has"type": "module"
in thepackage.json
so it should obviously be ESM
I think current Vite's usage of esbuild during vite build
is not for bundling but for transpilation (for target/format) + minification, which runs after Rollup did most of the work of bundling:
https://github.com/vitejs/vite/blob/5684fcd8d27110d098b3e1c19d851f44251588f1/packages/vite/src/node/plugins/esbuild.ts#L291-L294
At the point of renderChunk
hook, Vite uses esbuild's transform
API to process raw code string, so there's no concept of file system, module resolution, etc... and the situation is probably similar to this issue https://github.com/evanw/esbuild/issues/1878#issuecomment-999765754
What Esbuild does for this scenario seems to be only relying on syntactic clue (e.g. import/export
statement, exports/this
global variable access) and they don't provide any option to overwrite such guessing probably.
EDIT: Actually, it seems possible to overwrite the guessing by naming sourcefile
as .mjs
. From Vite config, setting rollupOptions.output.entryFileNames: "vite-output.mjs"
seems to work.
https://stackblitz.com/edit/github-cnfnjx-jk9zpq?file=vite.config.js
Got the same bug here. This is my Vite config inside my someModule.ts:
// some code here
try {
await build(
defineConfig({
logLevel: 'info',
plugins: [react(), cssByJs()],
build: {
minify: false,
emptyOutDir: false,
// changing the dist dir for easier automation
outDir: './build',
rollupOptions: {
input: {
'componentName': 'componentPath',
},
output: {
// to bundle everything
manualChunks: undefined,
// to prevent the hashes suffixes and /dist/assets dir
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
assetFileNames: '[name].[ext]',
},
},
},
}),
);
}catch(err){}
// some code here
And this is my component:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Form, Formik, Field } from 'formik';
function Component() {
return (
<Formik
initialValues={{ name: '' }}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
alert(JSON.stringify(values, null, 2));
}}
>
<Form>
<Field type='text' name='name' placeholder='Name' />
<button type='submit'>Submit</button>
</Form>
</Formik>
);
}
ReactDOM.createRoot(document.querySelector('body')!).render(Component());
It only happens when I use Formik
, which is so weird.
Here is the bundle's output:
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var require_test = __commonJS({
"test.js"(exports, module) {
// ...
// REACT AND FORMIK LIBRARY STUFF
// ...
function Component() {
return /* @__PURE__ */ jsxRuntimeExports.jsx(
Formik,
{
initialValues: { name: "" },
onSubmit: async (values) => {
await new Promise((r2) => setTimeout(r2, 500));
alert(JSON.stringify(values, null, 2));
},
children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Form, { children: [
/* @__PURE__ */ jsxRuntimeExports.jsx(Field, { type: "text", name: "name", placeholder: "Name" }),
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "submit", children: "Submit" })
] })
}
);
}
client.createRoot(document.querySelector("body")).render(Component());
}
});
export default require_test();
The last line is a syntax error in the browser without type='module'
html attribute in the script tag!