vercel / next.js

The React Framework
https://nextjs.org
MIT License
125.1k stars 26.72k forks source link

When using SWC, code wrapped in `if (typeof window === 'undefined')` is not removed from the browser bundle. #30892

Open rgoldfinger-quizlet opened 2 years ago

rgoldfinger-quizlet commented 2 years ago

What version of Next.js are you using?

12.0.2

What version of Node.js are you using?

16.8.0

What browser are you using?

Chrome / N/A

What operating system are you using?

macOS

How are you deploying your application?

other

Describe the Bug

When using SWC, code wrapped in if (typeof window === 'undefined') is not removed from the browser bundle.

Without a .babelrc in my project (and confirming that it's using SWC via lack of warn - Disabled SWC as replacement for Babel warning), I see that server-side libraries are loaded in the browser bundle.

See repro.

Expected Behavior

Code wrapped in if (typeof window === 'undefined') is not included in the browser bundle.

To Reproduce

Original code:

import { getDocumentData } from 'utils/data/documentData';
import type { QGetServerSidePropsContext } from 'utils/serverSideProps/types';

export async function withServerSideTranslations(
  context: QGetServerSidePropsContext
) {
  if (typeof window === 'undefined') {
    const serverSideTranslations = (
      await import('next-i18next/serverSideTranslations')
    ).serverSideTranslations;
    const data = await getDocumentData(context.req);
    return {
      props: await serverSideTranslations(
        data.localePreference || context.locale!
      ),
    };
  }
  return { props: {} };
}

Browser output using SWC:

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
  /* harmony export */ withServerSideTranslations: function () {
    return /* binding */ withServerSideTranslations;
  },
  /* harmony export */
});
/* harmony import */ var regenerator_runtime__WEBPACK_IMPORTED_MODULE_0__ =
  __webpack_require__(
    /*! regenerator-runtime */ "../../node_modules/regenerator-runtime/runtime.js"
  );
/* harmony import */ var regenerator_runtime__WEBPACK_IMPORTED_MODULE_0___default =
  /*#__PURE__*/ __webpack_require__.n(
    regenerator_runtime__WEBPACK_IMPORTED_MODULE_0__
  );
/* harmony import */ var utils_data_documentData__WEBPACK_IMPORTED_MODULE_1__ =
  __webpack_require__(
    /*! utils/data/documentData */ "./utils/data/documentData.ts"
  );
/* module decorator */ module = __webpack_require__.hmd(module);
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}
function _asyncToGenerator(fn) {
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      _next(undefined);
    });
  };
}
function _withServerSideTranslations() {
  _withServerSideTranslations = _asyncToGenerator(
    regenerator_runtime__WEBPACK_IMPORTED_MODULE_0___default().mark(
      function _callee(context) {
        var serverSideTranslations, data;
        return regenerator_runtime__WEBPACK_IMPORTED_MODULE_0___default().wrap(
          function _callee$(_ctx) {
            while (1)
              switch ((_ctx.prev = _ctx.next)) {
                case 0:
                  if (true) {
                    _ctx.next = 11;
                    break;
                  }
                  _ctx.next = 3;
                  return __webpack_require__
                    .e(
                      /*! import() */ "node_modules_next-i18next_serverSideTranslations_js"
                    )
                    .then(
                      __webpack_require__.t.bind(
                        __webpack_require__,
                        /*! next-i18next/serverSideTranslations */ "../../node_modules/next-i18next/serverSideTranslations.js",
                        23
                      )
                    );
                case 3:
                  serverSideTranslations = _ctx.sent.serverSideTranslations;
                  _ctx.next = 6;
                  return (0,
                  utils_data_documentData__WEBPACK_IMPORTED_MODULE_1__.getDocumentData)(
                    context.req
                  );
                case 6:
                  data = _ctx.sent;
                  _ctx.next = 9;
                  return serverSideTranslations(
                    data.localePreference || context.locale
                  );
                case 9:
                  _ctx.t0 = _ctx.sent;
                  return _ctx.abrupt("return", {
                    props: _ctx.t0,
                  });
                case 11:
                  return _ctx.abrupt("return", {
                    props: {},
                  });
                case 12:
                case "end":
                  return _ctx.stop();
              }
          },
          _callee
        );
      }
    )
  );
  return _withServerSideTranslations.apply(this, arguments);
}

