From 623c3c5b9f975d7b1fcad4be62d5d05d830cd688 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 01:25:58 +0000 Subject: [PATCH] Rewrite as a Rails-free Webpack 5 CLI Drop the webpacker/Rails mock and 0.x rake tasks. init now generates webpacker.yml, a webpack.config.js, an entry pack and package.json with major-pinned Webpack 5 tooling; compile/watch/clobber/check/info drive Webpack directly and keep Webpacker's manifest.json output. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LaJESXoiQCpeTydMCJ37uU --- .github/workflows/ci.yml | 19 ++ .travis.yml | 25 --- Gemfile | 3 - README.md | 77 ++++--- bin/webpacker-cli | 90 ++++---- lib/webpacker_cli.rb | 192 ++++++++++++------ lib/webpacker_cli/rails_template.rb | 74 ------- lib/webpacker_cli/templates/application.js | 3 + lib/webpacker_cli/templates/package.json | 19 ++ lib/webpacker_cli/templates/webpack.config.js | 83 ++++++++ lib/webpacker_cli/templates/webpacker.yml | 30 +++ lib/webpacker_cli/version.rb | 2 +- test/test_helper.rb | 12 +- test/webpacker_cli_test.rb | 84 ++++++-- webpacker_cli.gemspec | 11 +- 15 files changed, 450 insertions(+), 274 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml delete mode 100644 lib/webpacker_cli/rails_template.rb create mode 100644 lib/webpacker_cli/templates/application.js create mode 100644 lib/webpacker_cli/templates/package.json create mode 100644 lib/webpacker_cli/templates/webpack.config.js create mode 100644 lib/webpacker_cli/templates/webpacker.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6ccda0f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI +on: [push, pull_request] +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + ruby: ['3.1', '3.3', '3.4'] + node: ['20', '22'] + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: { ruby-version: '${{ matrix.ruby }}', bundler-cache: true } + - uses: actions/setup-node@v4 + with: { node-version: '${{ matrix.node }}' } + - run: bundle exec rake test + env: { WEBPACKER_CLI_E2E: '1' } diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6a548aa..0000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: ruby -rvm: -- 2.3.8 -- 2.4.5 -- 2.5.3 - -os: -- linux -- osx - -cache: - bundler: true - -before_install: -- ruby --version -- ruby -e 'require "pp"; pp RbConfig::CONFIG' - -install: -- nvm install node -- node -v -- npm i -g yarn - -script: -- bundle install -- bundle exec rake test diff --git a/Gemfile b/Gemfile index 34e7391..b4e2a20 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,3 @@ source "https://rubygems.org" -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } - -# Specify your gem's dependencies in webpacker-cli.gemspec gemspec diff --git a/README.md b/README.md index 1e990cf..996d66b 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,65 @@ [![Gem Version](https://badge.fury.io/rb/webpacker_cli.svg)](http://badge.fury.io/rb/webpacker_cli) -[![Build Status](https://travis-ci.org/danielpclark/webpacker-cli.svg)](https://travis-ci.org/danielpclark/webpacker-cli) +[![Build Status](https://github.com/danielpclark/webpacker-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/danielpclark/webpacker-cli/actions) # WebpackerCli -Bringing the impressive work of the Rails' **Webpacker** project **to be available for frameworks -other than Rails**. This is done by mocking the minimal amount of Rails for the Webpacker's -Rake commands to be able to run. +**Webpacker's conventions for any framework, without Rails.** Webpacker was abandoned by the Rails +team, and the old version of this tool depended on it (and on a mocked Rails app). Version 1.0 +removes that dependency entirely: the gem has **no runtime dependencies**, and drives plain +**Webpack 5** through a small, generated `webpack.config.js` that lives in your project. -Webpacker itself takes Webpack, which has a primary focus for pre-processing JavaScript related -code, and make pre-processing other kinds of files such as CSS, SASS, Images, VueJS, React, -CoffeeScript, and other similar content as simple as a couple very minor edits. Without Webpacker -you would need to go through quite a bit of extra work to get Webpack to handle non JS items. +You keep the parts of Webpacker that matter: -This project makes asset packing and cache invalidation easy for any web framework by bringing -Webpacker's tooling to the command line. This cli tool is not meant to be used in a Rails project -_(at least not until it's designed to not overwrite a few files that exist in Rails)_. +* `config/webpacker.yml` (`source_path`, `source_entry_path`, `public_output_path`, `extensions`, …) +* every file in `app/javascript/packs` is an entry point +* content-hashed output in `public/packs` plus a `manifest.json` for cache invalidation -This project has the same dependencies/prerequisites as [Webpacker](https://github.com/rails/webpacker#prerequisites). +## Prerequisites -## Installation +Ruby 3.0+, Node.js 20.9+, and Yarn 1.x or npm. -Install it yourself as: +## Installation $ gem install webpacker_cli -_Note: If you have trouble compiling the Nokogiri dependency on Linux try installing the following packages: `build-essential ruby-dev libxml2-dev`_ - ## Usage -General help is found with - - $ webpacker-cli --help - -The help menu will change after initialization within the project directory. - -And get started using it in your target project with: + $ webpacker-cli init # create config + entry pack + package.json, install Node packages + $ webpacker-cli compile # build packs into public/packs + $ webpacker-cli watch # rebuild on change + $ webpacker-cli clobber # remove compiled packs and cache + $ webpacker-cli check # verify prerequisites + $ webpacker-cli info # tool versions - $ webpacker-cli init +The environment comes from `WEBPACKER_ENV`, `NODE_ENV` or `RAILS_ENV` (default `development`). +Production builds use content hashes and minification, e.g. `NODE_ENV=production webpacker-cli compile`. -This creates the minimum files for Webpacker to work and runs `bundle` & `rake webpacker:install`. -From here you have your configuration files for Webpack/Webpacker in the `config` directory. - -To compile your assets you may now run: - - $ webpacker-cli compile - -To add support for another file type you can add a file to the `config/webpack/loaders` -directory. Then load it from `config/webpack/environment.js` and lastly add the file extension -to the `config/webpacker.yml` under `default.extensions`. Be sure to add the JS package with -a command like `yarn add`. +The generated `config/webpack/webpack.config.js` and `package.json` are yours to edit: add a loader +by installing its package and adding a rule, and add the extension to `default.extensions` in +`config/webpacker.yml`. > Server Tip: It is recommended to compile your assets upon deploy rather than per web request. -Once your assets are compiled for deployment you will need to have your web application route -the assets by the file stored in `public/packs/manifest.json` which will look similar to: +Route assets using `public/packs/manifest.json`: { "application.js": "/packs/application-9578bdd78b657fa4358f.js", "application.js.map": "/packs/application-9578bdd78b657fa4358f.js.map" } -This way Webpacker handles cache invalidation for you whenever you make changes to your assets. -So the `manifest.json` is the key-value pair of data used to be substitued in your web page views -for the `src` references. +## Staying current with Webpack + +`package.json` pins each tool to a **major version range** (`webpack ^5`, `webpack-cli ^6`, …), so +`yarn upgrade`/`npm update` picks up fixes without breaking changes. A new Webpack major is an +explicit, reviewed change to the templates in this gem (and to your project's config when you +choose to adopt it) — never a surprise. Existing projects are never overwritten by `init` unless +you pass `--force`. + +### Migrating from 0.x + +The Rails mock (`Gemfile`, `Rakefile`, `bin/rails`, `config/application.rb`) and the `webpacker:*` +rake tasks are gone. Delete those files, run `webpacker-cli init` for the new config, and move any +custom loaders from `config/webpack/loaders` into `webpack.config.js` rules. ## Example Integration diff --git a/bin/webpacker-cli b/bin/webpacker-cli index 7afe87f..6fd01f0 100755 --- a/bin/webpacker-cli +++ b/bin/webpacker-cli @@ -2,50 +2,56 @@ $LOAD_PATH.unshift File.expand_path('../lib', __dir__) -require 'tty/command' require 'webpacker_cli' -def init_menu - <<~INITIAL_MENU -To first initialize Webpacker in this project run the following command. - - webpacker-cli init - -This will generate the minimal content to use webpacker. - -Dependencies: - * Ruby 2.2+ - * Node.js 6.14.0+ - * Yarn 1.x+ - INITIAL_MENU -end - -# For menu creation -def rake_webpacker - TTY::Command. - new(printer: :null). - run!('rake webpacker') -end - -def usage - result = rake_webpacker - if result.failure? - puts init_menu +USAGE = <<~TEXT + webpacker-cli #{WebpackerCli::VERSION} + + Usage: webpacker-cli [options] + + Commands: + init Create config, entry pack and package.json, then install Node packages + --force overwrite existing files + --skip-install don't install Node packages + compile Compile packs into the public output directory (writes manifest.json) + watch Compile and recompile on changes + clobber Remove compiled packs and the Webpack cache + check Verify Node, package manager and installed packages + info Show tool versions + version Show the webpacker-cli version + + The environment is taken from WEBPACKER_ENV, NODE_ENV or RAILS_ENV (default: development). + Run `webpacker-cli init` first in your project directory. +TEXT + +command, *args = ARGV + +begin + case command + when 'init' + puts 'INITIALIZING WEBPACKER…' + WebpackerCli.init(force: args.include?('--force'), install: !args.include?('--skip-install')) + when 'compile' + WebpackerCli.compile + when 'watch' + WebpackerCli.watch + when 'clobber' + WebpackerCli.clobber + when 'check' + problems = WebpackerCli.check + puts(problems.empty? ? 'Everything looks good.' : problems) + exit(problems.empty? ? 0 : 1) + when 'info' + WebpackerCli.info.each { |k, v| puts "#{k}: #{v}" } + when 'version', '--version', '-v' + puts WebpackerCli::VERSION + when nil, 'help', '--help', '-h' + puts USAGE else - puts result.out.gsub(/webpacker:/, "webpacker-cli ") + warn "Unknown command: #{command}\n\n#{USAGE}" + exit 1 end -end - -case ARGV[0] -when /^(?:--help|-h)$/ then - usage -when /^init$/ - puts "INITIALIZING WEBPACKER…" - WebpackerCli.init() -when /^[a-z:]+$/ - TTY::Command. - new(color: false, uuid: false). - run("rake webpacker:#{ARGV.join(' ')}") -else - usage +rescue WebpackerCli::Error => e + warn e.message + exit 1 end diff --git a/lib/webpacker_cli.rb b/lib/webpacker_cli.rb index ac11e22..edde27a 100644 --- a/lib/webpacker_cli.rb +++ b/lib/webpacker_cli.rb @@ -1,80 +1,156 @@ -require 'webpacker_cli/version' -require 'webpacker_cli/rails_template' require 'fileutils' -require 'tty/command' -require 'bundler' +require 'json' +require 'open3' +require 'yaml' +require 'webpacker_cli/version' +# webpacker-cli: Webpacker's conventions (webpacker.yml, app/javascript/packs, +# public/packs/manifest.json) for any framework, without Rails. +# +# Ruby only scaffolds the project and launches Webpack; the Webpack setup itself +# is a plain JavaScript config generated into the project so it can be updated +# independently of this gem. module WebpackerCli - FILE_MANIFEST = %w[ - bin - bin/rails - config - config/application.rb - config/webpack/development.js - config/webpack/environment.js - config/webpack/loaders - config/webpack/production.js - config/webpack/test.js - package.json - Gemfile - Rakefile - ] + class Error < StandardError; end - class << self - def init(**opts) - dir = opts.fetch(:dir) { Dir.pwd } - test = opts.fetch(:test) { !!ENV['WEBPACKER_CLI'].to_s['TEST'] } + TEMPLATE_DIR = File.expand_path('webpacker_cli/templates', __dir__) - Dir.chdir(dir) do - File.open('Gemfile', 'w') {|file| - file.write(WebpackerCli::RailsTemplate.gemfile(test)) - } - File.open('Rakefile', 'w') {|file| file.write(WebpackerCli::RailsTemplate.rakefile) } + # Generated file => template it is copied from. + TEMPLATES = { + 'config/webpacker.yml' => 'webpacker.yml', + 'config/webpack/webpack.config.js' => 'webpack.config.js', + 'app/javascript/packs/application.js' => 'application.js', + 'package.json' => 'package.json' + }.freeze - FileUtils.mkdir_p('bin') - File.open('bin/rails', 'w') {|file| file.write(WebpackerCli::RailsTemplate.bin_rails) } + FILE_MANIFEST = (%w[config config/webpack app/javascript app/javascript/packs] + TEMPLATES.keys).freeze - File.open('package.json', 'w') {|file| file.write(WebpackerCli::RailsTemplate.package_json) } + WEBPACK_CONFIG = 'config/webpack/webpack.config.js'.freeze - FileUtils.mkdir_p('config/webpack/loaders') - File.open('config/application.rb', 'w') {|file| file.write(WebpackerCli::RailsTemplate.config_application) } - - env = { - BUNDLE_GEMFILE: "#{dir}/Gemfile" - } + class << self + # Scaffold a project. Existing files are kept unless force: true. + def init(dir: Dir.pwd, force: false, install: true, **) + dir = File.expand_path(dir) + FileUtils.mkdir_p(dir) - if test - env.update( { - GEM_HOME: ENV["GEM_HOME"], - GEM_PATH: ENV["GEM_PATH"], - }) + TEMPLATES.each do |dest, template| + path = File.join(dir, dest) + if File.exist?(path) && !force + puts " exist #{dest}" + next end + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.cp(File.join(TEMPLATE_DIR, template), path) + puts " create #{dest}" + end - bundler_env(test) do - TTY::Command. - new(color: false, uuid: false). - run(env, 'bundle install', chdir: dir) + install_packages(dir) if install + dir + end - TTY::Command. - new(color: false, uuid: false). - run(env, "bundle exec #{executable} install", chdir: dir) - end + def install_packages(dir) + manager = package_manager(dir) + run(dir, {}, manager, 'install') + end + + def compile(dir: Dir.pwd, env: current_env) + webpack(dir, env) + end + + def watch(dir: Dir.pwd, env: current_env) + webpack(dir, env, '--watch') + end + + # Remove compiled packs and the Webpack cache. + def clobber(dir: Dir.pwd, env: current_env) + cfg = settings(dir, env) + [File.join(cfg['public_root_path'], cfg['public_output_path']), cfg['cache_path']].each do |rel| + FileUtils.rm_rf(File.join(dir, rel)) + puts " remove #{rel}" end end - def bundler_env(test) - if test - Bundler.with_clean_env { yield } - else - yield + # Verify prerequisites, returning a list of problems (empty when healthy). + def check(dir: Dir.pwd) + problems = [] + problems << 'Node.js was not found in PATH' unless which('node') + problems << 'Neither yarn nor npm was found in PATH' unless which('yarn') || which('npm') + problems << "Missing #{WEBPACK_CONFIG}; run `webpacker-cli init`" unless File.exist?(File.join(dir, WEBPACK_CONFIG)) + unless File.exist?(File.join(dir, 'node_modules', '.bin', 'webpack')) + problems << 'Node packages are not installed; run `webpacker-cli init` or install them yourself' end + problems + end + + def info(dir: Dir.pwd) + { + 'webpacker-cli' => VERSION, + 'ruby' => RUBY_VERSION, + 'node' => capture('node', '--version', dir: dir), + 'package manager' => "#{package_manager(dir)} #{capture(package_manager(dir), '--version', dir: dir)}", + 'webpack' => webpack_version(dir) + } end + def current_env + ENV['WEBPACKER_ENV'] || ENV['NODE_ENV'] || ENV['RAILS_ENV'] || 'development' + end + + # Merged `default` + environment section of config/webpacker.yml. + def settings(dir, env = current_env) + file = File.join(dir, 'config', 'webpacker.yml') + raise Error, "Missing config/webpacker.yml; run `webpacker-cli init`" unless File.exist?(file) + all = YAML.safe_load(File.read(file), aliases: true) || {} + (all['default'] || {}).merge(all[env] || {}) + end + + def package_manager(dir) + return 'npm' if File.exist?(File.join(dir, 'package-lock.json')) && !File.exist?(File.join(dir, 'yarn.lock')) + return 'yarn' if File.exist?(File.join(dir, 'yarn.lock')) || which('yarn') + 'npm' + end + + # Path of the webpacker-cli executable (kept for integrations such as webpacker-rs). def executable - begin - Gem.bin_path("webpacker_cli", "webpacker-cli") - rescue - File.expand_path('../bin/webpacker-cli', __dir__) + Gem.bin_path('webpacker_cli', 'webpacker-cli') + rescue StandardError + File.expand_path('../bin/webpacker-cli', __dir__) + end + + private + + def webpack(dir, env, *args) + problems = check(dir: dir) + raise Error, problems.join("\n") unless problems.empty? + run(dir, { 'NODE_ENV' => env, 'WEBPACKER_ENV' => env }, + 'node_modules/.bin/webpack', '--config', WEBPACK_CONFIG, *args) + end + + def run(dir, env, *cmd) + success = system(env, *cmd, chdir: dir) + raise Error, "Command failed: #{cmd.join(' ')}" unless success + true + rescue Errno::ENOENT + raise Error, "Command not found: #{cmd.first}" + end + + def webpack_version(dir) + JSON.parse(File.read(File.join(dir, 'node_modules', 'webpack', 'package.json')))['version'] + rescue StandardError + 'not found' + end + + def capture(*cmd, dir:) + out, status = Open3.capture2e(*cmd, chdir: dir) + status.success? ? out.strip : 'not found' + rescue Errno::ENOENT + 'not found' + end + + def which(cmd) + ENV['PATH'].to_s.split(File::PATH_SEPARATOR).any? do |p| + f = File.join(p, cmd) + File.executable?(f) && !File.directory?(f) end end end diff --git a/lib/webpacker_cli/rails_template.rb b/lib/webpacker_cli/rails_template.rb deleted file mode 100644 index ba3aa77..0000000 --- a/lib/webpacker_cli/rails_template.rb +++ /dev/null @@ -1,74 +0,0 @@ -module WebpackerCli - module RailsTemplate - class << self - def gemfile(test = false) - %Q$source 'https://rubygems.org' -git_source(:github) { |repo| "https://github.com/\#{repo}.git" } - -gem 'rake', '>= 11' -gem 'webpacker_cli', '#{WebpackerCli::VERSION}'#{ - if test; ", path: '#{File.expand_path('../', __dir__)}'" end -} -$ + gemspec_deps - end - - def rakefile - %q$require_relative "config/application" - -Rails.application.load_tasks$ - end - - def config_application - %q$require "action_controller/railtie" -require "action_view/railtie" -require "webpacker" - -module TestApp - class Application < ::Rails::Application - config.secret_key_base = "abcdef" - config.eager_load = true - config.webpacker.check_yarn_integrity = false - end -end$ - end - - def package_json - %q${ - "name": "application", - "private": true, - "dependencies": { - "@rails/webpacker": "3.5" - }, - "devDependencies": { - "webpack-dev-server": "2.11.2" - } -}$ - end - - def bin_rails - %q$#!/usr/bin/env ruby -APP_PATH = File.expand_path('../config/application', __dir__) -require 'rails/commands'$ - end - - def gemspec_deps - gemspec.runtime_dependencies. - map {|i| "gem '#{i.name}', '#{i.requirement.requirements.join(' ')}'"}. - join("\n") - end - - def gemspec - begin - Gem::Specification.find_by_name "webpacker_cli" - rescue - local_spec = File.expand_path("../../webpacker_cli.gemspec", __dir__) - - raise "Gemspec not found for 'webpacker_cli'!" unless File.exist? local_spec - - eval File.read(local_spec) - end - end - end - end -end - diff --git a/lib/webpacker_cli/templates/application.js b/lib/webpacker_cli/templates/application.js new file mode 100644 index 0000000..fba2b60 --- /dev/null +++ b/lib/webpacker_cli/templates/application.js @@ -0,0 +1,3 @@ +// Entry point compiled by `webpacker-cli compile`. Every file in this +// directory becomes its own pack (e.g. "application.js" in manifest.json). +console.log('Hello World from webpacker-cli') diff --git a/lib/webpacker_cli/templates/package.json b/lib/webpacker_cli/templates/package.json new file mode 100644 index 0000000..b4b9005 --- /dev/null +++ b/lib/webpacker_cli/templates/package.json @@ -0,0 +1,19 @@ +{ + "name": "application", + "private": true, + "engines": { + "node": ">=20.9.0" + }, + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/preset-env": "^7.26.0", + "babel-loader": "^10.0.0", + "css-loader": "^7.1.0", + "css-minimizer-webpack-plugin": "^7.0.0", + "js-yaml": "^4.1.0", + "mini-css-extract-plugin": "^2.9.0", + "webpack": "^5.90.0", + "webpack-assets-manifest": "^6.0.0", + "webpack-cli": "^6.0.0" + } +} diff --git a/lib/webpacker_cli/templates/webpack.config.js b/lib/webpacker_cli/templates/webpack.config.js new file mode 100644 index 0000000..cd821f0 --- /dev/null +++ b/lib/webpacker_cli/templates/webpack.config.js @@ -0,0 +1,83 @@ +// Webpack configuration generated by webpacker-cli. This file is yours to edit. +// +// Conventions (identical to Rails' Webpacker): +// * every file in / is an entry point +// * output goes to / +// * a manifest.json mapping "application.js" => "/packs/application-.js" +// is written next to the output +const fs = require('fs') +const path = require('path') +const yaml = require('js-yaml') +const { WebpackAssetsManifest } = require('webpack-assets-manifest') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') + +const root = path.resolve(__dirname, '..', '..') +const envName = process.env.WEBPACKER_ENV || process.env.NODE_ENV || 'development' +const isProduction = envName === 'production' + +const settings = yaml.load(fs.readFileSync(path.join(root, 'config', 'webpacker.yml'), 'utf8')) +const cfg = Object.assign({}, settings.default, settings[envName]) + +const sourcePath = path.resolve(root, cfg.source_path) +const entryPath = path.join(sourcePath, cfg.source_entry_path) +const outputPath = path.resolve(root, cfg.public_root_path, cfg.public_output_path) +const publicPath = `/${cfg.public_output_path.replace(/^\/+|\/+$/g, '')}/` + +const entry = {} +const collect = (dir, prefix) => { + if (!fs.existsSync(dir)) return + for (const item of fs.readdirSync(dir, { withFileTypes: true })) { + if (item.isDirectory()) { + collect(path.join(dir, item.name), `${prefix}${item.name}/`) + } else if (/\.(m?js|jsx|ts|tsx|css)$/.test(item.name)) { + entry[prefix + item.name.replace(/\.[^.]+$/, '')] = path.join(dir, item.name) + } + } +} +collect(entryPath, '') + +const hash = isProduction ? '-[contenthash]' : '' + +module.exports = { + mode: isProduction ? 'production' : 'development', + devtool: isProduction ? 'source-map' : 'cheap-module-source-map', + context: root, + entry, + output: { + path: outputPath, + publicPath, + filename: `[name]${hash}.js`, + chunkFilename: `[name]${hash}.chunk.js`, + assetModuleFilename: `media/[name]${hash}[ext]`, + clean: true + }, + cache: { + type: 'filesystem', + cacheDirectory: path.resolve(root, cfg.cache_path), + buildDependencies: { config: [__filename] } + }, + module: { + rules: [ + { + test: /\.(js|mjs)$/, + exclude: /node_modules/, + use: { loader: 'babel-loader', options: { presets: [['@babel/preset-env', { targets: 'defaults' }]] } } + }, + { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] }, + { test: /\.(png|jpe?g|gif|svg|webp|avif|ico|woff2?|eot|ttf|otf)$/i, type: 'asset/resource' } + ] + }, + plugins: [ + new MiniCssExtractPlugin({ filename: `[name]${hash}.css`, chunkFilename: `[name]${hash}.chunk.css` }), + new WebpackAssetsManifest({ output: 'manifest.json', publicPath: true, writeToDisk: true }) + ], + optimization: { + minimizer: ['...', new CssMinimizerPlugin()] + }, + resolve: { + extensions: cfg.extensions, + modules: [sourcePath, 'node_modules'] + }, + performance: { hints: false } +} diff --git a/lib/webpacker_cli/templates/webpacker.yml b/lib/webpacker_cli/templates/webpacker.yml new file mode 100644 index 0000000..a8e09fa --- /dev/null +++ b/lib/webpacker_cli/templates/webpacker.yml @@ -0,0 +1,30 @@ +# Configuration for webpacker-cli. Same layout and key names as Rails' Webpacker. +default: &default + source_path: app/javascript + source_entry_path: packs + public_root_path: public + public_output_path: packs + cache_path: tmp/cache/webpacker + + # Files with these extensions are resolved when imported without an extension. + extensions: + - .js + - .mjs + - .json + - .css + - .png + - .jpg + - .jpeg + - .gif + - .svg + - .woff + - .woff2 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/lib/webpacker_cli/version.rb b/lib/webpacker_cli/version.rb index a764a48..8d8c5e1 100644 --- a/lib/webpacker_cli/version.rb +++ b/lib/webpacker_cli/version.rb @@ -1,3 +1,3 @@ module WebpackerCli - VERSION = "0.9.4" + VERSION = "1.0.0" end diff --git a/test/test_helper.rb b/test/test_helper.rb index 5ab5cc9..d0f44d0 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,11 +1,5 @@ -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +$LOAD_PATH.unshift File.expand_path('../lib', __dir__) require 'webpacker_cli' - -ENV['WEBPACKER_CLI'] = 'TEST' - +require 'tmpdir' +require 'json' require 'minitest/autorun' -require 'color_pound_spec_reporter' - -Minitest::Reporters.use! [ColorPoundSpecReporter.new] - -Minitest.after_run { ENV.delete('WEBPACKER_CLI') } diff --git a/test/webpacker_cli_test.rb b/test/webpacker_cli_test.rb index f10c614..bfb1996 100644 --- a/test/webpacker_cli_test.rb +++ b/test/webpacker_cli_test.rb @@ -1,26 +1,82 @@ require 'test_helper' +require 'open3' class WebpackerCliTest < Minitest::Test - attr_reader :dir - def setup - @dir = Dir.mktmpdir - Dir.chdir(@dir) - TTY::Command.new(color: false, uuid: false). - run("gem install bundler rake") + EXE = File.expand_path('../bin/webpacker-cli', __dir__) + + def in_tmp + Dir.mktmpdir { |dir| yield dir } + end + + def quietly + out = $stdout + $stdout = StringIO.new + yield + ensure + $stdout = out end def test_files_are_created - WebpackerCli.init(dir: dir) - WebpackerCli::FILE_MANIFEST.each do |file| - assert File.exist?("#{dir}/#{file}"), "File '#{file}' doesn't exist!" + in_tmp do |dir| + quietly { WebpackerCli.init(dir: dir, install: false) } + WebpackerCli::FILE_MANIFEST.each do |file| + assert File.exist?(File.join(dir, file)), "File '#{file}' doesn't exist!" + end + end + end + + def test_init_keeps_existing_files_unless_forced + in_tmp do |dir| + quietly { WebpackerCli.init(dir: dir, install: false) } + File.write(File.join(dir, 'config/webpacker.yml'), 'custom') + quietly { WebpackerCli.init(dir: dir, install: false) } + assert_equal 'custom', File.read(File.join(dir, 'config/webpacker.yml')) + quietly { WebpackerCli.init(dir: dir, install: false, force: true) } + refute_equal 'custom', File.read(File.join(dir, 'config/webpacker.yml')) + end + end + + def test_settings_merge_environment + in_tmp do |dir| + quietly { WebpackerCli.init(dir: dir, install: false) } + assert_equal 'packs', WebpackerCli.settings(dir, 'production')['public_output_path'] end end - def test_files_are_created_with_executable - TTY::Command.new(color: false, uuid: false). - run("#{WebpackerCli.executable} init") - WebpackerCli::FILE_MANIFEST.each do |file| - assert File.exist?("#{dir}/#{file}"), "File '#{file}' doesn't exist!" + def test_check_reports_missing_packages + in_tmp do |dir| + quietly { WebpackerCli.init(dir: dir, install: false) } + assert(WebpackerCli.check(dir: dir).any? { |p| p =~ /not installed/ }) + end + end + + def test_executable_init + in_tmp do |dir| + _, status = Open3.capture2e(EXE, 'init', '--skip-install', chdir: dir) + assert status.success? + assert File.exist?(File.join(dir, 'package.json')) + end + end + + def test_cli_help_and_version + out, status = Open3.capture2e(EXE, '--help') + assert status.success? + assert_match(/Usage: webpacker-cli/, out) + out, = Open3.capture2e(EXE, 'version') + assert_equal WebpackerCli::VERSION, out.strip + end + + # Full round trip against the real npm registry; skipped when unavailable. + def test_compile_end_to_end + skip 'set WEBPACKER_CLI_E2E=1 to run' unless ENV['WEBPACKER_CLI_E2E'] + in_tmp do |dir| + out, status = Open3.capture2e(EXE, 'init', chdir: dir) + assert status.success?, out + out, status = Open3.capture2e({ 'NODE_ENV' => 'production' }, EXE, 'compile', chdir: dir) + assert status.success?, out + manifest = JSON.parse(File.read(File.join(dir, 'public/packs/manifest.json'))) + assert_match %r{\A/packs/application-\w+\.js\z}, manifest['application.js'] + assert File.exist?(File.join(dir, 'public', manifest['application.js'])) end end end diff --git a/webpacker_cli.gemspec b/webpacker_cli.gemspec index 88eaeb6..3be1708 100644 --- a/webpacker_cli.gemspec +++ b/webpacker_cli.gemspec @@ -9,10 +9,10 @@ Gem::Specification.new do |spec| spec.email = ['6ftdan@gmail.com'] spec.summary = %q{Bringing Webpacker to any framework.} - spec.description = %q{Bringing the impressive work of the Rails' Webpacker project to be available for other frameworks.} + spec.description = %q{Webpacker's conventions (webpacker.yml, packs, manifest.json) for any framework, powered by Webpack 5 and without Rails.} spec.license = 'LGPL-3.0' spec.homepage = 'https://github.com/danielpclark/webpacker-cli' - spec.required_ruby_version = '>= 2.2' + spec.required_ruby_version = '>= 3.0' # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. @@ -23,11 +23,6 @@ Gem::Specification.new do |spec| spec.executables = ['webpacker-cli'] spec.require_paths = ['lib'] - spec.add_development_dependency 'rake', '>= 11.0' + spec.add_development_dependency 'rake', '>= 13.0' spec.add_development_dependency 'minitest', '~> 5.11' - spec.add_development_dependency 'minitest-reporters', '~> 1.1' - spec.add_development_dependency 'color_pound_spec_reporter', '~> 0.0.6' - spec.add_dependency 'bundler', '>= 1.16' - spec.add_dependency 'tty-command', '~> 0.8' - spec.add_dependency 'webpacker', '~> 4.0.0' end