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 developmentRails.application.config.before_initialize- runs beforeafter_initializeand does not reload in developmentRails.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 mvwhen 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?orzeitwerk?; they are gone - avoid touching constants during initialization unless the load order is explicit and intentional
- use
EasyInitHelperfor 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
requirecalls where necessary -
Patches
- use
Rys::Patcherfor 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 fromActiveRecord::Migration[8.1] - data migrations belong in
db/data/and inherit fromEasyExtensions::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?andzeitwerk?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.zeitwerkcompatibility file has been removed - legacy
api/locations still warn and should be migrated toapp/api
When updating older plugins, treat this document as the source of truth for the target structure, not as a dual-mode migration checklist.