function withServerSideTranslations(context) {
  return _withServerSideTranslations.apply(this, arguments);
}
var _a, _b;
// Legacy CSS implementations will `eval` browser code in a Node.js context
// to extract CSS. For backwards compatibility, we need to check we're in a
// browser context before continuing.
if (
  typeof self !== "undefined" &&
  // AMP / No-JS mode does not inject these helpers:
  "$RefreshHelpers$" in self
) {
  var currentExports = module.__proto__.exports;
  var prevExports =
    (_b =
      (_a = module.hot.data) === null || _a === void 0
        ? void 0
        : _a.prevExports) !== null && _b !== void 0
      ? _b
      : null;
  // This cannot happen in MainTemplate because the exports mismatch between
  // templating and execution.
  self.$RefreshHelpers$.registerExportsForReactRefresh(
    currentExports,
    module.id
  );
  // A module can be accepted automatically based on its exports, e.g. when
  // it is a Refresh Boundary.
  if (self.$RefreshHelpers$.isReactRefreshBoundary(currentExports)) {
    // Save the previous exports on update so we can compare the boundary
    // signatures.
    module.hot.dispose(function (data) {
      data.prevExports = currentExports;
    });
    // Unconditionally accept an update to this module, we'll check if it's
    // still a Refresh Boundary later.
    module.hot.accept();
    // This field is set when the previous version of this module was a
    // Refresh Boundary, letting us know we need to check for invalidation or
    // enqueue an update.
    if (prevExports !== null) {
      // A boundary can become ineligible if its exports are incompatible
      // with the previous exports.
      //
      // For example, if you add/remove/change exports, we'll want to
      // re-execute the importing modules, and force those components to
      // re-render. Similarly, if you convert a class component to a
      // function, we want to invalidate the boundary.
      if (
        self.$RefreshHelpers$.shouldInvalidateReactRefreshBoundary(
          prevExports,
          currentExports
        )
      ) {
        module.hot.invalidate();
      } else {
        self.$RefreshHelpers$.scheduleUpdate();
      }
    }
  } else {
    // Since we just executed the code for the module, it's possible that the
    // new exports made it ineligible for being a boundary.
    // We only care about the case when we were _previously_ a boundary,
    // because we already accepted this update (accidental side effect).
    var isNoLongerABoundary = prevExports !== null;
    if (isNoLongerABoundary) {
      module.hot.invalidate();
    }
  }
}

Browser output using Babel:

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
  /* harmony export */ withServerSideTranslations: function () {
    return /* binding */ withServerSideTranslations;
  },
  /* harmony export */
});
/* harmony import */ var _opt_projects_quizlet_node_modules_next_node_modules_babel_runtime_helpers_esm_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__ =
  __webpack_require__(
    /*! ../../node_modules/next/node_modules/@babel/runtime/helpers/esm/asyncToGenerator */ "../../node_modules/next/node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js"
  );
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_1__ =
  __webpack_require__(
    /*! core-js/modules/es.promise.js */ "../../node_modules/core-js/modules/es.promise.js"
  );
/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_1___default =
  /*#__PURE__*/ __webpack_require__.n(
    core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_1__
  );
/* harmony import */ var utils_data_documentData__WEBPACK_IMPORTED_MODULE_2__ =
  __webpack_require__(
    /*! utils/data/documentData */ "./utils/data/documentData.ts"
  );
