Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use with jsbundling-rails and integrate with webpack #171

Closed
mildred opened this issue Jan 27, 2023 · 1 comment
Closed

Use with jsbundling-rails and integrate with webpack #171

mildred opened this issue Jan 27, 2023 · 1 comment

Comments

@mildred
Copy link

mildred commented Jan 27, 2023

With Rails 7, when migrating away from webpacker and trying to move most of the javascript to importmaps, I came with a configuration that integrates nicely webpack and importmaps:

  • Use raw ES6 modules for application code
  • Use webpack for (legacy application code and) NPM dependencies by configuring webpack to generate ES6 modules

This is especially nice when you want to avoid fetching dependencies from the Internet and want to serve all the assets from the rails server. You can also use NPM tooling to track dependencies and audit them more easily. Also, some dependencies have a gazillion files and not all are working as raw ES6 modules and requires some bundling. See #153

I configured my webpack with:

  • ES6 support (experiments.outputModule and optimization.moduleIds = deterministic)
  • generating ES6 modules (output.library.type = module and output.environment.module = true)
  • loading external modules as ES6 modules (externalsType = module)
  • bundle splitting via ES6 modules (output.chunkLoading = import and output.chunkFormat = module with optimization.runtimeChunk = single)
  • generating files without a content hash in public/assets. I'm not putting them in app/assets/build to be fed to the rails asset pipeline because this breaks the importmaps

Then I configured config/importmap.rb to use paths from the webpack generated assets:

def webpack_url(filename)
  path = "/assets/#{filename}"
  path = "#{ENV['WEBPACK_DEV_SERVER']}#{path}" if ENV['WEBPACK_DEV_SERVER'].present?
  path
end

pin 'webpack.js', to: webpack_url('webpack.js')
pin 'bootstrap', to: webpack_url('bootstrap.js')
pin 'jquery', to: webpack_url('jquery.js'), preload: true

This way, bootstrap and jquery are available as ES6 modules declared in import maps.

It would be great if such integration was easier, in particular if there was a way to automatically handle webpack files with a content hash.


webpack.config.js:

const path = require('path')
const webpack = require('webpack')

const mode = (process.env.NODE_ENV === 'development') ? 'development' : 'production'
const production = (process.env.RAILS_ENV === 'production')

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  mode,
  experiments: { outputModule: true, },
  devtool: production ? 'source-map' : 'eval-source-map',

  // Declare the modules imported from outside
  // there you can declare the modules available in the importmap
  // you want to import as ES6 from within webpack 
  externalsType: 'module',
  externals: {
    '@hotwired/stimulus': '@hotwired/stimulus',
    'controllers/application': 'controllers/application',
    'lib/loader': 'lib/loader',
  },

  entry: {

    // application code bundled with webpack

    webpack: {
      dependOn: ['jquery', 'bootstrap'],
      import: [ './app/webpack/webpack.js' ],
    },

    // NPM dependencies available to webpack AND importmap as ES6 modules

    bootstrap: {
      dependOn: ['jquery'],
      import: 'bootstrap/dist/js/bootstrap.bundle.js',
    },
    jquery: './app/webpack/jquery.js',
  },

  // Where to put the generated files, and how to generate them (ES6 module)
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'public/assets/'),
    library: { type: 'module', },
    environment: { module: true, },
    chunkLoading: 'import',
    chunkFormat: 'module',

    sourceMapFilename: '[file]-[contenthash].digested.map',
  },

  // Configuration
  module: {
    rules: [
      // Add CSS/SASS/SCSS rule with loaders
      {
        test: /\.(?:sa|sc|c)ss$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
      },
      // Assets
      {
        test: /\.(png|jpe?g|gif|eot|woff2|woff|ttf|svg|ico)$/i,
        type: 'asset/resource',
      },
    ],
  },
  resolve: {
    // Add additional file types
    extensions: ['.js', '.jsx', '.scss', '.css'],
    modules: [path.resolve(__dirname, 'app/assets'), 'node_modules']
  },
  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single', // multiple entry points with code spliting
    minimize: !production,
  },
  watchOptions: {
    ignored: ['**/node_modules'],
  },
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
      'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization'
    }
  },
  plugins: [
    process.env.ANALYSE ? new BundleAnalyzerPlugin() : null,
    new RemoveEmptyScriptsPlugin(),
    new MiniCssExtractPlugin(),
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      jquery: 'jquery',
    }),
    new webpack.optimize.LimitChunkCountPlugin({
      maxChunks: 1
    })
  ].filter(x => x)
}
@mildred
Copy link
Author

mildred commented Jan 27, 2023

To handle webpack content hash:

  • add manifest plugin to webpack config:
// Generates a manifest
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');

module.exports = {
  ...
  plugins: [
    ...
    new WebpackManifestPlugin({
      path: path.resolve(__dirname, 'public/assets/'),
      publicPath: '/assets',
    }),
  ]
}

Add config/initializers/importmap.js

# Recompute importmaps when webpack manifest changes
Rails.application.config.importmap.paths << Rails.root.join("public/assets/manifest.json")

def webpack_url(filename, absolute: true)
  path = JSON(Rails.root.join('public/assets/manifest.json').read)[filename]
  path = "#{ENV![:WEBPACK_DEV_SERVER]}#{path}" if ENV![:WEBPACK_DEV_SERVER].present?
  path
end

Configure config/importmap.rb to use the function defined in the initializer:

pin 'jquery', to: webpack_url('jquery.js'), preload: true

If you want to reference a webpack bundle directly within a view, use webpack_url too.

@dhh dhh closed this as completed Apr 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants