panda-core 0.7.5 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5b6a96a40435b148afebf0b96b694e6e22f42519e6a8354bfb3553bdecb492a
4
- data.tar.gz: 88bdb192bd9796b261cc71030fea9a0fb355d3e36f2e71ef7b8f7f962c37757d
3
+ metadata.gz: d71882619f044e6c12c628c91dd32e1f3f8d35ed6c06c2d56a952df67b2d185d
4
+ data.tar.gz: 98ffdb33880b310143ba47d13f40edda3fe5568a9474d089fc56fa6f43b45cba
5
5
  SHA512:
6
- metadata.gz: 1c3415fba271ab61118cb85c53805b97609196a67db98a0f50a7364902a137b25162e788822c57f5c06232aa9e36e54ef26114dd118012fcfbb3dbcef96d6304
7
- data.tar.gz: 6cb4bf5de81695ffc7fa6ce0e452fd049f90284213cfcf6842653a944590b91942c9ba0175818c544dbec4f9566404b4eea87431d397b969e7c5bf15e1ab5c00
6
+ metadata.gz: e76cc361df60d271c1ce4489eb892731e5ae03bdbe7de1eaddd9ae60c3bb9a0c31319aa309f6a8be020a9fae34cd1cbdcf3bfcce3ebc7bc8379609412b870167
7
+ data.tar.gz: a16db81da2dd0248dc6e0dd9d306dcc2d134ddcb372b707c65c99a9a6c31a24c79e175ee01412abe46fe6f6148afe2bd648351fa876cca3bfd6ec6d39c646a04
data/Rakefile CHANGED
@@ -2,7 +2,6 @@ require "bundler/setup"
2
2
 
3
3
  APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
4
4
  load "rails/tasks/engine.rake"
5
- load "rails/tasks/statistics.rake"
6
5
 
7
6
  require "bundler/gem_tasks"
8
7
 
@@ -157,14 +157,28 @@ module Panda
157
157
  end
158
158
 
159
159
  def development_css_url
160
- # Try versioned file first
161
- version = asset_version
162
- versioned_file = "/panda-core-assets/panda-core-#{version}.css"
163
- return versioned_file if File.exist?(Rails.public_path.join("panda-core-assets", "panda-core-#{version}.css"))
160
+ assets_dir = Panda::Core::Engine.root.join("public", "panda-core-assets")
161
+
162
+ # In dev/test, look for timestamp-based files (latest one)
163
+ if Rails.env.test? || Rails.env.development?
164
+ # Find all timestamp-based CSS files (exclude symlinks)
165
+ css_files = Dir[assets_dir.join("panda-core-*.css")].reject { |f| File.symlink?(f) }
166
+
167
+ if css_files.any?
168
+ # Return the most recently created file
169
+ latest = css_files.max_by { |f| File.basename(f)[/\d+/].to_i }
170
+ return "/panda-core-assets/#{File.basename(latest)}"
171
+ end
172
+ else
173
+ # In production, try versioned file first
174
+ version = asset_version
175
+ versioned_file = "/panda-core-assets/panda-core-#{version}.css"
176
+ return versioned_file if File.exist?(Rails.public_path.join("panda-core-assets", "panda-core-#{version}.css"))
177
+ end
164
178
 
165
179
  # Fall back to unversioned file (always available from engine's public directory)
166
180
  unversioned_file = "/panda-core-assets/panda-core.css"
167
- return unversioned_file if File.exist?(Panda::Core::Engine.root.join("public", "panda-core-assets", "panda-core.css"))
181
+ return unversioned_file if File.exist?(assets_dir.join("panda-core.css"))
168
182
 
169
183
  nil
170
184
  end
@@ -19,6 +19,10 @@ ensure
19
19
  $stderr = original_stderr
20
20
  end
21
21
 
22
+ # Load shared configuration modules
23
+ require_relative "shared/inflections_config"
24
+ require_relative "shared/generator_config"
25
+
22
26
  # Load engine configuration modules
23
27
  require_relative "engine/test_config"
24
28
  require_relative "engine/autoload_config"
@@ -28,6 +32,9 @@ require_relative "engine/omniauth_config"
28
32
  require_relative "engine/phlex_config"
29
33
  require_relative "engine/admin_controller_config"
30
34
 
35
+ # Load module registry
36
+ require_relative "module_registry"
37
+
31
38
  module Panda
32
39
  module Core
33
40
  class Engine < ::Rails::Engine
@@ -49,6 +56,51 @@ module Panda
49
56
  initializer "panda_core.config" do |app|
50
57
  # Configuration is already initialized with defaults in Configuration class
51
58
  end
59
+
60
+ # Auto-compile CSS for test/development environments
61
+ initializer "panda_core.auto_compile_assets", after: :load_config_initializers do |app|
62
+ # Only auto-compile in test or when explicitly requested
63
+ next unless Rails.env.test? || ENV["PANDA_CORE_AUTO_COMPILE"] == "true"
64
+
65
+ # Use timestamp for cache busting in dev/test
66
+ timestamp = Time.now.to_i
67
+ assets_dir = Panda::Core::Engine.root.join("public", "panda-core-assets")
68
+ timestamped_css = assets_dir.join("panda-core-#{timestamp}.css")
69
+
70
+ # Check if any compiled CSS exists (timestamp-based)
71
+ existing_css = Dir[assets_dir.join("panda-core-*.css")].reject { |f| File.symlink?(f) }
72
+
73
+ if existing_css.empty?
74
+ warn "🐼 [Panda Core] Auto-compiling CSS for test environment..."
75
+
76
+ # Compile CSS with timestamp
77
+ require "open3"
78
+ require "fileutils"
79
+
80
+ FileUtils.mkdir_p(assets_dir)
81
+
82
+ # Get content paths from ModuleRegistry
83
+ content_paths = Panda::Core::ModuleRegistry.tailwind_content_paths
84
+ content_flags = content_paths.map { |path| "--content '#{path}'" }.join(" ")
85
+
86
+ # Compile directly to timestamped file with all registered module content
87
+ input_file = Panda::Core::Engine.root.join("app/assets/tailwind/application.css")
88
+ cmd = "bundle exec tailwindcss -i #{input_file} -o #{timestamped_css} #{content_flags} --minify"
89
+
90
+ _, stderr, status = Open3.capture3(cmd)
91
+
92
+ if status.success?
93
+ # Create unversioned symlink for fallback
94
+ symlink = assets_dir.join("panda-core.css")
95
+ FileUtils.rm_f(symlink) if File.exist?(symlink)
96
+ FileUtils.ln_sf(File.basename(timestamped_css), symlink)
97
+
98
+ warn "🐼 [Panda Core] CSS compilation successful (#{timestamped_css.size} bytes)"
99
+ else
100
+ warn "🐼 [Panda Core] CSS compilation failed: #{stderr}"
101
+ end
102
+ end
103
+ end
52
104
  end
53
105
  end
54
106
  end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Panda
