Skip to content

Plugin resets already generated sourcemaps in Webpack #2090

@jussimcius

Description

@jussimcius

TL;DR
For Typescript applications that use Webpack, @import-meta-env/unplugin seems to disregard already generated sourcemaps, creates and returns its own, generated based on already transpiled code. This resets sourcemap for input file, thus leading to wrong source-code view in the browser.

Problem
I have a Typescript React app, locally running on Webpack devserver. For Typescript, I use ts-loader and I also have @import-meta-env/unplugin to handle env variable injection.

tsconfig.json:

{
  "compilerOptions": {
    "allowJs": false,
    "jsx": "react-jsx",
    "lib": ["dom", "dom.iterable", "esnext"],
    "sourceMap": true,
    "target": "es6",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "declaration": false,
    "allowUmdGlobalAccess": true,
    "baseUrl": "./",
    "paths": {
      "@common/*": ["./src/common/*"],
      "@order-management/*": ["./src/order-management/*"]
    }
  },
  "include": ["./src/**/*"],
  "exclude": ["node_modules", "dist"]
}

webpack.config.js:

const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');
const StylelintPlugin = require('stylelint-webpack-plugin');
const { name, version } = require('./package.json');
const CopyPlugin = require('copy-webpack-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const ImportMetaEnvPlugin = require('@import-meta-env/unplugin');

module.exports = {
  entry: './src/index.ts',
  mode: 'development',
  devtool: 'inline-source-map',
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
    symlinks: false,
    plugins: [new TsconfigPathsPlugin()],
  },
  module: {
    rules: [
      { test: /\.(ts|js)x?$/, exclude: /node_modules/, use: 'ts-loader' },
      { test: /\.(?:ico|gif|png|jpg|jpeg)$/i, type: 'asset/resource' },
      { test: /\.(woff(2)?|eot|ttf|otf|svg|)$/, type: 'asset/inline' },
      {
        test: /\.css$/i,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
            options: {
              modules: {
                auto: true,
                localIdentName: '[path][name]__[local]--[hash:base64:5]',
              },
            },
          },
        ],
      },
    ],
  },
  output: {
    path: path.resolve(__dirname, './dist'),
    clean: true,
    filename: './static/js/[name].[contenthash:8].js',
    chunkFilename: './static/js/[name].[chunkhash].js',
    assetModuleFilename: './static/media/[name].[hash][ext]',
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: 1,
          name: 'libs',
        },
      },
    },
  },
  plugins: [
    new webpack.DefinePlugin({
      SERVICE_NAMESPACE: JSON.stringify('coffeeshop'),
      SERVICE_NAME: JSON.stringify(name),
      SERVICE_VERSION: JSON.stringify(version),
    }),
    new CopyPlugin({
      patterns: [
        {
          from: './public',
          filter: (file) =>
            !['index.html'].some((ignoredFile) => file.endsWith(ignoredFile)),
        },
      ],
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './public/index.html'),
    }),
    ImportMetaEnvPlugin.webpack({
      env: './config/.env',
      example: './config/.env.example',
    }),
    new ESLintPlugin({
      context: './src',
      extensions: ['.ts', '.tsx'],
      configType: 'flat',
    }),
    new StylelintPlugin({ context: './src', extensions: ['.css'] }),
  ],
  devServer: {
    static: path.join(__dirname, 'public'),
    open: false,
    port: 3000,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
    historyApiFallback: true,
    proxy: [
      {
        context: '/otel',
        target: 'http://localhost:4318',
        secure: false,
        pathRewrite: { '^/otel': '' },
      },
    ],
  },
  stats: 'verbose',
};

Debugging my app, in browser's devtools, I noticed that sourcemaps are wrong for all files that use import.meta.env.*. Further investigating this issue, I came to a conclusion that plugin @import-meta-env/unplugin is overriding sourcemaps. In my Webpack configuration I have 2 loaders which generate sourcemaps - ts-loader and @import-meta-env/unplugin. ts-loader is invoked first, transpiles the code and generates sourcemap. Then, transpiled code and generated sourcemap is passed to @import-meta-env/unplugin. It generates sourcemap for transpiled code and returns its own sourcemap just for that code (for example, ts-loader merges its own generated sourcemap and input sourcemap from other loaders).

Original source code (what I want to see in the browser):

import { createRoot } from 'react-dom/client';
import { App } from '@common/components';
import './global.css';
import { configureOpenTelemetry, getLogger } from '@common/utils';
import { OrderPage } from '@order-management/pages';
import { UserContextProvider } from '@common/context';

const environmentName = import.meta.env.ENVIRONMENT;

configureOpenTelemetry(
  SERVICE_NAMESPACE,
  SERVICE_NAME,
  SERVICE_VERSION,
  environmentName,
);

const logger = getLogger('application');

const container = document.getElementById('container');

if (!container) {
  throw new Error('Container not found.');
}

const root = createRoot(container);

root.render(
  <UserContextProvider>
    <App>
      <OrderPage />
    </App>
  </UserContextProvider>,
);

logger.logInfo('Application started...');

Actual view in the browser:

import { jsx as _jsx } from "react/jsx-runtime";
import { createRoot } from 'react-dom/client';
import { App } from '@common/components';
import './global.css';
import { configureOpenTelemetry, getLogger } from '@common/utils';
import { OrderPage } from '@order-management/pages';
import { UserContextProvider } from '@common/context';
const environmentName = import.meta.env.ENVIRONMENT;
configureOpenTelemetry(SERVICE_NAMESPACE, SERVICE_NAME, SERVICE_VERSION, environmentName);
const logger = getLogger('application');
const container = document.getElementById('container');
if (!container) {
    throw new Error('Container not found.');
}
const root = createRoot(container);
root.render(_jsx(UserContextProvider, { children: _jsx(App, { children: _jsx(OrderPage, {}) }) }));
logger.logInfo('Application started...');

I tried removing sourcemap generation functionality from @import-meta-env/unplugin, thus forcing it to return input sourcemap (ts-loader's) and the problem was solved.

import-meta-env/packages/unplugin/src /transform-dev.ts:

  return {
    code: s.toString(),
    //map: s.generateMap({ source: id, includeContent: true }),
  };

versions:

  • @import-meta-env/unplugin: 0.6.2
  • ts-loader: 9.5.2
  • webpack: 5.99.5
  • typescript: 5.8.3
  • react: 19.1.0
  • node: v20.18.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    help wantedExtra attention is needed

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions