如何确保资产在测试模式 (RSpec) 下存在于 Rail 7、cssbundling-rails、jsbundling-rails 中?

And*_*son 8 rspec ruby-on-rails yarnpkg esbuild

我正在将一个大型商业(专有)Rails 6 应用程序升级到 Rails 7。我们从未使用过 Webpacker,而是直接从 Bootstrap 之类的捆绑 gem 转向“Rails 7 方式”。

\n

事实证明,Rails 7 的“无节点”工作流程对于同时包含 CSS 和 JS 组件的组件没有好的答案。在我们的例子中,最明显的罪犯是 Bootstrap。面对通过导入映射维护 Bootstrap 的 JS“一半”和通过旧的 Bootstrap gem 或手动供应商之类的东西维护 CSS“一半”(是的,这里确实没有没有 Node 的其他解决方案),我们最终回到了一个完整的解决方案节点工作流程。

\n

这正在走到一起。所有提供 CSS 和/或 JS 的前端组件都已经可以在 NPM 中使用,因此现在所有组件都通过 & Yarn 进行管理,package.json并驱动从 、 \ 中提取的 SCSS 和 JS 组件的 Sass & \xc2 \ bin/devxa0compilation xc2\xa0 或; 因此,资产管道仅包含对内部和文件夹的引用。esbuildapp/assetsapp/javascriptnode_modules/...manifest.jsbuildimagesapp/assets

\n

感觉有点倒退了,所有重量级的文件名列表手动维护(不再支持通配符导入)以及现在在 Foreman 下运行的多个进程的复杂性与仅根据每个请求在 Sprockets 中同步处理的东西相比基础,但随着所有这些东西都被弃用/废弃软件,显然是时候更新了。

\n

这一切在开发和生产模式下都运行良好,但是测试呢?我们使用RSpec;在 CI 中,没有内置资产,开发人员不想每次运行时都必须记住运行esbuild\xc2\xa0 或\xc2\xa0 或其他任何内容。抛开其他不谈,它的速度相当慢。assets:precompilerspec

\n

当您想要使用最新资产运行测试时,在基于 Yarn/Node 的工作流程中专门使用cssbundling-rails和的官方惯用 Rails 7 解决方案是什么?jsbundling-rails

\n

And*_*son 4

虽然这很困难,但现在总比没有好;它将确保 CI 始终构建资产,并确保本地开发始终拥有最新的资产,即使在egbin/dev未运行时进行了修改。

# Under Rails 7 with 'cssbundling-rails' and/or the 'jsbundling-rails' gems,
# entirely external systems are used for asset management. With Sprockets no
# longer synchronously building assets on-demand and only when the source files
# changed, compiled assets might be (during local development) or will almost
# always be (CI systems) either out of date or missing when tests are run.
#
# People are used to "bundle exec rspec" and things working. The out-of-box gem
# 'cssbundling-rails' hooks into a vanilla Rails "prepare" task, running a full
# "css:build" task in response. This is quite slow and generates console spam
# on every test run, but points to a slightly better solution for RSpec.
#
# This class is a way of packaging that solution. The class wrapper is really
# just a namespace / container for the code.
#
# First, if you aren't already doing this, add the folllowing lines to
# "spec_helper.rb" somewhere *after* the "require 'rspec/rails'" line:
#
#     require 'rake'
#     YourAppName::Application.load_tasks
#
# ...and call MaintainTestAssets::maintain! (see that method's documentation
# for details). See also constants MaintainTestAssets::ASSET_SOURCE_FOLDERS and
# MaintainTestAssets::EXPECTED_ASSETS for things you may want to customise.
#
class MaintainTestAssets

  # All the places where you have asset files of any kind that you expect to be
  # dynamically compiled/transpiled/etc. via external tooling. The given arrays
  # are passed to "Rails.root.join..." to generate full pathnames.
  #
  # Folders are checked recursively. If any file timestamp therein is greater
  # than (newer than) any of EXPECTED_ASSETS, a rebuild is triggered.
  #
  ASSET_SOURCE_FOLDERS = [
    ['app', 'assets', 'stylesheets'],
    ['app', 'javascript'],
    ['vendor']
  ]

  # The leaf files that ASSET_SOURCE_FOLDERS will build. These are all checked
  # for in "File.join(Rails.root, 'app', 'assets', 'builds')". Where files are
  # written together - e.g. a ".js" and ".js.map" file - you only need to list
  # any one of the group of concurrently generated files.
  #
  # In a standard JS / CSS combination this would just be 'application.css' and
  # 'application.js', but more complex applications might have added or changed
  # entries in the "scripts" section of 'package.json'.
  #
  EXPECTED_ASSETS = %w{
    application.js
    application.css
  }

  # Call this method somewhere at test startup, e.g. in "spec_helper.rb" before
  # tests are actually run (just above "RSpec.configure..." works reasonably).
  #
  def self.maintain!
    run_build    = false
    newest_mtime = Time.now - 100.years

    # Find the newest modificaftion time across all source files of any type -
    # for simplicity, timestamps of JS vs CSS aren't considered
    #
    ASSET_SOURCE_FOLDERS.each do | relative_array |
      glob_path = Rails.root.join(*relative_array, '**', '*')

      Dir[glob_path].each do | filename |
        next if File.directory?(filename) # NOTE EARLY LOOP RESTART

        source_mtime = File.mtime(filename)
        newest_mtime = source_mtime if source_mtime > newest_mtime
      end
    end

    # Compile the built asset leaf names into full file names for convenience.
    #
    built_assets = EXPECTED_ASSETS.map do | leaf |
      Rails.root.join('app', 'assets', 'builds', leaf)
    end

    # If any of the source files are newer than expected built assets, or if
    # any of those assets are missing, trigger a rebuild task *and* force a new
    # timestamp on all output assets (just in case build script optimisations
    # result in a file being skipped as "already up to date", which would cause
    # the code here to otherwise keep trying to rebuild it on every run).
    #
    run_build = built_assets.any? do | filename |
      File.exist?(filename) == false || File.mtime(filename) < newest_mtime
    end

    if run_build
      Rake::Task['javascript:build'].invoke()
      Rake::Task[       'css:build'].invoke()

      built_assets.each { | filename | FileUtils.touch(filename, nocreate: true) }
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

(编辑)正如下面的评论者指出的那样,您需要确保 Rake 任务已加载到您的 中spec_helper.rb,例如:

require 'rake'
Rails.application.load_tasks
Run Code Online (Sandbox Code Playgroud)

  • 它对我有用,但我必须在 Rake::Task 调用之前添加 `Rails.application.load_tasks` (2认同)