4
+ module Core
5
+ # Module registry for Panda ecosystem components
6
+ #
7
+ # This class maintains a registry of all Panda modules (CMS, CMS Pro, Community, etc.)
8
+ # and their asset paths. Each module self-registers during engine initialization.
9
+ #
10
+ # Benefits:
11
+ # - Core doesn't need hardcoded knowledge of other modules
12
+ # - Supports private modules (e.g., panda-community in development)
13
+ # - Single source of truth for asset compilation
14
+ # - Scales automatically to future modules
15
+ #
16
+ # Usage:
17
+ # # In module's engine.rb (after class definition):
18
+ # Panda::Core::ModuleRegistry.register(
19
+ # gem_name: 'panda-cms',
20
+ # engine: 'Panda::CMS::Engine',
21
+ # paths: {
22
+ # views: 'app/views/panda/cms/**/*.erb',
23
+ # components: 'app/components/panda/cms/**/*.rb',
24
+ # stylesheets: 'app/assets/stylesheets/panda/cms/**/*.css'
25
+ # }
26
+ # )
27
+ #
28
+ class ModuleRegistry
29
+ @modules = {}
30
+
31
+ class << self
32
+ # Register a Panda module with its asset paths
33
+ #
34
+ # @param gem_name [String] Gem name (e.g., 'panda-cms')
35
+ # @param engine [String] Engine constant name (e.g., 'Panda::CMS::Engine')
36
+ # @param paths [Hash] Asset path patterns relative to engine root
37
+ # @option paths [String] :views View template paths
38
+ # @option paths [String] :components ViewComponent paths
39
+ # @option paths [String] :stylesheets Stylesheet paths (optional)
40
+ def register(gem_name:, engine:, paths:)
41
+ @modules[gem_name] = {
42
+ engine: engine,
43
+ paths: paths
44
+ }
45
+ end
46
+
47
+ # Returns all registered modules
48
+ #
49
+ # @return [Hash] Module registry
50
+ attr_reader :modules
51
+
52
+ # Returns content paths for Tailwind CSS scanning
53
+ #
54
+ # Tailwind needs to scan all files that might contain utility classes:
55
+ # - Views (ERB templates)
56
+ # - Components (ViewComponent classes)
57
+ # - JavaScript (Stimulus controllers, etc.)
58
+ #
59
+ # @return [Array<String>] Full paths for Tailwind --content flags
60
+ def tailwind_content_paths
61
+ paths = []
62
+
63
+ # Core's own content (always included)
64
+ core_root = Panda::Core::Engine.root
65
+ paths << "#{core_root}/app/views/panda/core/**/*.erb"
66
+ paths << "#{core_root}/app/components/panda/core/**/*.rb"
67
+
68
+ # Registered modules (only if engine is loaded)
69
+ @modules.each do |gem_name, info|
70
+ next unless engine_available?(info[:engine])
71
+
72
+ root = engine_root(info[:engine])
73
+ next unless root
74
+
75
+ # Add configured path types
76
+ paths << "#{root}/#{info[:paths][:views]}" if info[:paths][:views]
77
+ paths << "#{root}/#{info[:paths][:components]}" if info[:paths][:components]
78
+
79
+ # For Tailwind scanning, we also need to scan JavaScript for utility classes
80
+ # Check if module has JavaScript (via importmap or direct paths)
81
+ js_root = root.join("app/javascript")
82
+ if js_root.directory?
83
+ paths << "#{js_root}/**/*.js"
84
+ end
85
+ end
86
+
87
+ # Host application Panda overrides
88
+ # Applications can override any Panda views/components
89
+ if defined?(Rails.root)
90
+ paths << "#{Rails.root}/app/views/panda/**/*.erb"
91
+ paths << "#{Rails.root}/app/components/panda/**/*.rb"
92
+ paths << "#{Rails.root}/app/javascript/panda/**/*.js"
93
+ end
94
+
95
+ paths.compact
96
+ end
97
+
98
+ # Returns JavaScript source files by introspecting importmaps
99
+ #
100
+ # Instead of duplicating file lists, we read the importmap configuration
101
+ # that each engine already maintains. This provides a single source of truth.
102
+ #
103
+ # @return [Array<String>] Full paths to JavaScript source files
104
+ def javascript_sources
105
+ return [] unless defined?(Rails.application&.importmap)
106
+
107
+ sources = []
108
+
109
+ # Detect importmap-rails version and use appropriate API
110
+ importmap = Rails.application.importmap
111
+ entries = if importmap.respond_to?(:packages)
112
+ # importmap-rails 2.x - packages is a hash
113
+ importmap.packages
114
+ elsif importmap.respond_to?(:entries)
115
+ # importmap-rails 1.x - entries is an array
116
+ importmap.entries.map { |e| [e.name, e] }.to_h
117
+ else
118
+ {}
119
+ end
120
+
121
+ # Find all Panda-namespaced imports and resolve to file paths
122
+ entries.each do |name, config|
123
+ next unless name.to_s.match?(/^panda-/)
124
+
125
+ path = resolve_importmap_to_path(name, config)
126
+ sources << path if path
127
+ end
128
+
129
+ sources.compact.uniq
130
+ end
131
+
132
+ # Returns registered module names
133
+ #
134
+ # @return [Array<String>] List of registered gem names
135
+ def registered_modules
136
+ @modules.keys
137
+ end
138
+
139
+ # Check if a specific module is registered
140
+ #
141
+ # @param gem_name [String] Gem name to check
142
+ # @return [Boolean] True if module is registered
143
+ def registered?(gem_name)
144
+ @modules.key?(gem_name)
145
+ end
146
+
147
+ private
148
+
149
+ # Check if an engine constant is defined and available
150
+ #
151
+ # @param engine_name [String] Engine constant name
152
+ # @return [Boolean] True if engine is available
153
+ def engine_available?(engine_name)
154
+ Object.const_defined?(engine_name)
155
+ rescue NameError
156
+ false
157
+ end
158
+
159
+ # Get the root path of an engine
160
+ #
161
+ # @param engine_name [String] Engine constant name
162
+ # @return [Pathname, nil] Engine root path or nil if unavailable
163
+ def engine_root(engine_name)
164
+ return nil unless engine_available?(engine_name)
165
+
166
+ engine_class = Object.const_get(engine_name)
167
+ engine_class.root
168
+ rescue NoMethodError
169
+ nil
170
+ end
171
+
172
+ # Resolve an importmap entry to an actual file path
173
+ #
174
+ # Importmap entries can be:
175
+ # - Direct paths: "panda/cms/controllers/dashboard_controller.js"
176
+ # - Module names: "panda-cms/controllers/dashboard_controller"
177
+ #
178
+ # We need to find where these files actually live in the filesystem.
179
+ #
180
+ # @param name [String, Symbol] Import name
181
+ # @param config [Object] Importmap entry (structure varies by version)
182
+ # @return [String, nil] Full file path or nil if not found
183
+ def resolve_importmap_to_path(name, config)
184
+ # Extract path from config (API differs between importmap-rails versions)
185
+ relative_path = if config.respond_to?(:path)
186
+ config.path
187
+ elsif config.respond_to?(:[])
188
+ config[:path] || config["path"]
189
+ elsif config.is_a?(String)
190
+ config
191
+ end
192
+
193
+ return nil unless relative_path
194
+
195
+ # Try to find the file in registered engines
196
+ @modules.each do |gem_name, info|
197
+ next unless engine_available?(info[:engine])
198
+
199
+ root = engine_root(info[:engine])
200
+ next unless root
201
+
202
+ # Check common JavaScript locations
203
+ [
204
+ root.join("app/javascript", relative_path),
205
+ root.join("app/javascript", "#{relative_path}.js"),
206
+ root.join("app/assets/javascripts", relative_path),
207
+ root.join("app/assets/javascripts", "#{relative_path}.js")
208
+ ].each do |candidate|
209
+ return candidate.to_s if candidate.exist?
210
+ end
211
+ end
212
+
213
+ # Check Rails app if available
214
+ if defined?(Rails.root)
215
+ [
216
+ Rails.root.join("app/javascript", relative_path),
217
+ Rails.root.join("app/javascript", "#{relative_path}.js")
218
+ ].each do |candidate|
219
+ return candidate.to_s if candidate.exist?
220
+ end
221
+ end
222
+
223
+ nil
224
+ end
225
+ end
226
+ end
227
+ end
228
+ end
@@ -31,7 +31,14 @@ end
31
31
  # Load all support files from panda-core
