Project

General

Profile

Patch #44016 » 0002-Support-importmap-in-plugins.patch

Takashi Kato, 2026-05-02 09:05

View differences:

config/initializers/30-redmine.rb
125 125
  end
126 126
end
127 127

  
128
# Automatically execute asset precompilation on startup in case of changes have been detected in assets
129 128
Rails.application.config.after_initialize do |app|
129
  # Automatically execute asset precompilation on startup in case of changes have been detected in assets
130 130
  if app.config.assets.redmine_detect_update && app.assets.needs_precompile?
131 131
    app.assets.processor.process
132 132
  end
133

  
134
  Redmine::Plugin.loader.directories.each(&:draw_importmap)
133 135
end
134 136

  
135 137
Rails.application.deprecators[:redmine] = ActiveSupport::Deprecation.new('7.0', 'Redmine')
lib/generators/redmine_plugin/USAGE
22 22
      create  plugins/meetings/README.rdoc
23 23
      create  plugins/meetings/init.rb
24 24
      create  plugins/meetings/config/routes.rb
25
      create  plugins/meetings/config/importmap.rb
25 26
      create  plugins/meetings/config/locales/en.yml
26 27
      create  plugins/meetings/test/test_helper.rb
lib/generators/redmine_plugin/redmine_plugin_generator.rb
51 51
    template 'README.rdoc',    "#{plugin_path}/README.rdoc"
52 52
    template 'init.rb.erb',   "#{plugin_path}/init.rb"
53 53
    template 'routes.rb',    "#{plugin_path}/config/routes.rb"
54
    template 'importmap.rb',    "#{plugin_path}/config/importmap.rb"
54 55
    template 'en_rails_i18n.yml',    "#{plugin_path}/config/locales/en.yml"
55 56
    template 'test_helper.rb.erb',    "#{plugin_path}/test/test_helper.rb"
56 57
  end
lib/generators/redmine_plugin/templates/importmap.rb
1
# frozen_string_literal: true
2

  
3
# pin "foo"
4

  
5
# pin_all_from "app/javascript/locales", under: "locales"
6

  
7
# pin_all_from "app/javascript/controllers", under: "controllers"
lib/redmine/asset_path.rb
188 188

  
189 189
    def clear_cache
190 190
      @transition_map = nil
191
      @cached_assets_by_path = nil
191 192
      super
192 193
    end
193 194
  end
lib/redmine/importmap.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
module Redmine
21
  module Importmap
22
    def draw_importmap
23
      return unless has_importmap?
24

  
25
      begin
26
        instance_eval(importmap_path.read, importmap_path.to_s)
27
      rescue => e
28
        Rails.logger.warn "Importmap Error Occured. #{e}"
29
      end
30
    end
31

  
32
    def has_importmap?
33
      importmap_path.present?
34
    end
35

  
36
    def importmap_path
37
      path = File.join(full_path, 'config/importmap.rb')
38

  
39
      if File.exist? path
40
        Pathname.new(path)
41
      else
42
        nil
43
      end
44
    end
45

  
46
    # with foo plugin
47
    #
48
    # pin 'bar'
49
    #   => import { exampleFunction } from 'plugin_assets/foo/bar'
50
    #
51
    # with baz theme
52
    #
53
    # pin 'qux'
54
    #   => import { exampleFunction } from 'themes/baz/qux'
55
    def pin(name, to: nil, preload: true)
56
      modified_name = File.join(asset_prefix, name)
57
      to ||= name + '.js'
58
      asset_name    = File.join(asset_prefix, to)
59

  
60
      importmap.pin modified_name, to: asset_name, preload: preload
61
    end
62

  
63
    # with foo plugin
64
    # pin_all_from 'app/javascript/src', under: 'src'
65
    #   => import { exampleFunction } from 'plugin_assets/foo/src/example_module'
66
    #
67
    # with baz theme
68
    # pin_all_from 'javascripts/src', under: 'src'
69
    #   => import { exampleFunction } from 'themes/baz/src/example_module'
70
    #
71
    # The path name of the stimulus controller is not changed.
72
    def pin_all_from(directory, under: nil, to: nil, preload: true)
73
      extension_dir   = Pathname.new(full_path).relative_path_from(Rails.root).join(directory).to_s
74
      extension_under = under === 'controllers' ? 'controllers'
75
                                                : under ? File.join(asset_prefix, under)
76
                                                        : nil
77
      extension_to    = to ? to : File.join(asset_prefix, under)
78
      importmap.pin_all_from extension_dir, under: extension_under, to: extension_to, preload: preload
79
    end
80

  
81
    private
82

  
83
    def importmap
