Skip to content

File Structure and Transition

This guide describes the current file structure and loading rules for Easy8 with Zeitwerk. The transition period is over and Easy8 now supports Zeitwerk only.

Zeitwerk

Zeitwerk is now the only supported autoloader in Easy8. New code should follow Zeitwerk conventions directly; there is no classic-mode fallback, no ZEITWERK_POWERED switch, and no need to keep dual-loader compatibility.

The main requirements are:

  • proper file naming
  • proper module scoping
  • one class or module per file
  • explicit requires for code stored outside autoloaded directories

Directory Structure

The biggest requirement for Zeitwerk compatibility is correct naming and structure. Files must map cleanly to the constants they define.

Redmine

Redmine itself already supports Zeitwerk, so most changes concern Easy plugins, engines, and local extensions.

Plugins

Legacy plugins may still exist in plugins/, but new work should follow the current Easy8 structure and Zeitwerk conventions from the start.

Easy Plugins

plugins/easyproject/easy_plugins/ is the standard home for Easy8 plugins. Code placed there should already be organized in a Zeitwerk-compatible way.

RYSes

RYSes keep their overall structure, but their code still needs to respect Zeitwerk naming, scoping, and loading rules.

Load Order

With Zeitwerk-only loading, initialization order matters most for patches, hooks, menu registration, and anything that depends on framework or plugin constants.

RedmineExtensions::Reloader.to_prepare is no longer supported. Use standard Rails initialization hooks instead:

  • Rails.application.config.to_prepare - runs first and reloads in development
  • Rails.application.config.before_initialize - runs before after_initialize and does not reload in development
  • Rails.application.config.after_initialize - runs at the end of initialization and does not reload in development

Pick the hook based on when constants are available and whether the code needs development reloading.

Current Conventions For Plugins

The following rules are the current baseline for plugin code.

To keep git history when moving files between directories, use git mv when possible.

  • App directory
  • all modules and classes must be properly named
  • each file should define a single class or module that matches the file name
  • constants must be scoped according to the directory structure

Example:

# plugins/easyproject/easy_plugins/my_cool_plugin/app/models/pages/first_page.rb
module Pages
  class FirstPage
  end
end

Avoid legacy path hacks in plugin engines such as:

paths["app/models"] << "app/models/easy_page_modules"
paths["app/models"] << "app/models/easy_queries"
paths["app/models"] << "app/models/easy_rakes"

Move these directories to a valid autoloaded location instead, usually directly under app/.

  • Initializers
  • keep initialization code in config/initializers/
  • split responsibilities into small files, following the conventions established in easy_engines/easy_extensions/config/initializers/
  • wrap code in the correct Rails hook when it depends on constants that are not yet loaded
  • do not rely on autoloader-condition helpers such as classic? or zeitwerk?; they are gone
  • avoid touching constants during initialization unless the load order is explicit and intentional
  • use EasyInitHelper for reload-sensitive menus, permissions, view hooks, and lazy registry blocks

Common initializer layout: - 01_access_control.rb - permissions - 02_menu_manager.rb - menus - 03_hooks.rb - hooks - 04_features.rb - features - 05_assets.rb - asset paths - 06_tests.rb - test setup - 07_jobs.rb - jobs - 08_easy_settings.rb - default Easy settings - 10_api.rb - API extensions

Example:

# plugins/easyproject/easy_plugins/my_cool_plugin/config/initializers/01_access_control.rb
EasyInitHelper.register_access_control_block do
  Redmine::AccessControl.map do |map|
    map.project_module :my_cool_plugin do |project_module|
      project_module.permission :view_my_cool_plugin_entities, { my_cool_plugin_entities: [:index] }
    end
  end
end

Default EasySetting values belong in 08_easy_settings.rb, not in migrations.

  • Lib
  • do not treat lib/ as a dumping ground for autoloaded code
  • lib/ should contain code that is loaded explicitly when needed
  • if code is meant to autoload, move it to an appropriate autoloaded directory under app/ or another supported location
  • add explicit require calls where necessary

  • Patches

  • use Rys::Patcher for patches
  • organize patch code so load order is explicit and initializer-driven
  • avoid legacy patch-loading patterns that depended on the old transition layer

  • API code

  • legacy api/ directories are deprecated
  • place API code in app/api

  • Migrations

  • keep schema and data migrations separate
  • schema migrations belong in db/migrate/ and inherit from ActiveRecord::Migration[8.1]
  • data migrations belong in db/data/ and inherit from EasyExtensions::EasyDataMigration
  • RYSes may also use db/after_plugins/ for schema migrations that must run after platform engine migrations

Extra Info For Context

Some older guidance in this area referred to temporary compatibility layers used during the migration to Zeitwerk. Those transition aids are no longer part of the supported setup.

Notable current-state points:

  • classic? and zeitwerk? helper methods were temporary and have been removed
  • classic-only preload paths are gone
  • plugin tests are no longer gated by a Zeitwerk check
  • the separate lib/redmine/plugin.rb.zeitwerk compatibility file has been removed
  • legacy api/ locations still warn and should be migrated to app/api

When updating older plugins, treat this document as the source of truth for the target structure, not as a dual-mode migration checklist.