32
32
  # Files are now in lib/panda/core/testing/support/ to be included in the published gem
33
33
  support_path = File.expand_path("../support", __FILE__)
34
- Dir[File.join(support_path, "**/*.rb")].sort.each { |f| require f }
34
+
35
+ # Load system test infrastructure first (Capybara, Cuprite, helpers)
36
+ system_test_files = Dir[File.join(support_path, "system/**/*.rb")].sort
37
+ system_test_files.each { |f| require f }
38
+
39
+ # Load other support files
40
+ other_support_files = Dir[File.join(support_path, "**/*.rb")].sort.reject { |f| f.include?("/system/") }
41
+ other_support_files.each { |f| require f }
35
42
 
36
43
  RSpec.configure do |config|
37
44
  # Include panda-core route helpers by default
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared Capybara configuration for all Panda gems
4
+ # This provides standard Capybara setup with sensible defaults
5
+
6
+ # Increase wait time for CI environments where asset loading is slower
7
+ Capybara.default_max_wait_time = ENV["CI"].present? ? 10 : 5
8
+
9
+ # Normalize whitespaces when using `has_text?` and similar matchers,
10
+ # i.e., ignore newlines, trailing spaces, etc.
11
+ # That makes tests less dependent on slight UI changes.
12
+ Capybara.default_normalize_ws = true
13
+
14
+ # Where to store system tests artifacts (e.g. screenshots, downloaded files, etc.).
15
+ # It could be useful to be able to configure this path from the outside (e.g., on CI).
16
+ Capybara.save_path = ENV.fetch("CAPYBARA_ARTIFACTS", "./tmp/capybara")
17
+
18
+ # Disable animation so we're not waiting for it
19
+ Capybara.disable_animation = true
20
+
21
+ # See SystemTestHelpers#take_screenshot
22
+ # This allows us to track which session was last used for proper screenshot naming
23
+ Capybara.singleton_class.prepend(Module.new do
24
+ attr_accessor :last_used_session
25
+
26
+ def using_session(name, &block)
27
+ self.last_used_session = name
28
+ super
29
+ ensure
30
+ self.last_used_session = nil
31
+ end
32
+ end)
33
+
34
+ # Configure server host and port
35
+ Capybara.server_host = "127.0.0.1"
36
+ Capybara.server_port = ENV["CAPYBARA_PORT"]&.to_i # Let Capybara choose if not specified
37
+
38
+ # Configure Puma server with explicit options
39
+ # Use single-threaded mode to share database connection with tests
40
+ Capybara.register_server :puma do |app, port, host|
41
+ require "rack/handler/puma"
42
+ Rack::Handler::Puma.run(app, Port: port, Host: host, Silent: true, Threads: "1:1")
43
+ end
44
+ Capybara.server = :puma
45
+
46
+ # Do not set app_host here - let Capybara determine it from the server
47
+ # This avoids conflicts between what's configured and what's actually running
48
+
49
+ # RSpec configuration for Capybara
50
+ RSpec.configure do |config|
51
+ # Save screenshots on system test failures
52
+ config.after(:each, type: :system) do |example|
53
+ if example.exception
54
+ timestamp = Time.now.strftime("%Y-%m-%d-%H%M%S")
55
+ "tmp/capybara/failures/#{example.full_description.parameterize}_#{timestamp}"
56
+
57
+ # Screenshots are saved automatically by Capybara, but we could save additional artifacts here
58
+ # save_page("#{filename_base}.html")
59
+ # save_screenshot("#{filename_base}.png", full: true)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ferrum"
4
+ require "capybara/cuprite"
5
+
6
+ # Shared Cuprite driver configuration for all Panda gems
7
+ # This provides standard Cuprite setup with sensible defaults that work across gems
8
+ #
9
+ # Features:
10
+ # - :cuprite driver for standard desktop testing
11
+ # - :cuprite_mobile driver for mobile viewport testing
12
+ # - JavaScript error reporting enabled by default (js_errors: true)
13
+ # - CI-optimized browser options
14
+ # - Environment-based configuration (HEADLESS, INSPECTOR, SLOWMO)
15
+
16
+ module Panda
17
+ module Core
18
+ module Testing
19
+ module CupriteSetup
20
+ # Base Cuprite options shared across all drivers
21
+ def self.base_options
22
+ {
23
+ window_size: [1440, 1000],
24
+ inspector: ENV["INSPECTOR"].in?(%w[y 1 yes true]),
25
+ headless: !ENV["HEADLESS"].in?(%w[n 0 no false]),
26
+ slowmo: ENV["SLOWMO"]&.to_f || 0,
27
+ timeout: 30,
28
+ js_errors: true, # IMPORTANT: Report JavaScript errors as test failures
29
+ ignore_default_browser_options: false,
30
+ process_timeout: 10,
31
+ wait_for_network_idle: false, # Don't wait for all network requests
32
+ pending_connection_errors: false, # Don't fail on pending external connections
33
+ browser_options: {
34
+ "no-sandbox": nil,
35
+ "disable-gpu": nil,
36
+ "disable-dev-shm-usage": nil,
37
+ "disable-background-networking": nil,
38
+ "disable-default-apps": nil,
39
+ "disable-extensions": nil,
40
+ "disable-sync": nil,
41
+ "disable-translate": nil,
42
+ "no-first-run": nil,
43
+ "ignore-certificate-errors": nil,
44
+ "allow-insecure-localhost": nil,
45
+ "enable-features": "NetworkService,NetworkServiceInProcess",
46
+ "disable-blink-features": "AutomationControlled"
47
+ }
48
+ }
49
+ end
50
+
51
+ # Additional options for CI environments
52
+ def self.ci_browser_options
53
+ {
54
+ "disable-web-security": nil,
55
+ "allow-file-access-from-files": nil,
56
+ "allow-file-access": nil
57
+ }
58
+ end
59
+
60
+ # Configure standard desktop driver
61
+ def self.register_desktop_driver
62
+ options = base_options.dup
63
+
64
+ # Add CI-specific options
65
+ if ENV["GITHUB_ACTIONS"] == "true"
66
+ options[:browser_options].merge!(ci_browser_options)
67
+
68
+ puts "\nšŸ” Cuprite Configuration (Desktop):"
69
+ puts " Debug mode: #{ENV["DEBUG"]}"
70
+ puts " Headless: #{options[:headless]}"
71
+ puts " JS Errors: #{options[:js_errors]}"
72
+ puts " Browser options: #{options[:browser_options].keys.join(", ")}"
73
+ puts ""
74
+ end
75
+
76
+ Capybara.register_driver :cuprite do |app|
77
+ Capybara::Cuprite::Driver.new(app, **options)
78
+ end
79
+ end
80
+
81
+ # Configure mobile viewport driver
82
+ def self.register_mobile_driver
83
+ options = base_options.dup
84
+ options[:window_size] = [375, 667] # iPhone SE size
85
+
86
+ if ENV["GITHUB_ACTIONS"] == "true"
87
+ options[:browser_options].merge!(ci_browser_options)
88
+
89
+ puts "\nšŸ” Cuprite Configuration (Mobile):"
90
+ puts " Window size: #{options[:window_size]}"
91
+ puts " JS Errors: #{options[:js_errors]}"
92
+ puts ""
93
+ end
94
+
95
+ Capybara.register_driver :cuprite_mobile do |app|
96
+ Capybara::Cuprite::Driver.new(app, **options)
97
+ end
98
+ end
99
+
100
+ # Register all drivers
101
+ def self.setup!
102
+ register_desktop_driver
103
+ register_mobile_driver
104
+
105
+ # Set default drivers
106
+ Capybara.default_driver = :cuprite
107
+ Capybara.javascript_driver = :cuprite
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ # Auto-setup when required
115
+ Panda::Core::Testing::CupriteSetup.setup!
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Share database connection between test thread and server thread
4
+ # This allows the Puma server to see uncommitted transaction data from fixtures
5
+ class ActiveRecord::Base
6
+ mattr_accessor :shared_connection
7
+ @@shared_connection = nil
8
+
9
+ def self.connection
10
+ @@shared_connection || retrieve_connection
11
+ end
12
+ end
13
+
14
+ # Set up shared connection before system tests
15
+ RSpec.configure do |config|
16
+ config.before(:each, type: :system) do
17
+ ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
18
+ end
19
+
20
+ config.after(:each, type: :system) do
21
+ ActiveRecord::Base.shared_connection = nil
22
+ end
23
+ end