/* module decorator */ module = __webpack_require__.hmd(module);
function withServerSideTranslations(_x) {
  return _withServerSideTranslations.apply(this, arguments);
}
function _withServerSideTranslations() {
  _withServerSideTranslations = (0,
  _opt_projects_quizlet_node_modules_next_node_modules_babel_runtime_helpers_esm_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__[
    "default"
  ])(function* (context) {
    if (false) {
      var data, serverSideTranslations;
    }
    return {
      props: {},
    };
  });
  return _withServerSideTranslations.apply(this, arguments);
}
var _a, _b;
// Legacy CSS implementations will `eval` browser code in a Node.js context
// to extract CSS. For backwards compatibility, we need to check we're in a
// browser context before continuing.
if (
  typeof self !== "undefined" &&
  // AMP / No-JS mode does not inject these helpers:
  "$RefreshHelpers$" in self
) {
  var currentExports = module.__proto__.exports;
  var prevExports =
    (_b =
      (_a = module.hot.data) === null || _a === void 0
        ? void 0
        : _a.prevExports) !== null && _b !== void 0
      ? _b
      : null;
  // This cannot happen in MainTemplate because the exports mismatch between
  // templating and execution.
  self.$RefreshHelpers$.registerExportsForReactRefresh(
    currentExports,
    module.id
  );
  // A module can be accepted automatically based on its exports, e.g. when
  // it is a Refresh Boundary.
  if (self.$RefreshHelpers$.isReactRefreshBoundary(currentExports)) {
    // Save the previous exports on update so we can compare the boundary
    // signatures.
    module.hot.dispose(function (data) {
      data.prevExports = currentExports;
    });
    // Unconditionally accept an update to this module, we'll check if it's
    // still a Refresh Boundary later.
    module.hot.accept();
    // This field is set when the previous version of this module was a
    // Refresh Boundary, letting us know we need to check for invalidation or
    // enqueue an update.
    if (prevExports !== null) {
      // A boundary can become ineligible if its exports are incompatible
      // with the previous exports.
      //
      // For example, if you add/remove/change exports, we'll want to
      // re-execute the importing modules, and force those components to
      // re-render. Similarly, if you convert a class component to a
      // function, we want to invalidate the boundary.
      if (
        self.$RefreshHelpers$.shouldInvalidateReactRefreshBoundary(
          prevExports,
          currentExports
        )
      ) {
        module.hot.invalidate();
      } else {
        self.$RefreshHelpers$.scheduleUpdate();
      }
    }
  } else {
    // Since we just executed the code for the module, it's possible that the
    // new exports made it ineligible for being a boundary.
    // We only care about the case when we were _previously_ a boundary,
    // because we already accepted this update (accidental side effect).
    var isNoLongerABoundary = prevExports !== null;
    if (isNoLongerABoundary) {
      module.hot.invalidate();
    }
  }
}

Babel config for reference:

{
  "presets": ["next/babel"],
  "plugins": [
    "@babel/plugin-proposal-logical-assignment-operators",
    ["@babel/plugin-proposal-private-methods", { "loose": false }],
    ["@babel/plugin-proposal-private-property-in-object", { "loose": false }]
  ]
}
SirensOfTitan commented 2 years ago

It'd be amazing to this fixed, we're blocked on upgrading to next 12+SWC because of this. I'm super happy to attempt a PR if it would be helpful. Still seeing this on 12.0.7.

Thank you for an amazing project!

balazsorban44 commented 2 years ago

Hi, since the reproduction is somewhat obscure, could any of you add a minimal reproduction so that I can investigate, but also test out next@canary first if it might have been resolved already? Thanks. :pray:

SirensOfTitan commented 2 years ago

@balazsorban44: Here's a simple reproduction: https://github.com/SirensOfTitan/next-30892

  1. Run the project in dev mode: npm run dev and hit http://localhost:3000/, notice a blank page and an error related to fs not being found (in browser context)
  2. Move .babelrc.works to .babelrc and rerun project, notice that index renders successfully.
saiichihashimoto commented 2 years ago

Is there any update on this? Isomorphic code is a huge plus side of next.js and being unable to fork code in these specific cases creates convoluted code.

timneutkens commented 2 years ago

Looked at this and it's probably the same as this: https://github.com/vercel/next.js/issues/36514#issuecomment-1112041464.

AndersDJohnson commented 2 years ago

Had the same issue. Seems like a possible workaround is to refactor code to instead use typeof window !== 'object'. I think perhaps this might even work with both Terser and SWC, but to be determined.

adanperez commented 3 months ago

We are running into this issue with latest Next.js 14/15 with our typeof window !== 'object' that worked with babel.