vite icon indicating copy to clipboard operation
vite copied to clipboard

Unnecessary `export` in output

Open benmccann opened this issue 1 year ago • 7 comments

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

benmccann avatar Dec 18 '23 16:12 benmccann

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...

userquin avatar Dec 18 '23 19:12 userquin

it is esbuild in the minify option (default?), using terser works.

userquin avatar Dec 18 '23 20:12 userquin

https://vitejs.dev/config/build-options.html#build-minify

userquin avatar Dec 18 '23 21:12 userquin

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.

hi-ogawa avatar Dec 19 '23 02:12 hi-ogawa

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

hi-ogawa avatar Dec 19 '23 02:12 hi-ogawa

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

benmccann avatar Dec 19 '23 04:12 benmccann

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 the package.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

hi-ogawa avatar Dec 19 '23 04:12 hi-ogawa

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!

anasouardini avatar Mar 01 '24 19:03 anasouardini