84
      Rails.application.importmap
85
    end
86
  end
87
end
lib/redmine/plugin_loader.rb
17 17
# along with this program; if not, write to the Free Software
18 18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19 19

  
20
require 'redmine/importmap'
21

  
20 22
module Redmine
21 23
  class PluginPath
22 24
    attr_reader :assets_dir, :initializer, :prefix
25
    include Importmap
23 26

  
24 27
    def initialize(dir, prefix)
25 28
      @dir = dir
......
35 38
    def to_s
36 39
      @dir
37 40
    end
41
    alias_method :full_path, :to_s
38 42

  
39 43
    def has_assets_dir?
40 44
      File.directory?(@assets_dir)
......
44 48
      File.file?(@initializer)
45 49
    end
46 50

  
51
    def asset_prefix
52
      "#{@prefix}/#{File.basename(@dir)}"
53
    end
54

  
47 55
    def base_dir
48 56
      @base_dir ||= Pathname.new(assets_dir)
49 57
    end
50 58

  
51 59
    def asset_paths
52 60
      paths = base_dir.children.select(&:directory?)
61
      if has_importmap?
62
        plugin_root = Pathname.new(full_path)
63
        ['app/javascript', 'vendor/javascript'].each do |dir|
64
          jsdir = plugin_root.join(dir)
65
          paths << jsdir if jsdir.exist?
66
        end
67
      end
53 68
      paths
54 69
    end
55 70
  end
test/fixtures/plugins/foo_plugin/app/javascript/controllers/bar_controller.js
1
import { Controller } from "@hotwired/stimulus"
2

  
3
// Connects to data-controller="bar"
4
export default class extends Controller {
5
  connect() {
6
  }
7
}
test/fixtures/plugins/foo_plugin/app/javascript/locales/en.js
1
export const lang = {hello: 'hello'}
test/fixtures/plugins/foo_plugin/app/javascript/locales/ja.js
1
export const lang = {hello: 'こんにちは'}
test/fixtures/plugins/foo_plugin/config/importmap.rb
1
# frozen_string_literal: true
2

  
3
pin "foo"
4

  
5
pin_all_from "app/javascript/locales", under: "locales"
6

  
7
pin_all_from "app/javascript/controllers", under: "controllers"
test/test_helper.rb
512 512
      end
513 513
    end
514 514
  end
515

  
516
  module ImportmapTestHelper
517
    def path_to_asset(asset)
518
      resolver.resolve(asset)
519
    end
520
  end
515 521
end
test/unit/lib/redmine/plugin_loader_test.rb
23 23
  def setup
24 24
    clear_public
25 25

  
26
    @klass = Redmine::PluginLoader
27
    @klass.directory = Rails.root.join('test/fixtures/plugins')
28
    @klass.load
26
    Redmine::Plugin.clear
27
    @original_loader = ::Redmine::Plugin.loader
28
    loader = Redmine::PluginLoader.new directory: Rails.root.join('test/fixtures/plugins'), prefix: @original_loader.prefix
29
    Redmine::Plugin.loader = loader
30

  
31
    loader.public_directory = Rails.public_path.join(@original_loader.prefix)
32
    loader.add_autoload_paths
33
    loader.directories.each(&:run_initializer)
34

  
35
    config = Rails.application.config.assets.deep_dup
36
    Redmine::Plugin.all.each do |plugin|
37
      paths = plugin.asset_paths
38
      config.redmine_extension_paths << paths if paths.present?
39
    end
40

  
41
    assembly = Propshaft::Assembly.new(config)
42
    assembly.extend Redmine::ImportmapTestHelper
43
    assembly.load_path.clear_cache
44

  
45
    loader.directories.each(&:draw_importmap)
46
    json = Rails.application.importmap.to_json(resolver: assembly)
47
    @importmap = JSON.parse(json)['imports']
48
  end
49

  
50
  test 'esmodule is loaded' do
51
    assert_match %r{/assets/plugin_assets/foo_plugin/foo-\w{8}\.js}, @importmap['plugin_assets/foo_plugin/foo']
52
    assert_match %r{/assets/plugin_assets/foo_plugin/locales/ja-\w{8}\.js}, @importmap['plugin_assets/foo_plugin/locales/ja']
53
  end
54

  
55
  test 'stimulus controller is loaded' do
56
    assert_match %r{/assets/plugin_assets/foo_plugin/controllers/bar_controller-\w{8}\.js}, @importmap['controllers/bar_controller']
29 57
  end
30 58

  
31 59
  def teardown
60
     Redmine::Plugin.loader = @original_loader
32 61
    clear_public
33 62
  end
34 63

  
(2-2/2)